Skip to content

Commit

Permalink
Add new Docker SDK-based client (microsoft#2134)
Browse files Browse the repository at this point in the history
* Implement client for Docker SDK

* Add another stopped container state

* Assume linux if we can't tell what OS

* Disable stop

* Monkey patch in a label for compose proj

* Add getCurrentContext method
  • Loading branch information
bwateratmsft authored and Dmarch28 committed Mar 4, 2021
1 parent dc3f1c7 commit 5824bbc
Show file tree
Hide file tree
Showing 12 changed files with 696 additions and 135 deletions.
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
11 changes: 10 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,15 @@ export async function attachShellContainer(context: IActionContext, node?: Conta
});
}

let osType = await getDockerOSType(context);
let osType: DockerOSType;
try {
// TODO: get OS type from container instead of from system
osType = await getDockerOSType(context);
} 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[];
};
};
}
11 changes: 9 additions & 2 deletions src/docker/ContextManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ export interface ContextManager {
readonly onContextChanged: Event<DockerContext>;
refresh(): Promise<void>;
getContexts(): Promise<DockerContext[]>;
getCurrentContext(): Promise<DockerContext>;

inspect(actionContext: IActionContext, contextName: string): Promise<DockerContextInspection>;
use(actionContext: IActionContext, contextName: string): Promise<void>;
Expand Down Expand Up @@ -96,8 +97,9 @@ export class DockerContextManager implements ContextManager, Disposable {
this.refreshing = true;

this.contextsCache.clear();
const contexts = await this.contextsCache.getValue();
const currentContext = contexts.find(c => c.Current);

// Because the cache is cleared, this will load all the contexts before returning the current one
const currentContext = await this.getCurrentContext();

void ext.dockerClient?.dispose();

Expand Down Expand Up @@ -128,6 +130,11 @@ export class DockerContextManager implements ContextManager, Disposable {
return this.contextsCache.getValue();
}

public async getCurrentContext(): Promise<DockerContext> {
const contexts = await this.getContexts();
return contexts.find(c => c.Current);
}

public async inspect(actionContext: IActionContext, contextName: string): Promise<DockerContextInspection> {
const { stdout } = await execAsync(`docker context inspect ${contextName}`, { timeout: 10000 });

Expand Down
80 changes: 75 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,60 @@ 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 request = new ListRequest()
.setAll(true);

const response: ListResponse = await this.promisify(context, this.containersClient, this.containersClient.list, request, token);
const result = response.getContainersList();

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()
.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 +84,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()
.setId(ref)
.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 +156,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,
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);
}
}
77 changes: 77 additions & 0 deletions src/docker/DockerServeClient/DockerServeUtils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/*---------------------------------------------------------------------------------------------
* 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';

// Group 1 is container group name; group 2 is container name
const containerGroupAndName = /(?:([a-z0-9\-]+)_)?([a-z0-9\-]+)/i;

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(/=|:/i);
labels[label] = value;
});

// If the containers are in a group and there's no com.docker.compose.project label,
// use the group name as that label so that grouping in the UI works
let match: string;
if (labels['com.docker.compose.project'] === undefined &&
(match = containerGroupAndName.exec(container.id)?.[1])) { // Assignment and check is intentional
labels['com.docker.compose.project'] = match;
}

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
Loading

0 comments on commit 5824bbc

Please sign in to comment.