diff --git a/package-lock.json b/package-lock.json index fe51789fd4..7ed8e5a4e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -2648,9 +2648,9 @@ } }, "docker-modem": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.0.2.tgz", - "integrity": "sha512-Aq6NBJQm5najFlg4wRZtSrWXzQbQClh1kccAkUWIdVhuyHK6tYhmi9W9xtVaGmzBa0Nfuwi4AEbQzWtHZT+2Jw==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/docker-modem/-/docker-modem-2.0.4.tgz", + "integrity": "sha512-fj0pt7iEXCPCN9wDWJRyjQJ1POcmCwPmuId/Eg+bxULsxI7l9GHEyol4HY9fH4B/I69J67ATqQ09SOfzgwbZlg==", "requires": { "JSONStream": "1.3.2", "debug": "^3.2.6", diff --git a/package.json b/package.json index 4723b88a93..ce41948f01 100644 --- a/package.json +++ b/package.json @@ -1979,6 +1979,7 @@ "azure-storage": "^2.10.3", "deep-equal": "^1.1.0", "dockerfile-language-server-nodejs": "^0.0.21", + "docker-modem": "^2.0.4", "dockerode": "^3.0.2", "fs-extra": "^6.0.1", "glob": "7.1.2", diff --git a/src/debugging/coreclr/ChildProcessProvider.ts b/src/debugging/coreclr/ChildProcessProvider.ts index 80a4768e43..eb4cf6294f 100644 --- a/src/debugging/coreclr/ChildProcessProvider.ts +++ b/src/debugging/coreclr/ChildProcessProvider.ts @@ -4,6 +4,7 @@ import * as cp from 'child_process'; import * as process from 'process'; +import { execAsync } from '../../utils/execAsync'; export type ProcessProviderExecOptions = cp.ExecOptions & { progress?(content: string, process: cp.ChildProcess): void }; @@ -25,26 +26,7 @@ export class ChildProcessProvider implements ProcessProvider { } public async exec(command: string, options: ProcessProviderExecOptions): Promise<{ stdout: string, stderr: string }> { - return await new Promise<{ stdout: string, stderr: string }>( - (resolve, reject) => { - const p = cp.exec( - command, - options, - (error, stdout, stderr) => { - if (error) { - return reject(error); - } - - resolve({ stdout, stderr }); - }); - - if (options.progress) { - const progress = options.progress; - - p.stderr.on('data', (chunk: Buffer) => progress(chunk.toString(), p)); - p.stdout.on('data', (chunk: Buffer) => progress(chunk.toString(), p)); - } - }); + return await execAsync(command, options, options && options.progress); } } diff --git a/src/extension.ts b/src/extension.ts index 0b9a22f69e..3e85f4f278 100644 --- a/src/extension.ts +++ b/src/extension.ts @@ -4,7 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import * as assert from 'assert'; -import * as Dockerode from 'dockerode'; import * as fse from 'fs-extra'; import * as path from 'path'; import * as vscode from 'vscode'; @@ -26,11 +25,10 @@ import { ext } from './extensionVariables'; import { registerListeners } from './registerListeners'; import { registerTaskProviders } from './tasks/TaskHelper'; import { registerTrees } from './tree/registerTrees'; -import { addDockerSettingsToEnv } from './utils/addDockerSettingsToEnv'; import { Keytar } from './utils/keytar'; import { nps } from './utils/nps'; +import { refreshDockerode } from './utils/refreshDockerode'; import { DefaultTerminalProvider } from './utils/TerminalProvider'; -import { tryGetDefaultDockerContext } from './utils/tryGetDefaultDockerContext'; export type KeyInfo = { [keyName: string]: string }; @@ -120,7 +118,7 @@ export async function activateInternal(ctx: vscode.ExtensionContext, perfStats: registerDebugProvider(ctx); registerTaskProviders(ctx); - refreshDockerode(); + await refreshDockerode(); await consolidateDefaultRegistrySettings(); activateLanguageClient(ctx); @@ -186,7 +184,7 @@ namespace Configuration { e.affectsConfiguration('docker.certPath') || e.affectsConfiguration('docker.tlsVerify') || e.affectsConfiguration('docker.machineName')) { - refreshDockerode(); + await refreshDockerode(); } } )); @@ -251,22 +249,3 @@ function activateLanguageClient(ctx: vscode.ExtensionContext): void { ctx.subscriptions.push(client.start()); }); } - -/** - * Dockerode parses and handles the well-known `DOCKER_*` environment variables, but it doesn't let us pass those values as-is to the constructor - * Thus we will temporarily update `process.env` and pass nothing to the constructor - */ -function refreshDockerode(): void { - const oldEnv = process.env; - try { - process.env = { ...process.env }; // make a clone before we change anything - addDockerSettingsToEnv(process.env, oldEnv); - ext.dockerodeInitError = undefined; - ext.dockerode = new Dockerode(process.env.DOCKER_HOST ? undefined : tryGetDefaultDockerContext()); - } catch (error) { - // This will be displayed in the tree - ext.dockerodeInitError = error; - } finally { - process.env = oldEnv; - } -} diff --git a/src/utils/execAsync.ts b/src/utils/execAsync.ts new file mode 100644 index 0000000000..ff6b7c9b34 --- /dev/null +++ b/src/utils/execAsync.ts @@ -0,0 +1,23 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import * as cp from 'child_process'; + +export async function execAsync(command: string, options?: cp.ExecOptions, progress?: (content: string, process: cp.ChildProcess) => void): Promise<{ stdout: string, stderr: string }> { + return await new Promise((resolve, reject) => { + const p = cp.exec(command, options, (error, stdout, stderr) => { + if (error) { + return reject(error); + } + + return resolve({ stdout, stderr }); + }); + + if (progress) { + p.stderr.on('data', (chunk: Buffer) => progress(chunk.toString(), p)); + p.stdout.on('data', (chunk: Buffer) => progress(chunk.toString(), p)); + } + }); +} diff --git a/src/utils/refreshDockerode.ts b/src/utils/refreshDockerode.ts new file mode 100644 index 0000000000..2c87f87194 --- /dev/null +++ b/src/utils/refreshDockerode.ts @@ -0,0 +1,106 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See LICENSE.md in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { DockerOptions } from 'dockerode'; +import Dockerode = require('dockerode'); +import { ext } from '../extensionVariables'; +import { addDockerSettingsToEnv } from './addDockerSettingsToEnv'; +import { cloneObject } from './cloneObject'; +import { execAsync } from './execAsync'; +import { isWindows } from './osUtils'; + +const unix = 'unix://'; +const npipe = 'npipe://'; + +const SSH_URL_REGEX = /ssh:\/\//i; + +// Not exhaustive--only the properties we're interested in +interface IDockerEndpoint { + Host?: string; +} + +// Also not exhaustive--only the properties we're interested in +interface IDockerContext { + Endpoints: { [key: string]: IDockerEndpoint } +} + +/** + * Dockerode parses and handles the well-known `DOCKER_*` environment variables, but it doesn't let us pass those values as-is to the constructor + * Thus we will temporarily update `process.env` and pass nothing to the constructor + */ +export async function refreshDockerode(): Promise { + try { + const oldEnv = process.env; + const newEnv: NodeJS.ProcessEnv = cloneObject(process.env); // make a clone before we change anything + addDockerSettingsToEnv(newEnv, oldEnv); + + const dockerodeOptions = await getDockerodeOptions(newEnv); + + ext.dockerodeInitError = undefined; + process.env = newEnv; + try { + ext.dockerode = new Dockerode(dockerodeOptions); + } finally { + process.env = oldEnv; + } + } catch (error) { + // This will be displayed in the tree + ext.dockerodeInitError = error; + } +} + +async function getDockerodeOptions(newEnv: NodeJS.ProcessEnv): Promise { + // By this point any DOCKER_HOST from VSCode settings is already copied to process.env, so we can use it directly + + try { + if (newEnv.DOCKER_HOST && + SSH_URL_REGEX.test(newEnv.DOCKER_HOST) && + !newEnv.SSH_AUTH_SOCK) { + // If DOCKER_HOST is an SSH URL, we need to configure SSH_AUTH_SOCK for Dockerode + // Other than that, we use default settings, so return undefined + newEnv.SSH_AUTH_SOCK = await getSshAuthSock(); + return undefined; + } else if (!newEnv.DOCKER_HOST) { + // If DOCKER_HOST is unset, try to get default Docker context--this helps support WSL + return await getDefaultDockerContext(); + } + } catch { } // Best effort only + + // Use default options + return undefined; +} + +async function getSshAuthSock(): Promise { + if (isWindows()) { + return '\\\\.\\pipe\\openssh-ssh-agent'; + } else { + // On Mac and Linux, if SSH_AUTH_SOCK isn't set there's nothing we can do + // Running ssh-agent would yield a new agent that doesn't have the needed keys + await ext.ui.showWarningMessage('In order to use an SSH DOCKER_HOST on OS X and Linux, you must configure an ssh-agent.'); + } +} + +async function getDefaultDockerContext(): Promise { + const { stdout } = await execAsync('docker context inspect', { timeout: 5000 }); + const dockerContexts = JSON.parse(stdout); + const defaultHost: string = + dockerContexts && + dockerContexts.length > 0 && + dockerContexts[0].Endpoints && + dockerContexts[0].Endpoints.docker && + dockerContexts[0].Endpoints.docker.Host; + + if (defaultHost.indexOf(unix) === 0) { + return { + socketPath: defaultHost.substring(unix.length), // Everything after the unix:// (expecting unix:///var/run/docker.sock) + }; + } else if (defaultHost.indexOf(npipe) === 0) { + return { + socketPath: defaultHost.substring(npipe.length), // Everything after the npipe:// (expecting npipe:////./pipe/docker_engine or npipe:////./pipe/docker_wsl) + }; + } else { + return undefined; + } +} diff --git a/src/utils/tryGetDefaultDockerContext.ts b/src/utils/tryGetDefaultDockerContext.ts deleted file mode 100644 index bdefbfac80..0000000000 --- a/src/utils/tryGetDefaultDockerContext.ts +++ /dev/null @@ -1,46 +0,0 @@ -/*--------------------------------------------------------------------------------------------- - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. See LICENSE.md in the project root for license information. - *--------------------------------------------------------------------------------------------*/ - -import * as cp from 'child_process'; -import { DockerOptions } from 'dockerode'; - -const unix = 'unix://'; -const npipe = 'npipe://'; - -// Not exhaustive--only the properties we're interested in -interface IDockerEndpoint { - Host?: string; -} - -// Also not exhaustive--only the properties we're interested in -interface IDockerContext { - Endpoints: { [key: string]: IDockerEndpoint } -} - -export function tryGetDefaultDockerContext(): DockerOptions { - try { - const stdout = cp.execSync('docker context inspect', { timeout: 5000 }).toString(); - const dockerContexts = JSON.parse(stdout); - const defaultHost: string = - dockerContexts && - dockerContexts.length > 0 && - dockerContexts[0].Endpoints && - dockerContexts[0].Endpoints.docker && - dockerContexts[0].Endpoints.docker.Host; - - if (defaultHost.indexOf(unix) === 0) { - return { - socketPath: defaultHost.substring(unix.length), // Everything after the unix:// (expecting unix:///var/run/docker.sock) - }; - } else if (defaultHost.indexOf(npipe) === 0) { - return { - socketPath: defaultHost.substring(npipe.length), // Everything after the npipe:// (expecting npipe:////./pipe/docker_engine or npipe:////./pipe/docker_wsl) - }; - } - } catch { } // Best effort - - // We won't try harder than that; for more complicated scenarios user will need to set DOCKER_HOST etc. in environment or VSCode options - return undefined; -}