-
Notifications
You must be signed in to change notification settings - Fork 468
Add support for OCI image manifests and indexes #584
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from 1 commit
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -20,7 +20,9 @@ | |||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| "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 @@ | |||||||||||||||||||||||||
| return nil, core.Digest{}, fmt.Errorf("read: %s", err) | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Try Docker v2 manifest. | ||||||||||||||||||||||||||
| manifest, d, err := ParseManifestV2(b) | ||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||
| return manifest, d, err | ||||||||||||||||||||||||||
| return manifest, d, nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Retry with v2 manifest list. | ||||||||||||||||||||||||||
| return ParseManifestV2List(b) | ||||||||||||||||||||||||||
| // Try Docker v2 manifest list. | ||||||||||||||||||||||||||
| manifest, d, err = ParseManifestV2List(b) | ||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||
| return manifest, d, nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Try OCI image manifest. | ||||||||||||||||||||||||||
| manifest, d, err = ParseManifestOCI(b) | ||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||
| return manifest, d, nil | ||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // Try OCI image index. | ||||||||||||||||||||||||||
| return ParseManifestOCIIndex(b) | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||
| // ParseManifestV2 returns a parsed v2 manifest and its digest. | ||||||||||||||||||||||||||
|
|
@@ -86,6 +101,40 @@ | |||||||||||||||||||||||||
| 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") | ||||||||||||||||||||||||||
|
||||||||||||||||||||||||||
| return nil, core.Digest{}, errors.New("expected manifestlist.DeserializedManifestList for OCI index") | |
| return nil, core.Digest{}, fmt.Errorf("expected OCI image index, got %T", manifestIndex) |
Copilot
AI
Apr 8, 2026
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
ParseManifestOCIIndex asserts the returned type but does not validate SchemaVersion (unlike ParseManifestV2List, which checks SchemaVersion==2 on the same DeserializedManifestList type). For consistency and to avoid accepting unsupported index versions, store the cast result and validate SchemaVersion before returning.
| _, ok := manifestIndex.(*manifestlist.DeserializedManifestList) | |
| if !ok { | |
| return nil, core.Digest{}, errors.New("expected manifestlist.DeserializedManifestList for OCI index") | |
| } | |
| deserializedManifestIndex, ok := manifestIndex.(*manifestlist.DeserializedManifestList) | |
| if !ok { | |
| return nil, core.Digest{}, errors.New("expected manifestlist.DeserializedManifestList for OCI index") | |
| } | |
| version := deserializedManifestIndex.SchemaVersion | |
| if version != 2 { | |
| return nil, core.Digest{}, fmt.Errorf("unsupported OCI image index version: %d", version) | |
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,6 +1,7 @@ | ||
| package dockerutil_test | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "testing" | ||
|
|
||
| "github.com/docker/distribution/manifest/manifestlist" | ||
|
|
@@ -88,3 +89,68 @@ 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) | ||
| 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) | ||
|
Comment on lines
+130
to
+134
|
||
| require.Equal("sha256", d.Algo()) | ||
| } | ||
|
|
||
| func TestParseManifestOCIIndex(t *testing.T) { | ||
| require := require.New(t) | ||
| 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) | ||
|
Comment on lines
+147
to
+151
|
||
| require.Equal("sha256", d.Algo()) | ||
| } | ||
|
|
||
| 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()) | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Adding OCI index media types to the preheat event filter means Kraken will start processing push events whose manifest.References() typically include only child manifests (not layer/config blobs). With the current process() implementation, this can result in preheating only nested manifests rather than the actual layers unless additional manifest push events are also received. Consider either keeping index/list types out of this filter, or recursively fetching referenced manifests and preheating their references to ensure layers are warmed.