@@ -3,26 +3,31 @@ package libindex
33import (
44 "bufio"
55 "bytes"
6+ "cmp"
67 "context"
78 "encoding/hex"
89 "errors"
910 "fmt"
1011 "io"
1112 "log/slog"
13+ "mime"
1214 "net/http"
1315 "net/url"
1416 "os"
17+ "regexp"
1518 "runtime"
1619 "strings"
1720
1821 "go.opentelemetry.io/otel/attribute"
1922 "go.opentelemetry.io/otel/codes"
23+ semconv "go.opentelemetry.io/otel/semconv/v1.41.0"
2024 "go.opentelemetry.io/otel/trace"
2125 "golang.org/x/sync/errgroup"
2226
2327 "github.com/quay/claircore"
2428 "github.com/quay/claircore/indexer"
2529 "github.com/quay/claircore/internal/cache"
30+ "github.com/quay/claircore/internal/httpreader"
2631 "github.com/quay/claircore/internal/httputil"
2732 "github.com/quay/claircore/internal/wart"
2833 "github.com/quay/claircore/internal/zreader"
@@ -131,40 +136,85 @@ func (a *RemoteFetchArena) fetchInto(ctx context.Context, l *claircore.Layer, cl
131136 // cleanup logic associated with the pointer.
132137 //
133138 // Every new [*claircore.Layer] gets its own file descriptor via the
134- // [reopen] helper.
139+ // [reopen] helper, or eschews using a file at all .
135140 var spool * os.File
141+ var forceFetch bool
142+ var d details
143+ Fetch:
136144 spool , err = a .files .Get (ctx , key , func (ctx context.Context , _ string ) (* os.File , error ) {
137145 cacheHit = false
138- return a .fetchFileForCache (ctx , desc )
146+ d , err = a .inspect (ctx , desc )
147+ if err != nil {
148+ return nil , err
149+ }
150+ if ! forceFetch && d .Uncompressed && d .RangeOK {
151+ return nil , errUseHTTPReader
152+ }
153+ return a .fetchFileForCache (ctx , desc , d )
139154 })
140- if err != nil {
141- return err
142- }
143- // This is an owned, independent descriptor for the passed [*os.File].
144- f , err : = reopen (a .root , spool )
145- if err != nil {
146- return err
147- }
155+ switch err {
156+ case nil : // Reopen the returned [*os.File].
157+ var f * os. File
158+ // This is an owned, independent descriptor for the passed [*os.File].
159+ f , err = reopen (a .root , spool )
160+ if err != nil {
161+ return err
162+ }
148163
149- // If this succeeds, "f" is now owned by "l"
150- if err := l .Init (ctx , desc , f ); err != nil {
151- return errors .Join (err , f .Close ())
152- }
153- * cl = closeFunc (func () (err error ) {
154- err = errors .Join (l .Close (), f .Close ())
155- // Using this KeepAlive keeps the cached file descriptor live until
156- // all users of the blob have cleaned up. This should be after "f"
157- // is closed so that the cached-owned file descriptor outlives any
158- // reopened copies. There's no explicit association of these file
159- // descriptors, it's all kernel-side book-keeping.
160- runtime .KeepAlive (spool )
164+ // If this succeeds, "f" is now owned by "l"
165+ if err = l .Init (ctx , desc , f ); err != nil {
166+ return errors .Join (err , f .Close ())
167+ }
168+ * cl = closeFunc (func () (err error ) {
169+ err = errors .Join (l .Close (), f .Close ())
170+ // Using this KeepAlive keeps the cached file descriptor live until
171+ // all users of the blob have cleaned up. This should be after "f"
172+ // is closed so that the cached-owned file descriptor outlives any
173+ // reopened copies. There's no explicit association of these file
174+ // descriptors, it's all kernel-side book-keeping.
175+ runtime .KeepAlive (spool )
176+ return err
177+ })
178+ case errUseHTTPReader : // Try to construct an [httpreader.Reader].
179+ var opts []httpreader.Option
180+ if d .ContentLength > 0 {
181+ opts = append (opts , httpreader .WithSize (d .ContentLength ))
182+ }
183+ if len (desc .Headers ) != 0 {
184+ opts = append (opts , httpreader .WithHeaders (desc .Headers ))
185+ }
186+ var rd * httpreader.Reader
187+ rd , err = httpreader .New (ctx , a .wc , desc .URI , opts ... )
188+ switch {
189+ case err == nil :
190+ if err = l .Init (ctx , desc , rd ); err != nil {
191+ return errors .Join (err , rd .Close ())
192+ }
193+ * cl = closeFunc (func () (err error ) {
194+ err = errors .Join (l .Close (), rd .Close ())
195+ return err
196+ })
197+ a .logger (desc ).DebugContext (ctx , "using httpreader" )
198+ return nil
199+ case errors .Is (err , errors .ErrUnsupported ):
200+ err = nil
201+ forceFetch = true
202+ goto Fetch
203+ default :
204+ return err
205+ }
206+ default :
161207 return err
162- })
208+ }
163209
164210 return nil
165211 }
166212}
167213
214+ // ErrUseHTTPReader is a sentinel value to signal that the calling code should
215+ // construct an [httpreader.Reader] rather than reopen the [os.File].
216+ var errUseHTTPReader = errors .New ("use htttpreader.Reader" )
217+
168218// CloseFunc is an adapter in the vein of [http.HandlerFunc].
169219type closeFunc func () error
170220
@@ -173,12 +223,110 @@ func (f closeFunc) Close() error {
173223 return f ()
174224}
175225
226+ // Inspect makes a request to the layer's URI and examines the response for
227+ // useful information.
228+ func (a * RemoteFetchArena ) inspect (ctx context.Context , desc * claircore.LayerDescription ) (d details , err error ) {
229+ ctx , span := tracer .Start (ctx , "RemoteFetchArena.inspect" )
230+ defer func () {
231+ a .logger (desc ).DebugContext (ctx , "inspected resource" , "ok" , err == nil , "details" , & d )
232+ span .RecordError (err )
233+ span .End ()
234+ }()
235+ span .SetStatus (codes .Error , "" )
236+
237+ var req * http.Request
238+ var res * http.Response
239+ req , err = http .NewRequestWithContext (ctx , http .MethodGet , desc .URI , nil )
240+ if err != nil {
241+ return d , fmt .Errorf ("fetcher: failed to construct request: %w" , err )
242+ }
243+ req .Header = http .Header (desc .Headers ).Clone ()
244+ if req .Header == nil {
245+ req .Header = make (http.Header )
246+ }
247+ req .Header .Set (`claircore-reason` , `inspect` )
248+ req .Header .Set (`range` , `bytes=0-15` )
249+ res , err = a .wc .Do (req )
250+ if err != nil {
251+ return d , fmt .Errorf ("fetcher: request failed: %w" , err )
252+ }
253+ err = httputil .CheckResponse (res , http .StatusOK , http .StatusPartialContent )
254+ if err != nil {
255+ return d , fmt .Errorf ("fetcher: %w" , err )
256+ }
257+ head := make ([]byte , 16 )
258+ _ , err = io .ReadFull (res .Body , head )
259+ _ = res .Body .Close ()
260+ if err != nil {
261+ return d , fmt .Errorf ("fetcher: unexpected read: %w" , err )
262+ }
263+
264+ const (
265+ ctKey = `content-type`
266+ arKey = `accept-ranges`
267+ )
268+ ctVal := cmp .Or (res .Header .Get (ctKey ), `application/octet-stream` )
269+ ct , _ , err := mime .ParseMediaType (ctVal )
270+ if err != nil {
271+ return d , fmt .Errorf ("fetcher: %w" , err )
272+ }
273+ span .SetAttributes (
274+ semconv .HTTPResponseStatusCode (res .StatusCode ),
275+ semconv .HTTPResponseBodySize (int (res .ContentLength )),
276+ semconv .HTTPResponseHeader (ctKey , ct ),
277+ semconv .HTTPResponseHeader (arKey , res .Header .Get (arKey )),
278+ )
279+ if isOctetStream (ct ) && zreader .DetectCompression (head ) == zreader .KindNone {
280+ ct = `application/x-tar`
281+ }
282+ switch res .StatusCode {
283+ case http .StatusOK :
284+ d .ContentLength = res .ContentLength
285+ case http .StatusPartialContent :
286+ var cr httpreader.ContentRange
287+ if err := cr .Parse (res .Header .Get (`content-range` )); err == nil {
288+ d .ContentLength = cr .Length
289+ }
290+ }
291+ d .Uncompressed = ct == "application/x-tar" || strings .HasSuffix (ct , ".tar" )
292+ d .RangeOK = res .Header .Get (arKey ) == `bytes` || res .StatusCode == http .StatusPartialContent
293+
294+ span .SetStatus (codes .Ok , "" )
295+ return d , nil
296+ }
297+
298+ var _ slog.LogValuer = (* details )(nil )
299+
300+ // Details is the useful information reported by [RemoteFetchArena.inspect].
301+ type details struct {
302+ // The content length, if known.
303+ ContentLength int64
304+ // The resource reports to be known non-compressed contents.
305+ Uncompressed bool
306+ // The server reports supporting "bytes" via the "Accept-Ranges" header.
307+ RangeOK bool
308+ }
309+
310+ // LogValue implements [slog.LogValuer].
311+ func (d * details ) LogValue () slog.Value {
312+ return slog .GroupValue (
313+ slog .Int64 ("content-length" , d .ContentLength ),
314+ slog .Bool ("uncompressed" , d .Uncompressed ),
315+ slog .Bool ("range-ok" , d .RangeOK ),
316+ )
317+ }
318+
319+ // Logger returns a [*slog.Logger] with predefined attributes.
320+ func (a * RemoteFetchArena ) logger (desc * claircore.LayerDescription ) * slog.Logger {
321+ return slog .With ("arena" , a .root .Name (), "layer" , desc .Digest , "uri" , desc .URI )
322+ }
323+
176324// FetchFileForCache is the inner function used inside the [cache.Live].
177325//
178326// Because we know we're the only concurrent call that's dealing with this blob,
179327// 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 )
328+ func (a * RemoteFetchArena ) fetchFileForCache (ctx context.Context , desc * claircore.LayerDescription , _ details ) (* os.File , error ) {
329+ log := a . logger ( desc )
182330 ctx , span := tracer .Start (ctx , "RemoteFetchArena.fetchFileForCache" )
183331 defer span .End ()
184332 span .SetStatus (codes .Error , "" )
@@ -210,6 +358,7 @@ func (a *RemoteFetchArena) fetchFileForCache(ctx context.Context, desc *claircor
210358 URL : url ,
211359 Header : http .Header (desc .Headers ).Clone (),
212360 }).WithContext (ctx )
361+ req .Header .Set (`claircore-reason` , `fetch` )
213362 resp , err := a .wc .Do (req )
214363 if err != nil {
215364 return nil , fmt .Errorf ("fetcher: request failed: %w" , err )
@@ -235,10 +384,13 @@ func (a *RemoteFetchArena) fetchFileForCache(ctx context.Context, desc *claircor
235384 }
236385 defer zr .Close ()
237386 // Look at the content-type and optionally fix it up.
238- ct := resp .Header .Get ("content-type" )
387+ ct , _ , err := mime .ParseMediaType (resp .Header .Get ("content-type" ))
388+ if err != nil {
389+ return nil , fmt .Errorf ("fetcher: %w" , err )
390+ }
239391 log .DebugContext (ctx , "reported content-type" , "content-type" , ct )
240392 span .SetAttributes (payloadType (ct ), payloadCompression (kind ))
241- if ct == "" || ct == "text/plain" || ct == "binary/octet-stream" || ct == "application/octet-stream" {
393+ if ct == "" || ct == "text/plain" || isOctetStream ( ct ) {
242394 switch kind {
243395 case zreader .KindGzip :
244396 ct = "application/gzip"
@@ -420,3 +572,14 @@ func (p *FetchProxy) Close() error {
420572 }
421573 return errors .Join (errs ... )
422574}
575+
576+ // IsOctetStream tests if the incoming media type string is an "octet-stream"
577+ // type.
578+ //
579+ // Reports false for strings that are not of the form "<type>/<subtype>". For
580+ // interoperability purposes, the nonstandard "binary" type is accepted.
581+ func isOctetStream (ct string ) bool {
582+ return reOctetStream .MatchString (ct )
583+ }
584+
585+ var reOctetStream = regexp .MustCompile (`^(application|binary)/octet-stream$` )
0 commit comments