Skip to content

Commit e651585

Browse files
committed
${devcontainerId} (devcontainers/spec#62)
1 parent 01a956e commit e651585

File tree

5 files changed

+77
-29
lines changed

5 files changed

+77
-29
lines changed

src/spec-common/variableSubstitution.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*--------------------------------------------------------------------------------------------*/
55

66
import * as path from 'path';
7+
import * as crypto from 'crypto';
78

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

36+
export function beforeContainerSubstitute<T extends object>(idLabels: Record<string, string>, value: T): T {
37+
let devcontainerId: string | undefined;
38+
return substitute0(replaceDevContainerId.bind(undefined, () => devcontainerId || (devcontainerId = devcontainerIdForLabels(idLabels))), value);
39+
}
40+
3541
export function containerSubstitute<T extends object>(platform: NodeJS.Platform, configFile: URI | undefined, containerEnv: NodeJS.ProcessEnv, value: T): T {
3642
const isWindows = platform === 'win32';
3743
return substitute0(replaceContainerEnv.bind(undefined, isWindows, configFile, normalizeEnv(isWindows, containerEnv)), value);
@@ -44,7 +50,7 @@ function substitute0(replace: Replace, value: any): any {
4450
return resolveString(replace, value);
4551
} else if (Array.isArray(value)) {
4652
return value.map(s => substitute0(replace, s));
47-
} else if (value && typeof value === 'object') {
53+
} else if (value && typeof value === 'object' && !URI.isUri(value)) {
4854
const result: any = Object.create(null);
4955
Object.keys(value).forEach(key => {
5056
result[key] = substitute0(replace, value[key]);
@@ -118,6 +124,16 @@ function replaceContainerEnv(isWindows: boolean, configFile: URI | undefined, co
118124
}
119125
}
120126

127+
function replaceDevContainerId(getDevContainerId: () => string, match: string, variable: string) {
128+
switch (variable) {
129+
case 'devcontainerId':
130+
return getDevContainerId();
131+
132+
default:
133+
return match;
134+
}
135+
}
136+
121137
function lookupValue(isWindows: boolean, envObj: NodeJS.ProcessEnv, args: string[], match: string, configFile: URI | undefined) {
122138
if (args.length > 0) {
123139
let envVariableName = args[0];
@@ -141,3 +157,15 @@ function lookupValue(isWindows: boolean, envObj: NodeJS.ProcessEnv, args: string
141157
description: `'${match}'${configFile ? ` in ${path.posix.basename(configFile.path)}` : ''} can not be resolved because no environment variable name is given.`
142158
});
143159
}
160+
161+
function devcontainerIdForLabels(idLabels: Record<string, string>): string {
162+
const stringInput = JSON.stringify(idLabels, Object.keys(idLabels).sort()); // sort properties
163+
const bufferInput = Buffer.from(stringInput, 'utf-8');
164+
const hash = crypto.createHash('sha256')
165+
.update(bufferInput)
166+
.digest();
167+
const uniqueId = BigInt(`0x${hash.toString('hex')}`)
168+
.toString(32)
169+
.padStart(52, '0');
170+
return uniqueId;
171+
}

src/spec-node/configContainer.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,8 +9,8 @@ import * as jsonc from 'jsonc-parser';
99

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

5858
await runUserCommand({ ...params, common: { ...common, output: common.postCreate.output } }, config.initializeCommand, common.postCreate.onDidInput);

src/spec-node/devContainersSpecCLI.ts

Lines changed: 7 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import yargs, { Argv } from 'yargs';
99
import * as jsonc from 'jsonc-parser';
1010

1111
import { createDockerParams, createLog, experimentalImageMetadataDefault, launch, ProvisionOptions } from './devContainers';
12-
import { SubstitutedConfig, createContainerProperties, createFeaturesTempFolder, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig } from './utils';
12+
import { SubstitutedConfig, createContainerProperties, createFeaturesTempFolder, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution } from './utils';
1313
import { URI } from 'vscode-uri';
1414
import { ContainerError } from '../spec-common/errors';
1515
import { Log, LogLevel, makeLog, mapLogLevel } from '../spec-utils/log';
@@ -30,7 +30,7 @@ import { featuresTestOptions, featuresTestHandler } from './featuresCLI/test';
3030
import { featuresPackageHandler, featuresPackageOptions } from './featuresCLI/package';
3131
import { featuresPublishHandler, featuresPublishOptions } from './featuresCLI/publish';
3232
import { featuresInfoHandler, featuresInfoOptions } from './featuresCLI/info';
33-
import { containerSubstitute } from '../spec-common/variableSubstitution';
33+
import { beforeContainerSubstitute, containerSubstitute } from '../spec-common/variableSubstitution';
3434
import { getPackageConfig, PackageConfiguration } from '../spec-utils/product';
3535
import { getDevcontainerMetadata, getImageBuildInfo, getImageMetadataFromContainer, ImageMetadataEntry, mergeConfiguration, MergedDevContainerConfig } from './imageMetadata';
3636
import { templatesPublishHandler, templatesPublishOptions } from './templatesCLI/publish';
@@ -214,7 +214,7 @@ async function provision({
214214
};
215215
}) : [],
216216
updateRemoteUserUIDDefault,
217-
remoteEnv: keyValuesToRecord(addRemoteEnvs),
217+
remoteEnv: envListToObj(addRemoteEnvs),
218218
additionalCacheFroms: addCacheFroms,
219219
useBuildKit: buildkit,
220220
buildxPlatform: undefined,
@@ -597,7 +597,7 @@ async function doRunUserCommands({
597597
persistedFolder,
598598
additionalMounts: [],
599599
updateRemoteUserUIDDefault: 'never',
600-
remoteEnv: keyValuesToRecord(addRemoteEnvs),
600+
remoteEnv: envListToObj(addRemoteEnvs),
601601
additionalCacheFroms: [],
602602
useBuildKit: 'auto',
603603
buildxPlatform: undefined,
@@ -759,13 +759,8 @@ async function readConfiguration({
759759
};
760760
const container = containerId ? await inspectContainer(params, containerId) : await findDevContainer(params, idLabels);
761761
if (container) {
762-
const substitute1 = configuration.substitute;
763-
const substitute2: SubstituteConfig = config => containerSubstitute(cliHost.platform, configuration.config.configFilePath, envListToObj(container.Config.Env), config);
764-
configuration = {
765-
config: substitute2(configuration.config),
766-
raw: configuration.raw,
767-
substitute: config => substitute2(substitute1(config)),
768-
};
762+
configuration = addSubstitution(configuration, config => beforeContainerSubstitute(envListToObj(idLabels), config));
763+
configuration = addSubstitution(configuration, config => containerSubstitute(cliHost.platform, configuration.config.configFilePath, envListToObj(container.Config.Env), config));
769764
}
770765

771766
const additionalFeatures = additionalFeaturesJson ? jsonc.parse(additionalFeaturesJson) as Record<string, string | boolean | Record<string, string | boolean>> : {};
@@ -927,7 +922,7 @@ export async function doExec({
927922
persistedFolder,
928923
additionalMounts: [],
929924
updateRemoteUserUIDDefault: 'never',
930-
remoteEnv: keyValuesToRecord(addRemoteEnvs),
925+
remoteEnv: envListToObj(addRemoteEnvs),
931926
additionalCacheFroms: [],
932927
useBuildKit: 'auto',
933928
omitLoggerHeader: true,
@@ -989,16 +984,6 @@ export async function doExec({
989984
}
990985
}
991986

992-
function keyValuesToRecord(keyValues: string[]): Record<string, string> {
993-
return keyValues.reduce((envs, env) => {
994-
const i = env.indexOf('=');
995-
if (i !== -1) {
996-
envs[env.substring(0, i)] = env.substring(i + 1);
997-
}
998-
return envs;
999-
}, {} as Record<string, string>);
1000-
}
1001-
1002987
function getDefaultIdLabels(workspaceFolder: string) {
1003988
return [`${hostFolderLabel}=${workspaceFolder}`];
1004989
}

src/spec-node/utils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,15 @@ export interface SubstitutedConfig<T> {
120120

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

123+
export function addSubstitution<T>(config: SubstitutedConfig<T>, substitute: SubstituteConfig): SubstitutedConfig<T> {
124+
const substitute0 = config.substitute;
125+
return {
126+
config: substitute(config.config),
127+
raw: config.raw,
128+
substitute: value => substitute(substitute0(value)),
129+
};
130+
}
131+
123132
export async function startEventSeen(params: DockerResolverParameters, labels: Record<string, string>, canceled: Promise<void>, output: Log, trace: boolean) {
124133
const eventsProcess = await getEvents(params, { event: ['start'] });
125134
return {
@@ -350,10 +359,10 @@ export function envListToObj(list: string[] | null) {
350359
return (list || []).reduce((obj, pair) => {
351360
const i = pair.indexOf('=');
352361
if (i !== -1) {
353-
obj[pair.substr(0, i)] = pair.substr(i + 1);
362+
obj[pair.substring(0, i)] = pair.substring(i + 1);
354363
}
355364
return obj;
356-
}, {} as NodeJS.ProcessEnv);
365+
}, {} as Record<string, string>);
357366
}
358367

359368
export async function runUserCommand(params: DockerResolverParameters, command: string | string[] | undefined, onDidInput?: Event<string>) {

src/test/variableSubstitution.test.ts

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44

55
import * as assert from 'assert';
66

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

1010
describe('Variable substitution', function () {
@@ -141,4 +141,30 @@ describe('Variable substitution', function () {
141141
const result = containerSubstitute('linux', URI.file('/foo/bar/baz.json'), {}, raw);
142142
assert.strictEqual(result.foo, 'bardefaultbar');
143143
});
144+
145+
it(`replaces devcontainerId`, async () => {
146+
const raw = {
147+
test: '${devcontainerId}'
148+
};
149+
const result = beforeContainerSubstitute({ a: 'b' }, raw);
150+
assert.ok(/^[0-9a-v]{52}$/.test(result.test), `Got: ${result.test}`);
151+
});
152+
153+
it(`replaces devcontainerId and additional id labels matter`, async () => {
154+
const raw = {
155+
test: '${devcontainerId}'
156+
};
157+
const result1 = beforeContainerSubstitute({ a: 'b' }, raw);
158+
const result2 = beforeContainerSubstitute({ a: 'b', c: 'd' }, raw);
159+
assert.notStrictEqual(result1.test, result2.test);
160+
});
161+
162+
it(`replaces devcontainerId and label order does not matter`, async () => {
163+
const raw = {
164+
test: '${devcontainerId}'
165+
};
166+
const result1 = beforeContainerSubstitute({ c: 'd', a: 'b' }, raw);
167+
const result2 = beforeContainerSubstitute({ a: 'b', c: 'd' }, raw);
168+
assert.strictEqual(result1.test, result2.test);
169+
});
144170
});

0 commit comments

Comments
 (0)