Skip to content

Commit ed43a5b

Browse files
authored
Merge pull request #866 from devcontainers/joshspicer/template-metadata
add 'templates metadata' subcommand
2 parents 6c6aebf + a44bb9c commit ed43a5b

File tree

3 files changed

+113
-1
lines changed

3 files changed

+113
-1
lines changed

src/spec-node/devContainersSpecCLI.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ import { readFeaturesConfig } from './featureUtils';
4444
import { featuresGenerateDocsHandler, featuresGenerateDocsOptions } from './featuresCLI/generateDocs';
4545
import { templatesGenerateDocsHandler, templatesGenerateDocsOptions } from './templatesCLI/generateDocs';
4646
import { mapNodeOSToGOOS, mapNodeArchitectureToGOARCH } from '../spec-configuration/containerCollectionsOCI';
47+
import { templateMetadataHandler, templateMetadataOptions } from './templatesCLI/metadata';
4748

4849
const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell';
4950

@@ -85,6 +86,7 @@ const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,externa
8586
y.command('templates', 'Templates commands', (y: Argv) => {
8687
y.command('apply', 'Apply a template to the project', templateApplyOptions, templateApplyHandler);
8788
y.command('publish <target>', 'Package and publish templates', templatesPublishOptions, templatesPublishHandler);
89+
y.command('metadata <templateId>', 'Fetch a published Template\'s metadata', templateMetadataOptions, templateMetadataHandler);
8890
y.command('generate-docs', 'Generate documentation', templatesGenerateDocsOptions, templatesGenerateDocsHandler);
8991
});
9092
y.command(restArgs ? ['exec', '*'] : ['exec <cmd> [args..]'], 'Execute a command on a running dev container', execOptions, execHandler);
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { Argv } from 'yargs';
2+
import { LogLevel, mapLogLevel } from '../../spec-utils/log';
3+
import { getPackageConfig } from '../../spec-utils/product';
4+
import { createLog } from '../devContainers';
5+
import { fetchOCIManifestIfExists, getRef } from '../../spec-configuration/containerCollectionsOCI';
6+
7+
import { UnpackArgv } from '../devContainersSpecCLI';
8+
9+
export function templateMetadataOptions(y: Argv) {
10+
return y
11+
.options({
12+
'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' },
13+
})
14+
.positional('templateId', { type: 'string', demandOption: true, description: 'Template Identifier' });
15+
}
16+
17+
export type TemplateMetadataArgs = UnpackArgv<ReturnType<typeof templateMetadataOptions>>;
18+
19+
export function templateMetadataHandler(args: TemplateMetadataArgs) {
20+
(async () => await templateMetadata(args))().catch(console.error);
21+
}
22+
23+
async function templateMetadata({
24+
'log-level': inputLogLevel,
25+
'templateId': templateId,
26+
}: TemplateMetadataArgs) {
27+
const disposables: (() => Promise<unknown> | undefined)[] = [];
28+
const dispose = async () => {
29+
await Promise.all(disposables.map(d => d()));
30+
};
31+
32+
const pkg = getPackageConfig();
33+
34+
const output = createLog({
35+
logLevel: mapLogLevel(inputLogLevel),
36+
logFormat: 'text',
37+
log: (str) => process.stderr.write(str),
38+
terminalDimensions: undefined,
39+
}, pkg, new Date(), disposables);
40+
41+
const params = { output, env: process.env };
42+
output.write(`Fetching metadata for ${templateId}`, LogLevel.Trace);
43+
44+
const templateRef = getRef(output, templateId);
45+
if (!templateRef) {
46+
console.log(JSON.stringify({}));
47+
process.exit(1);
48+
}
49+
50+
const manifestContainer = await fetchOCIManifestIfExists(params, templateRef, undefined);
51+
if (!manifestContainer) {
52+
console.log(JSON.stringify({}));
53+
process.exit(1);
54+
}
55+
56+
const { manifestObj, canonicalId } = manifestContainer;
57+
output.write(`Template '${templateId}' resolved to '${canonicalId}'`, LogLevel.Trace);
58+
59+
// Templates must have been published with a CLI post commit
60+
// https://github.com/devcontainers/cli/commit/6c6aebfa7b74aea9d67760fd1e74b09573d31536
61+
// in order to contain attached metadata.
62+
const metadata = manifestObj.annotations?.['dev.containers.metadata'];
63+
if (!metadata) {
64+
output.write(`Template resolved to '${canonicalId}' but does not contain metadata on its manifest.`, LogLevel.Warning);
65+
output.write(`Ask the Template owner to republish this Template to populate the manifest.`, LogLevel.Warning);
66+
console.log(JSON.stringify({}));
67+
process.exit(1);
68+
}
69+
70+
const unescaped = JSON.parse(metadata);
71+
console.log(JSON.stringify(unescaped));
72+
await dispose();
73+
process.exit();
74+
}

src/test/container-templates/templatesCLICommands.test.ts

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ const pkg = require('../../../package.json');
1717
describe('tests apply command', async function () {
1818
this.timeout('120s');
1919

20-
const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp4'));
20+
const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp6'));
2121
const cli = `npx --prefix ${tmp} devcontainer`;
2222

2323
before('Install', async () => {
@@ -197,3 +197,39 @@ describe('tests generateTemplateDocumentation()', async function () {
197197
assert.isFalse(invalidDocsExists);
198198
});
199199
});
200+
201+
describe('template metadata', async function () {
202+
this.timeout('120s');
203+
204+
const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp7'));
205+
const cli = `npx --prefix ${tmp} devcontainer`;
206+
207+
// https://github.com/codspace/templates/pkgs/container/templates%2Fmytemplate/255979159?tag=1.0.4
208+
const templateId = 'ghcr.io/codspace/templates/mytemplate@sha256:57cbf968907c74c106b7b2446063d114743ab3f63345f7c108c577915c535185';
209+
210+
before('Install', async () => {
211+
await shellExec(`rm -rf ${tmp}/node_modules`);
212+
await shellExec(`rm -rf ${tmp}/output`);
213+
await shellExec(`mkdir -p ${tmp}`);
214+
await shellExec(`npm --prefix ${tmp} install devcontainers-cli-${pkg.version}.tgz`);
215+
});
216+
217+
it('successfully fetches metdata off a published Template', async function () {
218+
let success = false;
219+
let result: ExecResult | undefined = undefined;
220+
try {
221+
result = await shellExec(`${cli} templates metadata ${templateId} --log-level trace`);
222+
success = true;
223+
224+
} catch (error) {
225+
assert.fail('features test sub-command should not throw');
226+
}
227+
228+
assert.isTrue(success);
229+
assert.isDefined(result);
230+
const json = JSON.parse(result.stdout);
231+
assert.strictEqual('mytemplate', json.id);
232+
assert.strictEqual('Simple test', json.description);
233+
234+
});
235+
});

0 commit comments

Comments
 (0)