From c47cb271fe30ef8ee6ba940e6fa121d29935b737 Mon Sep 17 00:00:00 2001 From: max06 Date: Tue, 25 Jul 2023 21:52:09 +0000 Subject: [PATCH 1/5] feat: :sparkles: Support ${remoteUser} in feature definitions This change allows using `${remoteUser}` inside of devcontainer-feature.json similar to the exising ${devcontainerId}. --- src/spec-common/variableSubstitution.ts | 4 ++++ src/spec-node/configContainer.ts | 1 + .../.devcontainer/devcontainer.json | 8 ++++++++ .../test-feature/devcontainer-feature.json | 14 ++++++++++++++ .../.devcontainer/test-feature/install.sh | 3 +++ 5 files changed, 30 insertions(+) create mode 100644 src/test/configs/image-with-local-feature/.devcontainer/devcontainer.json create mode 100644 src/test/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json create mode 100644 src/test/configs/image-with-local-feature/.devcontainer/test-feature/install.sh diff --git a/src/spec-common/variableSubstitution.ts b/src/spec-common/variableSubstitution.ts index d973f0cd6..accdff3bb 100644 --- a/src/spec-common/variableSubstitution.ts +++ b/src/spec-common/variableSubstitution.ts @@ -15,6 +15,7 @@ export interface SubstitutionContext { localWorkspaceFolder?: string; containerWorkspaceFolder?: string; env: NodeJS.ProcessEnv; + remoteUser?: string; } export function substitute(context: SubstitutionContext, value: T): T { @@ -109,6 +110,9 @@ function replaceWithContext(isWindows: boolean, context: SubstitutionContext, ma case 'containerWorkspaceFolderBasename': return context.containerWorkspaceFolder !== undefined ? path.posix.basename(context.containerWorkspaceFolder) : match; + case 'remoteUser': + return context.remoteUser !== undefined ? context.remoteUser : match; + default: return match; } diff --git a/src/spec-node/configContainer.ts b/src/spec-node/configContainer.ts index dddd08d58..ecffa5c87 100644 --- a/src/spec-node/configContainer.ts +++ b/src/spec-node/configContainer.ts @@ -97,6 +97,7 @@ export async function readDevContainerConfigFile(cliHost: CLIHost, workspace: Wo containerWorkspaceFolder: workspaceConfig.workspaceFolder, configFile, env: cliHost.env, + remoteUser: updated.remoteUser }, value); const config: DevContainerConfig = substitute0(updated); if (typeof config.workspaceFolder === 'string') { diff --git a/src/test/configs/image-with-local-feature/.devcontainer/devcontainer.json b/src/test/configs/image-with-local-feature/.devcontainer/devcontainer.json new file mode 100644 index 000000000..0749ce284 --- /dev/null +++ b/src/test/configs/image-with-local-feature/.devcontainer/devcontainer.json @@ -0,0 +1,8 @@ +{ + "name": "Demo container", + "image": "mcr.microsoft.com/devcontainers/base:bullseye", + "features": { + "./test-feature": {} + }, + "remoteUser": "vscode" +} \ No newline at end of file diff --git a/src/test/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json b/src/test/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json new file mode 100644 index 000000000..f2da6e155 --- /dev/null +++ b/src/test/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json @@ -0,0 +1,14 @@ +{ + "$schema": "https://raw.githubusercontent.com/devcontainers/spec/main/schemas/devContainerFeature.schema.json", + "id": "test-feature", + "name": "test-feature", + "version": "0.0.1", + "description": "Testing a feature", + "mounts": [ + { + "source": "test-${devcontainerId}", + "target": "/home/${remoteUser}", + "type": "volume" + } + ] +} \ No newline at end of file diff --git a/src/test/configs/image-with-local-feature/.devcontainer/test-feature/install.sh b/src/test/configs/image-with-local-feature/.devcontainer/test-feature/install.sh new file mode 100644 index 000000000..41cde2f90 --- /dev/null +++ b/src/test/configs/image-with-local-feature/.devcontainer/test-feature/install.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +echo "I am the install script and I do nothing." \ No newline at end of file From 6bdccac5450f1bff00a7544d0e34c7074738c316 Mon Sep 17 00:00:00 2001 From: max06 Date: Tue, 25 Jul 2023 22:35:52 +0000 Subject: [PATCH 2/5] test: Add test for `image-with-local-feature` --- src/test/cli.build.test.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/test/cli.build.test.ts b/src/test/cli.build.test.ts index b16fd45fb..acbf930dc 100644 --- a/src/test/cli.build.test.ts +++ b/src/test/cli.build.test.ts @@ -315,5 +315,11 @@ describe('Dev Containers CLI', function () { const details = JSON.parse((await shellExec(`docker inspect test-subfolder-config`)).stdout)[0] as ImageDetails; assert.strictEqual(envListToObj(details.Config.Env).SUBFOLDER_CONFIG_IMAGE_ENV, 'true'); }); + + it('should build with a local feature', async () => { + const res = await shellExec(`${cli} build --workspace-folder ${__dirname}/configs/image-with-local-feature --image-name test-local-feature`) + const response = JSON.parse(res.stdout); + assert.strictEqual(response.outcome, 'success'); + }); }); }); From cf32d27714035a16455837b7b307aeba6b65f8af Mon Sep 17 00:00:00 2001 From: max06 Date: Wed, 26 Jul 2023 11:20:09 +0000 Subject: [PATCH 3/5] test: Improve test to use lifecycleHooks --- src/test/cli.build.test.ts | 6 ---- .../.devcontainer/devcontainer.json | 0 .../test-feature/devcontainer-feature.json | 3 +- .../.devcontainer/test-feature/install.sh | 0 .../container-features/lifecycleHooks.test.ts | 32 +++++++++++++++++++ 5 files changed, 34 insertions(+), 7 deletions(-) rename src/test/{ => container-features}/configs/image-with-local-feature/.devcontainer/devcontainer.json (100%) rename src/test/{ => container-features}/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json (76%) rename src/test/{ => container-features}/configs/image-with-local-feature/.devcontainer/test-feature/install.sh (100%) diff --git a/src/test/cli.build.test.ts b/src/test/cli.build.test.ts index acbf930dc..b16fd45fb 100644 --- a/src/test/cli.build.test.ts +++ b/src/test/cli.build.test.ts @@ -315,11 +315,5 @@ describe('Dev Containers CLI', function () { const details = JSON.parse((await shellExec(`docker inspect test-subfolder-config`)).stdout)[0] as ImageDetails; assert.strictEqual(envListToObj(details.Config.Env).SUBFOLDER_CONFIG_IMAGE_ENV, 'true'); }); - - it('should build with a local feature', async () => { - const res = await shellExec(`${cli} build --workspace-folder ${__dirname}/configs/image-with-local-feature --image-name test-local-feature`) - const response = JSON.parse(res.stdout); - assert.strictEqual(response.outcome, 'success'); - }); }); }); diff --git a/src/test/configs/image-with-local-feature/.devcontainer/devcontainer.json b/src/test/container-features/configs/image-with-local-feature/.devcontainer/devcontainer.json similarity index 100% rename from src/test/configs/image-with-local-feature/.devcontainer/devcontainer.json rename to src/test/container-features/configs/image-with-local-feature/.devcontainer/devcontainer.json diff --git a/src/test/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json b/src/test/container-features/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json similarity index 76% rename from src/test/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json rename to src/test/container-features/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json index f2da6e155..3a8b63cfd 100644 --- a/src/test/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json +++ b/src/test/container-features/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json @@ -10,5 +10,6 @@ "target": "/home/${remoteUser}", "type": "volume" } - ] + ], + "postCreateCommand": "/usr/bin/whoami && echo ${remoteUser} > /tmp/variable-substitution.testMarker" } \ No newline at end of file diff --git a/src/test/configs/image-with-local-feature/.devcontainer/test-feature/install.sh b/src/test/container-features/configs/image-with-local-feature/.devcontainer/test-feature/install.sh similarity index 100% rename from src/test/configs/image-with-local-feature/.devcontainer/test-feature/install.sh rename to src/test/container-features/configs/image-with-local-feature/.devcontainer/test-feature/install.sh diff --git a/src/test/container-features/lifecycleHooks.test.ts b/src/test/container-features/lifecycleHooks.test.ts index 325d2b900..af3de8664 100644 --- a/src/test/container-features/lifecycleHooks.test.ts +++ b/src/test/container-features/lifecycleHooks.test.ts @@ -371,6 +371,38 @@ describe('Feature lifecycle hooks', function () { }); }); + describe('lifecycle-hooks-with-variable-substitution', () => { + describe('devcontainer up', () => { + let containerId: string | null = null; + let containerUpStandardError: string; + const testFolder = `${__dirname}/configs/image-with-local-feature`; + + before(async () => { + await shellExec(`rm -f ${testFolder}/*.testMarker`, undefined, undefined, true); + const res = await devContainerUp(cli, testFolder, { 'logLevel': 'trace' }); + containerId = res.containerId; + containerUpStandardError = res.stderr; + }); + + after(async () => { + await devContainerDown({ containerId }); + await shellExec(`rm -f ${testFolder}/*.testMarker`, undefined, undefined, true); + }); + + it('executes lifecycle hooks with variable substitution', async () => { + const res = await shellExec(`${cli} exec --workspace-folder ${testFolder} cat /tmp/variable-substitution.testMarker`); + assert.strictEqual(res.error, null); + + const outputOfExecCommand = res.stdout; + console.log(outputOfExecCommand); + + // Executes the command that was installed by the local Feature's 'postCreateCommand'. + assert.match(outputOfExecCommand, /vscode/); + assert.match(containerUpStandardError, /Running the postCreateCommand from Feature '.\/test-feature/); + }); + }); + }); + describe('lifecycle-hooks-advanced', () => { describe(`devcontainer up`, () => { From 1afe943983ef259bc1138a312771d49b337cc6c1 Mon Sep 17 00:00:00 2001 From: max06 Date: Tue, 5 Sep 2023 20:53:51 +0000 Subject: [PATCH 4/5] Add additional tests --- .../.devcontainer/devcontainer.json | 3 +- .../test-feature/devcontainer-feature.json | 2 +- .../container-features/lifecycleHooks.test.ts | 29 +++++++++++++++---- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/test/container-features/configs/image-with-local-feature/.devcontainer/devcontainer.json b/src/test/container-features/configs/image-with-local-feature/.devcontainer/devcontainer.json index 0749ce284..dd5428211 100644 --- a/src/test/container-features/configs/image-with-local-feature/.devcontainer/devcontainer.json +++ b/src/test/container-features/configs/image-with-local-feature/.devcontainer/devcontainer.json @@ -4,5 +4,6 @@ "features": { "./test-feature": {} }, - "remoteUser": "vscode" + "remoteUser": "vscode", + "postCreateCommand": "echo ${remoteUser} > /tmp/container.variable-substitution.testMarker" } \ No newline at end of file diff --git a/src/test/container-features/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json b/src/test/container-features/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json index 3a8b63cfd..32ef54c9d 100644 --- a/src/test/container-features/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json +++ b/src/test/container-features/configs/image-with-local-feature/.devcontainer/test-feature/devcontainer-feature.json @@ -11,5 +11,5 @@ "type": "volume" } ], - "postCreateCommand": "/usr/bin/whoami && echo ${remoteUser} > /tmp/variable-substitution.testMarker" + "postCreateCommand": "/usr/bin/whoami && echo ${remoteUser} > /tmp/feature.variable-substitution.testMarker" } \ No newline at end of file diff --git a/src/test/container-features/lifecycleHooks.test.ts b/src/test/container-features/lifecycleHooks.test.ts index af3de8664..ade8310b2 100644 --- a/src/test/container-features/lifecycleHooks.test.ts +++ b/src/test/container-features/lifecycleHooks.test.ts @@ -390,15 +390,34 @@ describe('Feature lifecycle hooks', function () { }); it('executes lifecycle hooks with variable substitution', async () => { - const res = await shellExec(`${cli} exec --workspace-folder ${testFolder} cat /tmp/variable-substitution.testMarker`); - assert.strictEqual(res.error, null); + const res1 = await shellExec(`${cli} exec --workspace-folder ${testFolder} cat /tmp/feature.variable-substitution.testMarker`); + assert.strictEqual(res1.error, null); - const outputOfExecCommand = res.stdout; - console.log(outputOfExecCommand); + const outputOfExecCommand1 = res1.stdout; + console.log(outputOfExecCommand1); // Executes the command that was installed by the local Feature's 'postCreateCommand'. - assert.match(outputOfExecCommand, /vscode/); + assert.strictEqual(outputOfExecCommand1, 'vscode\n'); assert.match(containerUpStandardError, /Running the postCreateCommand from Feature '.\/test-feature/); + + // substitutuin in main devcontainer.json + const res2 = await shellExec(`${cli} exec --workspace-folder ${testFolder} cat /tmp/container.variable-substitution.testMarker`); + assert.strictEqual(res2.error, null); + + const outputOfExecCommand2 = res2.stdout; + console.log(outputOfExecCommand2); + + // Executes the command that was installed by the local Feature's 'postCreateCommand'. + assert.strictEqual(outputOfExecCommand2, 'vscode\n'); + assert.match(containerUpStandardError, /Running the postCreateCommand from devcontainer.json/); + + // Check if substituted mount path is in use + const res3 = await shellExec(`docker inspect ${containerId} --format '{{json .Mounts}}'`); + assert.strictEqual(res3.error, null); + + // json parse res3 + const mounts = JSON.parse(res3.stdout); + assert.exists(mounts.find((item: { Type: string; Destination: string }) => item.Type === 'volume' && item.Destination === '/home/vscode')); }); }); }); From 92ca28cd71cb627830c53a1d979554a4a45b14e9 Mon Sep 17 00:00:00 2001 From: max06 Date: Tue, 5 Sep 2023 20:57:37 +0000 Subject: [PATCH 5/5] Typos in comments --- src/test/container-features/lifecycleHooks.test.ts | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/test/container-features/lifecycleHooks.test.ts b/src/test/container-features/lifecycleHooks.test.ts index ade8310b2..2e61a93fd 100644 --- a/src/test/container-features/lifecycleHooks.test.ts +++ b/src/test/container-features/lifecycleHooks.test.ts @@ -390,24 +390,23 @@ describe('Feature lifecycle hooks', function () { }); it('executes lifecycle hooks with variable substitution', async () => { + // substitution in feature const res1 = await shellExec(`${cli} exec --workspace-folder ${testFolder} cat /tmp/feature.variable-substitution.testMarker`); assert.strictEqual(res1.error, null); const outputOfExecCommand1 = res1.stdout; console.log(outputOfExecCommand1); - // Executes the command that was installed by the local Feature's 'postCreateCommand'. assert.strictEqual(outputOfExecCommand1, 'vscode\n'); assert.match(containerUpStandardError, /Running the postCreateCommand from Feature '.\/test-feature/); - // substitutuin in main devcontainer.json + // substitution in main devcontainer.json const res2 = await shellExec(`${cli} exec --workspace-folder ${testFolder} cat /tmp/container.variable-substitution.testMarker`); assert.strictEqual(res2.error, null); const outputOfExecCommand2 = res2.stdout; console.log(outputOfExecCommand2); - // Executes the command that was installed by the local Feature's 'postCreateCommand'. assert.strictEqual(outputOfExecCommand2, 'vscode\n'); assert.match(containerUpStandardError, /Running the postCreateCommand from devcontainer.json/); @@ -415,7 +414,6 @@ describe('Feature lifecycle hooks', function () { const res3 = await shellExec(`docker inspect ${containerId} --format '{{json .Mounts}}'`); assert.strictEqual(res3.error, null); - // json parse res3 const mounts = JSON.parse(res3.stdout); assert.exists(mounts.find((item: { Type: string; Destination: string }) => item.Type === 'volume' && item.Destination === '/home/vscode')); });