Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 42 additions & 10 deletions src/spec-configuration/containerCollectionsOCI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@ export const DEVCONTAINER_COLLECTION_LAYER_MEDIATYPE = 'application/vnd.devconta

export type HEADERS = { 'authorization'?: string; 'user-agent': string; 'content-type'?: string; 'accept'?: string };

// ghcr.io/devcontainers/features/go:1.0.0
// Represents the unique OCI identifier for a Feature or Template.
// eg: ghcr.io/devcontainers/features/go:1.0.0
// Constructed by 'getRef()'
export interface OCIRef {
registry: string; // 'ghcr.io'
owner: string; // 'devcontainers'
Expand All @@ -24,11 +26,14 @@ export interface OCIRef {
version?: string; // '1.0.0'
}

// ghcr.io/devcontainers/features:latest
// Represents the unique OCI identifier for a Collection's Metadata artifact.
// eg: ghcr.io/devcontainers/features:latest
// Constructed by 'getCollectionRef()'
export interface OCICollectionRef {
registry: string; // 'ghcr.io'
path: string; // 'devcontainers/features'
version: 'latest'; // 'latest'
resource: string; // 'ghcr.io/devcontainers/features'
version: 'latest'; // 'latest' (always)
}

export interface OCILayer {
Expand Down Expand Up @@ -100,13 +105,15 @@ export function getRef(output: Log, input: string): OCIRef | undefined {

const path = `${namespace}/${id}`;

output.write(`resource: ${resource}`, LogLevel.Trace);
output.write(`id: ${id}`, LogLevel.Trace);
output.write(`version: ${version}`, LogLevel.Trace);
output.write(`owner: ${owner}`, LogLevel.Trace);
output.write(`namespace: ${namespace}`, LogLevel.Trace); // TODO: We assume 'namespace' includes at least one slash (eg: 'devcontainers/features')
output.write(`registry: ${registry}`, LogLevel.Trace);
output.write(`path: ${path}`, LogLevel.Trace);
output.write(`> input: ${input}`, LogLevel.Trace);
output.write(`>`, LogLevel.Trace);
output.write(`> resource: ${resource}`, LogLevel.Trace);
output.write(`> id: ${id}`, LogLevel.Trace);
output.write(`> version: ${version}`, LogLevel.Trace);
output.write(`> owner: ${owner}`, LogLevel.Trace);
output.write(`> namespace: ${namespace}`, LogLevel.Trace); // TODO: We assume 'namespace' includes at least one slash (eg: 'devcontainers/features')
output.write(`> registry: ${registry}`, LogLevel.Trace);
output.write(`> path: ${path}`, LogLevel.Trace);

// Validate results of parse.

Expand All @@ -131,6 +138,31 @@ export function getRef(output: Log, input: string): OCIRef | undefined {
};
}

export function getCollectionRef(output: Log, registry: string, namespace: string): OCICollectionRef | undefined {
// Normalize input by downcasing entire string
registry = registry.toLowerCase();
namespace = namespace.toLowerCase();

const path = namespace;
const resource = `${registry}/${path}`;

output.write(`> Inputs: registry='${registry}' namespace='${namespace}'`, LogLevel.Trace);
output.write(`>`, LogLevel.Trace);
output.write(`> resource: ${resource}`, LogLevel.Trace);

if (!regexForPath.exec(path)) {
output.write(`Parsed path '${path}' from input failed validation.`, LogLevel.Error);
return undefined;
}

return {
registry,
path,
resource,
version: 'latest'
};
}

// Validate if a manifest exists and is reachable about the declared feature/template.
// Specification: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#pulling-manifests
export async function fetchOCIManifestIfExists(output: Log, env: NodeJS.ProcessEnv, ref: OCIRef | OCICollectionRef, manifestDigest?: string, authToken?: string): Promise<OCIManifest | undefined> {
Expand Down
12 changes: 6 additions & 6 deletions src/spec-node/featuresCLI/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { loadNativeModule } from '../../spec-common/commonUtils';
import { PackageCommandInput } from '../collectionCommonUtils/package';
import { OCICollectionFileName } from '../collectionCommonUtils/packageCommandImpl';
import { publishOptions } from '../collectionCommonUtils/publish';
import { getRef, OCICollectionRef } from '../../spec-configuration/containerCollectionsOCI';
import { getCollectionRef, getRef, OCICollectionRef } from '../../spec-configuration/containerCollectionsOCI';
import { doPublishCommand, doPublishMetadata } from '../collectionCommonUtils/publishCommandImpl';

const collectionType = 'feature';
Expand Down Expand Up @@ -88,11 +88,11 @@ async function featuresPublish({
}
}

const featureCollectionRef: OCICollectionRef = {
registry: registry,
path: namespace,
version: 'latest'
};
const featureCollectionRef: OCICollectionRef | undefined = getCollectionRef(output, registry, namespace);
if (!featureCollectionRef) {
output.write(`(!) Could not parse provided collection identifier with registry '${registry}' and namespace '${namespace}'`, LogLevel.Error);
process.exit(1);
}

if (! await doPublishMetadata(featureCollectionRef, outputDir, output, collectionType)) {
output.write(`(!) ERR: Failed to publish '${featureCollectionRef.registry}/${featureCollectionRef.path}'`, LogLevel.Error);
Expand Down
12 changes: 6 additions & 6 deletions src/spec-node/templatesCLI/publish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import { loadNativeModule } from '../../spec-common/commonUtils';
import { PackageCommandInput } from '../collectionCommonUtils/package';
import { OCICollectionFileName } from '../collectionCommonUtils/packageCommandImpl';
import { packageTemplates } from './packageImpl';
import { getRef, OCICollectionRef } from '../../spec-configuration/containerCollectionsOCI';
import { getCollectionRef, getRef, OCICollectionRef } from '../../spec-configuration/containerCollectionsOCI';
import { doPublishCommand, doPublishMetadata } from '../collectionCommonUtils/publishCommandImpl';

const collectionType = 'template';
Expand Down Expand Up @@ -86,11 +86,11 @@ async function templatesPublish({
await doPublishCommand(t.version, templateRef, outputDir, output, collectionType);
}

const templateCollectionRef: OCICollectionRef = {
registry: registry,
path: namespace,
version: 'latest'
};
const templateCollectionRef: OCICollectionRef | undefined = getCollectionRef(output, registry, namespace);
if (!templateCollectionRef) {
output.write(`(!) Could not parse provided collection identifier with registry '${registry}' and namespace '${namespace}'`, LogLevel.Error);
process.exit(1);
}

await doPublishMetadata(templateCollectionRef, outputDir, output, collectionType);

Expand Down
54 changes: 53 additions & 1 deletion src/test/container-features/containerFeaturesOCI.test.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,61 @@
import { assert } from 'chai';
import { getRef, getManifest, getBlob } from '../../spec-configuration/containerCollectionsOCI';
import { getRef, getManifest, getBlob, getCollectionRef } from '../../spec-configuration/containerCollectionsOCI';
import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log';

export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace));

describe('getCollectionRef()', async function () {
this.timeout('120s');


it('valid getCollectionRef()', async () => {
const collectionRef = getCollectionRef(output, 'ghcr.io', 'devcontainers/templates');
if (!collectionRef) {
assert.fail('collectionRef should not be undefined');
}
assert.ok(collectionRef);
assert.equal(collectionRef.registry, 'ghcr.io');
assert.equal(collectionRef.path, 'devcontainers/templates');
assert.equal(collectionRef.resource, 'ghcr.io/devcontainers/templates');
assert.equal(collectionRef.version, 'latest');
});

it('valid getCollectionRef() that was originally uppercase', async () => {
const collectionRef = getCollectionRef(output, 'GHCR.IO', 'DEVCONTAINERS/TEMPLATES');
if (!collectionRef) {
assert.fail('collectionRef should not be undefined');
}
assert.ok(collectionRef);
assert.equal(collectionRef.registry, 'ghcr.io');
assert.equal(collectionRef.path, 'devcontainers/templates');
assert.equal(collectionRef.resource, 'ghcr.io/devcontainers/templates');
assert.equal(collectionRef.version, 'latest');
});

it('valid getCollectionRef() with port in registry', async () => {
const collectionRef = getCollectionRef(output, 'ghcr.io:8001', 'devcontainers/templates');
if (!collectionRef) {
assert.fail('collectionRef should not be undefined');
}
assert.ok(collectionRef);
assert.equal(collectionRef.registry, 'ghcr.io:8001');
assert.equal(collectionRef.path, 'devcontainers/templates');
assert.equal(collectionRef.resource, 'ghcr.io:8001/devcontainers/templates');
assert.equal(collectionRef.version, 'latest');
});

it('invalid getCollectionRef() with an invalid character in path', async () => {
const collectionRef = getCollectionRef(output, 'ghcr.io', 'devcont%ainers/templates');
assert.isUndefined(collectionRef);
});

it('invalid getCollectionRef() with too many slashes in path', async () => {
const collectionRef = getCollectionRef(output, 'ghcr.io', 'devcontainers//templates');
assert.isUndefined(collectionRef);
});

});

describe('getRef()', async function () {
this.timeout('120s');

Expand Down