From 6a047ab426b9ac28c2404edcfb9abd67aaf0d395 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH)" Date: Thu, 27 Aug 2026 22:25:15 +0800 Subject: [PATCH 1/2] Support Docker CLI argument prefixes Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8539f306-93c4-4831-9bcd-ec77196d5ec8 --- src/spec-common/dockerPathArgs.ts | 23 +++++++++++++++++++++++ src/spec-node/configContainer.ts | 4 ++-- src/spec-node/devContainers.ts | 7 ++++++- src/spec-node/devContainersSpecCLI.ts | 22 +++++++++++++++++++++- src/spec-node/dockerCompose.ts | 14 ++++++++------ src/spec-node/featuresCLI/utils.ts | 1 + src/spec-node/imageMetadata.ts | 4 ++-- src/spec-node/upgradeCommand.ts | 5 +++++ src/spec-node/utils.ts | 1 + src/spec-shutdown/dockerUtils.ts | 18 ++++++++++++++---- src/test/dockerUtils.test.ts | 22 ++++++++++++++++++++++ 11 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 src/spec-common/dockerPathArgs.ts diff --git a/src/spec-common/dockerPathArgs.ts b/src/spec-common/dockerPathArgs.ts new file mode 100644 index 000000000..cdaed4b18 --- /dev/null +++ b/src/spec-common/dockerPathArgs.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +export function parseDockerPathArgs(value: string | undefined): string[] | undefined { + if (value === undefined) { + return undefined; + } + + let parsed: unknown; + try { + parsed = JSON.parse(value); + } catch { + throw new Error('--docker-path-args must be a JSON array of strings.'); + } + + if (!Array.isArray(parsed) || !parsed.every(arg => typeof arg === 'string')) { + throw new Error('--docker-path-args must be a JSON array of strings.'); + } + + return parsed; +} diff --git a/src/spec-node/configContainer.ts b/src/spec-node/configContainer.ts index c0b21eb82..0cbfa5055 100644 --- a/src/spec-node/configContainer.ts +++ b/src/spec-node/configContainer.ts @@ -58,9 +58,9 @@ async function resolveWithLocalFolder(params: DockerResolverParameters, parsedAu const configWithRaw = addSubstitution(configs.config, config => beforeContainerSubstitute(envListToObj(idLabels), config)); const { config } = configWithRaw; - const { dockerCLI, dockerComposeCLI } = params; + const { dockerCLI, dockerPathArgs, dockerComposeCLI } = params; const { env } = common; - const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; + const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerPathArgs, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; await ensureNoDisallowedFeatures(cliParams, config, additionalFeatures, idLabels); await runInitializeCommand({ ...params, common: { ...common, output: common.lifecycleHook.output } }, config.initializeCommand, common.lifecycleHook.onDidInput); diff --git a/src/spec-node/devContainers.ts b/src/spec-node/devContainers.ts index 6ceed1951..037812db1 100644 --- a/src/spec-node/devContainers.ts +++ b/src/spec-node/devContainers.ts @@ -23,6 +23,7 @@ import { Event } from '../spec-utils/event'; export interface ProvisionOptions { dockerPath: string | undefined; + dockerPathArgs?: string[]; dockerComposePath: string | undefined; containerDataFolder: string | undefined; containerSystemDataFolder: string | undefined; @@ -168,6 +169,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables: const dockerComposePath = options.dockerComposePath || 'docker-compose'; const dockerComposeCLI = dockerComposeCLIConfig({ exec: cliHost.exec, + args: options.dockerPathArgs, env: cliHost.env, output: common.output, }, dockerPath, dockerComposePath); @@ -206,6 +208,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables: const buildKitVersion = options.useBuildKit === 'never' ? undefined : (await dockerBuildKitVersion({ cliHost, dockerCLI: dockerPath, + dockerPathArgs: options.dockerPathArgs, dockerComposeCLI, env: cliHost.env, output, @@ -213,11 +216,12 @@ export async function createDockerParams(options: ProvisionOptions, disposables: targetPlatformInfo })); - const cliVariant = await lookupCLIVariant({ exec: cliHost.exec, cmd: dockerPath, env: cliHost.env, output }); + const cliVariant = await lookupCLIVariant({ exec: cliHost.exec, cmd: dockerPath, args: options.dockerPathArgs, env: cliHost.env, output }); const dockerEngineVer = await dockerEngineVersion({ cliHost, dockerCLI: dockerPath, + dockerPathArgs: options.dockerPathArgs, dockerComposeCLI, env: cliHost.env, output, @@ -229,6 +233,7 @@ export async function createDockerParams(options: ProvisionOptions, disposables: common, parsedAuthority, dockerCLI: dockerPath, + dockerPathArgs: options.dockerPathArgs, cliVariant, dockerComposeCLI: dockerComposeCLI, dockerEnv: cliHost.env, diff --git a/src/spec-node/devContainersSpecCLI.ts b/src/spec-node/devContainersSpecCLI.ts index 832e9603f..8c4efc021 100644 --- a/src/spec-node/devContainersSpecCLI.ts +++ b/src/spec-node/devContainersSpecCLI.ts @@ -43,6 +43,7 @@ import { featuresUpgradeHandler, featuresUpgradeOptions } from './upgradeCommand import { readFeaturesConfig } from './featureUtils'; import { featuresGenerateDocsHandler, featuresGenerateDocsOptions } from './featuresCLI/generateDocs'; import { templatesGenerateDocsHandler, templatesGenerateDocsOptions } from './templatesCLI/generateDocs'; +import { parseDockerPathArgs } from '../spec-common/dockerPathArgs'; import { mapNodeOSToGOOS, mapNodeArchitectureToGOARCH } from '../spec-configuration/containerCollectionsOCI'; import { templateMetadataHandler, templateMetadataOptions } from './templatesCLI/metadata'; @@ -100,6 +101,7 @@ export type UnpackArgv = T extends Argv ? U : T; function provisionOptions(y: Argv) { return y.options({ 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' }, 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, @@ -185,6 +187,7 @@ function provisionHandler(args: ProvisionArgs) { async function provision({ 'user-data-folder': persistedFolder, 'docker-path': dockerPath, + 'docker-path-args': dockerPathArgs, 'docker-compose-path': dockerComposePath, 'container-data-folder': containerDataFolder, 'container-system-data-folder': containerSystemDataFolder, @@ -246,6 +249,7 @@ async function provision({ const options: ProvisionOptions = { dockerPath, + dockerPathArgs: parseDockerPathArgs(dockerPathArgs), dockerComposePath, containerDataFolder, containerSystemDataFolder, @@ -354,6 +358,7 @@ async function doProvision(options: ProvisionOptions, providedIdLabels: string[] function setUpOptions(y: Argv) { return y.options({ 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' }, 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, 'container-id': { type: 'string', required: true, description: 'Id of the container.' }, @@ -402,6 +407,7 @@ async function setUp(args: SetUpArgs) { async function doSetUp({ 'user-data-folder': persistedFolder, 'docker-path': dockerPath, + 'docker-path-args': dockerPathArgs, 'container-data-folder': containerDataFolder, 'container-system-data-folder': containerSystemDataFolder, 'container-id': containerId, @@ -431,6 +437,7 @@ async function doSetUp({ const configFile = configParam ? URI.file(path.resolve(process.cwd(), configParam)) : undefined; const params = await createDockerParams({ dockerPath, + dockerPathArgs: parseDockerPathArgs(dockerPathArgs), dockerComposePath: undefined, containerSessionDataFolder, containerDataFolder, @@ -524,6 +531,7 @@ function buildOptions(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.' }, 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' }, 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path. If not provided, defaults to the current directory.' }, '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.' }, @@ -580,6 +588,7 @@ async function build(args: BuildArgs) { async function doBuild({ 'user-data-folder': persistedFolder, 'docker-path': dockerPath, + 'docker-path-args': dockerPathArgs, 'docker-compose-path': dockerComposePath, 'workspace-folder': workspaceFolderArg, config: configParam, @@ -618,6 +627,7 @@ async function doBuild({ const additionalFeatures = additionalFeaturesJson ? jsonc.parse(additionalFeaturesJson) as Record> : {}; const params = await createDockerParams({ dockerPath, + dockerPathArgs: parseDockerPathArgs(dockerPathArgs), dockerComposePath, containerDataFolder: undefined, containerSystemDataFolder: undefined, @@ -676,7 +686,7 @@ async function doBuild({ throw new ContainerError({ description: '--push true cannot be used with --output.' }); } - const buildParams: DockerCLIParameters = { cliHost, dockerCLI: params.dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; + const buildParams: DockerCLIParameters = { cliHost, dockerCLI: params.dockerCLI, dockerPathArgs: params.dockerPathArgs, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; await ensureNoDisallowedFeatures(buildParams, config, additionalFeatures, undefined); // Support multiple use of `--image-name` @@ -787,6 +797,7 @@ function runUserCommandsOptions(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.' }, 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' }, 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, @@ -848,6 +859,7 @@ async function runUserCommands(args: RunUserCommandsArgs) { async function doRunUserCommands({ 'user-data-folder': persistedFolder, 'docker-path': dockerPath, + 'docker-path-args': dockerPathArgs, 'docker-compose-path': dockerComposePath, 'container-data-folder': containerDataFolder, 'container-system-data-folder': containerSystemDataFolder, @@ -892,6 +904,7 @@ async function doRunUserCommands({ const params = await createDockerParams({ dockerPath, + dockerPathArgs: parseDockerPathArgs(dockerPathArgs), dockerComposePath, containerDataFolder, containerSystemDataFolder, @@ -994,6 +1007,7 @@ function readConfigurationOptions(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.' }, 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' }, 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, 'workspace-folder': { type: 'string', description: 'Workspace folder path. The devcontainer.json will be looked up relative to this path. If --container-id, --id-label, and --workspace-folder are not provided, this defaults to the current directory.' }, 'mount-workspace-git-root': { type: 'boolean', default: true, description: 'Mount the workspace using its Git root.' }, @@ -1032,6 +1046,7 @@ function readConfigurationHandler(args: ReadConfigurationArgs) { async function readConfiguration({ // 'user-data-folder': persistedFolder, 'docker-path': dockerPath, + 'docker-path-args': dockerPathArgs, 'docker-compose-path': dockerComposePath, 'workspace-folder': workspaceFolderArg, 'mount-workspace-git-root': mountWorkspaceGitRoot, @@ -1090,6 +1105,7 @@ async function readConfiguration({ const dockerCLI = dockerPath || 'docker'; const dockerComposeCLI = dockerComposeCLIConfig({ exec: cliHost.exec, + args: parseDockerPathArgs(dockerPathArgs), env: cliHost.env, output, }, dockerCLI, dockerComposePath || 'docker-compose'); @@ -1100,6 +1116,7 @@ async function readConfiguration({ const params: DockerCLIParameters = { cliHost, dockerCLI, + dockerPathArgs: parseDockerPathArgs(dockerPathArgs), dockerComposeCLI, env: cliHost.env, output, @@ -1251,6 +1268,7 @@ function execOptions(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.' }, 'docker-path': { type: 'string', description: 'Docker CLI path.' }, + 'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' }, 'docker-compose-path': { type: 'string', description: 'Docker Compose CLI path.' }, 'container-data-folder': { type: 'string', description: 'Container data folder where user data inside the container will be stored.' }, 'container-system-data-folder': { type: 'string', description: 'Container system data folder where system data inside the container will be stored.' }, @@ -1313,6 +1331,7 @@ async function exec(args: ExecArgs) { export async function doExec({ 'user-data-folder': persistedFolder, 'docker-path': dockerPath, + 'docker-path-args': dockerPathArgs, 'docker-compose-path': dockerComposePath, 'container-data-folder': containerDataFolder, 'container-system-data-folder': containerSystemDataFolder, @@ -1346,6 +1365,7 @@ export async function doExec({ const overrideConfigFile = overrideConfig ? URI.file(path.resolve(process.cwd(), overrideConfig)) : undefined; const params = await createDockerParams({ dockerPath, + dockerPathArgs: parseDockerPathArgs(dockerPathArgs), dockerComposePath, containerDataFolder, containerSystemDataFolder, diff --git a/src/spec-node/dockerCompose.ts b/src/spec-node/dockerCompose.ts index 13b6caace..e87815472 100644 --- a/src/spec-node/dockerCompose.ts +++ b/src/spec-node/dockerCompose.ts @@ -25,9 +25,9 @@ const projectLabel = 'com.docker.compose.project'; const serviceLabel = 'com.docker.compose.service'; export async function openDockerComposeDevContainer(params: DockerResolverParameters, workspace: Workspace, config: SubstitutedConfig, idLabels: string[], additionalFeatures: Record>): Promise { - const { common, dockerCLI, dockerComposeCLI } = params; + const { common, dockerCLI, dockerPathArgs, dockerComposeCLI } = params; const { cliHost, env, output } = common; - const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; + const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerPathArgs, dockerComposeCLI, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; return _openDockerComposeDevContainer(params, buildParams, workspace, config, getRemoteWorkspaceFolder(config.config), idLabels, additionalFeatures); } @@ -151,11 +151,11 @@ export function getBuildInfoForService(composeService: any, cliHostPath: typeof export async function buildAndExtendDockerCompose(configWithRaw: SubstitutedConfig, projectName: string, params: DockerResolverParameters, localComposeFiles: string[], envFile: string | undefined, composeGlobalArgs: string[], runServices: string[], noCache: boolean, overrideFilePath: string, overrideFilePrefix: string, versionPrefix: string, additionalFeatures: Record>, canAddLabelsToContainer: boolean, additionalCacheFroms?: string[], noBuild?: boolean) { - const { common, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc } = params; + const { common, dockerCLI, dockerPathArgs, dockerComposeCLI: dockerComposeCLIFunc } = params; const { cliHost, env, output } = common; const { config } = configWithRaw; - const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI: dockerComposeCLIFunc, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; + const cliParams: DockerCLIParameters = { cliHost, dockerCLI, dockerPathArgs, dockerComposeCLI: dockerComposeCLIFunc, env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; const composeConfig = await readDockerComposeConfig(cliParams, localComposeFiles, envFile); const composeService = composeConfig.services[config.service]; @@ -705,16 +705,18 @@ export function dockerComposeCLIConfig(params: Omit; return () => { return result || (result = (async () => { + const { args: dockerPathArgs, ...baseParams } = params; let v2 = true; let stdout: Buffer; try { stdout = (await dockerComposeCLI({ - ...params, + ...baseParams, cmd: dockerCLICmd, + args: dockerPathArgs, }, 'compose', 'version', '--short')).stdout; } catch (err) { stdout = (await dockerComposeCLI({ - ...params, + ...baseParams, cmd: dockerComposeCLICmd, }, 'version', '--short')).stdout; v2 = false; diff --git a/src/spec-node/featuresCLI/utils.ts b/src/spec-node/featuresCLI/utils.ts index c981a4c2e..7ac2a00e2 100644 --- a/src/spec-node/featuresCLI/utils.ts +++ b/src/spec-node/featuresCLI/utils.ts @@ -30,6 +30,7 @@ export const staticProvisionParams = { export const staticExecParams = { 'user-data-folder': undefined, 'docker-path': undefined, + 'docker-path-args': undefined, 'docker-compose-path': undefined, 'container-data-folder': undefined, 'container-system-data-folder': undefined, diff --git a/src/spec-node/imageMetadata.ts b/src/spec-node/imageMetadata.ts index 60884592e..07a8cd107 100644 --- a/src/spec-node/imageMetadata.ts +++ b/src/spec-node/imageMetadata.ts @@ -331,7 +331,7 @@ export interface ImageBuildInfo { } export async function getImageBuildInfo(params: DockerResolverParameters | DockerCLIParameters, configWithRaw: SubstitutedConfig): Promise { - const { dockerCLI, dockerComposeCLI } = params; + const { dockerCLI, dockerPathArgs, dockerComposeCLI } = params; const { cliHost, output } = 'cliHost' in params ? params : params.common; const { config } = configWithRaw; @@ -350,7 +350,7 @@ export async function getImageBuildInfo(params: DockerResolverParameters | Docke const cwdEnvFile = cliHost.path.join(cliHost.cwd, '.env'); const envFile = Array.isArray(config.dockerComposeFile) && config.dockerComposeFile.length === 0 && await cliHost.isFile(cwdEnvFile) ? cwdEnvFile : undefined; const composeFiles = await getDockerComposeFilePaths(cliHost, config, cliHost.env, cliHost.cwd); - const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerComposeCLI, env: cliHost.env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; + const buildParams: DockerCLIParameters = { cliHost, dockerCLI, dockerPathArgs, dockerComposeCLI, env: cliHost.env, output, buildPlatformInfo: params.buildPlatformInfo, targetPlatformInfo: params.targetPlatformInfo }; const composeConfig = await readDockerComposeConfig(buildParams, composeFiles, envFile); const services = Object.keys(composeConfig.services || {}); diff --git a/src/spec-node/upgradeCommand.ts b/src/spec-node/upgradeCommand.ts index 8773fd5de..8a60c677d 100644 --- a/src/spec-node/upgradeCommand.ts +++ b/src/spec-node/upgradeCommand.ts @@ -19,12 +19,14 @@ import { isLocalFile, readLocalFile, writeLocalFile } from '../spec-utils/pfs'; import { readFeaturesConfig } from './featureUtils'; import { DevContainerConfig } from '../spec-configuration/configuration'; import { mapNodeArchitectureToGOARCH, mapNodeOSToGOOS } from '../spec-configuration/containerCollectionsOCI'; +import { parseDockerPathArgs } from '../spec-common/dockerPathArgs'; export function featuresUpgradeOptions(y: Argv) { return y .options({ 'workspace-folder': { type: 'string', description: 'Workspace folder. If --workspace-folder is not provided defaults to the current directory.' }, 'docker-path': { type: 'string', description: 'Path to docker executable.', default: 'docker' }, + 'docker-path-args': { type: 'string', hidden: true, description: 'JSON array of arguments inserted after the Docker CLI path.' }, '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.' }, @@ -56,6 +58,7 @@ export function featuresUpgradeHandler(args: FeaturesUpgradeArgs) { async function featuresUpgrade({ 'workspace-folder': workspaceFolderArg, 'docker-path': dockerPath, + 'docker-path-args': dockerPathArgs, config: configArg, 'docker-compose-path': dockerComposePath, 'log-level': inputLogLevel, @@ -83,6 +86,7 @@ async function featuresUpgrade({ }, pkg, sessionStart, disposables); const dockerComposeCLI = dockerComposeCLIConfig({ exec: cliHost.exec, + args: parseDockerPathArgs(dockerPathArgs), env: cliHost.env, output, }, dockerPath, dockerComposePath); @@ -93,6 +97,7 @@ async function featuresUpgrade({ const dockerParams: DockerCLIParameters = { cliHost, dockerCLI: dockerPath, + dockerPathArgs: parseDockerPathArgs(dockerPathArgs), dockerComposeCLI, env: cliHost.env, output, diff --git a/src/spec-node/utils.ts b/src/spec-node/utils.ts index e6cf6980f..9251980fc 100644 --- a/src/spec-node/utils.ts +++ b/src/spec-node/utils.ts @@ -109,6 +109,7 @@ export interface DockerResolverParameters { common: ResolverParameters; parsedAuthority: ParsedAuthority | undefined; dockerCLI: string; + dockerPathArgs?: string[]; cliVariant: CLIVariant; dockerComposeCLI: () => Promise; dockerEnv: NodeJS.ProcessEnv; diff --git a/src/spec-shutdown/dockerUtils.ts b/src/spec-shutdown/dockerUtils.ts index 0531f6b87..99ca922eb 100644 --- a/src/spec-shutdown/dockerUtils.ts +++ b/src/spec-shutdown/dockerUtils.ts @@ -49,6 +49,7 @@ export interface ContainerDetails { export interface DockerCLIParameters { cliHost: CLIHost; dockerCLI: string; + dockerPathArgs?: string[]; dockerComposeCLI: () => Promise; env: NodeJS.ProcessEnv; output: Log; @@ -77,6 +78,7 @@ export interface PartialPtyExecParameters { interface DockerResolverParameters { dockerCLI: string; + dockerPathArgs?: string[]; cliVariant: CLIVariant; dockerComposeCLI: () => Promise; dockerEnv: NodeJS.ProcessEnv; @@ -420,16 +422,20 @@ function toDockerExecArgs(containerName: string, user: string | undefined, param } export function toExecParameters(params: DockerCLIParameters | PartialExecParameters | DockerResolverParameters, compose?: DockerComposeCLI): PartialExecParameters { + const dockerPathArgs = 'dockerCLI' in params ? params.dockerPathArgs : undefined; + const args = compose + ? compose.cmd === ('dockerCLI' in params ? params.dockerCLI : undefined) ? [...dockerPathArgs || [], ...compose.args] : compose.args + : 'dockerCLI' in params ? dockerPathArgs : params.args; return 'dockerEnv' in params ? { exec: params.common.cliHost.exec, cmd: compose ? compose.cmd : params.dockerCLI, - args: compose ? compose.args : [], + args, env: params.dockerEnv, output: params.common.output, } : 'cliHost' in params ? { exec: params.cliHost.exec, cmd: compose ? compose.cmd : params.dockerCLI, - args: compose ? compose.args : [], + args, env: params.env, output: params.output, } : { @@ -439,18 +445,22 @@ export function toExecParameters(params: DockerCLIParameters | PartialExecParame } export function toPtyExecParameters(params: DockerCLIParameters | PartialPtyExecParameters | DockerResolverParameters, compose?: DockerComposeCLI): PartialPtyExecParameters { + const dockerPathArgs = 'dockerCLI' in params ? params.dockerPathArgs : undefined; + const args = compose + ? compose.cmd === ('dockerCLI' in params ? params.dockerCLI : undefined) ? [...dockerPathArgs || [], ...compose.args] : compose.args + : 'dockerCLI' in params ? dockerPathArgs : params.args; return 'dockerEnv' in params ? { ptyExec: params.common.cliHost.ptyExec, exec: params.common.cliHost.exec, cmd: compose ? compose.cmd : params.dockerCLI, - args: compose ? compose.args : [], + args, env: params.dockerEnv, output: params.common.output, } : 'cliHost' in params ? { ptyExec: params.cliHost.ptyExec, exec: params.cliHost.exec, cmd: compose ? compose.cmd : params.dockerCLI, - args: compose ? compose.args : [], + args, env: params.env, output: params.output, } : { diff --git a/src/test/dockerUtils.test.ts b/src/test/dockerUtils.test.ts index c9329d7c2..a1d1cdf82 100644 --- a/src/test/dockerUtils.test.ts +++ b/src/test/dockerUtils.test.ts @@ -8,6 +8,7 @@ import { inspectImageInRegistry, qualifyImageName } from '../spec-node/utils'; import assert from 'assert'; import { dockerCLI, listContainers, PartialExecParameters, removeContainer, toExecParameters } from '../spec-shutdown/dockerUtils'; import { createCLIParams } from './testUtils'; +import { parseDockerPathArgs } from '../spec-common/dockerPathArgs'; export const output = makeLog(createPlainLog(text => process.stdout.write(text), () => LogLevel.Trace)); @@ -49,6 +50,27 @@ describe('Docker utils', function () { assert.strictEqual(qualifyImageName('foo/random/image'), 'foo/random/image'); }); + it('prepends Docker path arguments', async () => { + const params = await createCLIParams(__dirname); + params.dockerPathArgs = ['--session', 'MyApp']; + + assert.deepStrictEqual(toExecParameters(params).args, ['--session', 'MyApp']); + assert.deepStrictEqual( + toExecParameters(params, { cmd: params.dockerCLI, args: ['compose'], version: '2' }).args, + ['--session', 'MyApp', 'compose']); + assert.deepStrictEqual( + toExecParameters(params, { cmd: 'docker-compose', args: [], version: '1' }).args, + []); + }); + + it('parses Docker path arguments', () => { + assert.deepStrictEqual(parseDockerPathArgs('["--session","MyApp"]'), ['--session', 'MyApp']); + assert.strictEqual(parseDockerPathArgs(undefined), undefined); + assert.throws(() => parseDockerPathArgs('{'), /JSON array of strings/); + assert.throws(() => parseDockerPathArgs('{}'), /JSON array of strings/); + assert.throws(() => parseDockerPathArgs('["--session",1]'), /JSON array of strings/); + }); + it('protects against concurrent removal', async () => { const params = await createCLIParams(__dirname); const verboseParams = { ...toExecParameters(params), output: makeLog(output, LogLevel.Info), print: 'continuous' as 'continuous' }; From db115f83cdff2e069aa822ae0ac550642156dee7 Mon Sep 17 00:00:00 2001 From: "Gordon Lam (SH)" Date: Thu, 27 Aug 2026 14:32:56 +0000 Subject: [PATCH 2/2] Document CLI argument prefix support Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8539f306-93c4-4831-9bcd-ec77196d5ec8 --- CHANGELOG.md | 1 + src/test/dockerUtils.test.ts | 1 + 2 files changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 134e0266e..8bfce61eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ Notable changes. ### [0.88.0] - Add WSLc support (https://github.com/devcontainers/cli/pull/1249) +- Add an internal Docker CLI argument-prefix option for provider-specific target selection. ## May 2026 diff --git a/src/test/dockerUtils.test.ts b/src/test/dockerUtils.test.ts index a1d1cdf82..0c30ad799 100644 --- a/src/test/dockerUtils.test.ts +++ b/src/test/dockerUtils.test.ts @@ -65,6 +65,7 @@ describe('Docker utils', function () { it('parses Docker path arguments', () => { assert.deepStrictEqual(parseDockerPathArgs('["--session","MyApp"]'), ['--session', 'MyApp']); + assert.deepStrictEqual(parseDockerPathArgs('[]'), []); assert.strictEqual(parseDockerPathArgs(undefined), undefined); assert.throws(() => parseDockerPathArgs('{'), /JSON array of strings/); assert.throws(() => parseDockerPathArgs('{}'), /JSON array of strings/);