Skip to content

Commit 719faf9

Browse files
committed
libindex: wire in httpreader.Reader
This handles the "easy" case of simply proxying reads for uncompressed tar archives to range requests. Future improvements would move the "spooling" out of this package and into the `fs.FS` implementation. Signed-off-by: Hank Donnay <hdonnay@redhat.com> Change-Id: I9d200dd841954054df0b187b9fd160a56a6a6964
1 parent f8f8a59 commit 719faf9

2 files changed

Lines changed: 324 additions & 130 deletions

File tree

libindex/fetcher.go

Lines changed: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)