Skip to content

Commit

Permalink
Change the config layer of the feature manifest to a empty descriptor
Browse files Browse the repository at this point in the history
This commit changes the config layer of the feature manifest to a empty descriptor according to the OCI image manifest specification to to increase the available feature registry.

Some registries that implement the OCI image manifest specification, such as AWS ECR, do not support pushing empty content (size 0). However, the manifest may contain layers with no content. Therefore, the OCI Image Manifest specification specifies an empty descriptor of non-zero size to represent a layer with no content.

Currently, the devcontainer feature manifest specifies the config layer as an empty content (size 0) layer. If the devcontainer feature manifest conforms to this specification, it can also available registries that cannot publish empty content (size 0).

Specification: https://github.com/opencontainers/image-spec/blob/8f3820ccf8f65db8744e626df17fe8a64462aece/manifest.md#guidelines-for-artifact-usage

Note: If the config is set to an empty descriptor, the manifest media type must be defined in the artifactType.
  • Loading branch information
ych-tnk committed Apr 30, 2024
1 parent c1c8b08 commit 1b290f0
Show file tree
Hide file tree
Showing 6 changed files with 28 additions and 10 deletions.
16 changes: 14 additions & 2 deletions src/spec-configuration/containerCollectionsOCI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,14 @@ export const DEVCONTAINER_MANIFEST_MEDIATYPE = 'application/vnd.devcontainers';
export const DEVCONTAINER_TAR_LAYER_MEDIATYPE = 'application/vnd.devcontainers.layer.v1+tar';
export const DEVCONTAINER_COLLECTION_LAYER_MEDIATYPE = 'application/vnd.devcontainers.collection.layer.v1+json';

// Empty Descriptor specified in OCI Image Specification.
// Following Spec: https://github.com/opencontainers/image-spec/blob/v1.1.0/manifest.md#guidance-for-an-empty-descriptor
export const OCI_EMPTY_DESCRIPTOR = {
mediaType: 'application/vnd.oci.empty.v1+json',
digest: 'sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a',
size: 2,
dataBytes: Buffer.from('{}')
};

export interface CommonParams {
env: NodeJS.ProcessEnv;
Expand Down Expand Up @@ -60,6 +68,7 @@ export interface OCILayer {
export interface OCIManifest {
digest?: string;
schemaVersion: number;
artifactType?: string;
mediaType: string;
config: {
digest: string;
Expand Down Expand Up @@ -295,10 +304,13 @@ export async function fetchOCIManifestIfExists(params: CommonParams, ref: OCIRef
return;
}

// If the config layer is an empty descriptor, the media type of the manifest must be defined in the artifactType.
// Specification: https://github.com/opencontainers/image-spec/blob/8f3820ccf8f65db8744e626df17fe8a64462aece/manifest.md#guidelines-for-artifact-usage
const { manifestObj } = manifestContainer;
const manifestMediaType = manifestObj.config.mediaType === OCI_EMPTY_DESCRIPTOR.mediaType ? manifestObj.artifactType : manifestObj.config.mediaType;

if (manifestObj.config.mediaType !== DEVCONTAINER_MANIFEST_MEDIATYPE) {
output.write(`(!) Unexpected manifest media type: ${manifestObj.config.mediaType}`, LogLevel.Error);
if (manifestMediaType !== DEVCONTAINER_MANIFEST_MEDIATYPE) {
output.write(`(!) Unexpected manifest media type: ${manifestMediaType}`, LogLevel.Error);
return undefined;
}

Expand Down
15 changes: 8 additions & 7 deletions src/spec-configuration/containerCollectionsOCIPush.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import * as crypto from 'crypto';
import { delay } from '../spec-common/async';
import { Log, LogLevel } from '../spec-utils/log';
import { isLocalFile } from '../spec-utils/pfs';
import { DEVCONTAINER_COLLECTION_LAYER_MEDIATYPE, DEVCONTAINER_TAR_LAYER_MEDIATYPE, fetchOCIManifestIfExists, OCICollectionRef, OCILayer, OCIManifest, OCIRef, CommonParams, ManifestContainer } from './containerCollectionsOCI';
import { OCI_EMPTY_DESCRIPTOR, DEVCONTAINER_MANIFEST_MEDIATYPE, DEVCONTAINER_COLLECTION_LAYER_MEDIATYPE, DEVCONTAINER_TAR_LAYER_MEDIATYPE, fetchOCIManifestIfExists, OCICollectionRef, OCILayer, OCIManifest, OCIRef, CommonParams, ManifestContainer } from './containerCollectionsOCI';
import { requestEnsureAuthenticated } from './httpOCIRegistry';

// (!) Entrypoint function to push a single feature/template to a registry.
Expand Down Expand Up @@ -44,8 +44,8 @@ export async function pushOCIFeatureOrTemplate(params: CommonParams, ociRef: OCI
{
name: 'configLayer',
digest: manifest.manifestObj.config.digest,
contents: Buffer.alloc(0),
size: manifest.manifestObj.config.size,
contents: OCI_EMPTY_DESCRIPTOR.dataBytes,
},
{
name: 'tgzLayer',
Expand Down Expand Up @@ -119,7 +119,7 @@ export async function pushCollectionMetadata(params: CommonParams, collectionRef
name: 'configLayer',
digest: manifest.manifestObj.config.digest,
size: manifest.manifestObj.config.size,
contents: Buffer.alloc(0),
contents: OCI_EMPTY_DESCRIPTOR.dataBytes,
},
{
name: 'collectionLayer',
Expand Down Expand Up @@ -383,15 +383,16 @@ export async function calculateManifestAndContentDigest(output: Log, ociRef: OCI
// A canonical manifest digest is the sha256 hash of the JSON representation of the manifest, without the signature content.
// See: https://docs.docker.com/registry/spec/api/#content-digests
// Below is an example of a serialized manifest that should resolve to '9726054859c13377c4c3c3c73d15065de59d0c25d61d5652576c0125f2ea8ed3'
// {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.devcontainers","digest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","size":0},"layers":[{"mediaType":"application/vnd.devcontainers.layer.v1+tar","digest":"sha256:b2006e7647191f7b47222ae48df049c6e21a4c5a04acfad0c4ef614d819de4c5","size":15872,"annotations":{"org.opencontainers.image.title":"go.tgz"}}]}
// {"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","artifactType":"application/vnd.devcontainers","config":{"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[{"mediaType":"application/vnd.devcontainers.layer.v1+tar","digest":"sha256:b2006e7647191f7b47222ae48df049c6e21a4c5a04acfad0c4ef614d819de4c5","size":15872,"annotations":{"org.opencontainers.image.title":"go.tgz"}}]}

let manifest: OCIManifest = {
schemaVersion: 2,
mediaType: 'application/vnd.oci.image.manifest.v1+json',
artifactType: DEVCONTAINER_MANIFEST_MEDIATYPE,
config: {
mediaType: 'application/vnd.devcontainers',
digest: 'sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855', // A zero byte digest for the devcontainer mediaType.
size: 0
mediaType: OCI_EMPTY_DESCRIPTOR.mediaType,
digest: OCI_EMPTY_DESCRIPTOR.digest,
size: OCI_EMPTY_DESCRIPTOR.size,
},
layers: [
dataLayer
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -341,7 +341,7 @@ describe('Test OCI Push Helper Functions', function () {
const { contentDigest, manifestBuffer } = manifestContainer;

// 'Expected' is taken from intermediate value in oras reference implementation, before hash calculation
assert.strictEqual('{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","config":{"mediaType":"application/vnd.devcontainers","digest":"sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855","size":0},"layers":[{"mediaType":"application/vnd.devcontainers.layer.v1+tar","digest":"sha256:b2006e7647191f7b47222ae48df049c6e21a4c5a04acfad0c4ef614d819de4c5","size":15872,"annotations":{"org.opencontainers.image.title":"go.tgz"}}]}', manifestBuffer.toString());
assert.strictEqual('{"schemaVersion":2,"mediaType":"application/vnd.oci.image.manifest.v1+json","artifactType":"application/vnd.devcontainers","config":{"mediaType":"application/vnd.oci.empty.v1+json","digest":"sha256:44136fa355b3678a1146ad16f7e8649e94fb4fc21fe77e8310c060f61caaff8a","size":2},"layers":[{"mediaType":"application/vnd.devcontainers.layer.v1+tar","digest":"sha256:b2006e7647191f7b47222ae48df049c6e21a4c5a04acfad0c4ef614d819de4c5","size":15872,"annotations":{"org.opencontainers.image.title":"go.tgz"}}]}', manifestBuffer.toString());

// This is the canonical digest of the manifest
assert.strictEqual('sha256:9726054859c13377c4c3c3c73d15065de59d0c25d61d5652576c0125f2ea8ed3', contentDigest);
Expand Down
1 change: 1 addition & 0 deletions src/test/container-features/featureAdvisories.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,7 @@ function getFeaturesConfig(featureId: string, featureConfigVersion: string, feat
featureRef: getRef(output, `${featureId}:${featureConfigVersion}`)!,
manifest: {
schemaVersion: 1,
artifactType: '',
mediaType: '',
config: {
digest: '',
Expand Down
3 changes: 3 additions & 0 deletions src/test/container-features/featureHelpers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,7 @@ chmod +x ./install.sh
},
manifest: {
schemaVersion: 1,
artifactType: '',
mediaType: '',
config: {
digest: '',
Expand Down Expand Up @@ -722,6 +723,7 @@ chmod +x ./install.sh
},
manifest: {
schemaVersion: 1,
artifactType: '',
mediaType: '',
config: {
digest: '',
Expand Down Expand Up @@ -809,6 +811,7 @@ chmod +x ./install.sh
},
manifest: {
schemaVersion: 1,
artifactType: '',
mediaType: '',
config: {
digest: '',
Expand Down
1 change: 1 addition & 0 deletions src/test/imageMetadata.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -563,6 +563,7 @@ function getFeaturesConfig(features: Feature[]): FeaturesConfig {
},
manifest: {
schemaVersion: 1,
artifactType: '',
mediaType: '',
config: {
digest: '',
Expand Down

0 comments on commit 1b290f0

Please sign in to comment.