@@ -6,9 +6,11 @@ import (
6
6
"fmt"
7
7
"net/http"
8
8
"path"
9
+ "strings"
9
10
"time"
10
11
11
12
"github.com/ldelossa/responserecorder"
13
+ oci "github.com/opencontainers/image-spec/specs-go/v1"
12
14
"github.com/prometheus/client_golang/prometheus"
13
15
"github.com/quay/claircore"
14
16
"github.com/quay/zlog"
@@ -90,8 +92,8 @@ func (h *IndexerV1) indexReport(w http.ResponseWriter, r *http.Request) {
90
92
apiError (w , http .StatusInternalServerError , "could not retrieve indexer state: %v" , err )
91
93
return
92
94
}
93
- var m claircore. Manifest
94
- if err := dec . Decode ( & m ); err != nil {
95
+ m , err := decodeManifest ( ctx , r , dec )
96
+ if err != nil {
95
97
apiError (w , http .StatusBadRequest , "failed to deserialize manifest: %v" , err )
96
98
return
97
99
}
@@ -109,9 +111,9 @@ func (h *IndexerV1) indexReport(w http.ResponseWriter, r *http.Request) {
109
111
return
110
112
}
111
113
112
- // TODO Do we need some sort of background context embedded in the HTTP
113
- // struct?
114
- report , err := h .srv .Index (ctx , & m )
114
+ // TODO(hank) We should switch on the content-type header and not send
115
+ // back the report if we've received an OCI manifest.
116
+ report , err := h .srv .Index (ctx , m )
115
117
if err != nil {
116
118
apiError (w , http .StatusInternalServerError , "failed to start scan: %v" , err )
117
119
return
@@ -342,3 +344,84 @@ var indexerv1wrapper = &wrapper{
342
344
[]string {"handler" },
343
345
),
344
346
}
347
+
348
+ const (
349
+ // Known manifest types we ingest.
350
+ typeOCIManifest = oci .MediaTypeImageManifest
351
+ typeNativeManifest = `application/vnd.projectquay.clair.mainfest.v1+json`
352
+ )
353
+
354
+ // DecodeManifest switches on the Request's Content-Type to consume the body.
355
+ //
356
+ // Defaults to expecting a native Claircore Manifest.
357
+ func decodeManifest (ctx context.Context , r * http.Request , dec * codec.Decoder ) (* claircore.Manifest , error ) {
358
+ var m claircore.Manifest
359
+
360
+ t := r .Header .Get ("content-type" )
361
+ if i := strings .IndexByte (t , ';' ); i != - 1 {
362
+ t = strings .TrimSpace (t [:i ])
363
+ }
364
+ switch t {
365
+ case typeOCIManifest :
366
+ var om oci.Manifest
367
+ if err := dec .Decode (& om ); err != nil {
368
+ return nil , err
369
+ }
370
+ if err := nativeFromOCI (& m , & om ); err != nil {
371
+ return nil , err
372
+ }
373
+ case typeNativeManifest , "application/json" , "" :
374
+ if err := dec .Decode (& m ); err != nil {
375
+ return nil , err
376
+ }
377
+ default :
378
+ return nil , fmt .Errorf ("unknown content-type %q" , t )
379
+ }
380
+ return & m , nil
381
+ }
382
+
383
+ // These are the layer types we accept inside an OCI Manifest.
384
+ var ociLayerTypes = map [string ]struct {}{
385
+ oci .MediaTypeImageLayer : {},
386
+ oci .MediaTypeImageLayerGzip : {},
387
+ oci .MediaTypeImageLayer + "+zstd" : {}, // The specs package doesn't have zstd, oddly.
388
+ }
389
+
390
+ // NativeFromOCI populates the Manifest from the OCI Manifest, reporting an
391
+ // error if something is invalid.
392
+ func nativeFromOCI (m * claircore.Manifest , o * oci.Manifest ) error {
393
+ const header = `header:`
394
+ var err error
395
+
396
+ m .Hash , err = claircore .ParseDigest (o .Config .Digest .String ())
397
+ if err != nil {
398
+ return fmt .Errorf ("unable to parse manifest digest %q: %w" , o .Config .Digest , err )
399
+ }
400
+
401
+ for _ , u := range o .Layers {
402
+ if len (u .URLs ) == 0 {
403
+ // Manifest is missing URLs.
404
+ // They're optional in the spec, but we need them for obvious reasons.
405
+ return fmt .Errorf ("missing URLs for layer %q" , u .Digest )
406
+ }
407
+ if _ , ok := ociLayerTypes [u .MediaType ]; ! ok {
408
+ return fmt .Errorf ("invalid media type for layer %q" , u .Digest )
409
+ }
410
+ l := claircore.Layer {
411
+ URI : u .URLs [0 ],
412
+ }
413
+ l .Hash , err = claircore .ParseDigest (u .Digest .String ())
414
+ if err != nil {
415
+ return fmt .Errorf ("unable to parse layer digest %q: %w" , u .Digest , err )
416
+ }
417
+ for k , v := range u .Annotations {
418
+ if ! strings .HasPrefix (k , header ) {
419
+ continue
420
+ }
421
+ l .Headers [strings .TrimPrefix (k , header )] = []string {v }
422
+ }
423
+ m .Layers = append (m .Layers , & l )
424
+ }
425
+
426
+ return nil
427
+ }
0 commit comments