@@ -6,9 +6,11 @@ import (
66 "fmt"
77 "net/http"
88 "path"
9+ "strings"
910 "time"
1011
1112 "github.com/ldelossa/responserecorder"
13+ oci "github.com/opencontainers/image-spec/specs-go/v1"
1214 "github.com/prometheus/client_golang/prometheus"
1315 "github.com/quay/claircore"
1416 "github.com/quay/zlog"
@@ -90,8 +92,8 @@ func (h *IndexerV1) indexReport(w http.ResponseWriter, r *http.Request) {
9092 apiError (w , http .StatusInternalServerError , "could not retrieve indexer state: %v" , err )
9193 return
9294 }
93- var m claircore. Manifest
94- if err := dec . Decode ( & m ); err != nil {
95+ m , err := decodeManifest ( ctx , r , dec )
96+ if err != nil {
9597 apiError (w , http .StatusBadRequest , "failed to deserialize manifest: %v" , err )
9698 return
9799 }
@@ -109,9 +111,9 @@ func (h *IndexerV1) indexReport(w http.ResponseWriter, r *http.Request) {
109111 return
110112 }
111113
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 )
115117 if err != nil {
116118 apiError (w , http .StatusInternalServerError , "failed to start scan: %v" , err )
117119 return
@@ -342,3 +344,84 @@ var indexerv1wrapper = &wrapper{
342344 []string {"handler" },
343345 ),
344346}
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