Add support for SSH remote Docker daemons (#1386)
* Support of Docker over SSH * Update docker-modem * Asyncify and refactoring
This commit is contained in:
Родитель
ea16dba953
Коммит
b58b33893b
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
Загрузка…
Ссылка в новой задаче