@@ -17,12 +17,14 @@ import (
1717
1818 "go.opentelemetry.io/otel/attribute"
1919 "go.opentelemetry.io/otel/codes"
20+ semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
2021 "go.opentelemetry.io/otel/trace"
2122 "golang.org/x/sync/errgroup"
2223
2324 "github.com/quay/claircore"
2425 "github.com/quay/claircore/indexer"
2526 "github.com/quay/claircore/internal/cache"
27+ "github.com/quay/claircore/internal/httpreader"
2628 "github.com/quay/claircore/internal/httputil"
2729 "github.com/quay/claircore/internal/wart"
2830 "github.com/quay/claircore/internal/zreader"
@@ -125,6 +127,43 @@ func (a *RemoteFetchArena) fetchInto(ctx context.Context, l *claircore.Layer, cl
125127 span .End ()
126128 }()
127129
130+ var d details
131+ d , err = a .inspect (ctx , desc )
132+ if err != nil {
133+ return err
134+ }
135+
136+ HTTPReader:
137+ switch { // Switch for a single case to be able to break.
138+ case d .Uncompressed :
139+ var opts []httpreader.Option
140+ if d .ContentLength > 0 {
141+ opts = append (opts , httpreader .WithSize (d .ContentLength ))
142+ }
143+ if len (desc .Headers ) != 0 {
144+ opts = append (opts , httpreader .WithHeaders (desc .Headers ))
145+ }
146+ var rd * httpreader.Reader
147+ rd , err = httpreader .New (ctx , a .wc , desc .URI , opts ... )
148+ switch {
149+ case err == nil :
150+ if err = l .Init (ctx , desc , rd ); err != nil {
151+ return err
152+ }
153+ * cl = closeFunc (func () (err error ) {
154+ err = errors .Join (l .Close (), rd .Close ())
155+ return err
156+ })
157+ a .logger (desc ).DebugContext (ctx , "using httpreader" )
158+ return nil
159+ case errors .Is (err , errors .ErrUnsupported ):
160+ err = nil
161+ break HTTPReader
162+ default :
163+ return err
164+ }
165+ }
166+
128167 // NB This is not closed on purpose. The [io.Closer] populated by this
129168 // function holds the pointer until that function is cleaned up. Once
130169 // nothing has a copy of this [*os.File], the runtime will run all the
@@ -135,19 +174,20 @@ func (a *RemoteFetchArena) fetchInto(ctx context.Context, l *claircore.Layer, cl
135174 var spool * os.File
136175 spool , err = a .files .Get (ctx , key , func (ctx context.Context , _ string ) (* os.File , error ) {
137176 cacheHit = false
138- return a .fetchFileForCache (ctx , desc )
177+ return a .fetchFileForCache (ctx , desc , d )
139178 })
140179 if err != nil {
141180 return err
142181 }
182+ var f * os.File
143183 // This is an owned, independent descriptor for the passed [*os.File].
144- f , err : = reopen (a .root , spool )
184+ f , err = reopen (a .root , spool )
145185 if err != nil {
146186 return err
147187 }
148188
149189 // If this succeeds, "f" is now owned by "l"
150- if err : = l .Init (ctx , desc , f ); err != nil {
190+ if err = l .Init (ctx , desc , f ); err != nil {
151191 return errors .Join (err , f .Close ())
152192 }
153193 * cl = closeFunc (func () (err error ) {
@@ -173,12 +213,105 @@ func (f closeFunc) Close() error {
173213 return f ()
174214}
175215
216+ // Inspect makes a request to the layer's URI and examines the response for
217+ // useful information.
218+ func (a * RemoteFetchArena ) inspect (ctx context.Context , desc * claircore.LayerDescription ) (d details , err error ) {
219+ ctx , span := tracer .Start (ctx , "RemoteFetchArena.inspect" )
220+ defer func () {
221+ a .logger (desc ).DebugContext (ctx , "inspected resource" , "ok" , err == nil , "details" , & d )
222+ span .RecordError (err )
223+ span .End ()
224+ }()
225+ span .SetStatus (codes .Error , "" )
226+
227+ var req * http.Request
228+ var res * http.Response
229+ req , err = http .NewRequestWithContext (ctx , http .MethodGet , desc .URI , nil )
230+ req .Header = http .Header (desc .Headers ).Clone ()
231+ req .Header .Set (`claircore-reason` , `inspect` )
232+ req .Header .Set (`range` , `bytes=0-15` )
233+ if err != nil {
234+ return d , fmt .Errorf ("fetcher: failed to construct request: %w" , err )
235+ }
236+ res , err = a .wc .Do (req )
237+ if err != nil {
238+ return d , fmt .Errorf ("fetcher: request failed: %w" , err )
239+ }
240+ err = httputil .CheckResponse (res , http .StatusOK , http .StatusPartialContent )
241+ if err != nil {
242+ return d , fmt .Errorf ("fetcher: %w" , err )
243+ }
244+ head := make ([]byte , 16 )
245+ _ , err = io .ReadFull (res .Body , head )
246+ _ = res .Body .Close ()
247+ if err != nil {
248+ return d , fmt .Errorf ("fetcher: unexpected read: %w" , err )
249+ }
250+
251+ const (
252+ ctKey = `content-type`
253+ arKey = `accept-ranges`
254+ )
255+ ct := res .Header .Get (ctKey )
256+ span .SetAttributes (
257+ semconv .HTTPResponseStatusCode (res .StatusCode ),
258+ semconv .HTTPResponseBodySize (int (res .ContentLength )),
259+ semconv .HTTPResponseHeader (ctKey , ct ),
260+ semconv .HTTPResponseHeader (arKey , res .Header .Get (arKey )),
261+ )
262+ if ct == `application/octet-stream` {
263+ if zreader .DetectCompression (head ) == zreader .KindNone {
264+ ct = `application/x-tar`
265+ }
266+ }
267+ switch res .StatusCode {
268+ case http .StatusOK :
269+ d .ContentLength = res .ContentLength
270+ case http .StatusPartialContent :
271+ var cr httpreader.ContentRange
272+ if err := cr .Parse (res .Header .Get (`content-range` )); err == nil {
273+ d .ContentLength = cr .Length
274+ }
275+ }
276+ d .Uncompressed = ct == "application/x-tar" || strings .HasSuffix (ct , ".tar" )
277+ d .RangeOK = res .Header .Get (arKey ) == `bytes` || res .StatusCode == http .StatusPartialContent
278+
279+ span .SetStatus (codes .Ok , "" )
280+ return d , nil
281+ }
282+
283+ var _ slog.LogValuer = (* details )(nil )
284+
285+ // Details is the useful information reported by [RemoteFetchArena.inspect].
286+ type details struct {
287+ // The content length, if known.
288+ ContentLength int64
289+ // The resource reports to be known non-compressed contents.
290+ Uncompressed bool
291+ // The server reports supporting "bytes" via the "Accept-Ranges" header.
292+ RangeOK bool
293+ }
294+
295+ // LogValue implements [slog.LogValuer].
296+ func (d * details ) LogValue () slog.Value {
297+ return slog .GroupValue (
298+ slog .Int64 ("content-length" , d .ContentLength ),
299+ slog .Bool ("uncompressed" , d .Uncompressed ),
300+ slog .Bool ("range-ok" , d .RangeOK ),
301+ )
302+ }
303+
304+ // Logger returns a [*slog.Logger] with predefined attributes.
305+ func (a * RemoteFetchArena ) logger (desc * claircore.LayerDescription ) * slog.Logger {
306+ return slog .With ("arena" , a .root .Name (), "layer" , desc .Digest , "uri" , desc .URI )
307+ }
308+
176309// FetchFileForCache is the inner function used inside the [cache.Live].
177310//
178311// Because we know we're the only concurrent call that's dealing with this blob,
179312// we can be a bit more lax.
180- func (a * RemoteFetchArena ) fetchFileForCache (ctx context.Context , desc * claircore.LayerDescription ) (* os.File , error ) {
181- log := slog . With ( "arena" , a . root . Name (), "layer" , desc . Digest , "uri" , desc . URI )
313+ func (a * RemoteFetchArena ) fetchFileForCache (ctx context.Context , desc * claircore.LayerDescription , _ details ) (* os.File , error ) {
314+ log := a . logger ( desc )
182315 ctx , span := tracer .Start (ctx , "RemoteFetchArena.fetchFileForCache" )
183316 defer span .End ()
184317 span .SetStatus (codes .Error , "" )
@@ -210,6 +343,7 @@ func (a *RemoteFetchArena) fetchFileForCache(ctx context.Context, desc *claircor
210343 URL : url ,
211344 Header : http .Header (desc .Headers ).Clone (),
212345 }).WithContext (ctx )
346+ req .Header .Set (`claircore-reason` , `fetch` )
213347 resp , err := a .wc .Do (req )
214348 if err != nil {
215349 return nil , fmt .Errorf ("fetcher: request failed: %w" , err )
0 commit comments