Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add 'templates metadata' subcommand #866

Merged
merged 2 commits into from
Aug 12, 2024
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
2 changes: 2 additions & 0 deletions src/spec-node/devContainersSpecCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ import { readFeaturesConfig } from './featureUtils';
import { featuresGenerateDocsHandler, featuresGenerateDocsOptions } from './featuresCLI/generateDocs';
import { templatesGenerateDocsHandler, templatesGenerateDocsOptions } from './templatesCLI/generateDocs';
import { mapNodeOSToGOOS, mapNodeArchitectureToGOARCH } from '../spec-configuration/containerCollectionsOCI';
import { templateMetadataHandler, templateMetadataOptions } from './templatesCLI/metadata';

const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell';

Expand Down Expand Up @@ -85,6 +86,7 @@ const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,externa
y.command('templates', 'Templates commands', (y: Argv) => {
y.command('apply', 'Apply a template to the project', templateApplyOptions, templateApplyHandler);
y.command('publish <target>', 'Package and publish templates', templatesPublishOptions, templatesPublishHandler);
y.command('metadata <templateId>', 'Fetch a published Template\'s metadata', templateMetadataOptions, templateMetadataHandler);
y.command('generate-docs', 'Generate documentation', templatesGenerateDocsOptions, templatesGenerateDocsHandler);
});
y.command(restArgs ? ['exec', '*'] : ['exec <cmd> [args..]'], 'Execute a command on a running dev container', execOptions, execHandler);
Expand Down
74 changes: 74 additions & 0 deletions src/spec-node/templatesCLI/metadata.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import { Argv } from 'yargs';
import { LogLevel, mapLogLevel } from '../../spec-utils/log';
import { getPackageConfig } from '../../spec-utils/product';
import { createLog } from '../devContainers';
import { fetchOCIManifestIfExists, getRef } from '../../spec-configuration/containerCollectionsOCI';

import { UnpackArgv } from '../devContainersSpecCLI';

export function templateMetadataOptions(y: Argv) {
return y
.options({
'log-level': { choices: ['info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' },
})
.positional('templateId', { type: 'string', demandOption: true, description: 'Template Identifier' });
}

export type TemplateMetadataArgs = UnpackArgv<ReturnType<typeof templateMetadataOptions>>;

export function templateMetadataHandler(args: TemplateMetadataArgs) {
(async () => await templateMetadata(args))().catch(console.error);
}

async function templateMetadata({
'log-level': inputLogLevel,
'templateId': templateId,
}: TemplateMetadataArgs) {
const disposables: (() => Promise<unknown> | undefined)[] = [];
const dispose = async () => {
await Promise.all(disposables.map(d => d()));
};

const pkg = getPackageConfig();

const output = createLog({
logLevel: mapLogLevel(inputLogLevel),
logFormat: 'text',
log: (str) => process.stderr.write(str),
terminalDimensions: undefined,
}, pkg, new Date(), disposables);

const params = { output, env: process.env };
output.write(`Fetching metadata for ${templateId}`, LogLevel.Trace);

const templateRef = getRef(output, templateId);
if (!templateRef) {
console.log(JSON.stringify({}));
process.exit(1);
}

const manifestContainer = await fetchOCIManifestIfExists(params, templateRef, undefined);
if (!manifestContainer) {
console.log(JSON.stringify({}));
process.exit(1);
}

const { manifestObj, canonicalId } = manifestContainer;
output.write(`Template '${templateId}' resolved to '${canonicalId}'`, LogLevel.Trace);

// Templates must have been published with a CLI post commit
// https://github.com/devcontainers/cli/commit/6c6aebfa7b74aea9d67760fd1e74b09573d31536
// in order to contain attached metadata.
const metadata = manifestObj.annotations?.['dev.containers.metadata'];
if (!metadata) {
output.write(`Template resolved to '${canonicalId}' but does not contain metadata on its manifest.`, LogLevel.Warning);
output.write(`Ask the Template owner to republish this Template to populate the manifest.`, LogLevel.Warning);
console.log(JSON.stringify({}));
process.exit(1);
}

const unescaped = JSON.parse(metadata);
console.log(JSON.stringify(unescaped));
await dispose();
process.exit();
}
38 changes: 37 additions & 1 deletion src/test/container-templates/templatesCLICommands.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ const pkg = require('../../../package.json');
describe('tests apply command', async function () {
this.timeout('120s');

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

before('Install', async () => {
Expand Down Expand Up @@ -197,3 +197,39 @@ describe('tests generateTemplateDocumentation()', async function () {
assert.isFalse(invalidDocsExists);
});
});

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

const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp7'));
const cli = `npx --prefix ${tmp} devcontainer`;

// https://github.com/codspace/templates/pkgs/container/templates%2Fmytemplate/255979159?tag=1.0.4
const templateId = 'ghcr.io/codspace/templates/mytemplate@sha256:57cbf968907c74c106b7b2446063d114743ab3f63345f7c108c577915c535185';

before('Install', async () => {
await shellExec(`rm -rf ${tmp}/node_modules`);
await shellExec(`rm -rf ${tmp}/output`);
await shellExec(`mkdir -p ${tmp}`);
await shellExec(`npm --prefix ${tmp} install devcontainers-cli-${pkg.version}.tgz`);
});

it('successfully fetches metdata off a published Template', async function () {
let success = false;
let result: ExecResult | undefined = undefined;
try {
result = await shellExec(`${cli} templates metadata ${templateId} --log-level trace`);
success = true;

} catch (error) {
assert.fail('features test sub-command should not throw');
}

assert.isTrue(success);
assert.isDefined(result);
const json = JSON.parse(result.stdout);
assert.strictEqual('mytemplate', json.id);
assert.strictEqual('Simple test', json.description);

});
});