Skip to content

Commit

Permalink
getCollectionRef() to centralize validating Collection identifier (d…
Browse files Browse the repository at this point in the history
…evcontainers#303)

* create getCollectionRef() to centralize validating Collection identifier

* missing a quote

* another quote
  • Loading branch information
joshspicer committed Nov 29, 2022
1 parent 1aeb848 commit d5bc1f2
Show file tree
Hide file tree
Showing 4 changed files with 107 additions and 23 deletions.
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

0 comments on commit d5bc1f2

Please sign in to comment.