Skip to content

Commit

Permalink
${devcontainerId} (devcontainers/spec#62)
Browse files Browse the repository at this point in the history
  • Loading branch information
chrmarti committed Oct 24, 2022
1 parent 01a956e commit e651585
Show file tree
Hide file tree
Showing 5 changed files with 77 additions and 29 deletions.
30 changes: 29 additions & 1 deletion src/spec-common/variableSubstitution.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as path from 'path';
import * as crypto from 'crypto';

import { ContainerError } from './errors';
import { URI } from 'vscode-uri';
Expand Down Expand Up @@ -32,6 +33,11 @@ export function substitute<T extends object>(context: SubstitutionContext, value
return substitute0(replace, value);
}

export function beforeContainerSubstitute<T extends object>(idLabels: Record<string, string>, value: T): T {
let devcontainerId: string | undefined;
return substitute0(replaceDevContainerId.bind(undefined, () => devcontainerId || (devcontainerId = devcontainerIdForLabels(idLabels))), value);
}

export function containerSubstitute<T extends object>(platform: NodeJS.Platform, configFile: URI | undefined, containerEnv: NodeJS.ProcessEnv, value: T): T {
const isWindows = platform === 'win32';
return substitute0(replaceContainerEnv.bind(undefined, isWindows, configFile, normalizeEnv(isWindows, containerEnv)), value);
Expand All @@ -44,7 +50,7 @@ function substitute0(replace: Replace, value: any): any {
return resolveString(replace, value);
} else if (Array.isArray(value)) {
return value.map(s => substitute0(replace, s));
} else if (value && typeof value === 'object') {
} else if (value && typeof value === 'object' && !URI.isUri(value)) {
const result: any = Object.create(null);
Object.keys(value).forEach(key => {
result[key] = substitute0(replace, value[key]);
Expand Down Expand Up @@ -118,6 +124,16 @@ function replaceContainerEnv(isWindows: boolean, configFile: URI | undefined, co
}
}

function replaceDevContainerId(getDevContainerId: () => string, match: string, variable: string) {
switch (variable) {
case 'devcontainerId':
return getDevContainerId();

default:
return match;
}
}

function lookupValue(isWindows: boolean, envObj: NodeJS.ProcessEnv, args: string[], match: string, configFile: URI | undefined) {
if (args.length > 0) {
let envVariableName = args[0];
Expand All @@ -141,3 +157,15 @@ function lookupValue(isWindows: boolean, envObj: NodeJS.ProcessEnv, args: string
description: `'${match}'${configFile ? ` in ${path.posix.basename(configFile.path)}` : ''} can not be resolved because no environment variable name is given.`
});
}

function devcontainerIdForLabels(idLabels: Record<string, string>): string {
const stringInput = JSON.stringify(idLabels, Object.keys(idLabels).sort()); // sort properties
const bufferInput = Buffer.from(stringInput, 'utf-8');
const hash = crypto.createHash('sha256')
.update(bufferInput)
.digest();
const uniqueId = BigInt(`0x${hash.toString('hex')}`)
.toString(32)
.padStart(52, '0');
return uniqueId;
}
6 changes: 3 additions & 3 deletions src/spec-node/configContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@ import * as jsonc from 'jsonc-parser';

import { openDockerfileDevContainer } from './singleContainer';
import { openDockerComposeDevContainer } from './dockerCompose';
import { ResolverResult, DockerResolverParameters, isDockerFileConfig, runUserCommand, createDocuments, getWorkspaceConfiguration, BindMountConsistency, uriToFsPath, DevContainerAuthority, isDevContainerAuthority, SubstituteConfig, SubstitutedConfig } from './utils';
import { substitute } from '../spec-common/variableSubstitution';
import { ResolverResult, DockerResolverParameters, isDockerFileConfig, runUserCommand, createDocuments, getWorkspaceConfiguration, BindMountConsistency, uriToFsPath, DevContainerAuthority, isDevContainerAuthority, SubstituteConfig, SubstitutedConfig, addSubstitution, envListToObj } from './utils';
import { beforeContainerSubstitute, substitute } from '../spec-common/variableSubstitution';
import { ContainerError } from '../spec-common/errors';
import { Workspace, workspaceFromPath, isWorkspacePath } from '../spec-utils/workspaces';
import { URI } from 'vscode-uri';
Expand Down Expand Up @@ -52,7 +52,7 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu
throw new ContainerError({ description: `No dev container config and no workspace found.` });
}
}
const configWithRaw = configs.config;
const configWithRaw = addSubstitution(configs.config, config => beforeContainerSubstitute(envListToObj(idLabels), config));
const { config } = configWithRaw;

await runUserCommand({ ...params, common: { ...common, output: common.postCreate.output } }, config.initializeCommand, common.postCreate.onDidInput);
Expand Down
29 changes: 7 additions & 22 deletions src/spec-node/devContainersSpecCLI.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import yargs, { Argv } from 'yargs';
import * as jsonc from 'jsonc-parser';

import { createDockerParams, createLog, experimentalImageMetadataDefault, launch, ProvisionOptions } from './devContainers';
import { SubstitutedConfig, createContainerProperties, createFeaturesTempFolder, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig } from './utils';
import { SubstitutedConfig, createContainerProperties, createFeaturesTempFolder, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution } from './utils';
import { URI } from 'vscode-uri';
import { ContainerError } from '../spec-common/errors';
import { Log, LogLevel, makeLog, mapLogLevel } from '../spec-utils/log';
Expand All @@ -30,7 +30,7 @@ import { featuresTestOptions, featuresTestHandler } from './featuresCLI/test';
import { featuresPackageHandler, featuresPackageOptions } from './featuresCLI/package';
import { featuresPublishHandler, featuresPublishOptions } from './featuresCLI/publish';
import { featuresInfoHandler, featuresInfoOptions } from './featuresCLI/info';
import { containerSubstitute } from '../spec-common/variableSubstitution';
import { beforeContainerSubstitute, containerSubstitute } from '../spec-common/variableSubstitution';
import { getPackageConfig, PackageConfiguration } from '../spec-utils/product';
import { getDevcontainerMetadata, getImageBuildInfo, getImageMetadataFromContainer, ImageMetadataEntry, mergeConfiguration, MergedDevContainerConfig } from './imageMetadata';
import { templatesPublishHandler, templatesPublishOptions } from './templatesCLI/publish';
Expand Down Expand Up @@ -214,7 +214,7 @@ async function provision({
};
}) : [],
updateRemoteUserUIDDefault,
remoteEnv: keyValuesToRecord(addRemoteEnvs),
remoteEnv: envListToObj(addRemoteEnvs),
additionalCacheFroms: addCacheFroms,
useBuildKit: buildkit,
buildxPlatform: undefined,
Expand Down Expand Up @@ -597,7 +597,7 @@ async function doRunUserCommands({
persistedFolder,
additionalMounts: [],
updateRemoteUserUIDDefault: 'never',
remoteEnv: keyValuesToRecord(addRemoteEnvs),
remoteEnv: envListToObj(addRemoteEnvs),
additionalCacheFroms: [],
useBuildKit: 'auto',
buildxPlatform: undefined,
Expand Down Expand Up @@ -759,13 +759,8 @@ async function readConfiguration({
};
const container = containerId ? await inspectContainer(params, containerId) : await findDevContainer(params, idLabels);
if (container) {
const substitute1 = configuration.substitute;
const substitute2: SubstituteConfig = config => containerSubstitute(cliHost.platform, configuration.config.configFilePath, envListToObj(container.Config.Env), config);
configuration = {
config: substitute2(configuration.config),
raw: configuration.raw,
substitute: config => substitute2(substitute1(config)),
};
configuration = addSubstitution(configuration, config => beforeContainerSubstitute(envListToObj(idLabels), config));
configuration = addSubstitution(configuration, config => containerSubstitute(cliHost.platform, configuration.config.configFilePath, envListToObj(container.Config.Env), config));
}

const additionalFeatures = additionalFeaturesJson ? jsonc.parse(additionalFeaturesJson) as Record<string, string | boolean | Record<string, string | boolean>> : {};
Expand Down Expand Up @@ -927,7 +922,7 @@ export async function doExec({
persistedFolder,
additionalMounts: [],
updateRemoteUserUIDDefault: 'never',
remoteEnv: keyValuesToRecord(addRemoteEnvs),
remoteEnv: envListToObj(addRemoteEnvs),
additionalCacheFroms: [],
useBuildKit: 'auto',
omitLoggerHeader: true,
Expand Down Expand Up @@ -989,16 +984,6 @@ export async function doExec({
}
}

function keyValuesToRecord(keyValues: string[]): Record<string, string> {
return keyValues.reduce((envs, env) => {
const i = env.indexOf('=');
if (i !== -1) {
envs[env.substring(0, i)] = env.substring(i + 1);
}
return envs;
}, {} as Record<string, string>);
}

function getDefaultIdLabels(workspaceFolder: string) {
return [`${hostFolderLabel}=${workspaceFolder}`];
}
13 changes: 11 additions & 2 deletions src/spec-node/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,15 @@ export interface SubstitutedConfig<T> {

export type SubstituteConfig = <U extends DevContainerConfig | ImageMetadataEntry>(value: U) => U;

export function addSubstitution<T>(config: SubstitutedConfig<T>, substitute: SubstituteConfig): SubstitutedConfig<T> {
const substitute0 = config.substitute;
return {
config: substitute(config.config),
raw: config.raw,
substitute: value => substitute(substitute0(value)),
};
}

export async function startEventSeen(params: DockerResolverParameters, labels: Record<string, string>, canceled: Promise<void>, output: Log, trace: boolean) {
const eventsProcess = await getEvents(params, { event: ['start'] });
return {
Expand Down Expand Up @@ -350,10 +359,10 @@ export function envListToObj(list: string[] | null) {
return (list || []).reduce((obj, pair) => {
const i = pair.indexOf('=');
if (i !== -1) {
obj[pair.substr(0, i)] = pair.substr(i + 1);
obj[pair.substring(0, i)] = pair.substring(i + 1);
}
return obj;
}, {} as NodeJS.ProcessEnv);
}, {} as Record<string, string>);
}

export async function runUserCommand(params: DockerResolverParameters, command: string | string[] | undefined, onDidInput?: Event<string>) {
Expand Down
28 changes: 27 additions & 1 deletion src/test/variableSubstitution.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

import * as assert from 'assert';

import { containerSubstitute, substitute } from '../spec-common/variableSubstitution';
import { beforeContainerSubstitute, containerSubstitute, substitute } from '../spec-common/variableSubstitution';
import { URI } from 'vscode-uri';

describe('Variable substitution', function () {
Expand Down Expand Up @@ -141,4 +141,30 @@ describe('Variable substitution', function () {
const result = containerSubstitute('linux', URI.file('/foo/bar/baz.json'), {}, raw);
assert.strictEqual(result.foo, 'bardefaultbar');
});

it(`replaces devcontainerId`, async () => {
const raw = {
test: '${devcontainerId}'
};
const result = beforeContainerSubstitute({ a: 'b' }, raw);
assert.ok(/^[0-9a-v]{52}$/.test(result.test), `Got: ${result.test}`);
});

it(`replaces devcontainerId and additional id labels matter`, async () => {
const raw = {
test: '${devcontainerId}'
};
const result1 = beforeContainerSubstitute({ a: 'b' }, raw);
const result2 = beforeContainerSubstitute({ a: 'b', c: 'd' }, raw);
assert.notStrictEqual(result1.test, result2.test);
});

it(`replaces devcontainerId and label order does not matter`, async () => {
const raw = {
test: '${devcontainerId}'
};
const result1 = beforeContainerSubstitute({ c: 'd', a: 'b' }, raw);
const result2 = beforeContainerSubstitute({ a: 'b', c: 'd' }, raw);
assert.strictEqual(result1.test, result2.test);
});
});

0 comments on commit e651585

Please sign in to comment.