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 new Docker SDK-based client #2134

Merged
merged 28 commits into from
Jul 13, 2020
Merged
Show file tree
Hide file tree
Changes from 17 commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5e279f0
Implement client for Docker SDK
bwateratmsft Jul 1, 2020
1ebbce1
Fix compile error
bwateratmsft Jul 1, 2020
e6dd34f
Add another stopped container state
bwateratmsft Jul 6, 2020
b7c40e5
Return to using official repo
bwateratmsft Jul 6, 2020
5d72bc4
Assume linux if we can't tell what OS
bwateratmsft Jul 6, 2020
485671f
Merge branch 'master' into bmw/dockerserve
bwateratmsft Jul 6, 2020
c56580f
Output lots of relevant task information
bwateratmsft Jul 7, 2020
be9f887
Single quotes
bwateratmsft Jul 7, 2020
b35dd79
Merge branch 'master' into bmw/dockerserve
bwateratmsft Jul 7, 2020
7de3556
Single quotes
bwateratmsft Jul 7, 2020
18d618c
Comment changes
bwateratmsft Jul 7, 2020
4c40815
Refactoring
bwateratmsft Jul 7, 2020
6d0f0f7
Install docker SDK from public source
bwateratmsft Jul 7, 2020
48c3226
Fix build break
bwateratmsft Jul 7, 2020
7a06217
Disable stop
bwateratmsft Jul 7, 2020
f740140
Merge branch 'master' into bmw/dockerserve
bwateratmsft Jul 7, 2020
33bd90f
Karol's feedback
bwateratmsft Jul 8, 2020
2c5569d
Merge branch 'master' into bmw/dockerserve
bwateratmsft Jul 8, 2020
84b79df
List all containers
bwateratmsft Jul 8, 2020
e726d86
Monkey patch in a label for compose proj
bwateratmsft Jul 8, 2020
13e0525
Merge commit 'c56580faaf' into bmw/dockerserve
bwateratmsft Jul 8, 2020
a4021ec
Remove mistaken change
bwateratmsft Jul 9, 2020
69a49a1
Remove another mistaken change
bwateratmsft Jul 9, 2020
14dd3b6
Use the prettier syntax
bwateratmsft Jul 9, 2020
0b8e74c
Add getCurrentContext method
bwateratmsft Jul 9, 2020
083c324
Merge branch 'master' into bmw/dockerserve
bwateratmsft Jul 10, 2020
f983398
Merge branch 'master' into bmw/dockerserve
bwateratmsft Jul 13, 2020
277031c
Note a todo item so that we can submit
bwateratmsft Jul 13, 2020
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
441 changes: 421 additions & 20 deletions package-lock.json

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2652,6 +2652,7 @@
"webpack-cli": "^3.3.12"
},
"dependencies": {
"@docker/sdk": "^0.1.7",
"adal-node": "^0.2.1",
"azure-arm-containerregistry": "^5.1.0",
"azure-arm-website": "^5.7.0",
Expand Down
10 changes: 9 additions & 1 deletion src/commands/containers/attachShellContainer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

import * as vscode from 'vscode';
import { IActionContext } from 'vscode-azureextensionui';
import { DockerOSType } from '../../docker/Common';
import { ext } from '../../extensionVariables';
import { localize } from '../../localize';
import { ContainerTreeItem } from '../../tree/containers/ContainerTreeItem';
Expand All @@ -20,7 +21,14 @@ export async function attachShellContainer(context: IActionContext, node?: Conta
});
}

let osType = await getDockerOSType(context);
let osType: DockerOSType;
try {
osType = await getDockerOSType(context);
Copy link
Collaborator Author

@bwateratmsft bwateratmsft Jul 9, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Get OS type from container instead of system? That would allow for Windows containers in ACI to work.

*Need the SDK to provide platform

} catch {
// Assume linux
osType = 'linux';
}

context.telemetry.properties.dockerOSType = osType;

let shellCommand: string;
Expand Down
11 changes: 7 additions & 4 deletions src/docker/Containers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@ export interface DockerPort {
readonly Type?: string;
}

// Ports from inspect have a different shape entirely
export interface InspectionPort {
readonly HostIp?: string;
readonly HostPort?: string;
}

export interface DockerContainer extends DockerObject {
readonly State: string;
readonly Status: string;
Expand All @@ -32,10 +38,7 @@ export interface DockerContainerInspection extends DockerObject {
};
readonly NetworkSettings?: {
readonly Ports?: {
readonly [portAndProtocol: string]: {
readonly HostIp?: string;
readonly HostPort?: string;
}[];
readonly [portAndProtocol: string]: InspectionPort[];
};
};
}
77 changes: 72 additions & 5 deletions src/docker/DockerServeClient/DockerServeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,11 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Containers as ContainersClient } from '@docker/sdk';
import { DeleteRequest, InspectRequest, InspectResponse, ListRequest, ListResponse } from '@docker/sdk/containers';
import { CancellationToken } from 'vscode';
import { IActionContext } from 'vscode-azureextensionui';
import { localize } from '../../localize';
import { DockerInfo, PruneResult } from '../Common';
import { DockerContainer, DockerContainerInspection } from '../Containers';
import { ContextChangeCancelClient } from '../ContextChangeCancelClient';
Expand All @@ -13,22 +16,57 @@ import { DockerImage, DockerImageInspection } from '../Images';
import { DockerNetwork, DockerNetworkInspection, DriverType } from '../Networks';
import { NotSupportedError } from '../NotSupportedError';
import { DockerVolume, DockerVolumeInspection } from '../Volumes';
import { containerPortsToInspectionPorts, containerToDockerContainer } from './DockerServeUtils';

// 20 s timeout for all calls (enough time for any call, but short enough to be UX-reasonable)
const dockerServeCallTimeout = 20 * 1000;

export class DockerServeClient extends ContextChangeCancelClient implements DockerApiClient {
private readonly containersClient: ContainersClient;

public constructor() {
super();
this.containersClient = new ContainersClient();
}

public dispose(): void {
super.dispose();
void this.containersClient?.close();
}

public async info(context: IActionContext, token?: CancellationToken): Promise<DockerInfo> {
throw new NotSupportedError(context);
}

public async getContainers(context: IActionContext, token?: CancellationToken): Promise<DockerContainer[]> {
throw new NotSupportedError(context);
const response: ListResponse = await this.promisify(context, this.containersClient, this.containersClient.list, new ListRequest(), token);
const result = response.getContainersList();
bwateratmsft marked this conversation as resolved.
Show resolved Hide resolved

return result.map(c => containerToDockerContainer(c.toObject()));
}

// #region Not supported by the Docker SDK yet
public async inspectContainer(context: IActionContext, ref: string, token?: CancellationToken): Promise<DockerContainerInspection> {
throw new NotSupportedError(context);
const request = new InspectRequest();
request.setId(ref);

const response: InspectResponse = await this.promisify(context, this.containersClient, this.containersClient.inspect, request, token);
const container = containerToDockerContainer(response.toObject().container);

if (!container) {
throw new Error(localize('vscode-docker.dockerServeClient.noContainer', 'No container with name \'{0}\' was found.', ref));
}

return {
...container,
NetworkSettings: {
Ports: containerPortsToInspectionPorts(container),
},
};
}

// #region Not supported by the Docker SDK yet
public async getContainerLogs(context: IActionContext, ref: string, token?: CancellationToken): Promise<NodeJS.ReadableStream> {
// Supported by SDK, but used only for debugging which will not work in ACI, and complicated to implement
throw new NotSupportedError(context);
}

Expand All @@ -43,14 +81,19 @@ export class DockerServeClient extends ContextChangeCancelClient implements Dock
public async restartContainer(context: IActionContext, ref: string, token?: CancellationToken): Promise<void> {
throw new NotSupportedError(context);
}
// #endregion Not supported by the Docker SDK yet

public async stopContainer(context: IActionContext, ref: string, token?: CancellationToken): Promise<void> {
// Supported by SDK, but is not really the same thing; containers in ACI must stop/start as a group
throw new NotSupportedError(context);
}
// #endregion Not supported by the Docker SDK yet

public async removeContainer(context: IActionContext, ref: string, token?: CancellationToken): Promise<void> {
throw new NotSupportedError(context);
const request = new DeleteRequest();
request.setId(ref);
request.setForce(true);

await this.promisify(context, this.containersClient, this.containersClient.delete, request, token)
}

// #region Not supported by the Docker SDK yet
Expand Down Expand Up @@ -110,4 +153,28 @@ export class DockerServeClient extends ContextChangeCancelClient implements Dock
throw new NotSupportedError(context);
}
// #endregion Not supported by the Docker SDK yet

private async promisify<TRequest, TResponse>(
context: IActionContext,
thisArg: unknown,
karolz-ms marked this conversation as resolved.
Show resolved Hide resolved
clientCallback: (request: TRequest, callback: (err: unknown, response: TResponse) => void) => unknown,
request: TRequest,
token?: CancellationToken): Promise<TResponse> {

const callPromise: Promise<TResponse> = new Promise((resolve, reject) => {
try {
clientCallback.call(thisArg, request, (err, response) => {
if (err) {
reject(err);
}

resolve(response);
});
} catch (err) {
reject(err);
}
});

return this.withTimeoutAndCancellations(context, async () => callPromise, dockerServeCallTimeout, token);
}
}
66 changes: 66 additions & 0 deletions src/docker/DockerServeClient/DockerServeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/

import { Container } from '@docker/sdk/containers';
import { DockerContainer, InspectionPort } from '../Containers';

export function containerToDockerContainer(container: Container.AsObject): DockerContainer | undefined {
if (!container) {
return undefined;
}

const ports = container.portsList.map(p => {
return {
IP: p.hostIp,
PublicPort: p.hostPort,
PrivatePort: p.containerPort,
Type: p.protocol,
};
});

const labels: { [key: string]: string } = {};
container.labelsList.forEach(l => {
const [label, value] = l.split('=');
labels[label] = value;
});

return {
Id: container.id,
Image: container.image,
Name: container.id, // TODO ?
State: container.status,
Status: container.status,
ImageID: undefined, // TODO ?
CreatedTime: undefined, // TODO ?
Labels: labels, // TODO--not yet supported on ACI
Ports: ports,
};
}

export function containerPortsToInspectionPorts(container: DockerContainer): { [portAndProtocol: string]: InspectionPort[] } | undefined {
if (container?.Ports === undefined) {
return undefined;
}

const result: { [portAndProtocol: string]: InspectionPort[] } = {};

for (const port of container.Ports) {
// Get the key
const key = `${port.PrivatePort}/${port.Type}`;

// If there's no entries for this key yet, create an empty list
if (result[key] === undefined) {
result[key] = [];
}

// Add the value to the list
result[key].push({
HostIp: port.IP,
HostPort: port.PublicPort.toString(),
});
}

return result;
}
5 changes: 2 additions & 3 deletions src/docker/DockerodeApiClient/DockerodeApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,9 @@ import { DockerApiClient } from '../DockerApiClient';
import { DockerImage, DockerImageInspection } from '../Images';
import { DockerNetwork, DockerNetworkInspection, DriverType } from '../Networks';
import { DockerVolume, DockerVolumeInspection } from '../Volumes';
import { getContainerName, getFullTagFromDigest } from './DockerodeUtils';
import { refreshDockerode } from './refreshDockerode';
import { getContainerName, getFullTagFromDigest, refreshDockerode } from './DockerodeUtils';

// 20 s timeout for all calls (enough time for a possible Dockerode refresh + the call, but short enough to be UX-reasonable)
// 20 s timeout for all calls (enough time for any call, but short enough to be UX-reasonable)
const dockerodeCallTimeout = 20 * 1000;

export class DockerodeApiClient extends ContextChangeCancelClient implements DockerApiClient {
Expand Down
91 changes: 91 additions & 0 deletions src/docker/DockerodeApiClient/DockerodeUtils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,15 @@
*--------------------------------------------------------------------------------------------*/

import Dockerode = require('dockerode');
import { Socket } from 'net';
import { CancellationTokenSource, workspace } from 'vscode';
import { ext } from '../../extensionVariables';
import { localize } from '../../localize';
import { addDockerSettingsToEnv } from '../../utils/addDockerSettingsToEnv';
import { cloneObject } from '../../utils/cloneObject';
import { isWindows } from '../../utils/osUtils';
import { TimeoutPromiseSource } from '../../utils/promiseUtils';
import { DockerContext } from '../Contexts';

export function getFullTagFromDigest(image: Dockerode.ImageInfo): string {
let repo = '<none>';
Expand All @@ -28,3 +37,85 @@ export function getContainerName(containerInfo: Dockerode.ContainerInfo): string

return canonicalName ?? names[0];
}
const SSH_URL_REGEX = /ssh:\/\//i;

/**
* 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 function refreshDockerode(currentContext: DockerContext): Dockerode {
// If the docker.dockerodeOptions setting is present, use it only
const config = workspace.getConfiguration('docker');
const overrideDockerodeOptions = config.get('dockerodeOptions');
// eslint-disable-next-line @typescript-eslint/tslint/config
if (overrideDockerodeOptions && Object.keys(overrideDockerodeOptions).length > 0) {
return new Dockerode(<Dockerode.DockerOptions>overrideDockerodeOptions);
}

// Set up environment variables
const oldEnv = process.env;
const newEnv: NodeJS.ProcessEnv = cloneObject(process.env); // make a clone before we change anything

if (currentContext.Name === 'default') {
// If the current context is default, just make use of addDockerSettingsToEnv + the current environment
addDockerSettingsToEnv(newEnv, oldEnv);
} else {
// Otherwise get the host from the Docker context
newEnv.DOCKER_HOST = currentContext.DockerEndpoint;
}

// If host is an SSH URL, we need to configure / validate SSH_AUTH_SOCK for Dockerode
if (newEnv.DOCKER_HOST && SSH_URL_REGEX.test(newEnv.DOCKER_HOST)) {
if (!newEnv.SSH_AUTH_SOCK && isWindows()) {
// On Windows, we can use this one by default
newEnv.SSH_AUTH_SOCK = '\\\\.\\pipe\\openssh-ssh-agent';
}

// Don't wait
void validateSshAuthSock(newEnv.SSH_AUTH_SOCK).then((result) => {
if (!result) {
// Don't wait
void ext.ui.showWarningMessage(localize('vscode-docker.utils.dockerode.sshAgent', 'In order to use an SSH DOCKER_HOST, you must configure an ssh-agent.'), { learnMoreLink: 'https://aka.ms/AA7assy' });
}
});
}

try {
process.env = newEnv;
return new Dockerode();
} finally {
process.env = oldEnv;
}
}

async function validateSshAuthSock(authSock: string): Promise<boolean> {
if (!authSock) {
// 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
return false;
}

const socket = new Socket();
const cts = new CancellationTokenSource();

const connectPromise = new Promise<boolean>(resolve => {
socket.on('error', (err) => {
cts.cancel();
resolve(false);
});

socket.on('connect', () => {
cts.cancel();
resolve(true);
});

socket.connect(authSock);
});

// Unfortunately Socket.setTimeout() does not actually work when attempting to establish a connection, so we need to race
return await Promise.race([connectPromise, new TimeoutPromiseSource(1000).promise])
.finally(() => {
socket.end();
cts.dispose();
});
}
Loading