Skip to content

Commit 49dfb01

Browse files
committed
httptransport: accept OCI manifests for indexing
Signed-off-by: Hank Donnay <[email protected]>
1 parent 45fae9f commit 49dfb01

File tree

4 files changed

+323
-9
lines changed

4 files changed

+323
-9
lines changed

go.mod

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ require (
1212
github.com/klauspost/compress v1.13.6
1313
github.com/ldelossa/responserecorder v1.0.2-0.20210711162258-40bec93a9325
1414
github.com/mattn/go-sqlite3 v1.11.0 // indirect
15+
github.com/opencontainers/go-digest v1.0.0
16+
github.com/opencontainers/image-spec v1.0.2
1517
github.com/prometheus/client_golang v1.9.0
1618
github.com/quay/clair/config v1.0.0
1719
github.com/quay/claircore v1.2.0

go.sum

+2-4
Original file line numberDiff line numberDiff line change
@@ -720,8 +720,9 @@ github.com/opencontainers/go-digest v1.0.0-rc1.0.20180430190053-c9281466c8b2/go.
720720
github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U=
721721
github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM=
722722
github.com/opencontainers/image-spec v1.0.0/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
723-
github.com/opencontainers/image-spec v1.0.1 h1:JMemWkRwHx4Zj+fVxWoMCFm/8sYGGrUVojFA6h/TRcI=
724723
github.com/opencontainers/image-spec v1.0.1/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
724+
github.com/opencontainers/image-spec v1.0.2 h1:9yCKha/T5XdGtO0q9Q9a6T5NUCsTn/DrBg0D7ufOcFM=
725+
github.com/opencontainers/image-spec v1.0.2/go.mod h1:BtxoFyWECRxE4U/7sNtV5W15zMzWCbyJoFRP3s7yZA0=
725726
github.com/opencontainers/runc v0.0.0-20190115041553-12f6a991201f/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
726727
github.com/opencontainers/runc v0.1.1/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
727728
github.com/opencontainers/runc v1.0.0-rc8.0.20190926000215-3e425f80a8c9/go.mod h1:qT5XzbpPznkRYVz/mWwUaVBUv2rmF59PVA73FjuZG0U=
@@ -922,7 +923,6 @@ github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijb
922923
github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
923924
github.com/urfave/cli v1.22.2 h1:gsqYFH8bb9ekPA12kRo0hfjngWQjkJPlN9R0N78BoUo=
924925
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
925-
github.com/urfave/cli v1.22.2/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0=
926926
github.com/urfave/cli/v2 v2.3.0 h1:qph92Y649prgesehzOrQjdWyxFOp/QVM+6imKHad91M=
927927
github.com/urfave/cli/v2 v2.3.0/go.mod h1:LJmUH05zAU44vOAcrfzZQKsZbVcdbOG8rtL3/XcUArI=
928928
github.com/vishvananda/netlink v0.0.0-20181108222139-023a6dafdcdf/go.mod h1:+SR5DhBJrl6ZM7CoCKvpw5BKroDKQ+PJqOg65H/2ktk=
@@ -1106,7 +1106,6 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd
11061106
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
11071107
golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
11081108
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
1109-
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
11101109
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d h1:20cMwl2fHAzkJMEA+8J4JgqBQcQGzbisXo31MIeenXI=
11111110
golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
11121111
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -1317,7 +1316,6 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
13171316
golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0=
13181317
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
13191318
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
1320-
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
13211319
golang.org/x/tools v0.1.7 h1:6j8CgantCy3yc8JGBqkDLMKWqZ0RDU2g1HVgacojGWQ=
13221320
golang.org/x/tools v0.1.7/go.mod h1:LGqMHiF4EqQNHR1JncWGqT5BVaXmza+X+BDGol+dOxo=
13231321
golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=

httptransport/indexer_v1.go

+88-5
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)