diff --git a/package.json b/package.json index 91eb41152f..d6ba80e3a1 100644 --- a/package.json +++ b/package.json @@ -2201,6 +2201,12 @@ "type": "boolean", "default": true, "description": "%vscode-docker.config.docker.showStartPage%" + }, + "docker.dockerPath": { + "type": "string", + "default": "docker", + "description": "%vscode-docker.config.docker.dockerPath%", + "scope": "machine-overridable" } } }, diff --git a/package.nls.json b/package.nls.json index 292ea8c793..3f7992122e 100644 --- a/package.nls.json +++ b/package.nls.json @@ -166,7 +166,7 @@ "vscode-docker.config.docker.volumes.sortBy": "The property to use to sort volumes in Docker view: CreatedTime or Label", "vscode-docker.config.docker.imageBuildContextPath": "Build context PATH to pass to Docker build command.", "vscode-docker.config.docker.truncateLongRegistryPaths": "Set to true to truncate long image and container registry paths in Docker view", - "vscode-docker.config.docker.truncateMaxLength": "Maximum length of a registry paths displayed in Docker view, including elipsis. The truncateLongRegistryPaths setting must be set to true for truncateMaxLength setting to be effective.", + "vscode-docker.config.docker.truncateMaxLength": "Maximum length of a registry paths displayed in Docker view, including ellipsis. The truncateLongRegistryPaths setting must be set to true for truncateMaxLength setting to be effective.", "vscode-docker.config.docker.dockerodeOptions": "If specified, this object will be passed to the Dockerode constructor. Takes precedence over DOCKER_HOST, the Docker Host setting, and any existing Docker contexts.", "vscode-docker.config.docker.host": "Equivalent to setting the DOCKER_HOST environment variable, for example, ssh://myuser@mymachine or tcp://1.2.3.4.", "vscode-docker.config.docker.context": "Equivalent to setting the DOCKER_CONTEXT environment variable.", @@ -188,6 +188,7 @@ "vscode-docker.config.docker.showRemoteWorkspaceWarning": "Set to true to prompt to switch from \"UI\" extension mode to \"Workspace\" extension mode if an operation is not supported in UI mode.", "vscode-docker.config.docker.scaffolding.templatePath": "The path to use for scaffolding templates.", "vscode-docker.config.docker.showStartPage": "Show the Docker extension Start Page when a new update is released.", + "vscode-docker.config.docker.dockerPath": "Absolute path to Docker client executable ('docker' command). If the path contains whitespace, it needs to be quoted appropriately.", "vscode-docker.config.deprecated": "This setting has been deprecated and will be removed in a future release.", "vscode-docker.commands.compose.down": "Compose Down", "vscode-docker.commands.compose.restart": "Compose Restart", diff --git a/src/commands/containers/attachShellContainer.ts b/src/commands/containers/attachShellContainer.ts index 7376ee7cce..6bc206a63a 100644 --- a/src/commands/containers/attachShellContainer.ts +++ b/src/commands/containers/attachShellContainer.ts @@ -8,6 +8,7 @@ import { DockerOSType } from '../../docker/Common'; import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem'; +import { dockerExePath } from '../../utils/dockerExePathProvider'; import { executeAsTask } from '../../utils/executeAsTask'; import { execAsync } from '../../utils/spawnAsync'; import { selectAttachCommand } from '../selectCommandTemplate'; @@ -46,7 +47,7 @@ export async function attachShellContainer(context: IActionContext, node?: Conta // If so use it, otherwise use sh try { // If this succeeds, bash is present (exit code 0) - await execAsync(`docker exec -i ${node.containerId} sh -c "which bash"`); + await execAsync(`${dockerExePath(context)} exec -i ${node.containerId} sh -c "which bash"`); shellCommand = 'bash'; } catch { shellCommand = 'sh'; diff --git a/src/commands/images/pullImage.ts b/src/commands/images/pullImage.ts index 4342251771..1770686bb5 100644 --- a/src/commands/images/pullImage.ts +++ b/src/commands/images/pullImage.ts @@ -7,6 +7,7 @@ import { IActionContext } from 'vscode-azureextensionui'; import { ext } from '../../extensionVariables'; import { localize } from '../../localize'; import { ImageTreeItem } from '../../tree/images/ImageTreeItem'; +import { dockerExePath } from '../../utils/dockerExePathProvider'; import { executeAsTask } from '../../utils/executeAsTask'; import { multiSelectNodes } from '../../utils/multiSelectNodes'; @@ -33,6 +34,6 @@ export async function pullImage(context: IActionContext, node?: ImageTreeItem, n continue; } - await executeAsTask(context, `docker pull ${n.fullTag}`, 'docker pull', { addDockerEnv: true }); + await executeAsTask(context, `${dockerExePath(context)} pull ${n.fullTag}`, 'docker pull', { addDockerEnv: true }); } } diff --git a/src/commands/images/pushImage.ts b/src/commands/images/pushImage.ts index 1c07ebfbe5..7cb4808643 100644 --- a/src/commands/images/pushImage.ts +++ b/src/commands/images/pushImage.ts @@ -10,6 +10,7 @@ import { localize } from '../../localize'; import { ImageTreeItem } from '../../tree/images/ImageTreeItem'; import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; import { RegistryTreeItemBase } from '../../tree/registries/RegistryTreeItemBase'; +import { dockerExePath } from '../../utils/dockerExePathProvider'; import { executeAsTask } from '../../utils/executeAsTask'; import { addImageTaggingTelemetry, tagImage } from './tagImage'; @@ -68,7 +69,7 @@ export async function pushImage(context: IActionContext, node: ImageTreeItem | u addImageTaggingTelemetry(context, finalTag, ''); // Finally push the image - await executeAsTask(context, `docker push ${finalTag}`, finalTag, { addDockerEnv: true }); + await executeAsTask(context, `${dockerExePath(context)} push ${finalTag}`, finalTag, { addDockerEnv: true }); } async function tryGetConnectedRegistryForPath(context: IActionContext, baseImagePath: string): Promise { diff --git a/src/commands/images/runAzureCliImage.ts b/src/commands/images/runAzureCliImage.ts index c23b83aa0e..ce648f2e9e 100644 --- a/src/commands/images/runAzureCliImage.ts +++ b/src/commands/images/runAzureCliImage.ts @@ -8,6 +8,7 @@ import * as os from 'os'; import * as vscode from 'vscode'; import { DialogResponses, IActionContext } from 'vscode-azureextensionui'; import { localize } from '../../localize'; +import { dockerExePath } from '../../utils/dockerExePathProvider'; import { executeAsTask } from '../../utils/executeAsTask'; import { getDockerOSType } from '../../utils/osUtils'; @@ -44,6 +45,6 @@ export async function runAzureCliImage(context: IActionContext): Promise { vol += ` -v ${workspaceFolder.uri.fsPath}:/workspace`; } - await executeAsTask(context, `docker run ${option} ${vol.trim()} -it --rm mcr.microsoft.com/azure-cli:latest`, 'Azure CLI', { addDockerEnv: true, focus: true }); + await executeAsTask(context, `${dockerExePath(context)} run ${option} ${vol.trim()} -it --rm mcr.microsoft.com/azure-cli:latest`, 'Azure CLI', { addDockerEnv: true, focus: true }); } } diff --git a/src/commands/registries/azure/deployImageToAci.ts b/src/commands/registries/azure/deployImageToAci.ts index dd61c14d46..f68b768a0a 100644 --- a/src/commands/registries/azure/deployImageToAci.ts +++ b/src/commands/registries/azure/deployImageToAci.ts @@ -12,6 +12,7 @@ import { registryExpectedContextValues } from '../../../tree/registries/registry import { RemoteTagTreeItem } from '../../../tree/registries/RemoteTagTreeItem'; import { executeAsTask } from '../../../utils/executeAsTask'; import { execAsync } from '../../../utils/spawnAsync'; +import { dockerExePath } from '../../../utils/dockerExePathProvider'; import { addImageTaggingTelemetry } from '../../images/tagImage'; export async function deployImageToAci(context: IActionContext, node?: RemoteTagTreeItem): Promise { @@ -35,13 +36,13 @@ export async function deployImageToAci(context: IActionContext, node?: RemoteTag title: localize('vscode-docker.commands.registries.deployImageToAci.gettingPorts', 'Determining ports from image...'), }; const ports = await vscode.window.withProgress(progressOptions, async () => { - return getImagePorts(node.fullTag); + return getImagePorts(node.fullTag, context); }); const portsArg = ports.map(port => `-p ${port}:${port}`).join(' '); addImageTaggingTelemetry(context, node.fullTag, ''); - const command = `docker --context ${aciContext.name} run -d ${portsArg} ${node.fullTag}`; + const command = `${dockerExePath(context)} --context ${aciContext.name} run -d ${portsArg} ${node.fullTag}`; const title = localize('vscode-docker.commands.registries.deployImageToAci.deploy', 'Deploy to ACI'); const options = { addDockerEnv: false, @@ -51,20 +52,20 @@ export async function deployImageToAci(context: IActionContext, node?: RemoteTag await executeAsTask(context, command, title, { ...options, rejectOnError: true }); } catch { // If it fails, try logging in and make one more attempt - await executeAsTask(context, `docker login azure --cloud-name ${await promptForAciCloud(context)}`, title, options); + await executeAsTask(context, `${dockerExePath(context)} login azure --cloud-name ${await promptForAciCloud(context)}`, title, options); await executeAsTask(context, command, title, options); } } -async function getImagePorts(fullTag: string): Promise { +async function getImagePorts(fullTag: string, context: IActionContext): Promise { try { const result: number[] = []; // 1. Pull the image to the default context - await execAsync(`docker --context default pull ${fullTag}`); + await execAsync(`${dockerExePath(context)} --context default pull ${fullTag}`); // 2. Inspect it in the default context to find out the ports to map - const { stdout } = await execAsync(`docker --context default inspect ${fullTag} --format="{{ json .Config.ExposedPorts }}"`); + const { stdout } = await execAsync(`${dockerExePath(context)} --context default inspect ${fullTag} --format="{{ json .Config.ExposedPorts }}"`); try { const portsJson = <{ [key: string]: never }>JSON.parse(stdout); diff --git a/src/commands/registries/logOutOfDockerCli.ts b/src/commands/registries/logOutOfDockerCli.ts index 63b2fbb30f..aeb31ee074 100644 --- a/src/commands/registries/logOutOfDockerCli.ts +++ b/src/commands/registries/logOutOfDockerCli.ts @@ -7,6 +7,7 @@ import { IActionContext } from 'vscode-azureextensionui'; import { ext } from '../../extensionVariables'; import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; import { RegistryTreeItemBase } from '../../tree/registries/RegistryTreeItemBase'; +import { dockerExePath } from '../../utils/dockerExePathProvider'; import { executeAsTask } from '../../utils/executeAsTask'; export async function logOutOfDockerCli(context: IActionContext, node?: RegistryTreeItemBase): Promise { @@ -15,5 +16,5 @@ export async function logOutOfDockerCli(context: IActionContext, node?: Registry } const creds = await node.getDockerCliCredentials(); - await executeAsTask(context, `docker logout ${creds.registryPath}`, 'Docker', { addDockerEnv: true }); + await executeAsTask(context, `${dockerExePath(context)} logout ${creds.registryPath}`, 'Docker', { addDockerEnv: true }); } diff --git a/src/commands/registries/pullImages.ts b/src/commands/registries/pullImages.ts index de6499f30d..a388ebb316 100644 --- a/src/commands/registries/pullImages.ts +++ b/src/commands/registries/pullImages.ts @@ -3,14 +3,15 @@ * Licensed under the MIT License. See LICENSE.md in the project root for license information. *--------------------------------------------------------------------------------------------*/ -import { IActionContext } from "vscode-azureextensionui"; +import { IActionContext } from 'vscode-azureextensionui'; import { ext } from '../../extensionVariables'; -import { registryExpectedContextValues } from "../../tree/registries/registryContextValues"; +import { registryExpectedContextValues } from '../../tree/registries/registryContextValues'; import { RegistryTreeItemBase } from '../../tree/registries/RegistryTreeItemBase'; -import { RemoteRepositoryTreeItemBase } from "../../tree/registries/RemoteRepositoryTreeItemBase"; +import { RemoteRepositoryTreeItemBase } from '../../tree/registries/RemoteRepositoryTreeItemBase'; import { RemoteTagTreeItem } from '../../tree/registries/RemoteTagTreeItem'; -import { executeAsTask } from "../../utils/executeAsTask"; -import { logInToDockerCli } from "./logInToDockerCli"; +import { executeAsTask } from '../../utils/executeAsTask'; +import { dockerExePath } from '../../utils/dockerExePathProvider'; +import { logInToDockerCli } from './logInToDockerCli'; export async function pullRepository(context: IActionContext, node?: RemoteRepositoryTreeItemBase): Promise { if (!node) { @@ -31,5 +32,5 @@ export async function pullImageFromRepository(context: IActionContext, node?: Re async function pullImages(context: IActionContext, node: RegistryTreeItemBase, imageRequest: string): Promise { await logInToDockerCli(context, node); - await executeAsTask(context, `docker pull ${node.baseImagePath}/${imageRequest}`, 'Docker', { addDockerEnv: true }); + await executeAsTask(context, `${dockerExePath(context)} pull ${node.baseImagePath}/${imageRequest}`, 'Docker', { addDockerEnv: true }); } diff --git a/src/commands/selectCommandTemplate.ts b/src/commands/selectCommandTemplate.ts index 418d4af23e..311ddcb3e1 100644 --- a/src/commands/selectCommandTemplate.ts +++ b/src/commands/selectCommandTemplate.ts @@ -9,6 +9,7 @@ import { ContextType } from '../docker/Contexts'; import { ext } from '../extensionVariables'; import { localize } from '../localize'; import { resolveVariables } from '../utils/resolveVariables'; +import { dockerExePath, DefaultDockerPath } from '../utils/dockerExePathProvider'; type TemplateCommand = 'build' | 'run' | 'runInteractive' | 'attach' | 'logs' | 'composeUp' | 'composeDown'; @@ -170,7 +171,16 @@ export async function selectCommandTemplate( context.telemetry.properties.commandContextType = `[${selectedTemplate.contextTypes?.join(', ') ?? ''}]`; context.telemetry.properties.currentContextType = currentContextType; - return resolveVariables(selectedTemplate.template, folder, additionalVariables); + let resolvedCommand = resolveVariables(selectedTemplate.template, folder, additionalVariables); + + if (resolvedCommand.startsWith(DefaultDockerPath + ' ')) { + const dockerPath = dockerExePath(context); + if (dockerPath !== DefaultDockerPath) { + resolvedCommand = dockerPath + resolvedCommand.substring(DefaultDockerPath.length); + } + } + + return resolvedCommand; } async function quickPickTemplate(templates: CommandTemplate[], templatePicker: TemplatePicker): Promise { diff --git a/src/docker/ContextManager.ts b/src/docker/ContextManager.ts index 73fe164475..1fa549a798 100644 --- a/src/docker/ContextManager.ts +++ b/src/docker/ContextManager.ts @@ -17,6 +17,7 @@ import { localize } from '../localize'; import { AsyncLazy } from '../utils/lazy'; import { isWindows } from '../utils/osUtils'; import { execAsync, spawnAsync } from '../utils/spawnAsync'; +import { dockerExePath } from '../utils/dockerExePathProvider'; import { ContextType, DockerContext, DockerContextInspection, isNewContextType } from './Contexts'; // CONSIDER @@ -172,7 +173,7 @@ export class DockerContextManager implements ContextManager, Disposable { } public async inspect(actionContext: IActionContext, contextName: string): Promise { - const { stdout } = await execAsync(`docker context inspect ${contextName}`, { timeout: 10000 }); + const { stdout } = await execAsync(`${dockerExePath(actionContext)} context inspect ${contextName}`, { timeout: 10000 }); // The result is an array with one entry const result: DockerContextInspection[] = JSON.parse(stdout) as DockerContextInspection[]; @@ -180,12 +181,12 @@ export class DockerContextManager implements ContextManager, Disposable { } public async use(actionContext: IActionContext, contextName: string): Promise { - const useCmd: string = `docker context use ${contextName}`; + const useCmd: string = `${dockerExePath(actionContext)} context use ${contextName}`; await execAsync(useCmd, ContextCmdExecOptions); } public async remove(actionContext: IActionContext, contextName: string): Promise { - const removeCmd: string = `docker context rm ${contextName}`; + const removeCmd: string = `${dockerExePath(actionContext)} context rm ${contextName}`; await spawnAsync(removeCmd, ContextCmdExecOptions); } @@ -261,7 +262,7 @@ export class DockerContextManager implements ContextManager, Disposable { // return that specified context as Current = true. This way we don't need extra logic below in parsing. // TODO: eventually change to `docker context ls --format json` // eslint-disable-next-line @typescript-eslint/naming-convention - const { stdout } = await execAsync('docker context ls --format="{{json .}}"', { ...ContextCmdExecOptions, env: { ...process.env, DOCKER_CONTEXT: dockerContextEnv } }); + const { stdout } = await execAsync(`${dockerExePath()} context ls --format="{{json .}}"`, { ...ContextCmdExecOptions, env: { ...process.env, DOCKER_CONTEXT: dockerContextEnv } }); try { // Try parsing as-is; newer CLIs output a JSON object array @@ -334,7 +335,7 @@ export class DockerContextManager implements ContextManager, Disposable { } else { // Otherwise we look at the output of `docker serve --help` // TODO: this is not a very good heuristic - const { stdout } = await execAsync('docker serve --help'); + const { stdout } = await execAsync(`${dockerExePath()} serve --help`); if (/^\s*Start an api server/i.test(stdout)) { result = true; diff --git a/src/tree/contexts/aci/AciContextCreateStep.ts b/src/tree/contexts/aci/AciContextCreateStep.ts index fcb0d82aa8..78fc28c4ba 100644 --- a/src/tree/contexts/aci/AciContextCreateStep.ts +++ b/src/tree/contexts/aci/AciContextCreateStep.ts @@ -9,6 +9,7 @@ import { ext } from '../../../extensionVariables'; import { localize } from '../../../localize'; import { executeAsTask } from '../../../utils/executeAsTask'; import { execAsync } from '../../../utils/spawnAsync'; +import { dockerExePath } from '../../../utils/dockerExePathProvider'; import { IAciWizardContext } from './IAciWizardContext'; export class AciContextCreateStep extends AzureWizardExecuteStep { @@ -20,7 +21,7 @@ export class AciContextCreateStep extends AzureWizardExecuteStep @@ -32,7 +33,8 @@ class DockerInstallStatusProvider implements IDockerInstallStatusProvider { public async isDockerInstalledRealTimeCheck(): Promise { try { - await execAsync('docker -v'); + + await execAsync(`${dockerExePath()} -v`); return true; // As long as the docker command did't throw exception, assume it is installed. } catch (error) { return false; // docker not installed diff --git a/src/utils/dockerExePathProvider.ts b/src/utils/dockerExePathProvider.ts new file mode 100644 index 0000000000..5d95ebdbe3 --- /dev/null +++ b/src/utils/dockerExePathProvider.ts @@ -0,0 +1,17 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as vscode from 'vscode'; +import { IActionContext } from 'vscode-azureextensionui'; + +export const DefaultDockerPath: string = 'docker'; + +export function dockerExePath(context?: IActionContext): string { + const retval = vscode.workspace.getConfiguration('docker').get('dockerPath', DefaultDockerPath); + if (retval !== DefaultDockerPath && context) { + context.telemetry.properties.nonstandardDockerPath = 'true'; + } + return retval; +}