-
Notifications
You must be signed in to change notification settings - Fork 468
fix: parsing of the OCI manifests #569
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 3 commits
d388e0e
fcfeb11
a7d0b46
53c4578
9d1d258
1906201
3693cda
316dfdc
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 | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -14,19 +14,22 @@ | |||||||||||||||||||||||||||||||||
| package dockerutil | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| import ( | ||||||||||||||||||||||||||||||||||
| "encoding/json" | ||||||||||||||||||||||||||||||||||
| "errors" | ||||||||||||||||||||||||||||||||||
| "fmt" | ||||||||||||||||||||||||||||||||||
| "io" | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| "github.com/docker/distribution" | ||||||||||||||||||||||||||||||||||
| "github.com/docker/distribution/manifest/manifestlist" | ||||||||||||||||||||||||||||||||||
| "github.com/docker/distribution/manifest/schema2" | ||||||||||||||||||||||||||||||||||
| "github.com/opencontainers/go-digest" | ||||||||||||||||||||||||||||||||||
| "github.com/uber/kraken/core" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| const ( | ||||||||||||||||||||||||||||||||||
| _v2ManifestType = "application/vnd.docker.distribution.manifest.v2+json" | ||||||||||||||||||||||||||||||||||
| _v2ManifestListType = "application/vnd.docker.distribution.manifest.list.v2+json" | ||||||||||||||||||||||||||||||||||
| _ociManifestType = "application/vnd.oci.image.manifest.v1+json" | ||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| func ParseManifest(r io.Reader) (distribution.Manifest, core.Digest, error) { | ||||||||||||||||||||||||||||||||||
|
|
@@ -35,13 +38,20 @@ func ParseManifest(r io.Reader) (distribution.Manifest, core.Digest, error) { | |||||||||||||||||||||||||||||||||
| return nil, core.Digest{}, fmt.Errorf("read: %s", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Try Docker v2 manifest first | ||||||||||||||||||||||||||||||||||
| manifest, d, err := ParseManifestV2(b) | ||||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||||
| return manifest, d, err | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Retry with v2 manifest list. | ||||||||||||||||||||||||||||||||||
| return ParseManifestV2List(b) | ||||||||||||||||||||||||||||||||||
| // Try Docker v2 manifest list | ||||||||||||||||||||||||||||||||||
| manifest, d, err = ParseManifestV2List(b) | ||||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||||
| return manifest, d, err | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // Try OCI manifest v1 (same structure as Docker v2) | ||||||||||||||||||||||||||||||||||
| return ParseOCIManifest(b) | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+54
to
+55
|
||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ParseManifestV2 returns a parsed v2 manifest and its digest. | ||||||||||||||||||||||||||||||||||
|
|
@@ -99,6 +109,85 @@ func GetManifestReferences(manifest distribution.Manifest) ([]core.Digest, error | |||||||||||||||||||||||||||||||||
| return refs, nil | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| // ParseOCIManifest parses an OCI image manifest v1. | ||||||||||||||||||||||||||||||||||
| // OCI manifests have the same structure as Docker v2 manifests, so we can parse them similarly. | ||||||||||||||||||||||||||||||||||
| func ParseOCIManifest(bytes []byte) (distribution.Manifest, core.Digest, error) { | ||||||||||||||||||||||||||||||||||
|
Comment on lines
+124
to
+126
|
||||||||||||||||||||||||||||||||||
| // First, try to parse as Docker v2 manifest (they have the same structure) | ||||||||||||||||||||||||||||||||||
| // The Docker Distribution library might accept it if we use schema2.MediaTypeManifest | ||||||||||||||||||||||||||||||||||
| manifest, desc, err := distribution.UnmarshalManifest(schema2.MediaTypeManifest, bytes) | ||||||||||||||||||||||||||||||||||
| if err == nil { | ||||||||||||||||||||||||||||||||||
| // Verify it's actually a schema2 manifest | ||||||||||||||||||||||||||||||||||
| if _, ok := manifest.(*schema2.DeserializedManifest); ok { | ||||||||||||||||||||||||||||||||||
| d, err := core.ParseSHA256Digest(string(desc.Digest)) | ||||||||||||||||||||||||||||||||||
| if err != nil { | ||||||||||||||||||||||||||||||||||
| return nil, core.Digest{}, fmt.Errorf("parse digest: %s", err) | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| return manifest, d, nil | ||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||
| return manifest, d, nil | |
| return manifest, d, nil | |
| } else { | |
| // If UnmarshalManifest succeeds but returns a non-schema2 manifest, | |
| // treat this as an unsupported type for Docker v2 and fall back to | |
| // manual OCI parsing below. This is intentional to avoid silently | |
| // accepting an unexpected manifest representation. |
Outdated
Copilot
AI
Feb 16, 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.
ParseOCIManifest returns successfully after UnmarshalManifest(schema2.MediaTypeManifest, ...) without validating SchemaVersion==2 (unlike ParseManifestV2). Adding the same schema-version check here helps avoid accepting malformed manifests and keeps behavior consistent across parsers.
Outdated
Copilot
AI
Feb 16, 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.
The first attempt to parse using distribution.UnmarshalManifest with schema2.MediaTypeManifest will likely fail for genuine OCI manifests because they have a different mediaType field. This means the code will always fall through to the manual parsing path for actual OCI manifests, making lines 115-127 effectively dead code for the OCI use case. While not harmful, this adds unnecessary complexity. Consider whether this initial attempt is needed, or document why it's included (e.g., for compatibility with manifests that are incorrectly labeled).
| // First, try to parse as Docker v2 manifest (they have the same structure) | |
| // The Docker Distribution library might accept it if we use schema2.MediaTypeManifest | |
| manifest, desc, err := distribution.UnmarshalManifest(schema2.MediaTypeManifest, bytes) | |
| if err == nil { | |
| // Verify it's actually a schema2 manifest | |
| if _, ok := manifest.(*schema2.DeserializedManifest); ok { | |
| d, err := core.ParseSHA256Digest(string(desc.Digest)) | |
| if err != nil { | |
| return nil, core.Digest{}, fmt.Errorf("parse digest: %s", err) | |
| } | |
| return manifest, d, nil | |
| } | |
| } | |
| // If that fails, parse manually by checking the mediaType in the JSON | |
| // Parse by checking the mediaType in the JSON |
Copilot
AI
Feb 16, 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.
The OCI manifest JSON structure is incomplete. OCI manifests include 'size' fields for both config and layers, which should be parsed and used when constructing the descriptors. The current implementation sets Size to 0 on lines 164 and 176, which may cause issues downstream. Update the struct to include size fields and populate them in the distribution.Descriptor objects.
Copilot
AI
Feb 16, 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.
The manual JSON fallback requires mediaType to equal the OCI manifest media type. In OCI image manifests, mediaType can be omitted; treating an empty mediaType as OCI (or checking presence more leniently) would make parsing robust for valid manifests that don't include this field.
| if manifestJSON.MediaType != _ociManifestType { | |
| return nil, core.Digest{}, fmt.Errorf("expected oci manifest type %s, got %s", _ociManifestType, manifestJSON.MediaType) | |
| // In OCI image manifests, mediaType may be omitted. Treat an empty mediaType | |
| // as OCI, but reject manifests that explicitly specify a non-OCI media type. | |
| if manifestJSON.MediaType != "" && manifestJSON.MediaType != _ociManifestType { | |
| return nil, core.Digest{}, fmt.Errorf("expected oci manifest type %s or empty, got %s", _ociManifestType, manifestJSON.MediaType) |
Outdated
Copilot
AI
Feb 16, 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.
Setting Size to 0 for descriptors is problematic. While the comment indicates that size is "not available in OCI manifest", OCI manifests do include size fields for both config and layers according to the OCI Image Manifest Specification. The manifestJSON struct should be extended to capture these size fields, and they should be used when creating the descriptors. Size 0 could cause issues with download progress tracking, storage allocation, or validation in downstream components.
Outdated
Copilot
AI
Feb 16, 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.
The manifestJSON struct is missing the MediaType field for config and layers. OCI manifests include mediaType fields for both the config and each layer, and these should be preserved when converting to schema2 format. Different media types indicate different layer formats (e.g., tar.gzip vs tar+gzip, foreign layers, etc.), and using the wrong media type could cause processing errors. The struct should capture these fields and use them in the distribution.Descriptor objects instead of hardcoding schema2.MediaTypeImageConfig and schema2.MediaTypeLayer.
Copilot
AI
Feb 16, 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.
The manual parsing of the OCI manifest doesn't preserve the original mediaType values from the config and layers. OCI manifests may have different mediaType values than Docker v2 (e.g., 'application/vnd.oci.image.layer.v1.tar+gzip' vs 'application/vnd.docker.image.rootfs.diff.tar.gzip'). By hardcoding schema2.MediaTypeImageConfig and schema2.MediaTypeLayer, the function loses this information. Consider extracting and using the actual mediaType values from the manifest JSON.
Outdated
Copilot
AI
Feb 16, 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.
The Versioned field is being set to manifestlist.SchemaVersion, but this appears to be incorrect. Based on the codebase patterns in ParseManifestV2 (lines 67-69), Docker v2 manifests should have SchemaVersion 2. The manifestlist.SchemaVersion constant is intended for manifest lists, not individual manifests. This will create a manifest with an incorrect schema version field, which could cause validation issues or unexpected behavior when the manifest is used.
Outdated
Copilot
AI
Feb 16, 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.
In the manually-built schema2.DeserializedManifest, Versioned: manifestlist.SchemaVersion does not populate the embedded schema version/media type information (and may not match the expected type for the embedded Versioned field). This can yield a manifest with SchemaVersion=0 / empty MediaType (or fail to compile depending on types). Set the version info explicitly (SchemaVersion=2 and an appropriate MediaType) using the correct embedded version struct so downstream consumers see a valid v2/OCI-equivalent manifest.
Copilot
AI
Feb 16, 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.
Based on the test coverage pattern in this codebase (dockerutil_test.go has tests for ParseManifestV2List), the new ParseOCIManifest function should have corresponding test coverage. Tests should include: valid OCI manifest parsing, invalid OCI manifest handling, digest calculation verification, and proper conversion to schema2 format. This ensures the OCI manifest parsing logic works correctly and prevents regressions.
Copilot
AI
Feb 16, 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.
The new ParseOCIManifest function lacks test coverage. The existing tests in dockerutil_test.go test ParseManifestV2List but don't cover OCI manifest parsing. Consider adding tests that verify OCI manifest parsing works correctly, including edge cases like missing fields or invalid digest formats.
Copilot
AI
Feb 16, 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.
The new ParseOCIManifest function lacks test coverage. The existing test file utils/dockerutil/dockerutil_test.go has tests for ParseManifestV2List but no tests for the new OCI manifest parsing functionality. Given that this is a critical new feature for parsing OCI manifests, comprehensive test coverage should be added to ensure correct behavior, including tests for valid OCI manifests, invalid media types, malformed JSON, and edge cases.
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.
The ParseManifest function's fallback logic has been improved, but there's a subtle issue: when ParseManifestV2 succeeds but ParseManifestV2List fails, the error from ParseManifestV2List is being ignored and the function falls through to ParseOCIManifest. This could mask legitimate errors. Consider whether the original design pattern (where ParseManifestV2List was the final fallback) was intentional, and whether the error from the previous attempt should be preserved or logged when falling through to the next parser.