Add support for SSH remote Docker daemons (#1386)

* Support of Docker over SSH

* Update docker-modem

* Asyncify and refactoring
This commit is contained in:
Brandon Waterloo [MSFT] 2019-11-05 07:58:13 -06:00 коммит произвёл GitHub
Родитель ea16dba953
Коммит b58b33893b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
7 изменённых файлов: 138 добавлений и 93 удалений

6
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",

Просмотреть файл

@ -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",

Просмотреть файл

@ -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);
}
}

Просмотреть файл

@ -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;
}
}

23
src/utils/execAsync.ts Normal file
Просмотреть файл

@ -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));
}
});
}

Просмотреть файл

@ -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<void> {
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<DockerOptions | undefined> {
// 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<string | undefined> {
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<DockerOptions | undefined> {
const { stdout } = await execAsync('docker context inspect', { timeout: 5000 });
const dockerContexts = <IDockerContext[]>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;
}
}

Просмотреть файл

@ -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 = <IDockerContext[]>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;
}