From 8b34ac2cacd7ea737255b1e33208a1306e8b9387 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 22 Aug 2023 12:02:34 +0200 Subject: [PATCH 001/291] Fallback when getent is not available --- src/spec-common/commonUtils.ts | 9 +++ src/spec-common/injectHeadless.ts | 4 +- .../containerFeaturesConfiguration.ts | 5 +- src/spec-shutdown/dockerUtils.ts | 3 +- src/spec-utils/strings.ts | 9 +++ .../generateFeaturesConfig.test.ts | 9 +-- src/test/getEntPasswd.test.ts | 66 +++++++++++++++++++ 7 files changed, 96 insertions(+), 9 deletions(-) create mode 100644 src/spec-utils/strings.ts create mode 100644 src/test/getEntPasswd.test.ts diff --git a/src/spec-common/commonUtils.ts b/src/spec-common/commonUtils.ts index 9b42484ed..940f2d72b 100644 --- a/src/spec-common/commonUtils.ts +++ b/src/spec-common/commonUtils.ts @@ -14,6 +14,7 @@ import { StringDecoder } from 'string_decoder'; import { toErrorText } from './errors'; import { Disposable, Event, NodeEventEmitter } from '../spec-utils/event'; import { isLocalFile } from '../spec-utils/pfs'; +import { escapeRegExCharacters } from '../spec-utils/strings'; import { Log, nullLog } from '../spec-utils/log'; import { ShellServer } from './shellServer'; @@ -581,3 +582,11 @@ export async function getLocalUsername() { } return localUsername; } + +export function getEntPasswdShellCommand(userNameOrId: string) { + const escapedForShell = userNameOrId.replace(/['\\]/g, '\\$&'); + const escapedForRexExp = escapeRegExCharacters(userNameOrId) + .replaceAll('\'', '\\\''); + // Leading space makes sure we don't concatenate to arithmetic expansion (https://tldp.org/LDP/abs/html/dblparens.html). + return ` (command -v getent >/dev/null 2>&1 && getent passwd '${escapedForShell}' || grep -E '^${escapedForRexExp}|^[^:]*:[^:]*:${escapedForRexExp}:' /etc/passwd)`; +} diff --git a/src/spec-common/injectHeadless.ts b/src/spec-common/injectHeadless.ts index f482a5ea4..2f36662f9 100644 --- a/src/spec-common/injectHeadless.ts +++ b/src/spec-common/injectHeadless.ts @@ -10,7 +10,7 @@ import * as crypto from 'crypto'; import { ContainerError, toErrorText, toWarningText } from './errors'; import { launch, ShellServer } from './shellServer'; -import { ExecFunction, CLIHost, PtyExecFunction, isFile, Exec, PtyExec } from './commonUtils'; +import { ExecFunction, CLIHost, PtyExecFunction, isFile, Exec, PtyExec, getEntPasswdShellCommand } from './commonUtils'; import { Disposable, Event, NodeEventEmitter } from '../spec-utils/event'; import { PackageConfiguration } from '../spec-utils/product'; import { URI } from 'vscode-uri'; @@ -286,7 +286,7 @@ async function getUserShell(containerEnv: NodeJS.ProcessEnv, passwdUser: PasswdU } export async function getUserFromPasswdDB(shellServer: ShellServer, userNameOrId: string) { - const { stdout } = await shellServer.exec(`getent passwd ${userNameOrId}`, { logOutput: false }); + const { stdout } = await shellServer.exec(getEntPasswdShellCommand(userNameOrId), { logOutput: false }); return parseUserInPasswdDB(stdout); } diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index 2bc937f51..dda542173 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -21,6 +21,7 @@ import { CommonParams, ManifestContainer, OCIManifest, OCIRef, getPublishedVersi import { Lockfile, readLockfile, writeLockfile } from './lockfile'; import { computeDependsOnInstallationOrder } from './containerFeaturesOrder'; import { logFeatureAdvisories } from './featureAdvisories'; +import { getEntPasswdShellCommand } from '../spec-common/commonUtils'; // v1 const V1_ASSET_NAME = 'devcontainer-features.tgz'; @@ -323,8 +324,8 @@ export function getFeatureLayers(featuresConfig: FeaturesConfig, containerUser: const builtinsEnvFile = `${path.posix.join(FEATURES_CONTAINER_TEMP_DEST_FOLDER, 'devcontainer-features.builtin.env')}`; let result = `RUN \\ -echo "_CONTAINER_USER_HOME=$(getent passwd ${containerUser} | cut -d: -f6)" >> ${builtinsEnvFile} && \\ -echo "_REMOTE_USER_HOME=$(getent passwd ${remoteUser} | cut -d: -f6)" >> ${builtinsEnvFile} +echo "_CONTAINER_USER_HOME=$(${getEntPasswdShellCommand(containerUser)} | cut -d: -f6)" >> ${builtinsEnvFile} && \\ +echo "_REMOTE_USER_HOME=$(${getEntPasswdShellCommand(remoteUser)} | cut -d: -f6)" >> ${builtinsEnvFile} `; diff --git a/src/spec-shutdown/dockerUtils.ts b/src/spec-shutdown/dockerUtils.ts index b78d7ed7c..f50e8b4db 100644 --- a/src/spec-shutdown/dockerUtils.ts +++ b/src/spec-shutdown/dockerUtils.ts @@ -8,6 +8,7 @@ import { toErrorText } from '../spec-common/errors'; import * as ptyType from 'node-pty'; import { Log, makeLog } from '../spec-utils/log'; import { Event } from '../spec-utils/event'; +import { escapeRegExCharacters } from '../spec-utils/strings'; export interface ContainerDetails { Id: string; @@ -351,7 +352,7 @@ function replacingDockerExecLog(original: Log, cmd: string, args: string[]) { } function replacingLog(original: Log, search: string, replace: string) { - const searchR = new RegExp(search.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'); + const searchR = new RegExp(escapeRegExCharacters(search), 'g'); const wrapped = makeLog({ ...original, get dimensions() { diff --git a/src/spec-utils/strings.ts b/src/spec-utils/strings.ts new file mode 100644 index 000000000..00a1352d4 --- /dev/null +++ b/src/spec-utils/strings.ts @@ -0,0 +1,9 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + + +export function escapeRegExCharacters(str: string) { + return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); +} diff --git a/src/test/container-features/generateFeaturesConfig.test.ts b/src/test/container-features/generateFeaturesConfig.test.ts index 480a17cb4..67b9754da 100644 --- a/src/test/container-features/generateFeaturesConfig.test.ts +++ b/src/test/container-features/generateFeaturesConfig.test.ts @@ -10,6 +10,7 @@ import { DevContainerConfig } from '../../spec-configuration/configuration'; import { URI } from 'vscode-uri'; import { getLocalCacheFolder } from '../../spec-node/utils'; import { shellExec } from '../testUtils'; +import { getEntPasswdShellCommand } from '../../spec-common/commonUtils'; export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace)); @@ -78,8 +79,8 @@ ENV MYKEYTWO="MY RESULT TWO"`; // getFeatureLayers const actualLayers = getFeatureLayers(featuresConfig, 'testContainerUser', 'testRemoteUser'); const expectedLayers = `RUN \\ -echo "_CONTAINER_USER_HOME=$(getent passwd testContainerUser | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env && \\ -echo "_REMOTE_USER_HOME=$(getent passwd testRemoteUser | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env +echo "_CONTAINER_USER_HOME=$(${getEntPasswdShellCommand('testContainerUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env && \\ +echo "_REMOTE_USER_HOME=$(${getEntPasswdShellCommand('testRemoteUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env COPY --chown=root:root --from=dev_containers_feature_content_source /tmp/build-features/first_0 /tmp/dev-container-features/first_0 RUN chmod -R 0755 /tmp/dev-container-features/first_0 \\ @@ -140,8 +141,8 @@ RUN chmod -R 0755 /tmp/dev-container-features/second_1 \\ // getFeatureLayers const actualLayers = getFeatureLayers(featuresConfig, 'testContainerUser', 'testRemoteUser'); const expectedLayers = `RUN \\ -echo "_CONTAINER_USER_HOME=$(getent passwd testContainerUser | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env && \\ -echo "_REMOTE_USER_HOME=$(getent passwd testRemoteUser | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env +echo "_CONTAINER_USER_HOME=$(${getEntPasswdShellCommand('testContainerUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env && \\ +echo "_REMOTE_USER_HOME=$(${getEntPasswdShellCommand('testRemoteUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env COPY --chown=root:root --from=dev_containers_feature_content_source /tmp/build-features/color_0 /tmp/dev-container-features/color_0 diff --git a/src/test/getEntPasswd.test.ts b/src/test/getEntPasswd.test.ts new file mode 100644 index 000000000..1dfae5d0a --- /dev/null +++ b/src/test/getEntPasswd.test.ts @@ -0,0 +1,66 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import { shellExec, output } from './testUtils'; +import { dockerExecFunction } from '../spec-shutdown/dockerUtils'; +import { plainExec } from '../spec-common/commonUtils'; +import { launch } from '../spec-common/shellServer'; +import { getUserFromPasswdDB } from '../spec-common/injectHeadless'; + +describe('getEntPasswdShellCommand', function () { + this.timeout('20s'); + + [ + { + image: 'busybox', + getentPath: undefined, + addUserOptions: '-D -h', + userName: 'foo\\bar', + }, + { + image: 'debian', + getentPath: '/usr/bin/getent', + addUserOptions: '--disabled-password --allow-all-names --gecos "" --home', + userName: 'foo\\bar', + }, + { + image: 'alpine', + getentPath: '/usr/bin/getent', + addUserOptions: '-D -h', + userName: 'foo_bar', // Alpine doesn't support backslash in user names. + }, + ].forEach(({ image, getentPath, addUserOptions, userName }) => { + it(`should work with ${image} ${getentPath ? 'with' : 'without'} getent command`, async () => { + const res = await shellExec(`docker run -d ${image} sleep inf`); + const containerId = res.stdout.trim(); + const exec = dockerExecFunction({ + exec: plainExec(undefined), + cmd: 'docker', + env: {}, + output, + }, containerId, 'root'); + const shellServer = await launch(exec, output); + + const which = await shellServer.exec('command -v getent') + .catch(() => undefined); + assert.strictEqual(which?.stdout.trim(), getentPath); + + await shellServer.exec(`adduser ${addUserOptions} /home/foo ${userName.replaceAll('\\', '\\\\')}`); + + const userByName = await getUserFromPasswdDB(shellServer, userName); + assert.ok(userByName); + assert.strictEqual(userByName.name, userName); + assert.strictEqual(userByName.home, '/home/foo'); + + const userById = await getUserFromPasswdDB(shellServer, userByName.uid); + assert.ok(userById); + assert.strictEqual(userById.name, userName); + assert.strictEqual(userById.home, '/home/foo'); + + await shellExec(`docker rm -f ${containerId}`); + }); + }); +}); From e2a188a27cd960f3bbc2175a1df0700f2c98a5eb Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 22 Aug 2023 12:03:04 +0200 Subject: [PATCH 002/291] Fix: Can return image index --- src/spec-node/utils.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spec-node/utils.ts b/src/spec-node/utils.ts index c107870a0..514d8c343 100644 --- a/src/spec-node/utils.ts +++ b/src/spec-node/utils.ts @@ -248,7 +248,7 @@ export async function inspectImageInRegistry(output: Log, platformInfo: { arch: let targetDigest: string | undefined = undefined; const manifest = await getManifest(params, manifestUrl, ref, 'application/vnd.docker.distribution.manifest.v2+json'); - if (manifest) { + if (manifest?.manifestObj.config) { // Checking for config because the above mime type sometimes returns an image index. targetDigest = manifest.manifestObj.config.digest; } else { // If we couldn't fetch the manifest, perhaps the registry supports querying for the 'Image Index' From b0cd4fcfbe40bf73dda5da89d2d8eaeda30a50ef Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 22 Aug 2023 12:03:31 +0200 Subject: [PATCH 003/291] Fix: mkdir /fake-path fails --- src/test/container-features/registryCompatibilityOCI.test.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/test/container-features/registryCompatibilityOCI.test.ts b/src/test/container-features/registryCompatibilityOCI.test.ts index 64cbcb629..1a19966d6 100644 --- a/src/test/container-features/registryCompatibilityOCI.test.ts +++ b/src/test/container-features/registryCompatibilityOCI.test.ts @@ -4,6 +4,7 @@ *--------------------------------------------------------------------------------------------*/ import { assert } from 'chai'; +import * as os from 'os'; import * as path from 'path'; import { devContainerDown, devContainerUp, shellExec } from '../testUtils'; @@ -122,7 +123,7 @@ describe('Registry Compatibility', function () { describe(name, async function () { ((authStrategyKey && !envVariableExists(authStrategyKey)) ? describe.skip : describe)('devcontainer up', async function () { - const authFolder = constructAuthFromStrategy(tmp, useAuthStrategy ?? AuthStrategy.Anonymous, authStrategyKey) || '/fake-path'; + const authFolder = constructAuthFromStrategy(tmp, useAuthStrategy ?? AuthStrategy.Anonymous, authStrategyKey) || path.join(os.homedir(), 'fake-path'); const gitHubToken = (useAuthStrategy === AuthStrategy.GitHubToken) ? (process.env.GITHUB_TOKEN ?? '') : ''; let containerId: string | null = null; @@ -145,7 +146,7 @@ describe('Registry Compatibility', function () { ((authStrategyKey && !envVariableExists(authStrategyKey)) ? describe.skip : describe)(`devcontainer features info manifest`, async function () { - const authFolder = constructAuthFromStrategy(tmp, useAuthStrategy ?? AuthStrategy.Anonymous, authStrategyKey) || '/fake-path'; + const authFolder = constructAuthFromStrategy(tmp, useAuthStrategy ?? AuthStrategy.Anonymous, authStrategyKey) || path.join(os.homedir(), 'fake-path'); const gitHubToken = (useAuthStrategy === AuthStrategy.GitHubToken) ? (process.env.GITHUB_TOKEN ?? '') : ''; it('fetches manifest', async function () { From 814af61cec22ed82e8668b312d3b3489289f584e Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 22 Aug 2023 12:18:13 +0200 Subject: [PATCH 004/291] Fix: Ensure image is in cache --- src/test/imageMetadata.test.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/test/imageMetadata.test.ts b/src/test/imageMetadata.test.ts index 7e6022cb1..24045ac4e 100644 --- a/src/test/imageMetadata.test.ts +++ b/src/test/imageMetadata.test.ts @@ -131,6 +131,7 @@ describe('Image Metadata', function () { const imageTestFolder = `${__dirname}/configs/${testFolderName}`; it(`build should collect metadata on image label [${testFolderName}, ${text}]`, async () => { + await shellExec(`docker pull ubuntu:latest`); const imageName = `${testFolderName}${options.useBuildKit ? '' : '-buildkit'}-test`; const buildKitOption = (options?.useBuildKit ?? false) ? '' : ' --buildkit=never'; From 199a4f1e0b339ebeed84f6d80b450222a3bcec91 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Tue, 22 Aug 2023 17:22:09 +0200 Subject: [PATCH 005/291] 0.51.0 --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5cd0523b..8fd9331db 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Notable changes. +## August 2023 + +### [0.51.0] +- Add `--cache-to` option to `devcontainer build` command (https://github.com/devcontainers/cli/pull/570) +- Fix: Fallback when getent is not available (https://github.com/microsoft/vscode-remote-release/issues/8811) + ## July 2023 ### [0.50.2] diff --git a/package.json b/package.json index 127586195..654985bc2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.50.2", + "version": "0.51.0", "bin": { "devcontainer": "devcontainer.js" }, From c960d97bd79d717f39c00d794cc3b86395d41266 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Thu, 24 Aug 2023 10:43:52 +0200 Subject: [PATCH 006/291] Handle missing entry in /etc/passwd gracefully --- src/spec-common/commonUtils.ts | 2 +- src/spec-common/injectHeadless.ts | 3 +++ src/test/getEntPasswd.test.ts | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/spec-common/commonUtils.ts b/src/spec-common/commonUtils.ts index 940f2d72b..5d5f1c0c7 100644 --- a/src/spec-common/commonUtils.ts +++ b/src/spec-common/commonUtils.ts @@ -588,5 +588,5 @@ export function getEntPasswdShellCommand(userNameOrId: string) { const escapedForRexExp = escapeRegExCharacters(userNameOrId) .replaceAll('\'', '\\\''); // Leading space makes sure we don't concatenate to arithmetic expansion (https://tldp.org/LDP/abs/html/dblparens.html). - return ` (command -v getent >/dev/null 2>&1 && getent passwd '${escapedForShell}' || grep -E '^${escapedForRexExp}|^[^:]*:[^:]*:${escapedForRexExp}:' /etc/passwd)`; + return ` (command -v getent >/dev/null 2>&1 && getent passwd '${escapedForShell}' || grep -E '^${escapedForRexExp}|^[^:]*:[^:]*:${escapedForRexExp}:' /etc/passwd || true)`; } diff --git a/src/spec-common/injectHeadless.ts b/src/spec-common/injectHeadless.ts index 2f36662f9..ba2606356 100644 --- a/src/spec-common/injectHeadless.ts +++ b/src/spec-common/injectHeadless.ts @@ -287,6 +287,9 @@ async function getUserShell(containerEnv: NodeJS.ProcessEnv, passwdUser: PasswdU export async function getUserFromPasswdDB(shellServer: ShellServer, userNameOrId: string) { const { stdout } = await shellServer.exec(getEntPasswdShellCommand(userNameOrId), { logOutput: false }); + if (!stdout.trim()) { + return undefined; + } return parseUserInPasswdDB(stdout); } diff --git a/src/test/getEntPasswd.test.ts b/src/test/getEntPasswd.test.ts index 1dfae5d0a..d3c5945aa 100644 --- a/src/test/getEntPasswd.test.ts +++ b/src/test/getEntPasswd.test.ts @@ -60,6 +60,9 @@ describe('getEntPasswdShellCommand', function () { assert.strictEqual(userById.name, userName); assert.strictEqual(userById.home, '/home/foo'); + const nonexistentUser = await getUserFromPasswdDB(shellServer, '123456'); + assert.strictEqual(undefined, nonexistentUser); + await shellExec(`docker rm -f ${containerId}`); }); }); From 7866e193a49a31ab542c6d3bbe34cc99bcf58dc9 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Mon, 28 Aug 2023 17:09:26 +0200 Subject: [PATCH 007/291] 0.51.1 --- CHANGELOG.md | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fd9331db..5e7314cd2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,9 @@ Notable changes. ## August 2023 +### [0.51.1] +- Handle missing entry in /etc/passwd gracefully (https://github.com/microsoft/vscode-remote-release/issues/8875) + ### [0.51.0] - Add `--cache-to` option to `devcontainer build` command (https://github.com/devcontainers/cli/pull/570) - Fix: Fallback when getent is not available (https://github.com/microsoft/vscode-remote-release/issues/8811) diff --git a/package.json b/package.json index 654985bc2..e4cc906ec 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.51.0", + "version": "0.51.1", "bin": { "devcontainer": "devcontainer.js" }, From 9da6357785f63ee0b0171482fb700d8aac86ef77 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Thu, 31 Aug 2023 10:14:56 -0700 Subject: [PATCH 008/291] Surface buildkit policy errors more clearly (#627) --- src/spec-node/dockerCompose.ts | 6 +++++- src/spec-node/singleContainer.ts | 6 +++++- src/spec-node/utils.ts | 6 ++++++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts index c209fe95b..6dbbd4d05 100644 --- a/src/spec-node/dockerCompose.ts +++ b/src/spec-node/dockerCompose.ts @@ -6,7 +6,7 @@ import * as yaml from 'js-yaml'; import * as shellQuote from 'shell-quote'; -import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, DockerResolverParameters, inspectDockerImage, getEmptyContextFolder, getFolderImageName, SubstitutedConfig, checkDockerSupportForGPU } from './utils'; +import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, DockerResolverParameters, inspectDockerImage, getEmptyContextFolder, getFolderImageName, SubstitutedConfig, checkDockerSupportForGPU, isBuildKitImagePolicyError } from './utils'; import { ContainerProperties, setupInContainer, ResolverProgress } from '../spec-common/injectHeadless'; import { ContainerError } from '../spec-common/errors'; import { Workspace } from '../spec-utils/workspaces'; @@ -274,6 +274,10 @@ ${cacheFromOverrideContent} await dockerComposeCLI(infoParams, ...args); } } catch (err) { + if (isBuildKitImagePolicyError(err)) { + throw new ContainerError({ description: 'Could not resolve image due to policy.', originalError: err, data: { fileWithError: localComposeFiles[0] } }); + } + throw err instanceof ContainerError ? err : new ContainerError({ description: 'An error occurred building the Docker Compose images.', originalError: err, data: { fileWithError: localComposeFiles[0] } }); } } diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index a590d72b7..0aa45fa22 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -4,7 +4,7 @@ *--------------------------------------------------------------------------------------------*/ -import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, getDockerfilePath, getDockerContextPath, DockerResolverParameters, isDockerFileConfig, uriToWSLFsPath, WorkspaceConfiguration, getFolderImageName, inspectDockerImage, logUMask, SubstitutedConfig, checkDockerSupportForGPU } from './utils'; +import { createContainerProperties, startEventSeen, ResolverResult, getTunnelInformation, getDockerfilePath, getDockerContextPath, DockerResolverParameters, isDockerFileConfig, uriToWSLFsPath, WorkspaceConfiguration, getFolderImageName, inspectDockerImage, logUMask, SubstitutedConfig, checkDockerSupportForGPU, isBuildKitImagePolicyError } from './utils'; import { ContainerProperties, setupInContainer, ResolverProgress, ResolverParameters } from '../spec-common/injectHeadless'; import { ContainerError, toErrorText } from '../spec-common/errors'; import { ContainerDetails, listContainers, DockerCLIParameters, inspectContainers, dockerCLI, dockerPtyCLI, toPtyExecParameters, ImageDetails, toExecParameters } from '../spec-shutdown/dockerUtils'; @@ -248,6 +248,10 @@ async function buildAndExtendImage(buildParams: DockerResolverParameters, config await dockerCLI(infoParams, ...args); } } catch (err) { + if (isBuildKitImagePolicyError(err)) { + throw new ContainerError({ description: 'Could not resolve image due to policy.', originalError: err, data: { fileWithError: dockerfilePath } }); + } + throw new ContainerError({ description: 'An error occurred building the image.', originalError: err, data: { fileWithError: dockerfilePath } }); } diff --git a/src/spec-node/utils.ts b/src/spec-node/utils.ts index 514d8c343..5c27c46dc 100644 --- a/src/spec-node/utils.ts +++ b/src/spec-node/utils.ts @@ -205,6 +205,12 @@ export async function checkDockerSupportForGPU(params: DockerCLIParameters | Doc return runtimeFound; } +export function isBuildKitImagePolicyError(err: any): boolean { + const imagePolicyErrorString = 'could not resolve image due to policy'; + return (err?.cmdOutput && typeof err.cmdOutput === 'string' && err.cmdOutput.indexOf(imagePolicyErrorString) > -1) || + (err?.stderr && typeof err.stderr === 'string' && err.stderr.indexOf(imagePolicyErrorString) > -1); +} + export async function inspectDockerImage(params: DockerResolverParameters | DockerCLIParameters, imageName: string, pullImageOnError: boolean) { try { return await inspectImage(params, imageName); From 64807ce26938a792c0f8be4c7525d18b49f04b55 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Thu, 31 Aug 2023 11:46:40 -0700 Subject: [PATCH 009/291] v0.51.2 (#630) --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5e7314cd2..6ca5e2d61 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Notable changes. ## August 2023 +### [0.51.2] + +- Surface buildkit policy errors (https://github.com/devcontainers/cli/pull/627) + ### [0.51.1] - Handle missing entry in /etc/passwd gracefully (https://github.com/microsoft/vscode-remote-release/issues/8875) diff --git a/package.json b/package.json index e4cc906ec..92013c1c2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.51.1", + "version": "0.51.2", "bin": { "devcontainer": "devcontainer.js" }, From 9536a9f5e3f492cf0721dcedb9f2aa358670335c Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Fri, 8 Sep 2023 16:32:21 +0200 Subject: [PATCH 010/291] Update UID only if GID is in use --- scripts/updateUID.Dockerfile | 6 ++- src/test/configs/updateUID/.devcontainer.json | 6 +++ src/test/configs/updateUID/Dockerfile | 4 ++ .../configs/updateUIDOnly/.devcontainer.json | 9 ++++ src/test/configs/updateUIDOnly/Dockerfile | 7 +++ src/test/testUtils.ts | 4 +- src/test/updateUID.test.ts | 50 +++++++++++++++++++ 7 files changed, 82 insertions(+), 4 deletions(-) create mode 100644 src/test/configs/updateUID/.devcontainer.json create mode 100644 src/test/configs/updateUID/Dockerfile create mode 100644 src/test/configs/updateUIDOnly/.devcontainer.json create mode 100644 src/test/configs/updateUIDOnly/Dockerfile create mode 100644 src/test/updateUID.test.ts diff --git a/scripts/updateUID.Dockerfile b/scripts/updateUID.Dockerfile index 64ef383e0..9f6c9a854 100644 --- a/scripts/updateUID.Dockerfile +++ b/scripts/updateUID.Dockerfile @@ -18,9 +18,11 @@ RUN eval $(sed -n "s/${REMOTE_USER}:[^:]*:\([^:]*\):\([^:]*\):[^:]*:\([^:]*\).*/ echo "UIDs and GIDs are the same ($NEW_UID:$NEW_GID)."; \ elif [ "$OLD_UID" != "$NEW_UID" -a -n "$EXISTING_USER" ]; then \ echo "User with UID exists ($EXISTING_USER=$NEW_UID)."; \ - elif [ "$OLD_GID" != "$NEW_GID" -a -n "$EXISTING_GROUP" ]; then \ - echo "Group with GID exists ($EXISTING_GROUP=$NEW_GID)."; \ else \ + if [ "$OLD_GID" != "$NEW_GID" -a -n "$EXISTING_GROUP" ]; then \ + echo "Group with GID exists ($EXISTING_GROUP=$NEW_GID)."; \ + NEW_GID="$OLD_GID"; \ + fi; \ echo "Updating UID:GID from $OLD_UID:$OLD_GID to $NEW_UID:$NEW_GID."; \ sed -i -e "s/\(${REMOTE_USER}:[^:]*:\)[^:]*:[^:]*/\1${NEW_UID}:${NEW_GID}/" /etc/passwd; \ if [ "$OLD_GID" != "$NEW_GID" ]; then \ diff --git a/src/test/configs/updateUID/.devcontainer.json b/src/test/configs/updateUID/.devcontainer.json new file mode 100644 index 000000000..432e69d7e --- /dev/null +++ b/src/test/configs/updateUID/.devcontainer.json @@ -0,0 +1,6 @@ +{ + "build": { + "dockerfile": "Dockerfile" + }, + "remoteUser": "foo" +} \ No newline at end of file diff --git a/src/test/configs/updateUID/Dockerfile b/src/test/configs/updateUID/Dockerfile new file mode 100644 index 000000000..bbf53798e --- /dev/null +++ b/src/test/configs/updateUID/Dockerfile @@ -0,0 +1,4 @@ +FROM debian:latest + +RUN addgroup --gid 4321 foo +RUN adduser --uid 1234 --gid 4321 foo diff --git a/src/test/configs/updateUIDOnly/.devcontainer.json b/src/test/configs/updateUIDOnly/.devcontainer.json new file mode 100644 index 000000000..9b3d35d64 --- /dev/null +++ b/src/test/configs/updateUIDOnly/.devcontainer.json @@ -0,0 +1,9 @@ +{ + "build": { + "dockerfile": "Dockerfile", + "args": { + "LOCAL_GID": "${localEnv:LOCAL_GID}" + } + }, + "remoteUser": "foo" +} \ No newline at end of file diff --git a/src/test/configs/updateUIDOnly/Dockerfile b/src/test/configs/updateUIDOnly/Dockerfile new file mode 100644 index 000000000..b08e22300 --- /dev/null +++ b/src/test/configs/updateUIDOnly/Dockerfile @@ -0,0 +1,7 @@ +FROM debian:latest + +ARG LOCAL_GID +RUN addgroup --gid $LOCAL_GID bar || true + +RUN addgroup --gid 4321 foo +RUN adduser --uid 1234 --gid 4321 foo diff --git a/src/test/testUtils.ts b/src/test/testUtils.ts index e24e953d3..ce2383b8d 100644 --- a/src/test/testUtils.ts +++ b/src/test/testUtils.ts @@ -87,13 +87,13 @@ export async function shellPtyExec(command: string, options: { stdin?: string } }).then(res => ({ code: 0, ...res }), error => error); } -export async function devContainerUp(cli: string, workspaceFolder: string, options?: { cwd?: string; useBuildKit?: boolean; userDataFolder?: string; logLevel?: string; extraArgs?: string; prefix?: string }): Promise { +export async function devContainerUp(cli: string, workspaceFolder: string, options?: { cwd?: string; useBuildKit?: boolean; userDataFolder?: string; logLevel?: string; extraArgs?: string; prefix?: string; env?: NodeJS.ProcessEnv }): Promise { const buildkitOption = (options?.useBuildKit ?? false) ? '' : ' --buildkit=never'; const userDataFolderOption = (options?.userDataFolder ?? false) ? ` --user-data-folder=${options?.userDataFolder}` : ''; const logLevelOption = (options?.logLevel ?? false) ? ` --log-level ${options?.logLevel}` : ''; const extraArgs = (options?.extraArgs ?? false) ? ` ${options?.extraArgs}` : ''; const prefix = (options?.prefix ?? false) ? `${options?.prefix} ` : ''; - const shellExecOptions = { cwd: options?.cwd }; + const shellExecOptions = { cwd: options?.cwd, env: options?.env }; const res = await shellExec(`${prefix}${cli} up --workspace-folder ${workspaceFolder}${buildkitOption}${userDataFolderOption}${extraArgs} ${logLevelOption}`, shellExecOptions); const response = JSON.parse(res.stdout); assert.equal(response.outcome, 'success'); diff --git a/src/test/updateUID.test.ts b/src/test/updateUID.test.ts new file mode 100644 index 000000000..c96bbb733 --- /dev/null +++ b/src/test/updateUID.test.ts @@ -0,0 +1,50 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as assert from 'assert'; +import * as path from 'path'; +import { devContainerDown, devContainerUp, shellExec } from './testUtils'; + +const pkg = require('../../package.json'); + +(process.platform === 'linux' ? describe : describe.skip)('Dev Containers CLI', function () { + this.timeout('120s'); + + const tmp = path.relative(process.cwd(), path.join(__dirname, 'tmp')); + const cli = `npx --prefix ${tmp} devcontainer`; + + before('Install', async () => { + await shellExec(`rm -rf ${tmp}/node_modules`); + await shellExec(`mkdir -p ${tmp}`); + await shellExec(`npm --prefix ${tmp} install devcontainers-cli-${pkg.version}.tgz`); + }); + + describe('updateUID', () => { + it('should update UID and GID', async () => { + const testFolder = `${__dirname}/configs/updateUID`; + const containerId = (await devContainerUp(cli, testFolder)).containerId; + const uid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -u`); + assert.strictEqual(uid.stdout.trim(), String(process.getuid!())); + const gid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -g`); + assert.strictEqual(gid.stdout.trim(), String(process.getgid!())); + await devContainerDown({ containerId }); + }); + + it('should update only UID when GID exists', async () => { + const testFolder = `${__dirname}/configs/updateUIDOnly`; + const containerId = (await devContainerUp(cli, testFolder, { + env: { + ...process.env, + LOCAL_GID: String(process.getgid!()) + } + })).containerId; + const uid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -u`); + assert.strictEqual(uid.stdout.trim(), String(process.getuid!())); + const gid = await shellExec(`${cli} exec --workspace-folder ${testFolder} id -g`); + assert.strictEqual(gid.stdout.trim(), String(4321)); + await devContainerDown({ containerId }); + }); + }); +}); From 71bb59f70fce61a6e9ac5bd2684d6a16a74d9e92 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Wed, 13 Sep 2023 09:38:02 -0700 Subject: [PATCH 011/291] empty (no characters) lockfiles will trigger lockfile to init/populate (#637) --- .../containerFeaturesConfiguration.ts | 6 +-- src/spec-configuration/lockfile.ts | 14 ++++--- .../featuresCLI/resolveDependencies.ts | 2 +- .../.devcontainer/devcontainer-lock.json | 0 .../.devcontainer/devcontainer.json | 6 +++ .../.devcontainer/devcontainer-lock.json | 0 .../.devcontainer/devcontainer.json | 6 +++ src/test/container-features/lockfile.test.ts | 37 +++++++++++++++++++ 8 files changed, 62 insertions(+), 9 deletions(-) create mode 100644 src/test/container-features/configs/lockfile-generate-from-empty-file-frozen/.devcontainer/devcontainer-lock.json create mode 100644 src/test/container-features/configs/lockfile-generate-from-empty-file-frozen/.devcontainer/devcontainer.json create mode 100644 src/test/container-features/configs/lockfile-generate-from-empty-file/.devcontainer/devcontainer-lock.json create mode 100644 src/test/container-features/configs/lockfile-generate-from-empty-file/.devcontainer/devcontainer.json diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index dda542173..38ce796d2 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -547,7 +547,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar const ociCacheDir = await prepareOCICache(dstFolder); - const lockfile = await readLockfile(config); + const { lockfile, initLockfile } = await readLockfile(config); const processFeature = async (_userFeature: DevContainerFeature) => { return await processFeatureIdentifier(params, configPath, workspaceRoot, _userFeature, lockfile); @@ -570,7 +570,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar await fetchFeatures(params, featuresConfig, locallyCachedFeatureSet, dstFolder, localFeaturesFolder, ociCacheDir, lockfile); await logFeatureAdvisories(params, featuresConfig); - await writeLockfile(params, config, featuresConfig); + await writeLockfile(params, config, featuresConfig, initLockfile); return featuresConfig; } @@ -582,7 +582,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co return { features: {} }; } - const lockfile = await readLockfile(config); + const { lockfile } = await readLockfile(config); const features: Record = {}; diff --git a/src/spec-configuration/lockfile.ts b/src/spec-configuration/lockfile.ts index bf32b9dd8..e1a1094c8 100644 --- a/src/spec-configuration/lockfile.ts +++ b/src/spec-configuration/lockfile.ts @@ -13,7 +13,7 @@ export interface Lockfile { features: Record; } -export async function writeLockfile(params: ContainerFeatureInternalParams, config: DevContainerConfig, featuresConfig: FeaturesConfig) { +export async function writeLockfile(params: ContainerFeatureInternalParams, config: DevContainerConfig, featuresConfig: FeaturesConfig, forceInitLockfile?: boolean) { const lockfilePath = getLockfilePath(config); const oldLockfileContent = await readLocalFile(lockfilePath) .catch(err => { @@ -22,7 +22,7 @@ export async function writeLockfile(params: ContainerFeatureInternalParams, conf } }); - if (!oldLockfileContent && !params.experimentalLockfile && !params.experimentalFrozenLockfile) { + if (!forceInitLockfile && !oldLockfileContent && !params.experimentalLockfile && !params.experimentalFrozenLockfile) { return; } @@ -63,13 +63,17 @@ export async function writeLockfile(params: ContainerFeatureInternalParams, conf } } -export async function readLockfile(config: DevContainerConfig): Promise { +export async function readLockfile(config: DevContainerConfig): Promise<{ lockfile?: Lockfile; initLockfile?: boolean }> { try { const content = await readLocalFile(getLockfilePath(config)); - return JSON.parse(content.toString()) as Lockfile; + // If empty file, use as maker to initialize lockfile when build completes. + if (content.toString().trim() === '') { + return { initLockfile: true }; + } + return { lockfile: JSON.parse(content.toString()) as Lockfile }; } catch (err) { if (err?.code === 'ENOENT') { - return undefined; + return {}; } throw err; } diff --git a/src/spec-node/featuresCLI/resolveDependencies.ts b/src/spec-node/featuresCLI/resolveDependencies.ts index 096f75528..74a1cf121 100644 --- a/src/spec-node/featuresCLI/resolveDependencies.ts +++ b/src/spec-node/featuresCLI/resolveDependencies.ts @@ -83,7 +83,7 @@ async function featuresResolveDependencies({ env: process.env, }; - const lockfile = await readLockfile(config); + const { lockfile } = await readLockfile(config); const processFeature = async (_userFeature: DevContainerFeature) => { return await processFeatureIdentifier(params, configPath, workspaceFolder, _userFeature, lockfile); }; diff --git a/src/test/container-features/configs/lockfile-generate-from-empty-file-frozen/.devcontainer/devcontainer-lock.json b/src/test/container-features/configs/lockfile-generate-from-empty-file-frozen/.devcontainer/devcontainer-lock.json new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/container-features/configs/lockfile-generate-from-empty-file-frozen/.devcontainer/devcontainer.json b/src/test/container-features/configs/lockfile-generate-from-empty-file-frozen/.devcontainer/devcontainer.json new file mode 100644 index 000000000..a300846d2 --- /dev/null +++ b/src/test/container-features/configs/lockfile-generate-from-empty-file-frozen/.devcontainer/devcontainer.json @@ -0,0 +1,6 @@ +{ + "image": "mcr.microsoft.com/devcontainers/base", + "features": { + "ghcr.io/devcontainers/features/dotnet:2": {} + } +} \ No newline at end of file diff --git a/src/test/container-features/configs/lockfile-generate-from-empty-file/.devcontainer/devcontainer-lock.json b/src/test/container-features/configs/lockfile-generate-from-empty-file/.devcontainer/devcontainer-lock.json new file mode 100644 index 000000000..e69de29bb diff --git a/src/test/container-features/configs/lockfile-generate-from-empty-file/.devcontainer/devcontainer.json b/src/test/container-features/configs/lockfile-generate-from-empty-file/.devcontainer/devcontainer.json new file mode 100644 index 000000000..a300846d2 --- /dev/null +++ b/src/test/container-features/configs/lockfile-generate-from-empty-file/.devcontainer/devcontainer.json @@ -0,0 +1,6 @@ +{ + "image": "mcr.microsoft.com/devcontainers/base", + "features": { + "ghcr.io/devcontainers/features/dotnet:2": {} + } +} \ No newline at end of file diff --git a/src/test/container-features/lockfile.test.ts b/src/test/container-features/lockfile.test.ts index f592d9a09..c49bcc28e 100644 --- a/src/test/container-features/lockfile.test.ts +++ b/src/test/container-features/lockfile.test.ts @@ -138,4 +138,41 @@ describe('Lockfile', function () { assert.equal(response.outcome, 'error'); } }); + + it('empty lockfile should init', async () => { + const workspaceFolder = path.join(__dirname, 'configs/lockfile-generate-from-empty-file'); + const lockfilePath = path.join(workspaceFolder, '.devcontainer', 'devcontainer-lock.json'); + const cleanup = async () => { + await rmLocal(lockfilePath, { force: true }); + await shellExec(`touch ${lockfilePath}`); + }; + + await cleanup(); + const res = await shellExec(`${cli} build --workspace-folder ${workspaceFolder}`); + const response = JSON.parse(res.stdout); + assert.equal(response.outcome, 'success'); + const actual = JSON.parse((await readLocalFile(lockfilePath)).toString()); + assert.ok(actual.features['ghcr.io/devcontainers/features/dotnet:2']); + await cleanup(); + }); + + it('empty lockfile should not init when frozen', async () => { + const workspaceFolder = path.join(__dirname, 'configs/lockfile-generate-from-empty-file-frozen'); + const lockfilePath = path.join(workspaceFolder, '.devcontainer', 'devcontainer-lock.json'); + const cleanup = async () => { + await rmLocal(lockfilePath, { force: true }); + await shellExec(`touch ${lockfilePath}`); + }; + + await cleanup(); + try { + await shellExec(`${cli} build --workspace-folder ${workspaceFolder} --experimental-frozen-lockfile`); + await cleanup(); + } catch (res) { + const response = JSON.parse(res.stdout); + assert.equal(response.outcome, 'error'); + assert.equal(response.message, 'Lockfile does not match.'); + await cleanup(); + } + }); }); \ No newline at end of file From efb3ffca505e252a58d8bf4d48da5ffe4becf0e2 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Wed, 13 Sep 2023 10:15:04 -0700 Subject: [PATCH 012/291] v0.51.3 (#639) * v0.51.3 * Update CHANGELOG.md Co-authored-by: Samruddhi Khandale --------- Co-authored-by: Samruddhi Khandale --- CHANGELOG.md | 7 +++++++ package.json | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6ca5e2d61..963e86553 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,13 @@ Notable changes. +## September 2023 + +### [0.51.3] + +- Update UID only if GID is in use (https://github.com/microsoft/vscode-remote-release/issues/7284) +- Empty lockfile in workspaceFolder will initialize lockfile (https://github.com/devcontainers/cli/pull/637) + ## August 2023 ### [0.51.2] diff --git a/package.json b/package.json index 92013c1c2..c0736c51d 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.51.2", + "version": "0.51.3", "bin": { "devcontainer": "devcontainer.js" }, From 722548e6e51abc16bcaa115c638f8b1940de693f Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Fri, 29 Sep 2023 09:25:53 -0700 Subject: [PATCH 013/291] Recreate lockfile with `upgrade` command (#645) * add --regenerate flag to recreate lockfile * move to its own upgrade command * linting * promote to top-level command --- src/spec-node/devContainersSpecCLI.ts | 19 ++- src/spec-node/featureUtils.ts | 13 ++ src/spec-node/upgradeCommand.ts | 118 ++++++++++++++++++ .../.devcontainer.json | 9 ++ .../lockfile-upgrade-command/.gitignore | 1 + .../outdated.devcontainer-lock.json | 24 ++++ .../upgraded.devcontainer-lock.json | 24 ++++ src/test/container-features/lockfile.test.ts | 12 ++ 8 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 src/spec-node/featureUtils.ts create mode 100644 src/spec-node/upgradeCommand.ts create mode 100644 src/test/container-features/configs/lockfile-upgrade-command/.devcontainer.json create mode 100644 src/test/container-features/configs/lockfile-upgrade-command/.gitignore create mode 100644 src/test/container-features/configs/lockfile-upgrade-command/outdated.devcontainer-lock.json create mode 100644 src/test/container-features/configs/lockfile-upgrade-command/upgraded.devcontainer-lock.json diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index adf89ea52..c95b7cd86 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -10,7 +10,7 @@ import textTable from 'text-table'; import * as jsonc from 'jsonc-parser'; import { createDockerParams, createLog, launch, ProvisionOptions } from './devContainers'; -import { SubstitutedConfig, createContainerProperties, createFeaturesTempFolder, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution, findContainerAndIdLabels, getCacheFolder } from './utils'; +import { SubstitutedConfig, createContainerProperties, envListToObj, inspectDockerImage, isDockerFileConfig, SubstituteConfig, addSubstitution, findContainerAndIdLabels, getCacheFolder } from './utils'; import { URI } from 'vscode-uri'; import { ContainerError } from '../spec-common/errors'; import { Log, LogDimensions, LogLevel, makeLog, mapLogLevel } from '../spec-utils/log'; @@ -18,18 +18,18 @@ import { probeRemoteEnv, runLifecycleHooks, runRemoteCommand, UserEnvProbe, setu import { extendImage } from './containerFeatures'; import { DockerCLIParameters, dockerPtyCLI, inspectContainer } from '../spec-shutdown/dockerUtils'; import { buildAndExtendDockerCompose, dockerComposeCLIConfig, getDefaultImageName, getProjectName, readDockerComposeConfig, readVersionPrefix } from './dockerCompose'; -import { DevContainerConfig, DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig, getDockerComposeFilePaths } from '../spec-configuration/configuration'; +import { DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig, getDockerComposeFilePaths } from '../spec-configuration/configuration'; import { workspaceFromPath } from '../spec-utils/workspaces'; import { readDevContainerConfigFile } from './configContainer'; import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn, uriToFsPath } from '../spec-configuration/configurationCommonUtils'; import { CLIHost, getCLIHost } from '../spec-common/cliHost'; import { loadNativeModule, processSignals } from '../spec-common/commonUtils'; -import { FeaturesConfig, generateFeaturesConfig, getContainerFeaturesFolder, loadVersionInfo } from '../spec-configuration/containerFeaturesConfiguration'; +import { loadVersionInfo } from '../spec-configuration/containerFeaturesConfiguration'; import { featuresTestOptions, featuresTestHandler } from './featuresCLI/test'; import { featuresPackageHandler, featuresPackageOptions } from './featuresCLI/package'; import { featuresPublishHandler, featuresPublishOptions } from './featuresCLI/publish'; import { beforeContainerSubstitute, containerSubstitute, substitute } from '../spec-common/variableSubstitution'; -import { getPackageConfig, PackageConfiguration } from '../spec-utils/product'; +import { getPackageConfig, } from '../spec-utils/product'; import { getDevcontainerMetadata, getImageBuildInfo, getImageMetadataFromContainer, ImageMetadataEntry, lifecycleCommandOriginMapFromMetadata, mergeConfiguration, MergedDevContainerConfig } from './imageMetadata'; import { templatesPublishHandler, templatesPublishOptions } from './templatesCLI/publish'; import { templateApplyHandler, templateApplyOptions } from './templatesCLI/apply'; @@ -39,6 +39,8 @@ import { Event, NodeEventEmitter } from '../spec-utils/event'; import { ensureNoDisallowedFeatures } from './disallowedFeatures'; import { featuresResolveDependenciesHandler, featuresResolveDependenciesOptions } from './featuresCLI/resolveDependencies'; import { getFeatureIdWithoutVersion } from '../spec-configuration/containerFeaturesOCI'; +import { featuresUpgradeHandler, featuresUpgradeOptions } from './upgradeCommand'; +import { readFeaturesConfig } from './featureUtils'; const defaultDefaultUserEnvProbe: UserEnvProbe = 'loginInteractiveShell'; @@ -68,6 +70,7 @@ const mountRegex = /^type=(bind|volume),source=([^,]+),target=([^,]+)(?:,externa y.command('run-user-commands', 'Run user commands', runUserCommandsOptions, runUserCommandsHandler); y.command('read-configuration', 'Read configuration', readConfigurationOptions, readConfigurationHandler); y.command('outdated', 'Show current and available versions', outdatedOptions, outdatedHandler); + y.command('upgrade', 'Upgrade lockfile', featuresUpgradeOptions, featuresUpgradeHandler); y.command('features', 'Features commands', (y: Argv) => { y.command('test [target]', 'Test Features', featuresTestOptions, featuresTestHandler); y.command('package ', 'Package Features', featuresPackageOptions, featuresPackageHandler); @@ -1047,14 +1050,6 @@ async function readConfiguration({ process.exit(0); } -async function readFeaturesConfig(params: DockerCLIParameters, pkg: PackageConfiguration, config: DevContainerConfig, extensionPath: string, skipFeatureAutoMapping: boolean, additionalFeatures: Record>): Promise { - const { cliHost, output } = params; - const { cwd, env, platform } = cliHost; - const featuresTmpFolder = await createFeaturesTempFolder({ cliHost, package: pkg }); - const cacheFolder = await getCacheFolder(cliHost); - return generateFeaturesConfig({ extensionPath, cacheFolder, cwd, output, env, skipFeatureAutoMapping, platform }, featuresTmpFolder, config, getContainerFeaturesFolder, additionalFeatures); -} - function outdatedOptions(y: Argv) { return y.options({ 'user-data-folder': { type: 'string', description: 'Host path to a directory that is intended to be persisted and share state between sessions.' }, diff --git a/src/spec-node/featureUtils.ts b/src/spec-node/featureUtils.ts new file mode 100644 index 000000000..ca8b1d680 --- /dev/null +++ b/src/spec-node/featureUtils.ts @@ -0,0 +1,13 @@ +import { DevContainerConfig } from '../spec-configuration/configuration'; +import { FeaturesConfig, generateFeaturesConfig, getContainerFeaturesFolder } from '../spec-configuration/containerFeaturesConfiguration'; +import { DockerCLIParameters } from '../spec-shutdown/dockerUtils'; +import { PackageConfiguration } from '../spec-utils/product'; +import { createFeaturesTempFolder, getCacheFolder } from './utils'; + +export async function readFeaturesConfig(params: DockerCLIParameters, pkg: PackageConfiguration, config: DevContainerConfig, extensionPath: string, skipFeatureAutoMapping: boolean, additionalFeatures: Record>): Promise { + const { cliHost, output } = params; + const { cwd, env, platform } = cliHost; + const featuresTmpFolder = await createFeaturesTempFolder({ cliHost, package: pkg }); + const cacheFolder = await getCacheFolder(cliHost); + return generateFeaturesConfig({ extensionPath, cacheFolder, cwd, output, env, skipFeatureAutoMapping, platform }, featuresTmpFolder, config, getContainerFeaturesFolder, additionalFeatures); +} \ No newline at end of file diff --git a/src/spec-node/upgradeCommand.ts b/src/spec-node/upgradeCommand.ts new file mode 100644 index 000000000..52b208fd7 --- /dev/null +++ b/src/spec-node/upgradeCommand.ts @@ -0,0 +1,118 @@ +import { Argv } from 'yargs'; +import { UnpackArgv } from './devContainersSpecCLI'; +import { dockerComposeCLIConfig } from './dockerCompose'; +import { Log, LogLevel, mapLogLevel } from '../spec-utils/log'; +import { createLog } from './devContainers'; +import { getPackageConfig } from '../spec-utils/product'; +import { DockerCLIParameters } from '../spec-shutdown/dockerUtils'; +import path from 'path'; +import { getCLIHost } from '../spec-common/cliHost'; +import { loadNativeModule } from '../spec-common/commonUtils'; +import { URI } from 'vscode-uri'; +import { workspaceFromPath } from '../spec-utils/workspaces'; +import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn, uriToFsPath } from '../spec-configuration/configurationCommonUtils'; +import { readDevContainerConfigFile } from './configContainer'; +import { ContainerError } from '../spec-common/errors'; +import { getCacheFolder } from './utils'; +import { getLockfilePath, writeLockfile } from '../spec-configuration/lockfile'; +import { writeLocalFile } from '../spec-utils/pfs'; +import { readFeaturesConfig } from './featureUtils'; + +export function featuresUpgradeOptions(y: Argv) { + return y + .options({ + 'workspace-folder': { type: 'string', description: 'Workspace folder.', demandOption: true }, + 'docker-path': { type: 'string', description: 'Path to docker executable.', default: 'docker' }, + 'docker-compose-path': { type: 'string', description: 'Path to docker-compose executable.', default: 'docker-compose' }, + 'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' }, + 'log-level': { choices: ['error' as 'error', 'info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' }, + }); +} + +export type FeaturesUpgradeArgs = UnpackArgv>; + +export function featuresUpgradeHandler(args: FeaturesUpgradeArgs) { + (async () => await featuresUpgrade(args))().catch(console.error); +} + +async function featuresUpgrade({ + 'workspace-folder': workspaceFolderArg, + 'docker-path': dockerPath, + config: configArg, + 'docker-compose-path': dockerComposePath, + 'log-level': inputLogLevel, +}: FeaturesUpgradeArgs) { + const disposables: (() => Promise | undefined)[] = []; + const dispose = async () => { + await Promise.all(disposables.map(d => d())); + }; + let output: Log | undefined; + try { + const workspaceFolder = path.resolve(process.cwd(), workspaceFolderArg); + const configFile = configArg ? URI.file(path.resolve(process.cwd(), configArg)) : undefined; + const cliHost = await getCLIHost(workspaceFolder, loadNativeModule, true); + const extensionPath = path.join(__dirname, '..', '..'); + const sessionStart = new Date(); + const pkg = getPackageConfig(); + const output = createLog({ + logLevel: mapLogLevel(inputLogLevel), + logFormat: 'text', + log: text => process.stderr.write(text), + terminalDimensions: undefined, + }, pkg, sessionStart, disposables); + const dockerComposeCLI = dockerComposeCLIConfig({ + exec: cliHost.exec, + env: cliHost.env, + output, + }, dockerPath, dockerComposePath); + const dockerParams: DockerCLIParameters = { + cliHost, + dockerCLI: dockerPath, + dockerComposeCLI, + env: cliHost.env, + output, + }; + + const workspace = workspaceFromPath(cliHost.path, workspaceFolder); + const configPath = configFile ? configFile : await getDevContainerConfigPathIn(cliHost, workspace.configFolderPath); + const configs = configPath && await readDevContainerConfigFile(cliHost, workspace, configPath, true, output) || undefined; + if (!configs) { + throw new ContainerError({ description: `Dev container config (${uriToFsPath(configFile || getDefaultDevContainerConfigPath(cliHost, workspace!.configFolderPath), cliHost.platform)}) not found.` }); + } + const config = configs.config.config; + const cacheFolder = await getCacheFolder(cliHost); + const params = { + extensionPath, + cacheFolder, + cwd: cliHost.cwd, + output, + env: cliHost.env, + skipFeatureAutoMapping: false, + platform: cliHost.platform, + }; + + const bold = process.stdout.isTTY ? '\x1b[1m' : ''; + const clear = process.stdout.isTTY ? '\x1b[0m' : ''; + output.raw(`${bold}Upgrading lockfile...\n${clear}\n`, LogLevel.Info); + + // Truncate existing lockfile + const lockfilePath = getLockfilePath(config); + await writeLocalFile(lockfilePath, ''); + // Update lockfile + const featuresConfig = await readFeaturesConfig(dockerParams, pkg, config, extensionPath, false, {}); + if (!featuresConfig) { + throw new ContainerError({ description: `Failed to update lockfile` }); + } + await writeLockfile(params, config, featuresConfig, true); + } catch (err) { + if (output) { + output.write(err && (err.stack || err.message) || String(err)); + } else { + console.error(err); + } + await dispose(); + process.exit(1); + } + await dispose(); + process.exit(0); +} \ No newline at end of file diff --git a/src/test/container-features/configs/lockfile-upgrade-command/.devcontainer.json b/src/test/container-features/configs/lockfile-upgrade-command/.devcontainer.json new file mode 100644 index 000000000..e86e720cb --- /dev/null +++ b/src/test/container-features/configs/lockfile-upgrade-command/.devcontainer.json @@ -0,0 +1,9 @@ +{ + "image": "mcr.microsoft.com/devcontainers/base:ubuntu", + "features": { + "ghcr.io/devcontainers/features/git:1.1.5": "latest", + "ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c": "latest", + "ghcr.io/devcontainers/features/github-cli:1.0.9": "latest", + "ghcr.io/devcontainers/features/azure-cli:1.2.1": "latest" + } +} \ No newline at end of file diff --git a/src/test/container-features/configs/lockfile-upgrade-command/.gitignore b/src/test/container-features/configs/lockfile-upgrade-command/.gitignore new file mode 100644 index 000000000..743e650e4 --- /dev/null +++ b/src/test/container-features/configs/lockfile-upgrade-command/.gitignore @@ -0,0 +1 @@ +.devcontainer-lock.json \ No newline at end of file diff --git a/src/test/container-features/configs/lockfile-upgrade-command/outdated.devcontainer-lock.json b/src/test/container-features/configs/lockfile-upgrade-command/outdated.devcontainer-lock.json new file mode 100644 index 000000000..2567ef7f9 --- /dev/null +++ b/src/test/container-features/configs/lockfile-upgrade-command/outdated.devcontainer-lock.json @@ -0,0 +1,24 @@ +{ + "features": { + "ghcr.io/devcontainers/features/azure-cli:1.2.0": { + "version": "1.2.0", + "resolved": "ghcr.io/devcontainers/features/azure-cli@sha256:cb2832052c03202e321c84389116a3981b5b24b8c6d0532841c46b03500e1415", + "integrity": "sha256:cb2832052c03202e321c84389116a3981b5b24b8c6d0532841c46b03500e1415" + }, + "ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c": { + "version": "1.0.6", + "resolved": "ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c", + "integrity": "sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c" + }, + "ghcr.io/devcontainers/features/git:1.1.0": { + "version": "1.1.0", + "resolved": "ghcr.io/devcontainers/features/git@sha256:bc4b9ae3f843a35edfea7b9295a0e89958d2ddfe8b2bf327ec1a5f7cf3c5a2fa", + "integrity": "sha256:bc4b9ae3f843a35edfea7b9295a0e89958d2ddfe8b2bf327ec1a5f7cf3c5a2fa" + }, + "ghcr.io/devcontainers/features/github-cli:1.0.2": { + "version": "1.0.2", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:0b52d28bcaf2054bf70fd932161f93aae34830031d29747680acdd500e02cc09", + "integrity": "sha256:0b52d28bcaf2054bf70fd932161f93aae34830031d29747680acdd500e02cc09" + } + } +} \ No newline at end of file diff --git a/src/test/container-features/configs/lockfile-upgrade-command/upgraded.devcontainer-lock.json b/src/test/container-features/configs/lockfile-upgrade-command/upgraded.devcontainer-lock.json new file mode 100644 index 000000000..bb0b7103b --- /dev/null +++ b/src/test/container-features/configs/lockfile-upgrade-command/upgraded.devcontainer-lock.json @@ -0,0 +1,24 @@ +{ + "features": { + "ghcr.io/devcontainers/features/azure-cli:1.2.1": { + "version": "1.2.1", + "resolved": "ghcr.io/devcontainers/features/azure-cli@sha256:a00aa292592a8df58a940d6f6dfcf2bfd3efab145f62a17ccb12656528793134", + "integrity": "sha256:a00aa292592a8df58a940d6f6dfcf2bfd3efab145f62a17ccb12656528793134" + }, + "ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c": { + "version": "1.0.6", + "resolved": "ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c", + "integrity": "sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c" + }, + "ghcr.io/devcontainers/features/git:1.1.5": { + "version": "1.1.5", + "resolved": "ghcr.io/devcontainers/features/git@sha256:2ab83ca71d55d5c00a1255b07f3a83a53cd2de77ce8b9637abad38095d672a5b", + "integrity": "sha256:2ab83ca71d55d5c00a1255b07f3a83a53cd2de77ce8b9637abad38095d672a5b" + }, + "ghcr.io/devcontainers/features/github-cli:1.0.9": { + "version": "1.0.9", + "resolved": "ghcr.io/devcontainers/features/github-cli@sha256:9024deeca80347dea7603a3bb5b4951988f0bf5894ba036a6ee3f29c025692c6", + "integrity": "sha256:9024deeca80347dea7603a3bb5b4951988f0bf5894ba036a6ee3f29c025692c6" + } + } +} \ No newline at end of file diff --git a/src/test/container-features/lockfile.test.ts b/src/test/container-features/lockfile.test.ts index c49bcc28e..5c61073fa 100644 --- a/src/test/container-features/lockfile.test.ts +++ b/src/test/container-features/lockfile.test.ts @@ -117,6 +117,18 @@ describe('Lockfile', function () { assert.ok(azure.latest); }); + it('upgrade command', async () => { + const workspaceFolder = path.join(__dirname, 'configs/lockfile-upgrade-command'); + + const lockfilePath = path.join(workspaceFolder, '.devcontainer-lock.json'); + await cpLocal(path.join(workspaceFolder, 'outdated.devcontainer-lock.json'), lockfilePath); + + await shellExec(`${cli} upgrade --workspace-folder ${workspaceFolder}`); + const actual = await readLocalFile(lockfilePath); + const expected = await readLocalFile(path.join(workspaceFolder, 'upgraded.devcontainer-lock.json')); + assert.equal(actual.toString(), expected.toString()); + }); + it('OCI feature integrity', async () => { const workspaceFolder = path.join(__dirname, 'configs/lockfile-oci-integrity'); From d2c1bc89c39f79b8a8da437964976965f3400e81 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Tue, 3 Oct 2023 09:11:56 -0700 Subject: [PATCH 014/291] v0.52.0 (#649) --- CHANGELOG.md | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 963e86553..c4cc0b99a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,12 @@ Notable changes. +## October 2023 + +### [0.52.0] + +- Add `upgrade` command to generate an updated lockfile (https://github.com/devcontainers/cli/pull/645) + ## September 2023 ### [0.51.3] diff --git a/package.json b/package.json index c0736c51d..b0e59cf96 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.51.3", + "version": "0.52.0", "bin": { "devcontainer": "devcontainer.js" }, From 347abda2f952490090d43187dcd5b2cb8381fd16 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Mon, 9 Oct 2023 13:09:54 -0700 Subject: [PATCH 015/291] plugin changes --- src/spec-common/commonUtils.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/spec-common/commonUtils.ts b/src/spec-common/commonUtils.ts index 5d5f1c0c7..41bd911f6 100644 --- a/src/spec-common/commonUtils.ts +++ b/src/spec-common/commonUtils.ts @@ -218,6 +218,16 @@ export async function runCommand(options: { resolve({ cmdOutput }); } }); + + p.onData(data => { + if (data.includes('authorization denied by plugin')) { + reject({ + message: `Command failed due to authorization denied by plugin: ${cmd} ${(args || []).join(' ')}`, + cmdOutput, + }); + } + }); + p.exit.then(({ code, signal }) => { try { if (print === 'end') { From c8ba785a1f7f2f63862d11ede8bd8d8af7b2f384 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Thu, 12 Oct 2023 18:45:30 +0000 Subject: [PATCH 016/291] updates description if error due to plugin --- src/spec-common/commonUtils.ts | 9 --------- src/spec-node/singleContainer.ts | 8 +++++++- 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/spec-common/commonUtils.ts b/src/spec-common/commonUtils.ts index 41bd911f6..e1cda9124 100644 --- a/src/spec-common/commonUtils.ts +++ b/src/spec-common/commonUtils.ts @@ -218,15 +218,6 @@ export async function runCommand(options: { resolve({ cmdOutput }); } }); - - p.onData(data => { - if (data.includes('authorization denied by plugin')) { - reject({ - message: `Command failed due to authorization denied by plugin: ${cmd} ${(args || []).join(' ')}`, - cmdOutput, - }); - } - }); p.exit.then(({ code, signal }) => { try { diff --git a/src/spec-node/singleContainer.ts b/src/spec-node/singleContainer.ts index 0aa45fa22..71edefe51 100644 --- a/src/spec-node/singleContainer.ts +++ b/src/spec-node/singleContainer.ts @@ -71,8 +71,14 @@ export async function openDockerfileDevContainer(params: DockerResolverParameter } function createSetupError(originalError: any, container: ContainerDetails | undefined, params: DockerResolverParameters, containerProperties: ContainerProperties | undefined, config: DevContainerConfig | undefined): ContainerError { + let description = 'An error occurred setting up the container.'; + + if (originalError?.cmdOutput?.includes('docker: Error response from daemon: authorization denied by plugin')) { + description = originalError.cmdOutput; + } + const err = originalError instanceof ContainerError ? originalError : new ContainerError({ - description: 'An error occurred setting up the container.', + description, originalError }); if (container) { From 25315e044005396628a060b989c32c009d87e0e1 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Thu, 12 Oct 2023 18:47:19 +0000 Subject: [PATCH 017/291] removes extra line --- src/spec-common/commonUtils.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/spec-common/commonUtils.ts b/src/spec-common/commonUtils.ts index e1cda9124..5d5f1c0c7 100644 --- a/src/spec-common/commonUtils.ts +++ b/src/spec-common/commonUtils.ts @@ -218,7 +218,6 @@ export async function runCommand(options: { resolve({ cmdOutput }); } }); - p.exit.then(({ code, signal }) => { try { if (print === 'end') { From 858b64e433bfb65351db8a037036075a77778b99 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Mon, 16 Oct 2023 18:32:43 +0000 Subject: [PATCH 018/291] v0.52.1 --- CHANGELOG.md | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c4cc0b99a..c29de4934 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,10 @@ Notable changes. ## October 2023 +### [0.52.1] + +- Updates create error description to include cause for docker auth plugin errors (https://github.com/devcontainers/cli/pull/656) + ### [0.52.0] - Add `upgrade` command to generate an updated lockfile (https://github.com/devcontainers/cli/pull/645) diff --git a/package.json b/package.json index b0e59cf96..626507b84 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.52.0", + "version": "0.52.1", "bin": { "devcontainer": "devcontainer.js" }, From 5ce1f6cea20a51a634cba877c00a7e7422928314 Mon Sep 17 00:00:00 2001 From: Samruddhi Khandale Date: Thu, 19 Oct 2023 20:36:20 +0000 Subject: [PATCH 019/291] Docker compose: Update description --- src/spec-node/dockerCompose.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts index 6dbbd4d05..398a4d58c 100644 --- a/src/spec-node/dockerCompose.ts +++ b/src/spec-node/dockerCompose.ts @@ -423,7 +423,13 @@ async function startContainer(params: DockerResolverParameters, buildParams: Doc } } catch (err) { cancel!(); - throw new ContainerError({ description: 'An error occurred starting Docker Compose up.', originalError: err, data: { fileWithError: localComposeFiles[0] } }); + + let description = 'An error occurred starting Docker Compose up.'; + if (err?.cmdOutput?.includes('Cannot create container for service app: authorization denied by plugin')) { + description = err.cmdOutput; + } + + throw new ContainerError({ description, originalError: err, data: { fileWithError: localComposeFiles[0] } }); } await started; From c377116d51b2d8d51fd5c96d8a7db180c8301a44 Mon Sep 17 00:00:00 2001 From: "Cody A. Taylor" Date: Tue, 24 Oct 2023 11:56:10 -0400 Subject: [PATCH 020/291] template apply: Provide default feature options. (#662) The `options` of a feature is never validated. So therefore, if the user does not spell it out, the value is simply `undefined`. https://github.com/devcontainers/cli/blob/71180e791b13371f8313d981dfcf06b3c8fcd2ab/src/spec-node/templatesCLI/apply.ts#L123-L129 This drops the entire feature from the intended edit. --- src/spec-configuration/containerTemplatesOCI.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/spec-configuration/containerTemplatesOCI.ts b/src/spec-configuration/containerTemplatesOCI.ts index acd40a702..9df3d8d79 100644 --- a/src/spec-configuration/containerTemplatesOCI.ts +++ b/src/spec-configuration/containerTemplatesOCI.ts @@ -161,7 +161,7 @@ async function addFeatures(output: Log, newFeatures: TemplateFeatureOption[], co const propertyPath = ['features', newFeature.id]; edits = edits.concat( - jsonc.modify(updatedText, propertyPath, newFeature.options, { formattingOptions: {} } + jsonc.modify(updatedText, propertyPath, newFeature.options ?? {}, { formattingOptions: {} } )); updatedText = jsonc.applyEdits(updatedText, edits); From 88cbc17666ae1e962260044b525ed4e70cd811f2 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Wed, 8 Nov 2023 13:46:28 -0800 Subject: [PATCH 021/291] fix version sorting and report major version in `outdated` (#670) * fix version sorting and report major version in outdated * convert wantedMajor and latestMajor to string * update test to expect just the full semver version in the publishedVersions object * break into a new function * update test and some names for clarity * more consistent naming --- .../containerCollectionsOCI.ts | 39 +++-- .../containerFeaturesConfiguration.ts | 6 +- .../publishCommandImpl.ts | 30 ++-- src/spec-node/featuresCLI/info.ts | 18 +- src/spec-node/featuresCLI/publish.ts | 4 +- src/spec-node/templatesCLI/publish.ts | 2 +- .../.devcontainer.json | 3 +- .../containerFeaturesOCIPush.test.ts | 24 +-- .../featuresCLICommands.test.ts | 160 +++++++++++++++--- src/test/container-features/lockfile.test.ts | 8 + 10 files changed, 218 insertions(+), 76 deletions(-) diff --git a/src/spec-configuration/containerCollectionsOCI.ts b/src/spec-configuration/containerCollectionsOCI.ts index 16cab0ad8..1ffb810c9 100644 --- a/src/spec-configuration/containerCollectionsOCI.ts +++ b/src/spec-configuration/containerCollectionsOCI.ts @@ -433,9 +433,28 @@ async function getJsonWithMimeType(params: CommonParams, url: string, ref: OC } } -// Lists published versions/tags of a feature/template +// Gets published tags and sorts them by ascending semantic version. +// Omits any tags (eg: 'latest', or major/minor tags '1','1.0') that are not semantic versions. +export async function getVersionsStrictSorted(params: CommonParams, ref: OCIRef): Promise { + const { output } = params; + + const publishedTags = await getPublishedTags(params, ref); + if (!publishedTags) { + return; + } + + const sortedVersions = publishedTags + .filter(f => semver.valid(f)) // Remove all major,minor,latest tags + .sort((a, b) => semver.compare(a, b)); + + output.write(`Published versions (sorted) for '${ref.id}': ${JSON.stringify(sortedVersions, undefined, 2)}`, LogLevel.Trace); + + return sortedVersions; +} + +// Lists published tags of a Feature/Template // Specification: https://github.com/opencontainers/distribution-spec/blob/v1.0.1/spec.md#content-discovery -export async function getPublishedVersions(params: CommonParams, ref: OCIRef, sorted: boolean = false): Promise { +export async function getPublishedTags(params: CommonParams, ref: OCIRef): Promise { const { output } = params; try { const url = `https://${ref.registry}/v2/${ref.namespace}/${ref.id}/tags/list`; @@ -470,18 +489,10 @@ export async function getPublishedVersions(params: CommonParams, ref: OCIRef, so const publishedVersionsResponse: OCITagList = JSON.parse(body); - if (!sorted) { - return publishedVersionsResponse.tags; - } - - // Sort tags in descending order, removing latest. - const hasLatest = publishedVersionsResponse.tags.includes('latest'); - const sortedVersions = publishedVersionsResponse.tags - .filter(f => f !== 'latest') - .sort((a, b) => semver.compareIdentifiers(a, b)); - - - return hasLatest ? ['latest', ...sortedVersions] : sortedVersions; + // Return published tags from the registry as-is, meaning: + // - Not necessarily sorted + // - *Including* major/minor/latest tags + return publishedVersionsResponse.tags; } catch (e) { output.write(`Failed to parse published versions: ${e}`, LogLevel.Error); return; diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index 38ce796d2..24a65fb0f 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -17,7 +17,7 @@ import { Log, LogLevel } from '../spec-utils/log'; import { request } from '../spec-utils/httpRequest'; import { fetchOCIFeature, tryGetOCIFeatureSet, fetchOCIFeatureManifestIfExistsFromUserIdentifier } from './containerFeaturesOCI'; import { uriToFsPath } from './configurationCommonUtils'; -import { CommonParams, ManifestContainer, OCIManifest, OCIRef, getPublishedVersions, getRef } from './containerCollectionsOCI'; +import { CommonParams, ManifestContainer, OCIManifest, OCIRef, getRef, getVersionsStrictSorted } from './containerCollectionsOCI'; import { Lockfile, readLockfile, writeLockfile } from './lockfile'; import { computeDependsOnInstallationOrder } from './containerFeaturesOrder'; import { logFeatureAdvisories } from './featureAdvisories'; @@ -591,7 +591,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co const updatedFeatureId = getBackwardCompatibleFeatureId(output, userFeatureId); const featureRef = getRef(output, updatedFeatureId); if (featureRef) { - const versions = (await getPublishedVersions(params, featureRef, true)) + const versions = (await getVersionsStrictSorted(params, featureRef)) ?.reverse(); if (versions) { const lockfileVersion = lockfile?.features[userFeatureId]?.version; @@ -613,7 +613,9 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co features[userFeatureId] = { current: lockfileVersion || wanted, wanted, + wantedMajor: wanted && semver.major(wanted)?.toString(), latest: versions[0], + latestMajor: semver.major(versions[0])?.toString(), }; } } diff --git a/src/spec-node/collectionCommonUtils/publishCommandImpl.ts b/src/spec-node/collectionCommonUtils/publishCommandImpl.ts index ad646ca6d..c7321fb93 100644 --- a/src/spec-node/collectionCommonUtils/publishCommandImpl.ts +++ b/src/spec-node/collectionCommonUtils/publishCommandImpl.ts @@ -1,22 +1,22 @@ import path from 'path'; import * as semver from 'semver'; import { Log, LogLevel } from '../../spec-utils/log'; -import { CommonParams, getPublishedVersions, OCICollectionRef, OCIRef } from '../../spec-configuration/containerCollectionsOCI'; +import { CommonParams, getPublishedTags, OCICollectionRef, OCIRef } from '../../spec-configuration/containerCollectionsOCI'; import { OCICollectionFileName } from './packageCommandImpl'; import { pushCollectionMetadata, pushOCIFeatureOrTemplate } from '../../spec-configuration/containerCollectionsOCIPush'; let semanticVersions: string[] = []; -function updateSemanticVersionsList(publishedVersions: string[], version: string, range: string, publishVersion: string) { +function updateSemanticTagsList(publishedTags: string[], version: string, range: string, publishVersion: string) { // Reference: https://github.com/npm/node-semver#ranges-1 - const publishedMaxVersion = semver.maxSatisfying(publishedVersions, range); + const publishedMaxVersion = semver.maxSatisfying(publishedTags, range); if (publishedMaxVersion === null || semver.compare(version, publishedMaxVersion) === 1) { semanticVersions.push(publishVersion); } return; } -export function getSemanticVersions(version: string, publishedVersions: string[], output: Log) { - if (publishedVersions.includes(version)) { +export function getSemanticTags(version: string, tags: string[], output: Log) { + if (tags.includes(version)) { output.write(`(!) WARNING: Version ${version} already exists, skipping ${version}...`, LogLevel.Warning); return undefined; } @@ -31,10 +31,10 @@ export function getSemanticVersions(version: string, publishedVersions: string[] // Adds semantic versions depending upon the existings (published) versions // eg. 1.2.3 --> [1, 1.2, 1.2.3, latest] - updateSemanticVersionsList(publishedVersions, version, `${parsedVersion.major}.x.x`, `${parsedVersion.major}`); - updateSemanticVersionsList(publishedVersions, version, `${parsedVersion.major}.${parsedVersion.minor}.x`, `${parsedVersion.major}.${parsedVersion.minor}`); + updateSemanticTagsList(tags, version, `${parsedVersion.major}.x.x`, `${parsedVersion.major}`); + updateSemanticTagsList(tags, version, `${parsedVersion.major}.${parsedVersion.minor}.x`, `${parsedVersion.major}.${parsedVersion.minor}`); semanticVersions.push(version); - updateSemanticVersionsList(publishedVersions, version, `x.x.x`, 'latest'); + updateSemanticTagsList(tags, version, `x.x.x`, 'latest'); return semanticVersions; } @@ -43,24 +43,24 @@ export async function doPublishCommand(params: CommonParams, version: string, oc const { output } = params; output.write(`Fetching published versions...`, LogLevel.Info); - const publishedVersions = await getPublishedVersions(params, ociRef); + const publishedTags = await getPublishedTags(params, ociRef); - if (!publishedVersions) { + if (!publishedTags) { return; } - const semanticVersions: string[] | undefined = getSemanticVersions(version, publishedVersions, output); + const semanticTags: string[] | undefined = getSemanticTags(version, publishedTags, output); - if (!!semanticVersions) { - output.write(`Publishing versions: ${semanticVersions.toString()}...`, LogLevel.Info); + if (!!semanticTags) { + output.write(`Publishing tags: ${semanticTags.toString()}...`, LogLevel.Info); const pathToTgz = path.join(outputDir, archiveName); - const digest = await pushOCIFeatureOrTemplate(params, ociRef, pathToTgz, semanticVersions, collectionType, featureAnnotations); + const digest = await pushOCIFeatureOrTemplate(params, ociRef, pathToTgz, semanticTags, collectionType, featureAnnotations); if (!digest) { output.write(`(!) ERR: Failed to publish ${collectionType}: '${ociRef.resource}'`, LogLevel.Error); return; } output.write(`Published ${collectionType}: '${ociRef.id}'`, LogLevel.Info); - return { publishedVersions: semanticVersions, digest }; + return { publishedTags: semanticTags, digest }; } return {}; // Not an error if no versions were published, likely they just already existed and were skipped. diff --git a/src/spec-node/featuresCLI/info.ts b/src/spec-node/featuresCLI/info.ts index 3cfb71619..4a2c7eb9e 100644 --- a/src/spec-node/featuresCLI/info.ts +++ b/src/spec-node/featuresCLI/info.ts @@ -1,5 +1,5 @@ import { Argv } from 'yargs'; -import { OCIManifest, OCIRef, fetchOCIManifestIfExists, getPublishedVersions, getRef } from '../../spec-configuration/containerCollectionsOCI'; +import { OCIManifest, OCIRef, fetchOCIManifestIfExists, getPublishedTags, getRef } from '../../spec-configuration/containerCollectionsOCI'; import { Log, LogLevel, mapLogLevel } from '../../spec-utils/log'; import { getPackageConfig } from '../../spec-utils/product'; import { createLog } from '../devContainers'; @@ -27,7 +27,7 @@ export function featuresInfoHandler(args: FeaturesInfoArgs) { interface InfoJsonOutput { manifest?: OCIManifest; canonicalId?: string; - publishedVersions?: string[]; + publishedTags?: string[]; } async function featuresInfo({ @@ -86,12 +86,12 @@ async function featuresInfo({ // --- Get all published tags for resource if (mode === 'tags' || mode === 'verbose') { - const publishedVersions = await getTags(params, featureRef); + const publishedTags = await getTags(params, featureRef); if (outputFormat === 'text') { - console.log(encloseStringInBox('Published Version')); - console.log(`${publishedVersions.join('\n ')}`); + console.log(encloseStringInBox('Published Tags')); + console.log(`${publishedTags.join('\n ')}`); } else { - jsonOutput.publishedVersions = publishedVersions; + jsonOutput.publishedTags = publishedTags; } } @@ -145,8 +145,8 @@ async function getManifest(params: { output: Log; env: NodeJS.ProcessEnv; output async function getTags(params: { output: Log; env: NodeJS.ProcessEnv; outputFormat: string }, featureRef: OCIRef) { const { outputFormat } = params; - const publishedVersions = await getPublishedVersions(params, featureRef, true); - if (!publishedVersions || publishedVersions.length === 0) { + const publishedTags = await getPublishedTags(params, featureRef); + if (!publishedTags || publishedTags.length === 0) { if (outputFormat === 'json') { console.log(JSON.stringify({})); } else { @@ -154,7 +154,7 @@ async function getTags(params: { output: Log; env: NodeJS.ProcessEnv; outputForm } process.exit(1); } - return publishedVersions; + return publishedTags; } function encloseStringInBox(str: string, indent: number = 0) { diff --git a/src/spec-node/featuresCLI/publish.ts b/src/spec-node/featuresCLI/publish.ts index 29838e408..eff229596 100644 --- a/src/spec-node/featuresCLI/publish.ts +++ b/src/spec-node/featuresCLI/publish.ts @@ -100,7 +100,7 @@ async function featuresPublish({ process.exit(1); } - const isPublished = (publishResult?.digest && publishResult?.publishedVersions.length > 0); + const isPublished = (publishResult?.digest && publishResult?.publishedTags.length > 0); let thisResult = isPublished ? { ...publishResult, version: f.version, @@ -126,7 +126,7 @@ async function featuresPublish({ process.exit(1); } - if (publishResult?.digest && publishResult?.publishedVersions.length > 0) { + if (publishResult?.digest && publishResult?.publishedTags.length > 0) { publishedLegacyIds.push(legacyId); } } diff --git a/src/spec-node/templatesCLI/publish.ts b/src/spec-node/templatesCLI/publish.ts index 02e8599f8..858eebe4b 100644 --- a/src/spec-node/templatesCLI/publish.ts +++ b/src/spec-node/templatesCLI/publish.ts @@ -94,7 +94,7 @@ async function templatesPublish({ process.exit(1); } - const thisResult = (publishResult?.digest && publishResult?.publishedVersions?.length > 0) ? { + const thisResult = (publishResult?.digest && publishResult?.publishedTags?.length > 0) ? { ...publishResult, version: t.version, } : {}; diff --git a/src/test/container-features/configs/lockfile-outdated-command/.devcontainer.json b/src/test/container-features/configs/lockfile-outdated-command/.devcontainer.json index a25c8f05d..9aed056da 100644 --- a/src/test/container-features/configs/lockfile-outdated-command/.devcontainer.json +++ b/src/test/container-features/configs/lockfile-outdated-command/.devcontainer.json @@ -4,6 +4,7 @@ "ghcr.io/devcontainers/features/git:1.0": "latest", "ghcr.io/devcontainers/features/git-lfs@sha256:24d5802c837b2519b666a8403a9514c7296d769c9607048e9f1e040e7d7e331c": "latest", "ghcr.io/devcontainers/features/github-cli": "latest", - "ghcr.io/devcontainers/features/azure-cli:0": "latest" + "ghcr.io/devcontainers/features/azure-cli:0": "latest", + "ghcr.io/codspace/versioning/foo:0.3.1": "latest" } } \ No newline at end of file diff --git a/src/test/container-features/containerFeaturesOCIPush.test.ts b/src/test/container-features/containerFeaturesOCIPush.test.ts index b6206a7a1..3a648cac2 100644 --- a/src/test/container-features/containerFeaturesOCIPush.test.ts +++ b/src/test/container-features/containerFeaturesOCIPush.test.ts @@ -15,7 +15,7 @@ export const output = makeLog(createPlainLog(text => process.stdout.write(text), const testAssetsDir = `${__dirname}/assets`; interface PublishResult { - publishedVersions: string[]; + publishedTags: string[]; digest: string; version: string; publishedLegacyIds?: string[]; @@ -87,7 +87,7 @@ registry`; const color = result['color']; assert.isDefined(color); assert.isDefined(color.digest); - assert.deepEqual(color.publishedVersions, [ + assert.deepEqual(color.publishedTags, [ '1', '1.0', '1.0.0', @@ -99,7 +99,7 @@ registry`; const hello = result['hello']; assert.isDefined(hello); assert.isDefined(hello.digest); - assert.deepEqual(hello.publishedVersions, [ + assert.deepEqual(hello.publishedTags, [ '1', '1.0', '1.0.0', @@ -123,8 +123,8 @@ registry`; assert.isTrue(success); assert.isDefined(infoTagsResult); const tags = JSON.parse(infoTagsResult.stdout); - const publishedVersions: string[] = tags['publishedVersions']; - assert.equal(publishedVersions.length, 4); + const publishedTags: string[] = tags['publishedTags']; + assert.equal(publishedTags.length, 4); success = false; // Reset success flag. try { @@ -172,15 +172,15 @@ registry`; assert.isObject(color); // Check that the color object has no properties assert.isUndefined(color.digest); - assert.isUndefined(color.publishedVersions); + assert.isUndefined(color.publishedTags); assert.isUndefined(color.version); // -- The breakfix version of hello was updated, so major and minor should be published again, too. const hello = result['hello']; assert.isDefined(hello); assert.isDefined(hello.digest); - assert.isArray(hello.publishedVersions); - assert.deepEqual(hello.publishedVersions, [ + assert.isArray(hello.publishedTags); + assert.deepEqual(hello.publishedTags, [ '1', '1.0', '1.0.1', @@ -219,7 +219,7 @@ registry`; const newColor = result['new-color']; assert.isDefined(newColor); assert.isDefined(newColor.digest); - assert.deepEqual(newColor.publishedVersions, [ + assert.deepEqual(newColor.publishedTags, [ '1', '1.0', '1.0.1', @@ -234,7 +234,7 @@ registry`; const hello = result['hello']; assert.isDefined(hello); assert.isDefined(hello.digest); - assert.deepEqual(hello.publishedVersions, [ + assert.deepEqual(hello.publishedTags, [ '1', '1.0', '1.0.0', @@ -299,8 +299,8 @@ registry`; assert.isTrue(success); assert.isDefined(infoTagsResult); const tags = JSON.parse(infoTagsResult.stdout); - const publishedVersions: string[] = tags['publishedVersions']; - assert.equal(publishedVersions.length, 4); + const publishedTags: string[] = tags['publishedTags']; + assert.equal(publishedTags.length, 4); }); }); diff --git a/src/test/container-features/featuresCLICommands.test.ts b/src/test/container-features/featuresCLICommands.test.ts index 3adf3cc3a..a9dc6a3a1 100644 --- a/src/test/container-features/featuresCLICommands.test.ts +++ b/src/test/container-features/featuresCLICommands.test.ts @@ -3,8 +3,8 @@ import path from 'path'; import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log'; import { isLocalFile, readLocalFile } from '../../spec-utils/pfs'; import { ExecResult, shellExec } from '../testUtils'; -import { getSemanticVersions } from '../../spec-node/collectionCommonUtils/publishCommandImpl'; -import { getRef, getPublishedVersions } from '../../spec-configuration/containerCollectionsOCI'; +import { getSemanticTags } from '../../spec-node/collectionCommonUtils/publishCommandImpl'; +import { getRef, getPublishedTags, getVersionsStrictSorted } from '../../spec-configuration/containerCollectionsOCI'; export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace)); const pkg = require('../../../package.json'); @@ -484,75 +484,195 @@ describe('CLI features subcommands', async function () { describe('test function getSermanticVersions', () => { it('should generate correct semantic versions for first publishing', async () => { let version = '1.0.0'; - let publishedVersions: string[] = []; + let publishedTags: string[] = []; let expectedSemVer = ['1', '1.0', '1.0.0', 'latest']; - let semanticVersions = getSemanticVersions(version, publishedVersions, output); + let semanticVersions = getSemanticTags(version, publishedTags, output); assert.equal(semanticVersions?.toString(), expectedSemVer.toString()); }); it('should generate correct semantic versions for publishing new patch version', async () => { let version = '1.0.1'; - let publishedVersions = ['1', '1.0', '1.0.0', 'latest']; + let publishedTags = ['1', '1.0', '1.0.0', 'latest']; let expectedSemVer = ['1', '1.0', '1.0.1', 'latest']; - let semanticVersions = getSemanticVersions(version, publishedVersions, output); + let semanticVersions = getSemanticTags(version, publishedTags, output); assert.equal(semanticVersions?.toString(), expectedSemVer.toString()); }); it('should generate correct semantic versions for publishing new minor version', async () => { let version = '1.1.0'; - let publishedVersions = ['1', '1.0', '1.0.0', '1.0.1', 'latest']; + let publishedTags = ['1', '1.0', '1.0.0', '1.0.1', 'latest']; let expectedSemVer = ['1', '1.1', '1.1.0', 'latest']; - let semanticVersions = getSemanticVersions(version, publishedVersions, output); + let semanticVersions = getSemanticTags(version, publishedTags, output); assert.equal(semanticVersions?.toString(), expectedSemVer.toString()); }); it('should generate correct semantic versions for publishing new major version', async () => { let version = '2.0.0'; - let publishedVersions = ['1', '1.0', '1.0.0', 'latest']; + let publishedTags = ['1', '1.0', '1.0.0', 'latest']; let expectedSemVer = ['2', '2.0', '2.0.0', 'latest']; - let semanticVersions = getSemanticVersions(version, publishedVersions, output); + let semanticVersions = getSemanticTags(version, publishedTags, output); assert.equal(semanticVersions?.toString(), expectedSemVer.toString()); }); it('should generate correct semantic versions for publishing hotfix patch version', async () => { let version = '1.0.2'; - let publishedVersions = ['1', '1.0', '1.0.0', '1.0.1', '1.1', '1.1.0', '2', '2.0', '2.0.0', 'latest']; + let publishedTags = ['1', '1.0', '1.0.0', '1.0.1', '1.1', '1.1.0', '2', '2.0', '2.0.0', 'latest']; let expectedSemVer = ['1.0', '1.0.2']; - let semanticVersions = getSemanticVersions(version, publishedVersions, output); + let semanticVersions = getSemanticTags(version, publishedTags, output); assert.equal(semanticVersions?.toString(), expectedSemVer.toString()); }); it('should generate correct semantic versions for publishing hotfix minor version', async () => { let version = '1.0.1'; - let publishedVersions = ['1', '1.0', '1.0.0', '2', '2.0', '2.0.0', 'latest']; + let publishedTags = ['1', '1.0', '1.0.0', '2', '2.0', '2.0.0', 'latest']; let expectedSemVer = ['1', '1.0', '1.0.1']; - let semanticVersions = getSemanticVersions(version, publishedVersions, output); + let semanticVersions = getSemanticTags(version, publishedTags, output); assert.equal(semanticVersions?.toString(), expectedSemVer.toString()); }); it('should return undefined for already published version', async () => { let version = '1.0.1'; - let publishedVersions = ['1', '1.0', '1.0.0', '1.0.1', '2', '2.0', '2.0.0', 'latest']; + let publishedTags = ['1', '1.0', '1.0.0', '1.0.1', '2', '2.0', '2.0.0', 'latest']; - let semanticVersions = getSemanticVersions(version, publishedVersions, output); + let semanticVersions = getSemanticTags(version, publishedTags, output); assert.isUndefined(semanticVersions); }); }); -describe('test function getPublishedVersions', async () => { +describe('test functions getVersionsStrictSorted and getPublishedTags', async () => { it('should list published versions', async () => { const resource = 'ghcr.io/devcontainers/features/node'; const featureRef = getRef(output, resource); if (!featureRef) { assert.fail('featureRef should not be undefined'); } - const versionsList = await getPublishedVersions({ output, env: process.env }, featureRef) ?? []; - assert.includeMembers(versionsList, ['1', '1.0', '1.0.0', 'latest']); + const publishedTags = await getPublishedTags({ output, env: process.env }, featureRef) ?? []; + assert.includeMembers(publishedTags, ['1', '1.0', '1.0.0', 'latest']); }); -}); + + it('should list published versions in an advanced case', async () => { + // https://github.com/codspace/versioning/pkgs/container/versioning%2Ffoo/versions + const resource = 'ghcr.io/codspace/versioning/foo'; + const ref = getRef(output, resource); + if (!ref) { + assert.fail('ref should not be undefined'); + } + const versionsList = await getVersionsStrictSorted({ output, env: process.env }, ref) ?? []; + console.log(versionsList); + const expectedVersions = [ + '0.0.0', + '0.0.1', + '0.0.2', + '0.1.0', + '0.2.0', + '0.3.0', + '0.3.1', + '0.3.2', + '0.3.3', + '0.3.4', + '0.3.5', + '0.3.6', + '0.3.7', + '0.3.8', + '0.3.9', + '0.3.10', + '0.3.11', + '0.3.12', + '0.4.0', + '1.0.0', + '1.1.0', + '2.0.0', + '2.1.0', + '2.2.0', + '2.2.1', + '2.3.0', + '2.4.0', + '2.5.0', + '2.6.0', + '2.7.0', + '2.8.0', + '2.9.0', + '2.10.0', + '2.10.1', + '2.11.0', + '2.11.1', + ]; + // Order matters here + assert.deepStrictEqual(versionsList, expectedVersions); + + + const publishedTags = await getPublishedTags({ output, env: process.env }, ref) ?? []; + const expectedTags = [ + 'latest', + '0', + '1', + '2', + '0.0', + '0.0.0', + '0.0.1', + '0.0.2', + '0.1', + '0.1.0', + '0.2', + '0.2.0', + '0.3', + '0.3.0', + '0.3.1', + '0.3.10', + '0.3.11', + '0.3.12', + '0.3.2', + '0.3.3', + '0.3.4', + '0.3.5', + '0.3.6', + '0.3.7', + '0.3.8', + '0.3.9', + '0.4', + '0.4.0', + '1.0', + '1.0.0', + '1.1', + '1.1.0', + '2.0', + '2.0.0', + '2.1', + '2.1.0', + '2.10', + '2.10.0', + '2.10.1', + '2.11', + '2.11.0', + '2.11.1', + '2.2', + '2.2.0', + '2.2.1', + '2.3', + '2.3.0', + '2.4', + '2.4.0', + '2.5', + '2.5.0', + '2.6', + '2.6.0', + '2.7', + '2.7.0', + '2.8', + '2.8.0', + '2.9', + '2.9.0' + ]; + // Order is not guaranteed here (up to however the registry returns the tags) + assert.strictEqual(publishedTags.length, expectedTags.length); + assert.includeMembers(publishedTags, expectedTags); + + }); + +}); \ No newline at end of file diff --git a/src/test/container-features/lockfile.test.ts b/src/test/container-features/lockfile.test.ts index 5c61073fa..57be8ec9d 100644 --- a/src/test/container-features/lockfile.test.ts +++ b/src/test/container-features/lockfile.test.ts @@ -115,6 +115,14 @@ describe('Lockfile', function () { assert.strictEqual(azure.current, undefined); assert.strictEqual(azure.wanted, undefined); assert.ok(azure.latest); + + const foo = response.features['ghcr.io/codspace/versioning/foo:0.3.1']; + assert.ok(foo); + assert.strictEqual(foo.current, '0.3.1'); + assert.strictEqual(foo.wanted, '0.3.1'); + assert.strictEqual(foo.wantedMajor, '0'); + assert.strictEqual(foo.latest, '2.11.1'); + assert.strictEqual(foo.latestMajor, '2'); }); it('upgrade command', async () => { From 9444540283b236298c28f397dea879e7ec222ca1 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Fri, 10 Nov 2023 13:32:33 -0800 Subject: [PATCH 022/291] add --dry-run to upgrade command (#679) --- src/spec-configuration/lockfile.ts | 11 +++++++++-- src/spec-node/upgradeCommand.ts | 10 +++++++++- src/test/container-features/lockfile.test.ts | 9 +++++++++ 3 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/spec-configuration/lockfile.ts b/src/spec-configuration/lockfile.ts index e1a1094c8..dc9d77ba9 100644 --- a/src/spec-configuration/lockfile.ts +++ b/src/spec-configuration/lockfile.ts @@ -13,7 +13,7 @@ export interface Lockfile { features: Record; } -export async function writeLockfile(params: ContainerFeatureInternalParams, config: DevContainerConfig, featuresConfig: FeaturesConfig, forceInitLockfile?: boolean) { +export async function writeLockfile(params: ContainerFeatureInternalParams, config: DevContainerConfig, featuresConfig: FeaturesConfig, forceInitLockfile?: boolean, dryRun?: boolean): Promise { const lockfilePath = getLockfilePath(config); const oldLockfileContent = await readLocalFile(lockfilePath) .catch(err => { @@ -51,7 +51,13 @@ export async function writeLockfile(params: ContainerFeatureInternalParams, conf features: {} as Record, }); - const newLockfileContent = Buffer.from(JSON.stringify(lockfile, null, 2)); + const newLockfileContentString = JSON.stringify(lockfile, null, 2); + + if (dryRun) { + return newLockfileContentString; + } + + const newLockfileContent = Buffer.from(newLockfileContentString); if (params.experimentalFrozenLockfile && !oldLockfileContent) { throw new Error('Lockfile does not exist.'); } @@ -61,6 +67,7 @@ export async function writeLockfile(params: ContainerFeatureInternalParams, conf } await writeLocalFile(lockfilePath, newLockfileContent); } + return; } export async function readLockfile(config: DevContainerConfig): Promise<{ lockfile?: Lockfile; initLockfile?: boolean }> { diff --git a/src/spec-node/upgradeCommand.ts b/src/spec-node/upgradeCommand.ts index 52b208fd7..cfbee0025 100644 --- a/src/spec-node/upgradeCommand.ts +++ b/src/spec-node/upgradeCommand.ts @@ -26,6 +26,7 @@ export function featuresUpgradeOptions(y: Argv) { 'docker-compose-path': { type: 'string', description: 'Path to docker-compose executable.', default: 'docker-compose' }, 'config': { type: 'string', description: 'devcontainer.json path. The default is to use .devcontainer/devcontainer.json or, if that does not exist, .devcontainer.json in the workspace folder.' }, 'log-level': { choices: ['error' as 'error', 'info' as 'info', 'debug' as 'debug', 'trace' as 'trace'], default: 'info' as 'info', description: 'Log level.' }, + 'dry-run': { type: 'boolean', description: 'Write generated lockfile to standard out instead of to disk.' }, }); } @@ -41,6 +42,7 @@ async function featuresUpgrade({ config: configArg, 'docker-compose-path': dockerComposePath, 'log-level': inputLogLevel, + 'dry-run': dryRun, }: FeaturesUpgradeArgs) { const disposables: (() => Promise | undefined)[] = []; const dispose = async () => { @@ -103,7 +105,13 @@ async function featuresUpgrade({ if (!featuresConfig) { throw new ContainerError({ description: `Failed to update lockfile` }); } - await writeLockfile(params, config, featuresConfig, true); + const lockFile = await writeLockfile(params, config, featuresConfig, true, dryRun); + if (dryRun) { + if (!lockFile) { + throw new ContainerError({ description: `Failed to generate lockfile.` }); + } + console.log(lockFile); + } } catch (err) { if (output) { output.write(err && (err.stack || err.message) || String(err)); diff --git a/src/test/container-features/lockfile.test.ts b/src/test/container-features/lockfile.test.ts index 57be8ec9d..85ef93750 100644 --- a/src/test/container-features/lockfile.test.ts +++ b/src/test/container-features/lockfile.test.ts @@ -137,6 +137,15 @@ describe('Lockfile', function () { assert.equal(actual.toString(), expected.toString()); }); + it('upgrade command in --dry-run mode', async () => { + const workspaceFolder = path.join(__dirname, 'configs/lockfile-dependson'); + const res = await shellExec(`${cli} upgrade --dry-run --workspace-folder ${workspaceFolder}`); + const lockfile = JSON.parse(res.stdout); + assert.ok(lockfile); + assert.ok(lockfile.features); + assert.ok(lockfile.features['ghcr.io/codspace/dependson/A:2']); + }); + it('OCI feature integrity', async () => { const workspaceFolder = path.join(__dirname, 'configs/lockfile-oci-integrity'); From 7d0d40680b1705565de00cd48e096a1d7d8af571 Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Mon, 13 Nov 2023 09:32:04 -0800 Subject: [PATCH 023/291] v0.53.0 (#680) --- CHANGELOG.md | 9 +++++++++ package.json | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c29de4934..f05c34645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ Notable changes. +## November 2023 + +### [0.53.0] + +- add `--dry-run` to `upgrade` command (https://github.com/devcontainers/cli/pull/679) +- Fix version sorting and report major version in `outdated` command (https://github.com/devcontainers/cli/pull/670) + - NOTE: This changes the signature of the `features info` command and the output of publishing Features/Templates. The key `publishedVersions` has been renamed to `publishedTags` to better mirror the key's values. +- Docker compose: Updates create error description to include cause for docker auth plugin errors (https://github.com/devcontainers/cli/pull/660) + ## October 2023 ### [0.52.1] diff --git a/package.json b/package.json index 626507b84..1226987c6 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@devcontainers/cli", "description": "Dev Containers CLI", - "version": "0.52.1", + "version": "0.53.0", "bin": { "devcontainer": "devcontainer.js" }, From 53ee019bd4fa17d8a43a76b13b33d91c332f14ad Mon Sep 17 00:00:00 2001 From: Josh Spicer Date: Tue, 14 Nov 2023 05:39:17 -0800 Subject: [PATCH 024/291] Force deterministic order for 'outdated' command (#681) --- .../containerFeaturesConfiguration.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index 24a65fb0f..b6c897638 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -584,7 +584,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co const { lockfile } = await readLockfile(config); - const features: Record = {}; + const resolved: Record = {}; await Promise.all(userFeatures.map(async userFeature => { const userFeatureId = userFeature.userFeatureId; @@ -610,7 +610,7 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co wanted = wantedFeature?.version; } } - features[userFeatureId] = { + resolved[userFeatureId] = { current: lockfileVersion || wanted, wanted, wantedMajor: wanted && semver.major(wanted)?.toString(), @@ -621,7 +621,13 @@ export async function loadVersionInfo(params: ContainerFeatureInternalParams, co } })); - return { features }; + // Reorder Features to match the order in which they were specified in config + return { + features: userFeatures.reduce((acc, userFeature) => { + acc[userFeature.userFeatureId] = resolved[userFeature.userFeatureId]; + return acc; + }, {} as Record) + }; } async function findOCIFeatureMetadata(params: ContainerFeatureInternalParams, manifest: ManifestContainer) { From bd19f61fa0fb1d9d5b7c9ae3057975ffcc6276a1 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Wed, 15 Nov 2023 18:04:29 +0100 Subject: [PATCH 025/291] Remove vscode-dev-containers dependency (#682) --- CONTRIBUTING.md | 2 - esbuild.js | 1 - package.json | 11 +- .../containerFeaturesConfiguration.ts | 104 ++---------------- src/spec-node/containerFeatures.ts | 55 +-------- src/spec-node/featureUtils.ts | 4 +- .../.devcontainer/devcontainer.json | 3 - .../.devcontainer/devcontainer.json | 3 - .../.devcontainer/devcontainer.json | 3 - .../.devcontainer/devcontainer.json | 3 - .../.devcontainer/devcontainer.json | 3 - .../simple/devcontainer-features.json | 61 ---------- .../simple/install.sh | 3 - .../container-features/featureHelpers.test.ts | 2 +- .../generateFeaturesConfig.test.ts | 81 +------------- yarn.lock | 4 - 16 files changed, 20 insertions(+), 323 deletions(-) delete mode 100644 src/test/container-features/example-v1-features-sets/simple/devcontainer-features.json delete mode 100644 src/test/container-features/example-v1-features-sets/simple/install.sh diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b264f4c67..e3790d260 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -29,8 +29,6 @@ The specification repo uses the following [labels](https://github.com/microsoft/ - Create a PR: - Updating the package version in the `package.json`. - - Updating the `vscode-dev-containers` version in the `package.json`'s dependencies (if there is an update). - - Run `yarn` to update `yarn.lock`. - List notable changes in the `CHANGELOG.md`. - Update ThirdPartyNotices.txt with any new dependencies. - After the PR is merged to `main` wait for the CI workflow to succeed (this builds the artifact that will be published). (TBD: Let the `publish-dev-containers` workflow wait for the CI workflow.) diff --git a/esbuild.js b/esbuild.js index 9a85ccea3..2e386ea74 100644 --- a/esbuild.js +++ b/esbuild.js @@ -65,7 +65,6 @@ const watch = process.argv.indexOf('--watch') !== -1; minify, platform: 'node', target: 'node14.17.0', - external: ['vscode-dev-containers'], mainFields: ['module', 'main'], outdir: 'dist', plugins: [plugin], diff --git a/package.json b/package.json index 1226987c6..5418b5bd2 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,9 @@ "node": "^16.13.0 || >=18.0.0" }, "scripts": { - "compile": "npm-run-all clean-dist definitions compile-dev", - "watch": "npm-run-all clean-dist definitions compile-watch", - "package": "npm-run-all clean-dist definitions compile-prod store-packagejson patch-packagejson npm-pack restore-packagejson", + "compile": "npm-run-all clean-dist compile-dev", + "watch": "npm-run-all clean-dist compile-watch", + "package": "npm-run-all clean-dist compile-prod store-packagejson patch-packagejson npm-pack restore-packagejson", "store-packagejson": "copyfiles package.json build-tmp/", "patch-packagejson": "node build/patch-packagejson.js", "restore-packagejson": "copyfiles --up 1 build-tmp/package.json .", @@ -32,10 +32,7 @@ "tsc-b": "tsc -b", "tsc-b-w": "tsc -b -w", "precommit": "node build/hygiene.js", - "definitions": "npm-run-all definitions-clean definitions-copy", "lint": "eslint -c .eslintrc.js --rulesdir ./build/eslint --max-warnings 0 --ext .ts ./src", - "definitions-clean": "rimraf dist/node_modules/vscode-dev-containers", - "definitions-copy": "copyfiles \"node_modules/vscode-dev-containers/container-features/{devcontainer-features.json,feature-scripts.env,fish-debian.sh,homebrew-debian.sh,install.sh}\" dist", "npm-pack": "npm pack", "clean": "npm-run-all clean-dist clean-built", "clean-dist": "rimraf dist", @@ -52,7 +49,6 @@ "ThirdPartyNotices.txt", "devcontainer.js", "dist/spec-node/devContainersSpecCLI.js", - "dist/node_modules/vscode-dev-containers", "package.json", "scripts/updateUID.Dockerfile" ], @@ -106,7 +102,6 @@ "stream-to-pull-stream": "^1.7.3", "tar": "^6.1.13", "text-table": "^0.2.0", - "vscode-dev-containers": "https://github.com/microsoft/vscode-dev-containers/releases/download/v0.245.2/vscode-dev-containers-0.245.2.tgz", "vscode-uri": "^3.0.7", "yargs": "~17.7.1" } diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index b6c897638..f82b57646 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -22,6 +22,7 @@ import { Lockfile, readLockfile, writeLockfile } from './lockfile'; import { computeDependsOnInstallationOrder } from './containerFeaturesOrder'; import { logFeatureAdvisories } from './featureAdvisories'; import { getEntPasswdShellCommand } from '../spec-common/commonUtils'; +import { ContainerError } from '../spec-common/errors'; // v1 const V1_ASSET_NAME = 'devcontainer-features.tgz'; @@ -116,7 +117,7 @@ export function parseMount(str: string): Mount { .reduce((acc, [key, value]) => ({ ...acc, [(normalizedMountKeys[key] || key)]: value }), {}) as Mount; } -export type SourceInformation = LocalCacheSourceInformation | GithubSourceInformation | DirectTarballSourceInformation | FilePathSourceInformation | OCISourceInformation; +export type SourceInformation = GithubSourceInformation | DirectTarballSourceInformation | FilePathSourceInformation | OCISourceInformation; interface BaseSourceInformation { type: string; @@ -124,10 +125,6 @@ interface BaseSourceInformation { userFeatureIdWithoutVersion?: string; } -export interface LocalCacheSourceInformation extends BaseSourceInformation { - type: 'local-cache'; -} - export interface OCISourceInformation extends BaseSourceInformation { type: 'oci'; featureRef: OCIRef; @@ -215,18 +212,6 @@ export interface ContainerFeatureInternalParams { experimentalFrozenLockfile?: boolean; } -export const multiStageBuildExploration = false; - -const isTsnode = path.basename(process.argv[0]) === 'ts-node' || process.argv.indexOf('ts-node/register') !== -1; - -export function getContainerFeaturesFolder(_extensionPath: string | { distFolder: string }) { - if (isTsnode) { - return path.join(require.resolve('vscode-dev-containers/package.json'), '..', 'container-features'); - } - const distFolder = typeof _extensionPath === 'string' ? path.join(_extensionPath, 'dist') : _extensionPath.distFolder; - return path.join(distFolder, 'node_modules', 'vscode-dev-containers', 'container-features'); -} - // TODO: Move to node layer. export function getContainerFeaturesBaseDockerFile(contentSourceRootPath: string) { return ` @@ -467,29 +452,6 @@ async function askGitHubApiForTarballUri(sourceInformation: GithubSourceInformat return undefined; } -export async function loadFeaturesJson(jsonBuffer: Buffer, filePath: string, output: Log): Promise { - if (jsonBuffer.length === 0) { - output.write('Parsed featureSet is empty.', LogLevel.Error); - return undefined; - } - - const featureSet: FeatureSet = jsonc.parse(jsonBuffer.toString()); - if (!featureSet?.features || featureSet.features.length === 0) { - output.write('Parsed featureSet contains no features.', LogLevel.Error); - return undefined; - } - output.write(`Loaded ${filePath}, which declares ${featureSet.features.length} features and ${(!!featureSet.sourceInformation) ? 'contains' : 'does not contain'} explicit source info.`, - LogLevel.Trace); - - return updateFromOldProperties(featureSet); -} - -export async function loadV1FeaturesJsonFromDisk(pathToDirectory: string, output: Log): Promise { - const filePath = path.join(pathToDirectory, V1_DEVCONTAINER_FEATURES_FILE_NAME); - const jsonBuffer: Buffer = await readLocalFile(filePath); - return loadFeaturesJson(jsonBuffer, filePath, output); -} - function updateFromOldProperties(original: T): T { // https://github.com/microsoft/dev-container-spec/issues/1 if (!original.features.find(f => f.extensions || f.settings)) { @@ -522,7 +484,7 @@ function updateFromOldProperties string, additionalFeatures: Record>) { +export async function generateFeaturesConfig(params: ContainerFeatureInternalParams, dstFolder: string, config: DevContainerConfig, additionalFeatures: Record>) { const { output } = params; const workspaceRoot = params.cwd; @@ -533,15 +495,6 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar return undefined; } - // load local cache of features; - // TODO: Update so that cached features are always version 2 - const localFeaturesFolder = getLocalFeaturesFolder(params.extensionPath); - const locallyCachedFeatureSet = await loadV1FeaturesJsonFromDisk(localFeaturesFolder, output); // TODO: Pass dist folder instead to also work with the devcontainer.json support package. - if (!locallyCachedFeatureSet) { - output.write('Failed to load locally cached features', LogLevel.Error); - return undefined; - } - let configPath = config.configFilePath && uriToFsPath(config.configFilePath, params.platform); output.write(`configPath: ${configPath}`, LogLevel.Trace); @@ -567,7 +520,7 @@ export async function generateFeaturesConfig(params: ContainerFeatureInternalPar // Fetch features, stage into the appropriate build folder, and read the feature's devcontainer-feature.json output.write('--- Fetching User Features ----', LogLevel.Trace); - await fetchFeatures(params, featuresConfig, locallyCachedFeatureSet, dstFolder, localFeaturesFolder, ociCacheDir, lockfile); + await fetchFeatures(params, featuresConfig, dstFolder, ociCacheDir, lockfile); await logFeatureAdvisories(params, featuresConfig); await writeLockfile(params, config, featuresConfig, initLockfile); @@ -758,9 +711,10 @@ export async function getFeatureIdType(params: CommonParams, userFeatureId: stri // (1) A feature backed by a GitHub Release // Syntax: //[@version] - // DEPRECATED: This is a legacy feature-set ID + // Legacy feature-set ID if (!userFeatureId.includes('/') && !userFeatureId.includes('\\')) { - return { type: 'local-cache', manifest: undefined }; + output.write(`Legacy feature '${userFeatureId}' not supported. Please check https://containers.dev/features for replacements.`, LogLevel.Error); + throw new ContainerError({ description: `Legacy feature '${userFeatureId}' not supported. Please check https://containers.dev/features for replacements.` }); } // Direct tarball reference @@ -797,9 +751,6 @@ export function getBackwardCompatibleFeatureId(output: Log, id: string) { deprecatedFeaturesIntoOptions.set('maven', 'java'); deprecatedFeaturesIntoOptions.set('jupyterlab', 'python'); - // TODO: add warning logs once we have context on the new location for these Features. - // const deprecatedFeatures = ['fish', 'homebrew']; - const newFeaturePath = 'ghcr.io/devcontainers/features'; // Note: Pin the versionBackwardComp to '1' to avoid breaking changes. const versionBackwardComp = '1'; @@ -840,29 +791,6 @@ export async function processFeatureIdentifier(params: CommonParams, configPath: const { type, manifest } = await getFeatureIdType(params, userFeature.userFeatureId, lockfile); - // cached feature - // Resolves deprecated features (fish, maven, gradle, homebrew, jupyterlab) - if (type === 'local-cache') { - output.write(`Cached feature found.`); - - let feat: Feature = { - id: userFeature.userFeatureId, - name: userFeature.userFeatureId, - value: userFeature.options, - included: true, - }; - - let newFeaturesSet: FeatureSet = { - sourceInformation: { - type: 'local-cache', - userFeatureId: originalUserFeatureId - }, - features: [feat], - }; - - return newFeaturesSet; - } - // remote tar file if (type === 'direct-tarball') { output.write(`Remote tar file found.`); @@ -1036,7 +964,7 @@ export async function processFeatureIdentifier(params: CommonParams, configPath: // throw new Error(`Unsupported feature source type: ${type}`); } -async function fetchFeatures(params: { extensionPath: string; cwd: string; output: Log; env: NodeJS.ProcessEnv }, featuresConfig: FeaturesConfig, localFeatures: FeatureSet, dstFolder: string, localFeaturesFolder: string, ociCacheDir: string, lockfile: Lockfile | undefined) { +async function fetchFeatures(params: { extensionPath: string; cwd: string; output: Log; env: NodeJS.ProcessEnv }, featuresConfig: FeaturesConfig, dstFolder: string, ociCacheDir: string, lockfile: Lockfile | undefined) { const featureSets = featuresConfig.featureSets; for (let idx = 0; idx < featureSets.length; idx++) { // Index represents the previously computed installation order. const featureSet = featureSets[idx]; @@ -1045,10 +973,6 @@ async function fetchFeatures(params: { extensionPath: string; cwd: string; outpu continue; } - if (!localFeatures) { - continue; - } - const { output } = params; const feature = featureSet.features[0]; @@ -1086,18 +1010,6 @@ async function fetchFeatures(params: { extensionPath: string; cwd: string; outpu continue; } - if (sourceInfoType === 'local-cache') { - // create copy of the local features to set the environment variables for them. - await mkdirpLocal(featCachePath); - await cpDirectoryLocal(localFeaturesFolder, featCachePath); - - if (!(await applyFeatureConfigToFeature(output, featureSet, feature, featCachePath, undefined))) { - const err = `Failed to parse feature '${featureDebugId}'. Please check your devcontainer.json 'features' attribute.`; - throw new Error(err); - } - continue; - } - if (sourceInfoType === 'file-path') { output.write(`Detected local file path`, LogLevel.Trace); await mkdirpLocal(featCachePath); diff --git a/src/spec-node/containerFeatures.ts b/src/spec-node/containerFeatures.ts index e9091b179..5b0a2159c 100644 --- a/src/spec-node/containerFeatures.ts +++ b/src/spec-node/containerFeatures.ts @@ -4,13 +4,11 @@ *--------------------------------------------------------------------------------------------*/ import * as path from 'path'; -import { StringDecoder } from 'string_decoder'; -import * as tar from 'tar'; import { DevContainerConfig } from '../spec-configuration/configuration'; import { dockerCLI, dockerPtyCLI, ImageDetails, toExecParameters, toPtyExecParameters } from '../spec-shutdown/dockerUtils'; -import { LogLevel, makeLog, toErrorText } from '../spec-utils/log'; -import { FeaturesConfig, getContainerFeaturesFolder, getContainerFeaturesBaseDockerFile, getFeatureInstallWrapperScript, getFeatureLayers, getFeatureMainValue, getFeatureValueObject, generateFeaturesConfig, Feature, V1_DEVCONTAINER_FEATURES_FILE_NAME, generateContainerEnvs } from '../spec-configuration/containerFeaturesConfiguration'; +import { LogLevel, makeLog } from '../spec-utils/log'; +import { FeaturesConfig, getContainerFeaturesBaseDockerFile, getFeatureInstallWrapperScript, getFeatureLayers, getFeatureMainValue, getFeatureValueObject, generateFeaturesConfig, Feature, generateContainerEnvs } from '../spec-configuration/containerFeaturesConfiguration'; import { readLocalFile } from '../spec-utils/pfs'; import { includeAllConfiguredFeatures } from '../spec-utils/product'; import { createFeaturesTempFolder, DockerResolverParameters, getCacheFolder, getFolderImageName, getEmptyContextFolder, SubstitutedConfig } from './utils'; @@ -124,15 +122,12 @@ export async function getExtendImageBuildInfo(params: DockerResolverParameters, // Creates the folder where the working files will be setup. const dstFolder = await createFeaturesTempFolder(params.common); - // Extracts the local cache of features. - await createLocalFeatures(params, dstFolder); - // Processes the user's configuration. const platform = params.common.cliHost.platform; const cacheFolder = await getCacheFolder(params.common.cliHost); const { experimentalLockfile, experimentalFrozenLockfile } = params; - const featuresConfig = await generateFeaturesConfig({ ...params.common, platform, cacheFolder, experimentalLockfile, experimentalFrozenLockfile }, dstFolder, config.config, getContainerFeaturesFolder, additionalFeatures); + const featuresConfig = await generateFeaturesConfig({ ...params.common, platform, cacheFolder, experimentalLockfile, experimentalFrozenLockfile }, dstFolder, config.config, additionalFeatures); if (!featuresConfig) { if (canAddLabelsToContainer && !imageBuildInfo.dockerfile) { return { @@ -170,50 +165,6 @@ export function generateContainerEnvsV1(featuresConfig: FeaturesConfig) { return result; } -async function createLocalFeatures(params: DockerResolverParameters, dstFolder: string) -{ - const { common } = params; - const { cliHost, output } = common; - - // Name of the local cache folder inside the working directory - const localCacheBuildFolderName = 'local-cache'; - - const srcFolder = getContainerFeaturesFolder(common.extensionPath); - output.write(`local container features stored at: ${srcFolder}`); - await cliHost.mkdirp(`${dstFolder}/${localCacheBuildFolderName}`); - const create = tar.c({ - cwd: srcFolder, - filter: path => (path !== './Dockerfile' && path !== `./${V1_DEVCONTAINER_FEATURES_FILE_NAME}`), - }, ['.']); - const createExit = new Promise((resolve, reject) => { - create.on('error', reject); - create.on('finish', resolve); - }); - const extract = await cliHost.exec({ - cmd: 'tar', - args: [ - '--no-same-owner', - '-x', - '-f', '-', - ], - cwd: `${dstFolder}/${localCacheBuildFolderName}`, - output, - }); - const stdoutDecoder = new StringDecoder(); - extract.stdout.on('data', (chunk: Buffer) => { - output.write(stdoutDecoder.write(chunk)); - }); - const stderrDecoder = new StringDecoder(); - extract.stderr.on('data', (chunk: Buffer) => { - output.write(toErrorText(stderrDecoder.write(chunk))); - }); - create.pipe(extract.stdin); - await Promise.all([ - extract.exit, - createExit, // Allow errors to surface. - ]); -} - export interface ImageBuildOptions { dstFolder: string; dockerfileContent: string; diff --git a/src/spec-node/featureUtils.ts b/src/spec-node/featureUtils.ts index ca8b1d680..0b3fabc01 100644 --- a/src/spec-node/featureUtils.ts +++ b/src/spec-node/featureUtils.ts @@ -1,5 +1,5 @@ import { DevContainerConfig } from '../spec-configuration/configuration'; -import { FeaturesConfig, generateFeaturesConfig, getContainerFeaturesFolder } from '../spec-configuration/containerFeaturesConfiguration'; +import { FeaturesConfig, generateFeaturesConfig } from '../spec-configuration/containerFeaturesConfiguration'; import { DockerCLIParameters } from '../spec-shutdown/dockerUtils'; import { PackageConfiguration } from '../spec-utils/product'; import { createFeaturesTempFolder, getCacheFolder } from './utils'; @@ -9,5 +9,5 @@ export async function readFeaturesConfig(params: DockerCLIParameters, pkg: Packa const { cwd, env, platform } = cliHost; const featuresTmpFolder = await createFeaturesTempFolder({ cliHost, package: pkg }); const cacheFolder = await getCacheFolder(cliHost); - return generateFeaturesConfig({ extensionPath, cacheFolder, cwd, output, env, skipFeatureAutoMapping, platform }, featuresTmpFolder, config, getContainerFeaturesFolder, additionalFeatures); + return generateFeaturesConfig({ extensionPath, cacheFolder, cwd, output, env, skipFeatureAutoMapping, platform }, featuresTmpFolder, config, additionalFeatures); } \ No newline at end of file diff --git a/src/test/configs/compose-Dockerfile-with-features/.devcontainer/devcontainer.json b/src/test/configs/compose-Dockerfile-with-features/.devcontainer/devcontainer.json index a07df72a4..191c5202c 100644 --- a/src/test/configs/compose-Dockerfile-with-features/.devcontainer/devcontainer.json +++ b/src/test/configs/compose-Dockerfile-with-features/.devcontainer/devcontainer.json @@ -1,6 +1,3 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/javascript-node-mongo -// Update the VARIANT arg in docker-compose.yml to pick a Node.js version { "name": "Node.js & Mongo DB", "dockerComposeFile": "docker-compose.yml", diff --git a/src/test/configs/compose-Dockerfile-with-target/.devcontainer/devcontainer.json b/src/test/configs/compose-Dockerfile-with-target/.devcontainer/devcontainer.json index d5a6cac77..50ed4540b 100644 --- a/src/test/configs/compose-Dockerfile-with-target/.devcontainer/devcontainer.json +++ b/src/test/configs/compose-Dockerfile-with-target/.devcontainer/devcontainer.json @@ -1,6 +1,3 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/javascript-node-mongo -// Update the VARIANT arg in docker-compose.yml to pick a Node.js version { "name": "Node.js & Mongo DB", "dockerComposeFile": "docker-compose.yml", diff --git a/src/test/configs/compose-Dockerfile-without-features/.devcontainer/devcontainer.json b/src/test/configs/compose-Dockerfile-without-features/.devcontainer/devcontainer.json index c0b8b97fb..7e5578da0 100644 --- a/src/test/configs/compose-Dockerfile-without-features/.devcontainer/devcontainer.json +++ b/src/test/configs/compose-Dockerfile-without-features/.devcontainer/devcontainer.json @@ -1,6 +1,3 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/javascript-node-mongo -// Update the VARIANT arg in docker-compose.yml to pick a Node.js version { "name": "Node.js & Mongo DB", "dockerComposeFile": "docker-compose.yml", diff --git a/src/test/configs/compose-image-with-features/.devcontainer/devcontainer.json b/src/test/configs/compose-image-with-features/.devcontainer/devcontainer.json index a07df72a4..191c5202c 100644 --- a/src/test/configs/compose-image-with-features/.devcontainer/devcontainer.json +++ b/src/test/configs/compose-image-with-features/.devcontainer/devcontainer.json @@ -1,6 +1,3 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/javascript-node-mongo -// Update the VARIANT arg in docker-compose.yml to pick a Node.js version { "name": "Node.js & Mongo DB", "dockerComposeFile": "docker-compose.yml", diff --git a/src/test/configs/compose-image-without-features/.devcontainer/devcontainer.json b/src/test/configs/compose-image-without-features/.devcontainer/devcontainer.json index 71ac63e92..c1e7a8b1b 100644 --- a/src/test/configs/compose-image-without-features/.devcontainer/devcontainer.json +++ b/src/test/configs/compose-image-without-features/.devcontainer/devcontainer.json @@ -1,6 +1,3 @@ -// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at: -// https://github.com/microsoft/vscode-dev-containers/tree/v0.234.0/containers/javascript-node-mongo -// Update the VARIANT arg in docker-compose.yml to pick a Node.js version { "name": "Node.js & Mongo DB", "dockerComposeFile": "docker-compose.yml", diff --git a/src/test/container-features/example-v1-features-sets/simple/devcontainer-features.json b/src/test/container-features/example-v1-features-sets/simple/devcontainer-features.json deleted file mode 100644 index 3c17d69e3..000000000 --- a/src/test/container-features/example-v1-features-sets/simple/devcontainer-features.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "features": - [ - { - "id": "first", - "name": "First Feature", - "containerEnv": { - "MYKEYONE": "MY RESULT ONE" - }, - "options": { - "version": { - "type": "string", - "proposals": ["latest", "1.0", "2.0"], - "default": "latest", - "description": "Version option." - }, - "option1": { - "type": "boolean", - "default": true, - "description": "Boolean option." - }, - "option2": { - "type": "string", - "enum": ["yes", "no", "maybe"], - "default": "maybe", - "description": "Enum option." - } - } - }, - { - "id": "second", - "name": "Second Feature", - "containerEnv": { - "MYKEYTWO": "MY RESULT TWO" - }, - "privileged": true, - "options": { - "version": { - "type": "string", - "proposals": ["latest", "1.0", "2.0"], - "default": "latest", - "description": "Version option." - } - } - }, - { - "id": "third", - "name": "Third Feature", - "containerEnv": { - "MYKEYTHREE": "MYRESULTHREE" - }, - "options": { - "option1": { - "type": "boolean", - "default": true, - "description": "Boolean option." - } - } - } - ] -} \ No newline at end of file diff --git a/src/test/container-features/example-v1-features-sets/simple/install.sh b/src/test/container-features/example-v1-features-sets/simple/install.sh deleted file mode 100644 index f0e75c640..000000000 --- a/src/test/container-features/example-v1-features-sets/simple/install.sh +++ /dev/null @@ -1,3 +0,0 @@ -#!/bin/bash - -echo 'test123' \ No newline at end of file diff --git a/src/test/container-features/featureHelpers.test.ts b/src/test/container-features/featureHelpers.test.ts index 86f3543f0..fb3c079ed 100644 --- a/src/test/container-features/featureHelpers.test.ts +++ b/src/test/container-features/featureHelpers.test.ts @@ -48,7 +48,7 @@ const assertFeatureIdInvariant = (id: string) => { }; describe('validate processFeatureIdentifier', async function () { - // const VALID_TYPES = ['local-cache', 'github-repo', 'direct-tarball', 'file-path', 'oci']; + // const VALID_TYPES = ['github-repo', 'direct-tarball', 'file-path', 'oci']; // In the real implementation, the cwd is passed by the calling function with the value of `--workspace-folder`. // See: https://github.com/devcontainers/cli/blob/45541ba21437bf6c16826762f084ab502157789b/src/spec-node/devContainersSpecCLI.ts#L152-L153 diff --git a/src/test/container-features/generateFeaturesConfig.test.ts b/src/test/container-features/generateFeaturesConfig.test.ts index 67b9754da..32647a71c 100644 --- a/src/test/container-features/generateFeaturesConfig.test.ts +++ b/src/test/container-features/generateFeaturesConfig.test.ts @@ -1,5 +1,5 @@ import { assert } from 'chai'; -import { generateFeaturesConfig, getFeatureLayers, FeatureSet, getContainerFeaturesFolder, generateContainerEnvs } from '../../spec-configuration/containerFeaturesConfiguration'; +import { generateFeaturesConfig, getFeatureLayers, FeatureSet } from '../../spec-configuration/containerFeaturesConfiguration'; import { createPlainLog, LogLevel, makeLog } from '../../spec-utils/log'; import * as path from 'path'; import * as process from 'process'; @@ -23,81 +23,6 @@ describe('validate generateFeaturesConfig()', function () { const cacheFolder = path.join(os.tmpdir(), `devcontainercli-test-${crypto.randomUUID()}`); const params = { extensionPath: '', cwd: '', output, env, cacheFolder, persistedFolder: '', skipFeatureAutoMapping: false, platform }; - // Mocha executes with the root of the project as the cwd. - const localFeaturesFolder = (_: string) => { - return './src/test/container-features/example-v1-features-sets/simple'; - }; - - it('should correctly return a featuresConfig with v1 local features', async function () { - - const version = 'unittest'; - const tmpFolder: string = path.join(await getLocalCacheFolder(), 'container-features', `${version}-${Date.now()}`); - await mkdirpLocal(tmpFolder); - - - const config: DevContainerConfig = { - configFilePath: URI.from({ 'scheme': 'https' }), - dockerFile: '.', - features: { - first: { - 'version': 'latest' - }, - second: { - 'value': true - }, - }, - }; - - const featuresConfig = await generateFeaturesConfig(params, tmpFolder, config, localFeaturesFolder, {}); - if (!featuresConfig) { - assert.fail(); - } - - assert.strictEqual(featuresConfig?.featureSets.length, 2); - - const first = featuresConfig.featureSets[0].features.find((f) => f.id === 'first'); - assert.exists(first); - - const second = featuresConfig.featureSets[1].features.find((f) => f.id === 'second'); - assert.exists(second); - - assert.isObject(first?.value); - assert.isObject(second?.value); - - // -- Test containerFeatures.ts helper functions - - // generateContainerEnvs - const actualEnvs = featuresConfig.featureSets - .map(set => set.features - .map(f => generateContainerEnvs(f.containerEnv))) - .flat() - .join('\n'); - const expectedEnvs = `ENV MYKEYONE="MY RESULT ONE" -ENV MYKEYTWO="MY RESULT TWO"`; - assert.strictEqual(actualEnvs, expectedEnvs); - - // getFeatureLayers - const actualLayers = getFeatureLayers(featuresConfig, 'testContainerUser', 'testRemoteUser'); - const expectedLayers = `RUN \\ -echo "_CONTAINER_USER_HOME=$(${getEntPasswdShellCommand('testContainerUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env && \\ -echo "_REMOTE_USER_HOME=$(${getEntPasswdShellCommand('testRemoteUser')} | cut -d: -f6)" >> /tmp/dev-container-features/devcontainer-features.builtin.env - -COPY --chown=root:root --from=dev_containers_feature_content_source /tmp/build-features/first_0 /tmp/dev-container-features/first_0 -RUN chmod -R 0755 /tmp/dev-container-features/first_0 \\ -&& cd /tmp/dev-container-features/first_0 \\ -&& chmod +x ./install.sh \\ -&& ./install.sh - -COPY --chown=root:root --from=dev_containers_feature_content_source /tmp/build-features/second_1 /tmp/dev-container-features/second_1 -RUN chmod -R 0755 /tmp/dev-container-features/second_1 \\ -&& cd /tmp/dev-container-features/second_1 \\ -&& chmod +x ./install.sh \\ -&& ./install.sh - -`; - assert.strictEqual(actualLayers, expectedLayers); - }); - it('should correctly return a featuresConfig with v2 local features', async function () { const version = 'unittest'; const tmpFolder: string = path.join(await getLocalCacheFolder(), 'container-features', `${version}-${Date.now()}`); @@ -120,7 +45,7 @@ RUN chmod -R 0755 /tmp/dev-container-features/second_1 \\ }, }; - const featuresConfig = await generateFeaturesConfig({ ...params, cwd: tmpFolder }, tmpFolder, config, localFeaturesFolder, {}); + const featuresConfig = await generateFeaturesConfig({ ...params, cwd: tmpFolder }, tmpFolder, config, {}); if (!featuresConfig) { assert.fail(); } @@ -186,7 +111,7 @@ RUN chmod -R 0755 /tmp/dev-container-features/hello_1 \\ params.skipFeatureAutoMapping = true; - const featuresConfig = await generateFeaturesConfig(params, tmpFolder, config, getContainerFeaturesFolder, {}); + const featuresConfig = await generateFeaturesConfig(params, tmpFolder, config, {}); if (!featuresConfig) { assert.fail(); } diff --git a/yarn.lock b/yarn.lock index af078368b..dac131730 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3603,10 +3603,6 @@ vinyl@^3.0.0: replace-ext "^2.0.0" teex "^1.0.1" -"vscode-dev-containers@https://github.com/microsoft/vscode-dev-containers/releases/download/v0.245.2/vscode-dev-containers-0.245.2.tgz": - version "0.245.2" - resolved "https://github.com/microsoft/vscode-dev-containers/releases/download/v0.245.2/vscode-dev-containers-0.245.2.tgz#027fbe9132cd5c1457308ba5db6e6ae813635114" - vscode-uri@^3.0.7: version "3.0.7" resolved "https://registry.yarnpkg.com/vscode-uri/-/vscode-uri-3.0.7.tgz#6d19fef387ee6b46c479e5fb00870e15e58c1eb8" From 2d24543380dfc4d54e76b582536b52226af133c8 Mon Sep 17 00:00:00 2001 From: Christof Marti Date: Wed, 15 Nov 2023 15:37:52 +0100 Subject: [PATCH 026/291] Remove unused code --- src/spec-common/commonUtils.ts | 3 - src/spec-common/git.ts | 9 --- .../configurationCommonUtils.ts | 2 +- .../containerFeaturesConfiguration.ts | 16 ----- src/spec-configuration/editableFiles.ts | 18 ----- src/spec-node/configContainer.ts | 4 +- src/spec-node/dockerfileUtils.ts | 4 -- src/spec-node/utils.ts | 3 +- src/spec-shutdown/dockerUtils.ts | 52 -------------- src/spec-shutdown/shutdownUtils.ts | 16 ----- src/spec-utils/event.ts | 31 -------- src/spec-utils/log.ts | 10 --- src/spec-utils/types.ts | 6 -- src/spec-utils/workspaces.ts | 71 ------------------- 14 files changed, 4 insertions(+), 241 deletions(-) delete mode 100644 src/spec-shutdown/shutdownUtils.ts delete mode 100644 src/spec-utils/types.ts diff --git a/src/spec-common/commonUtils.ts b/src/spec-common/commonUtils.ts index 5d5f1c0c7..0e23fafc7 100644 --- a/src/spec-common/commonUtils.ts +++ b/src/spec-common/commonUtils.ts @@ -70,9 +70,6 @@ export function equalPaths(platform: NodeJS.Platform, a: string, b: string) { return a.toLowerCase() === b.toLowerCase(); } -export const tsnode = path.join(__dirname, '..', '..', 'node_modules', '.bin', 'ts-node'); -export const isTsnode = path.basename(process.argv[0]) === 'ts-node' || process.argv.indexOf('ts-node/register') !== -1; - export async function runCommandNoPty(options: { exec: ExecFunction; cmd: string; diff --git a/src/spec-common/git.ts b/src/spec-common/git.ts index dcab2036a..4bbf50730 100644 --- a/src/spec-common/git.ts +++ b/src/spec-common/git.ts @@ -31,12 +31,3 @@ export async function findGitRootFolder(cliHost: FileHost | CLIHost, folderPath: return undefined; } } - -export interface GitCloneOptions { - url: string; - tokenEnvVar?: string; - branch?: string; - recurseSubmodules?: boolean; - env?: NodeJS.ProcessEnv; - fullClone?: boolean; -} diff --git a/src/spec-configuration/configurationCommonUtils.ts b/src/spec-configuration/configurationCommonUtils.ts index 9e45d613b..371125f52 100644 --- a/src/spec-configuration/configurationCommonUtils.ts +++ b/src/spec-configuration/configurationCommonUtils.ts @@ -10,7 +10,7 @@ import { URI } from 'vscode-uri'; import { CLIHostDocuments } from './editableFiles'; import { FileHost } from '../spec-utils/pfs'; -export { FileHost, FileTypeBitmask } from '../spec-utils/pfs'; +export { FileHost } from '../spec-utils/pfs'; const enum CharCode { Slash = 47, diff --git a/src/spec-configuration/containerFeaturesConfiguration.ts b/src/spec-configuration/containerFeaturesConfiguration.ts index f82b57646..51ae98890 100644 --- a/src/spec-configuration/containerFeaturesConfiguration.ts +++ b/src/spec-configuration/containerFeaturesConfiguration.ts @@ -157,15 +157,6 @@ export interface GithubSourceInformation extends BaseSourceInformation { userFeatureIdWithoutVersion: string; } -export interface GithubSourceInformationInput { - owner: string; - repo: string; - ref?: string; - sha?: string; - tag?: string; -} - - export interface FeatureSet { features: Feature[]; internalVersion?: string; @@ -193,13 +184,6 @@ export interface GithubApiReleaseAsset { updated_at: string; } -// Supports the `node` layer by collapsing all the individual features into a single `features` array. -// Regardless of their origin. -// Information is lost, but for the node layer we need not care about which set a given feature came from. -export interface CollapsedFeaturesConfig { - allFeatures: Feature[]; -} - export interface ContainerFeatureInternalParams { extensionPath: string; cacheFolder: string; diff --git a/src/spec-configuration/editableFiles.ts b/src/spec-configuration/editableFiles.ts index eb8d303b5..d233a15fc 100644 --- a/src/spec-configuration/editableFiles.ts +++ b/src/spec-configuration/editableFiles.ts @@ -161,21 +161,3 @@ export function createDocuments(fileHost: FileHost, shellServer?: ShellServer): export interface ShellServer { exec(cmd: string, options?: { logOutput?: boolean; stdin?: Buffer }): Promise<{ stdout: string; stderr: string }>; } - -const editQueues = new Map Promise)[]>(); - -export async function runEdit(uri: URI, edit: () => Promise) { - const uriString = uri.toString(); - let queue = editQueues.get(uriString); - if (!queue) { - editQueues.set(uriString, queue = []); - } - queue.push(edit); - if (queue.length === 1) { - while (queue.length) { - await queue[0](); - queue.shift(); - } - editQueues.delete(uriString); - } -} diff --git a/src/spec-node/configContainer.ts b/src/spec-node/configContainer.ts index dddd08d58..ea94fe4ea 100644 --- a/src/spec-node/configContainer.ts +++ b/src/spec-node/configContainer.ts @@ -9,7 +9,7 @@ import * as jsonc from 'jsonc-parser'; import { openDockerfileDevContainer } from './singleContainer'; import { openDockerComposeDevContainer } from './dockerCompose'; -import { ResolverResult, DockerResolverParameters, isDockerFileConfig, runInitializeCommand, createDocuments, getWorkspaceConfiguration, BindMountConsistency, uriToFsPath, DevContainerAuthority, isDevContainerAuthority, SubstituteConfig, SubstitutedConfig, addSubstitution, envListToObj, findContainerAndIdLabels } from './utils'; +import { ResolverResult, DockerResolverParameters, isDockerFileConfig, runInitializeCommand, getWorkspaceConfiguration, BindMountConsistency, uriToFsPath, DevContainerAuthority, isDevContainerAuthority, SubstituteConfig, SubstitutedConfig, addSubstitution, envListToObj, findContainerAndIdLabels } from './utils'; import { beforeContainerSubstitute, substitute } from '../spec-common/variableSubstitution'; import { ContainerError } from '../spec-common/errors'; import { Workspace, workspaceFromPath, isWorkspacePath } from '../spec-utils/workspaces'; @@ -20,8 +20,8 @@ import { getDefaultDevContainerConfigPath, getDevContainerConfigPathIn } from '. import { DevContainerConfig, DevContainerFromDockerComposeConfig, DevContainerFromDockerfileConfig, DevContainerFromImageConfig, updateFromOldProperties } from '../spec-configuration/configuration'; import { ensureNoDisallowedFeatures } from './disallowedFeatures'; import { DockerCLIParameters } from '../spec-shutdown/dockerUtils'; +import { createDocuments } from '../spec-configuration/editableFiles'; -export { getWellKnownDevContainerPaths as getPossibleDevContainerPaths } from '../spec-configuration/configurationCommonUtils'; export async function resolve(params: DockerResolverParameters, configFile: URI | undefined, overrideConfigFile: URI | undefined, providedIdLabels: string[] | undefined, additionalFeatures: Record>): Promise { if (configFile && !/\/\.?devcontainer\.json$/.test(configFile.path)) { diff --git a/src/spec-node/dockerfileUtils.ts b/src/spec-node/dockerfileUtils.ts index 2e25ced9e..0f34476c7 100644 --- a/src/spec-node/dockerfileUtils.ts +++ b/src/spec-node/dockerfileUtils.ts @@ -6,10 +6,6 @@ import * as semver from 'semver'; import { Mount } from '../spec-configuration/containerFeaturesConfiguration'; -export { getConfigFilePath, getDockerfilePath, isDockerFileConfig, resolveConfigFilePath } from '../spec-configuration/configuration'; -export { uriToFsPath, parentURI } from '../spec-configuration/configurationCommonUtils'; -export { CLIHostDocuments, Documents, createDocuments, Edit, fileDocuments, RemoteDocuments } from '../spec-configuration/editableFiles'; - const findFromLines = new RegExp(/^(?\s*FROM.*)/, 'gmi'); const parseFromLine = /FROM\s+(?--platform=\S+\s+)?(?"?[^\s]+"?)(\s+AS\s+(?