diff --git a/go.mod b/go.mod index d0979a12d..ce0a9723a 100644 --- a/go.mod +++ b/go.mod @@ -27,6 +27,7 @@ require ( github.com/jmoiron/sqlx v0.0.0-20190319043955-cdf62fdf55f6 github.com/mattn/go-sqlite3 v1.14.0 github.com/opencontainers/go-digest v1.0.0 + github.com/opencontainers/image-spec v1.0.2 github.com/pressly/goose v2.6.0+incompatible github.com/satori/go.uuid v1.2.0 github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72 @@ -88,7 +89,6 @@ require ( github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect - github.com/opencontainers/image-spec v1.0.2 // indirect github.com/opencontainers/runc v1.0.2 // indirect github.com/opencontainers/runtime-spec v1.0.3-0.20210326190908-1c3f411f0417 // indirect github.com/pkg/errors v0.9.1 // indirect diff --git a/proxy/proxyserver/preheat.go b/proxy/proxyserver/preheat.go index 72cfbf839..a62eb76c7 100644 --- a/proxy/proxyserver/preheat.go +++ b/proxy/proxyserver/preheat.go @@ -32,7 +32,11 @@ import ( "github.com/uber/kraken/utils/log" ) -var _manifestRegexp = regexp.MustCompile(`^application/vnd.docker.distribution.manifest.v\d\+(json|prettyjws)`) +var _manifestRegexp = regexp.MustCompile( + `^application/vnd\.docker\.distribution\.manifest\.v\d\+(json|prettyjws)` + + `|^application/vnd\.oci\.image\.manifest\.v1\+json` + + `|^application/vnd\.oci\.image\.index\.v1\+json`, +) // PreheatHandler defines the handler of preheat. type PreheatHandler struct { diff --git a/utils/dockerutil/dockerutil.go b/utils/dockerutil/dockerutil.go index 8d0f386bc..1f2ee1501 100644 --- a/utils/dockerutil/dockerutil.go +++ b/utils/dockerutil/dockerutil.go @@ -20,7 +20,9 @@ import ( "github.com/docker/distribution" "github.com/docker/distribution/manifest/manifestlist" + "github.com/docker/distribution/manifest/ocischema" "github.com/docker/distribution/manifest/schema2" + specs "github.com/opencontainers/image-spec/specs-go/v1" "github.com/uber/kraken/core" ) @@ -35,13 +37,26 @@ func ParseManifest(r io.Reader) (distribution.Manifest, core.Digest, error) { return nil, core.Digest{}, fmt.Errorf("read: %s", err) } - manifest, d, err := ParseManifestV2(b) - if err == nil { - return manifest, d, err + type attempt struct { + name string + fn func([]byte) (distribution.Manifest, core.Digest, error) + } + attempts := []attempt{ + {"docker v2 manifest", ParseManifestV2}, + {"docker v2 manifest list", ParseManifestV2List}, + {"OCI image manifest", ParseManifestOCI}, + {"OCI image index", ParseManifestOCIIndex}, } - // Retry with v2 manifest list. - return ParseManifestV2List(b) + var errs []string + for _, a := range attempts { + m, d, err := a.fn(b) + if err == nil { + return m, d, nil + } + errs = append(errs, fmt.Sprintf("%s: %s", a.name, err)) + } + return nil, core.Digest{}, fmt.Errorf("unrecognized manifest format: [%s]", strings.Join(errs, "; ")) } // ParseManifestV2 returns a parsed v2 manifest and its digest. @@ -71,13 +86,13 @@ func ParseManifestV2List(bytes []byte) (distribution.Manifest, core.Digest, erro if err != nil { return nil, core.Digest{}, fmt.Errorf("unmarshal manifestlist: %s", err) } - deserializedManifestList, ok := manifestList.(*manifestlist.DeserializedManifestList) + deserializedManifestIndex, ok := manifestIndex.(*manifestlist.DeserializedManifestList) if !ok { - return nil, core.Digest{}, errors.New("expected manifestlist.DeserializedManifestList") + return nil, core.Digest{}, fmt.Errorf("expected OCI image index, got %T", manifestIndex) } - version := deserializedManifestList.SchemaVersion + version := deserializedManifestIndex.SchemaVersion if version != 2 { - return nil, core.Digest{}, fmt.Errorf("unsupported manifest list version: %d", version) + return nil, core.Digest{}, fmt.Errorf("unsupported OCI image index version: %d", version) } d, err := core.ParseSHA256Digest(string(desc.Digest)) if err != nil { @@ -86,6 +101,40 @@ func ParseManifestV2List(bytes []byte) (distribution.Manifest, core.Digest, erro return manifestList, d, nil } +// ParseManifestOCI returns a parsed OCI image manifest and its digest. +func ParseManifestOCI(bytes []byte) (distribution.Manifest, core.Digest, error) { + manifest, desc, err := distribution.UnmarshalManifest(specs.MediaTypeImageManifest, bytes) + if err != nil { + return nil, core.Digest{}, fmt.Errorf("unmarshal OCI manifest: %s", err) + } + _, ok := manifest.(*ocischema.DeserializedManifest) + if !ok { + return nil, core.Digest{}, errors.New("expected ocischema.DeserializedManifest") + } + d, err := core.ParseSHA256Digest(string(desc.Digest)) + if err != nil { + return nil, core.Digest{}, fmt.Errorf("parse digest: %s", err) + } + return manifest, d, nil +} + +// ParseManifestOCIIndex returns a parsed OCI image index and its digest. +func ParseManifestOCIIndex(bytes []byte) (distribution.Manifest, core.Digest, error) { + manifestIndex, desc, err := distribution.UnmarshalManifest(specs.MediaTypeImageIndex, bytes) + if err != nil { + return nil, core.Digest{}, fmt.Errorf("unmarshal OCI image index: %s", err) + } + _, ok := manifestIndex.(*manifestlist.DeserializedManifestList) + if !ok { + return nil, core.Digest{}, errors.New("expected manifestlist.DeserializedManifestList for OCI index") + } + d, err := core.ParseSHA256Digest(string(desc.Digest)) + if err != nil { + return nil, core.Digest{}, fmt.Errorf("parse digest: %s", err) + } + return manifestIndex, d, nil +} + // GetManifestReferences returns a list of references by a V2 manifest func GetManifestReferences(manifest distribution.Manifest) ([]core.Digest, error) { var refs []core.Digest @@ -100,5 +149,10 @@ func GetManifestReferences(manifest distribution.Manifest) ([]core.Digest, error } func GetSupportedManifestTypes() string { - return fmt.Sprintf("%s,%s", _v2ManifestType, _v2ManifestListType) -} + return fmt.Sprintf("%s,%s,%s,%s", + _v2ManifestType, + _v2ManifestListType, + specs.MediaTypeImageManifest, + specs.MediaTypeImageIndex, + ) +} \ No newline at end of file diff --git a/utils/dockerutil/dockerutil_test.go b/utils/dockerutil/dockerutil_test.go index 199d0accd..e2913d761 100644 --- a/utils/dockerutil/dockerutil_test.go +++ b/utils/dockerutil/dockerutil_test.go @@ -1,6 +1,7 @@ package dockerutil_test import ( + "bytes" "testing" "github.com/docker/distribution/manifest/manifestlist" @@ -88,3 +89,82 @@ func TestParseManifestV2List(t *testing.T) { }) } } + +var testOCIManifestBytes = []byte(`{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { + "mediaType": "application/vnd.oci.image.config.v1+json", + "size": 985, + "digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b" + }, + "layers": [ + { + "mediaType": "application/vnd.oci.image.layer.v1.tar+gzip", + "size": 153263, + "digest": "sha256:62d8908bee94c202b2d35224a221aaa2058318bfa9879fa541efaecba272331b" + } + ] + }`) + +var testOCIIndexBytes = []byte(`{ + "schemaVersion": 2, + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "size": 985, + "digest": "sha256:1a9ec845ee94c202b2d5da74a24f0ed2058318bfa9879fa541efaecba272e86b", + "platform": { + "architecture": "amd64", + "os": "linux" + } + } + ] + }`) + +func TestParseManifestOCI(t *testing.T) { + require := require.New(t) + + // Success case + manifest, d, err := dockerutil.ParseManifestOCI(testOCIManifestBytes) + require.NoError(err) + mediaType, _, err := manifest.Payload() + require.NoError(err) + require.Equal("application/vnd.oci.image.manifest.v1+json", mediaType) + require.Equal("sha256", d.Algo()) + require.NotEmpty(d.Hex()) + + // Failure case: passing a Docker manifest should fail + _, _, err = dockerutil.ParseManifestOCI(testManifestBytes) + require.Error(err) +} + +func TestParseManifestOCIIndex(t *testing.T) { + require := require.New(t) + + // Success case + manifest, d, err := dockerutil.ParseManifestOCIIndex(testOCIIndexBytes) + require.NoError(err) + mediaType, _, err := manifest.Payload() + require.NoError(err) + require.Equal("application/vnd.oci.image.index.v1+json", mediaType) + require.Equal("sha256", d.Algo()) + require.NotEmpty(d.Hex()) + + // Failure case: passing a Docker manifest should fail + _, _, err = dockerutil.ParseManifestOCIIndex(testManifestBytes) + require.Error(err) +} + +func TestParseManifestOCIViaParseManifest(t *testing.T) { + require := require.New(t) + + _, d, err := dockerutil.ParseManifest(bytes.NewReader(testOCIManifestBytes)) + require.NoError(err) + require.Equal("sha256", d.Algo()) + + _, d, err = dockerutil.ParseManifest(bytes.NewReader(testOCIIndexBytes)) + require.NoError(err) + require.Equal("sha256", d.Algo()) +}