Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for SSH remote Docker daemons #1386

Merged
merged 12 commits into from
Nov 5, 2019
6 changes: 3 additions & 3 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
22 changes: 2 additions & 20 deletions src/debugging/coreclr/ChildProcessProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 };

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

Expand Down
27 changes: 3 additions & 24 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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 };

Expand Down Expand Up @@ -120,7 +118,7 @@ export async function activateInternal(ctx: vscode.ExtensionContext, perfStats:
registerDebugProvider(ctx);
registerTaskProviders(ctx);

refreshDockerode();
await refreshDockerode();

await consolidateDefaultRegistrySettings();
activateLanguageClient(ctx);
Expand Down Expand Up @@ -186,7 +184,7 @@ namespace Configuration {
e.affectsConfiguration('docker.certPath') ||
e.affectsConfiguration('docker.tlsVerify') ||
e.affectsConfiguration('docker.machineName')) {
refreshDockerode();
await refreshDockerode();
}
}
));
Expand Down Expand Up @@ -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 changes: 23 additions & 0 deletions src/utils/execAsync.ts
Original file line number Diff line number Diff line change
@@ -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 }> {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
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));
}
});
}
106 changes: 106 additions & 0 deletions src/utils/refreshDockerode.ts
Original file line number Diff line number Diff line change
@@ -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> {
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved
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;
}
}
46 changes: 0 additions & 46 deletions src/utils/tryGetDefaultDockerContext.ts

This file was deleted.