From ef6d0f3244006293ac52a6f3921448e2c5603de1 Mon Sep 17 00:00:00 2001 From: Rodney Osodo Date: Sun, 18 Jan 2026 22:44:00 +0300 Subject: [PATCH] feat(image): add WASM OCI image support Add comprehensive support for WebAssembly OCI images, including WASM content layers, configuration blobs, compression, and encryption. Signed-off-by: Rodney Osodo --- image/internal/image/oci.go | 9 ++++++--- image/internal/manifest/manifest.go | 4 ++++ image/manifest/oci.go | 25 +++++++++++++++++-------- image/manifest/oci_test.go | 7 +++++++ 4 files changed, 34 insertions(+), 11 deletions(-) diff --git a/image/internal/image/oci.go b/image/internal/image/oci.go index 8ddb2875e0..9677e25cfc 100644 --- a/image/internal/image/oci.go +++ b/image/internal/image/oci.go @@ -85,7 +85,7 @@ func (m *manifestOCI1) ConfigBlob(ctx context.Context) ([]byte, error) { // layers in the resulting configuration isn't guaranteed to be returned to due how // old image manifests work (docker v2s1 especially). func (m *manifestOCI1) OCIConfig(ctx context.Context) (*imgspecv1.Image, error) { - if m.m.Config.MediaType != imgspecv1.MediaTypeImageConfig { + if m.m.Config.MediaType != imgspecv1.MediaTypeImageConfig && m.m.Config.MediaType != internalManifest.WasmConfigMediaType { return nil, internalManifest.NewNonImageArtifactError(&m.m.Manifest) } @@ -244,7 +244,7 @@ func (m *manifestOCI1) layerEditsOfOCIOnlyFeatures(options *types.ManifestUpdate // value. // This does not change the state of the original manifestOCI1 object. func (m *manifestOCI1) convertToManifestSchema2(_ context.Context, options *types.ManifestUpdateOptions) (*manifestSchema2, error) { - if m.m.Config.MediaType != imgspecv1.MediaTypeImageConfig { + if m.m.Config.MediaType != imgspecv1.MediaTypeImageConfig && m.m.Config.MediaType != internalManifest.WasmConfigMediaType { return nil, internalManifest.NewNonImageArtifactError(&m.m.Manifest) } @@ -290,6 +290,9 @@ func (m *manifestOCI1) convertToManifestSchema2(_ context.Context, options *type case ociencspec.MediaTypeLayerEnc, ociencspec.MediaTypeLayerGzipEnc, ociencspec.MediaTypeLayerZstdEnc, ociencspec.MediaTypeLayerNonDistributableEnc, ociencspec.MediaTypeLayerNonDistributableGzipEnc, ociencspec.MediaTypeLayerNonDistributableZstdEnc: return nil, fmt.Errorf("during manifest conversion: encrypted layers (%q) are not supported in docker images", layers[idx].MediaType) + case internalManifest.WasmContentLayerMediaType, internalManifest.WasmContentLayerMediaType + "+gzip", internalManifest.WasmContentLayerMediaType + "+zstd", + internalManifest.WasmContentLayerMediaType + "+encrypted", internalManifest.WasmContentLayerMediaType + "+gzip+encrypted", internalManifest.WasmContentLayerMediaType + "+zstd+encrypted": + return nil, fmt.Errorf("during manifest conversion: WASM layers (%q) are not supported in docker images", layers[idx].MediaType) default: return nil, fmt.Errorf("Unknown media type during manifest conversion: %q", layers[idx].MediaType) } @@ -306,7 +309,7 @@ func (m *manifestOCI1) convertToManifestSchema2(_ context.Context, options *type // value. // This does not change the state of the original manifestOCI1 object. func (m *manifestOCI1) convertToManifestSchema1(ctx context.Context, options *types.ManifestUpdateOptions) (genericManifest, error) { - if m.m.Config.MediaType != imgspecv1.MediaTypeImageConfig { + if m.m.Config.MediaType != imgspecv1.MediaTypeImageConfig && m.m.Config.MediaType != internalManifest.WasmConfigMediaType { return nil, internalManifest.NewNonImageArtifactError(&m.m.Manifest) } diff --git a/image/internal/manifest/manifest.go b/image/internal/manifest/manifest.go index 46e1e4df17..1f8509df4c 100644 --- a/image/internal/manifest/manifest.go +++ b/image/internal/manifest/manifest.go @@ -34,6 +34,10 @@ const ( DockerV2Schema2ForeignLayerMediaType = "application/vnd.docker.image.rootfs.foreign.diff.tar" // DockerV2Schema2ForeignLayerMediaType is the MIME type used for gzipped schema 2 foreign layers. DockerV2Schema2ForeignLayerMediaTypeGzip = "application/vnd.docker.image.rootfs.foreign.diff.tar.gzip" + // WasmContentLayerMediaType is the MIME type used for WASM content layers. + WasmContentLayerMediaType = "application/vnd.wasm.content.layer.v1" + // WasmConfigMediaType is the MIME type used for WASM config blobs. + WasmConfigMediaType = "application/vnd.wasm.config.v1+json" ) // GuessMIMEType guesses MIME type of a manifest and returns it _if it is recognized_, or "" if unknown or unrecognized. diff --git a/image/manifest/oci.go b/image/manifest/oci.go index 286d58c423..bde9a4b96b 100644 --- a/image/manifest/oci.go +++ b/image/manifest/oci.go @@ -47,7 +47,10 @@ func SupportedOCI1MediaType(m string) error { imgspecv1.MediaTypeImageLayerNonDistributable, imgspecv1.MediaTypeImageLayerNonDistributableGzip, imgspecv1.MediaTypeImageLayerNonDistributableZstd, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. imgspecv1.MediaTypeImageManifest, imgspecv1.MediaTypeLayoutHeader, - ociencspec.MediaTypeLayerEnc, ociencspec.MediaTypeLayerGzipEnc: + ociencspec.MediaTypeLayerEnc, ociencspec.MediaTypeLayerGzipEnc, + manifest.WasmContentLayerMediaType, manifest.WasmContentLayerMediaType + "+gzip", manifest.WasmContentLayerMediaType + "+zstd", + manifest.WasmContentLayerMediaType + "+encrypted", manifest.WasmContentLayerMediaType + "+gzip+encrypted", manifest.WasmContentLayerMediaType + "+zstd+encrypted", + manifest.WasmConfigMediaType: return nil default: return fmt.Errorf("unsupported OCIv1 media type: %q", m) @@ -116,6 +119,11 @@ var oci1CompressionMIMETypeSets = []compressionMIMETypeSet{ compressiontypes.GzipAlgorithmName: imgspecv1.MediaTypeImageLayerGzip, compressiontypes.ZstdAlgorithmName: imgspecv1.MediaTypeImageLayerZstd, }, + { + mtsUncompressed: manifest.WasmContentLayerMediaType, + compressiontypes.GzipAlgorithmName: manifest.WasmContentLayerMediaType + "+gzip", + compressiontypes.ZstdAlgorithmName: manifest.WasmContentLayerMediaType + "+zstd", + }, } // UpdateLayerInfos replaces the original layers with the specified BlobInfos (size+digest+urls+mediatype), in order (the root layer first, and then successive layered layers) @@ -173,7 +181,8 @@ func getEncryptedMediaType(mediatype string) (string, error) { unsuffixedMediatype := parts[0] switch unsuffixedMediatype { case DockerV2Schema2LayerMediaType, imgspecv1.MediaTypeImageLayer, - imgspecv1.MediaTypeImageLayerNonDistributable: //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + imgspecv1.MediaTypeImageLayerNonDistributable, //nolint:staticcheck // NonDistributable layers are deprecated, but we want to continue to support manipulating pre-existing images. + manifest.WasmContentLayerMediaType: return mediatype + "+encrypted", nil } @@ -199,11 +208,11 @@ func (m *OCI1) Serialize() ([]byte, error) { // Inspect returns various information for (skopeo inspect) parsed from the manifest and configuration. func (m *OCI1) Inspect(configGetter func(types.BlobInfo) ([]byte, error)) (*types.ImageInspectInfo, error) { - if m.Config.MediaType != imgspecv1.MediaTypeImageConfig { - // We could return at least the layers, but that’s already available in a better format via types.Image.LayerInfos. + if m.Config.MediaType != imgspecv1.MediaTypeImageConfig && m.Config.MediaType != manifest.WasmConfigMediaType { + // We could return at least the layers, but that's already available in a better format via types.Image.LayerInfos. // Most software calling this without human intervention is going to expect the values to be realistic and relevant, // and is probably better served by failing; we can always re-visit that later if we fail now, but - // if we started returning some data for OCI artifacts now, we couldn’t start failing in this function later. + // if we started returning some data for OCI artifacts now, we couldn't start failing in this function later. return nil, manifest.NewNonImageArtifactError(&m.Manifest) } @@ -253,8 +262,8 @@ func (m *OCI1) ImageID(diffIDs []digest.Digest) (string, error) { // gives us the option to not fail, and return some value, in the future, // without committing to that approach now. // (The only known caller of ImageID is storage/storageImageDestination.computeID, - // which can’t work with non-image artifacts.) - if m.Config.MediaType != imgspecv1.MediaTypeImageConfig { + // which can't work with non-image artifacts.) + if m.Config.MediaType != imgspecv1.MediaTypeImageConfig && m.Config.MediaType != manifest.WasmConfigMediaType { return "", manifest.NewNonImageArtifactError(&m.Manifest) } @@ -269,7 +278,7 @@ func (m *OCI1) ImageID(diffIDs []digest.Digest) (string, error) { // NOTE: Even if this returns true, the relevant format might not accept all compression algorithms; the set of accepted // algorithms depends not on the current format, but possibly on the target of a conversion. func (m *OCI1) CanChangeLayerCompression(mimeType string) bool { - if m.Config.MediaType != imgspecv1.MediaTypeImageConfig { + if m.Config.MediaType != imgspecv1.MediaTypeImageConfig && m.Config.MediaType != manifest.WasmConfigMediaType { return false } return compressionVariantsRecognizeMIMEType(oci1CompressionMIMETypeSets, mimeType) diff --git a/image/manifest/oci_test.go b/image/manifest/oci_test.go index 50d2b463e0..acd392c534 100644 --- a/image/manifest/oci_test.go +++ b/image/manifest/oci_test.go @@ -38,6 +38,13 @@ func TestSupportedOCI1MediaType(t *testing.T) { {imgspecv1.MediaTypeImageLayerZstd, false}, {imgspecv1.MediaTypeImageManifest, false}, {imgspecv1.MediaTypeLayoutHeader, false}, + {"application/vnd.wasm.content.layer.v1", false}, + {"application/vnd.wasm.content.layer.v1+gzip", false}, + {"application/vnd.wasm.content.layer.v1+zstd", false}, + {"application/vnd.wasm.content.layer.v1+encrypted", false}, + {"application/vnd.wasm.content.layer.v1+gzip+encrypted", false}, + {"application/vnd.wasm.content.layer.v1+zstd+encrypted", false}, + {"application/vnd.wasm.config.v1+json", false}, {"application/vnd.oci.image.layer.nondistributable.v1.tar+unknown", true}, } for _, d := range data {