From ff24b1af5c1ca5fd660b94340ca63307998d5ef2 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Thu, 1 Aug 2024 17:03:27 +0000 Subject: [PATCH] add 'templates metadata' subcommand --- src/spec-node/devContainersSpecCLI.ts | 2 + src/spec-node/templatesCLI/metadata.ts | 74 ++++++++++++++++++++++++++ 2 files changed, 76 insertions(+) create mode 100644 src/spec-node/templatesCLI/metadata.ts diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 6f48f8df2..b4f71a801 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -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'; @@ -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 ', 'Package and publish templates', templatesPublishOptions, templatesPublishHandler); + y.command('metadata ', 'Fetch a published Template\'s metadata', templateMetadataOptions, templateMetadataHandler); y.command('generate-docs', 'Generate documentation', templatesGenerateDocsOptions, templatesGenerateDocsHandler); }); y.command(restArgs ? ['exec', '*'] : ['exec [args..]'], 'Execute a command on a running dev container', execOptions, execHandler); diff --git a/src/spec-node/templatesCLI/metadata.ts b/src/spec-node/templatesCLI/metadata.ts new file mode 100644 index 000000000..72c69a133 --- /dev/null +++ b/src/spec-node/templatesCLI/metadata.ts @@ -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>; + +export function templateMetadataHandler(args: TemplateMetadataArgs) { + (async () => await templateMetadata(args))().catch(console.error); +} + +async function templateMetadata({ + 'log-level': inputLogLevel, + 'templateId': templateId, +}: TemplateMetadataArgs) { + const disposables: (() => Promise | 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}' and 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(); +}