Skip to content

Commit d5bc1f2

Browse files
authored
getCollectionRef() to centralize validating Collection identifier (devcontainers#303)
* create getCollectionRef() to centralize validating Collection identifier * missing a quote * another quote
1 parent 1aeb848 commit d5bc1f2

4 files changed

Lines changed: 107 additions & 23 deletions

File tree

src/spec-configuration/containerCollectionsOCI.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,9 @@ export const DEVCONTAINER_COLLECTION_LAYER_MEDIATYPE = 'application/vnd.devconta
1313

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

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

27-
// ghcr.io/devcontainers/features:latest
29+
// Represents the unique OCI identifier for a Collection's Metadata artifact.
30+
// eg: ghcr.io/devcontainers/features:latest
31+
// Constructed by 'getCollectionRef()'
2832
export interface OCICollectionRef {
2933
registry: string; // 'ghcr.io'
3034
path: string; // 'devcontainers/features'
31-
version: 'latest'; // 'latest'
35+
resource: string; // 'ghcr.io/devcontainers/features'
36+
version: 'latest'; // 'latest' (always)
3237
}
3338

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

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

103-
output.write(`resource: ${resource}`, LogLevel.Trace);
104-
output.write(`id: ${id}`, LogLevel.Trace);
105-
output.write(`version: ${version}`, LogLevel.Trace);
106-
output.write(`owner: ${owner}`, LogLevel.Trace);
107-
output.write(`namespace: ${namespace}`, LogLevel.Trace); // TODO: We assume 'namespace' includes at least one slash (eg: 'devcontainers/features')
108-
output.write(`registry: ${registry}`, LogLevel.Trace);
109-
output.write(`path: ${path}`, LogLevel.Trace);
108+
output.write(`> input: ${input}`, LogLevel.Trace);
109+
output.write(`>`, LogLevel.Trace);
110+
output.write(`> resource: ${resource}`, LogLevel.Trace);
111+
output.write(`> id: ${id}`, LogLevel.Trace);
112+
output.write(`> version: ${version}`, LogLevel.Trace);
113+
output.write(`> owner: ${owner}`, LogLevel.Trace);
114+
output.write(`> namespace: ${namespace}`, LogLevel.Trace); // TODO: We assume 'namespace' includes at least one slash (eg: 'devcontainers/features')
115+
output.write(`> registry: ${registry}`, LogLevel.Trace);
116+
output.write(`> path: ${path}`, LogLevel.Trace);
110117

111118
// Validate results of parse.
112119

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

141+
export function getCollectionRef(output: Log, registry: string, namespace: string): OCICollectionRef | undefined {
142+
// Normalize input by downcasing entire string
143+
registry = registry.toLowerCase();
144+
namespace = namespace.toLowerCase();
145+
146+
const path = namespace;
147+
const resource = `${registry}/${path}`;
148+
149+
output.write(`> Inputs: registry='${registry}' namespace='${namespace}'`, LogLevel.Trace);
150+
output.write(`>`, LogLevel.Trace);
151+
output.write(`> resource: ${resource}`, LogLevel.Trace);
152+
153+
if (!regexForPath.exec(path)) {
154+
output.write(`Parsed path '${path}' from input failed validation.`, LogLevel.Error);
155+
return undefined;
156+
}
157+
158+
return {
159+
registry,
160+
path,
161+
resource,
162+
version: 'latest'
163+
};
164+
}
165+
134166
// Validate if a manifest exists and is reachable about the declared feature/template.
135167
// Specification: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#pulling-manifests
136168
export async function fetchOCIManifestIfExists(output: Log, env: NodeJS.ProcessEnv, ref: OCIRef | OCICollectionRef, manifestDigest?: string, authToken?: string): Promise<OCIManifest | undefined> {

src/spec-node/featuresCLI/publish.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { loadNativeModule } from '../../spec-common/commonUtils';
1212
import { PackageCommandInput } from '../collectionCommonUtils/package';
1313
import { OCICollectionFileName } from '../collectionCommonUtils/packageCommandImpl';
1414
import { publishOptions } from '../collectionCommonUtils/publish';
15-
import { getRef, OCICollectionRef } from '../../spec-configuration/containerCollectionsOCI';
15+
import { getCollectionRef, getRef, OCICollectionRef } from '../../spec-configuration/containerCollectionsOCI';
1616
import { doPublishCommand, doPublishMetadata } from '../collectionCommonUtils/publishCommandImpl';
1717

1818
const collectionType = 'feature';
@@ -88,11 +88,11 @@ async function featuresPublish({
8888
}
8989
}
9090

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

9797
if (! await doPublishMetadata(featureCollectionRef, outputDir, output, collectionType)) {
9898
output.write(`(!) ERR: Failed to publish '${featureCollectionRef.registry}/${featureCollectionRef.path}'`, LogLevel.Error);

src/spec-node/templatesCLI/publish.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import { loadNativeModule } from '../../spec-common/commonUtils';
1212
import { PackageCommandInput } from '../collectionCommonUtils/package';
1313
import { OCICollectionFileName } from '../collectionCommonUtils/packageCommandImpl';
1414
import { packageTemplates } from './packageImpl';
15-
import { getRef, OCICollectionRef } from '../../spec-configuration/containerCollectionsOCI';
15+
import { getCollectionRef, getRef, OCICollectionRef } from '../../spec-configuration/containerCollectionsOCI';
1616
import { doPublishCommand, doPublishMetadata } from '../collectionCommonUtils/publishCommandImpl';
1717

1818
const collectionType = 'template';
@@ -86,11 +86,11 @@ async function templatesPublish({
8686
await doPublishCommand(t.version, templateRef, outputDir, output, collectionType);
8787
}
8888

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

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

src/test/container-features/containerFeaturesOCI.test.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,61 @@
11
import { assert } from 'chai';
2-
import { getRef, getManifest, getBlob } from '../../spec-configuration/containerCollectionsOCI';
2+
import { getRef, getManifest, getBlob, getCollectionRef } from '../../spec-configuration/containerCollectionsOCI';
33
import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log';
44

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

7+
describe('getCollectionRef()', async function () {
8+
this.timeout('120s');
9+
10+
11+
it('valid getCollectionRef()', async () => {
12+
const collectionRef = getCollectionRef(output, 'ghcr.io', 'devcontainers/templates');
13+
if (!collectionRef) {
14+
assert.fail('collectionRef should not be undefined');
15+
}
16+
assert.ok(collectionRef);
17+
assert.equal(collectionRef.registry, 'ghcr.io');
18+
assert.equal(collectionRef.path, 'devcontainers/templates');
19+
assert.equal(collectionRef.resource, 'ghcr.io/devcontainers/templates');
20+
assert.equal(collectionRef.version, 'latest');
21+
});
22+
23+
it('valid getCollectionRef() that was originally uppercase', async () => {
24+
const collectionRef = getCollectionRef(output, 'GHCR.IO', 'DEVCONTAINERS/TEMPLATES');
25+
if (!collectionRef) {
26+
assert.fail('collectionRef should not be undefined');
27+
}
28+
assert.ok(collectionRef);
29+
assert.equal(collectionRef.registry, 'ghcr.io');
30+
assert.equal(collectionRef.path, 'devcontainers/templates');
31+
assert.equal(collectionRef.resource, 'ghcr.io/devcontainers/templates');
32+
assert.equal(collectionRef.version, 'latest');
33+
});
34+
35+
it('valid getCollectionRef() with port in registry', async () => {
36+
const collectionRef = getCollectionRef(output, 'ghcr.io:8001', 'devcontainers/templates');
37+
if (!collectionRef) {
38+
assert.fail('collectionRef should not be undefined');
39+
}
40+
assert.ok(collectionRef);
41+
assert.equal(collectionRef.registry, 'ghcr.io:8001');
42+
assert.equal(collectionRef.path, 'devcontainers/templates');
43+
assert.equal(collectionRef.resource, 'ghcr.io:8001/devcontainers/templates');
44+
assert.equal(collectionRef.version, 'latest');
45+
});
46+
47+
it('invalid getCollectionRef() with an invalid character in path', async () => {
48+
const collectionRef = getCollectionRef(output, 'ghcr.io', 'devcont%ainers/templates');
49+
assert.isUndefined(collectionRef);
50+
});
51+
52+
it('invalid getCollectionRef() with too many slashes in path', async () => {
53+
const collectionRef = getCollectionRef(output, 'ghcr.io', 'devcontainers//templates');
54+
assert.isUndefined(collectionRef);
55+
});
56+
57+
});
58+
759
describe('getRef()', async function () {
860
this.timeout('120s');
961

0 commit comments

Comments
 (0)