Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 13 additions & 4 deletions package-lock.json

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

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2751,6 +2751,7 @@
"@types/request-promise-native": "^1.0.17",
"@types/semver": "^7.3.4",
"@types/string-replace-webpack-plugin": "^0.1.0",
"@types/tar-stream": "^2.2.0",
"@types/vscode": "1.52.0",
"@types/xml2js": "^0.4.7",
"@typescript-eslint/eslint-plugin": "^4.14.0",
Expand Down Expand Up @@ -2797,6 +2798,7 @@
"request-promise-native": "^1.0.9",
"semver": "^7.3.4",
"tar": "^6.1.0",
"tar-stream": "^2.2.0",
"vscode-azureappservice": "^0.72.2",
"vscode-azureextensionui": "^0.38.4",
"vscode-languageclient": "^7.0.0",
Expand Down
1 change: 1 addition & 0 deletions src/docker/DockerApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface DockerApiClient extends Disposable {
inspectContainer(context: IActionContext, ref: string, token?: CancellationToken): Promise<DockerContainerInspection>;
execInContainer(context: IActionContext, ref: string, command: string[] | DockerExecCommandProvider, options?: DockerExecOptions, token?: CancellationToken): Promise<{ stdout: string, stderr: string }>;
getContainerFile(context: IActionContext, ref: string, path: string, token?: CancellationToken): Promise<Buffer>;
putContainerFile(context: IActionContext, ref: string, path: string, content: Buffer, token?: CancellationToken): Promise<void>;
getContainerLogs(context: IActionContext, ref: string, token?: CancellationToken): Promise<NodeJS.ReadableStream>;
pruneContainers(context: IActionContext, token?: CancellationToken): Promise<PruneResult | undefined>;
startContainer(context: IActionContext, ref: string, token?: CancellationToken): Promise<void>;
Expand Down
4 changes: 4 additions & 0 deletions src/docker/DockerServeClient/DockerServeClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,10 @@ export class DockerServeClient extends ContextChangeCancelClient implements Dock
throw new NotSupportedError(context);
}

public async putContainerFile(context: IActionContext, ref: string, path: string, content: Buffer, token?: CancellationToken): Promise<void> {
throw new NotSupportedError(context);
}

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 Down
79 changes: 65 additions & 14 deletions src/docker/DockerodeApiClient/DockerodeApiClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
*--------------------------------------------------------------------------------------------*/

import Dockerode = require('dockerode');
import * as nodepath from 'path';
import * as stream from 'stream';
import * as tar from 'tar';
import * as tarstream from 'tar-stream';
import { CancellationToken } from 'vscode';
import { IActionContext, parseError } from 'vscode-azureextensionui';
import { localize } from '../../localize';
Expand Down Expand Up @@ -149,28 +150,78 @@ export class DockerodeApiClient extends ContextChangeCancelClient implements Doc

return await new Promise(
(resolve, reject) => {
const tarParser = new tar.Parse();
let entry: { content?: Buffer, error?: Error };

tarParser.on('entry', (entry: tar.ReadEntry) => {
const chunks = [];
const tarStream = tarstream.extract();

entry.on('data', chunk => {
chunks.push(chunk);
});
tarStream.on('entry', (header, entryStream, next) => {
if (entry) {
//
// We already extracted the first entry, so just skip the rest...
//

entry.on('error', error => {
reject(error);
});
// When the entry stream has been drained, go on to the next entry...
entryStream.on('end', next);

entry.on('end', () => {
resolve(Buffer.concat(chunks));
});
// Drain the entry stream...
entryStream.resume();
} else {
//
// This is the first entry, so extract its content...
//

const chunks = [];

entryStream.on('data', chunk => {
chunks.push(chunk);
});

entryStream.on('error', error => {
entry = { error };
});

entryStream.on('end', () => {
entry = { content: Buffer.concat(chunks) };

// The entry stream is done, so go on to the next entry...
next();
});
}
});

tarStream.on('finish', () => {
//
// The archive has been extracted, so return the result...
//

if (entry.error) {
reject(entry.error);
} else if (entry.content) {
resolve(entry.content);
} else {
reject(new Error(localize('vscode-docker.utils.dockerode.failedToExtractContainerFile', 'Failed to extract container file from archive.')));
}
});

archiveStream.pipe(tarParser);
archiveStream.pipe(tarStream);
});
}

public async putContainerFile(context: IActionContext, ref: string, path: string, content: Buffer, token?: CancellationToken): Promise<void> {
const container = this.dockerodeClient.getContainer(ref);

const directory = nodepath.dirname(path);
const filename = nodepath.basename(path);

const pack = tarstream.pack();

pack.entry({ name: filename }, content);

pack.finalize();

await this.callWithErrorHandling(context, async () => container.putArchive(pack, { path: directory }));
}

public async getContainerLogs(context: IActionContext, ref: string, token?: CancellationToken): Promise<NodeJS.ReadableStream> {
const container = this.dockerodeClient.getContainer(ref);
return this.callWithErrorHandling(context, async () => container.logs({ follow: true, stdout: true }));
Expand Down
53 changes: 52 additions & 1 deletion src/docker/files/ContainerFilesProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/

import * as vscode from 'vscode';
import { callWithTelemetryAndErrorHandling } from 'vscode-azureextensionui';
import { localize } from '../../localize';
import { DockerOSType } from '../Common';
import { DockerApiClient } from '../DockerApiClient';
Expand All @@ -22,6 +23,12 @@ class UnrecognizedContainerOSError extends Error {
}
}

class UnsupportedServerOSError extends Error {
public constructor() {
super(localize('docker.files.containerFilesProvider.supportedServerOS', 'This operation is not supported on this Docker host.'));
}
}

export class ContainerFilesProvider extends vscode.Disposable implements vscode.FileSystemProvider {
private readonly changeEmitter: vscode.EventEmitter<vscode.FileChangeEvent[]> = new vscode.EventEmitter<vscode.FileChangeEvent[]>();

Expand Down Expand Up @@ -161,7 +168,37 @@ export class ContainerFilesProvider extends vscode.Disposable implements vscode.
}

public writeFile(uri: vscode.Uri, content: Uint8Array, options: { create: boolean; overwrite: boolean; }): void | Thenable<void> {
throw new MethodNotImplementedError();
const method =
async (): Promise<void> => {
const dockerUri = DockerUri.parse(uri);

let serverOS = dockerUri.options?.serverOS;

if (serverOS === undefined) {
const version = await this.dockerClientProvider().version(undefined);

serverOS = version.Os;
}

switch (serverOS) {
case 'linux':

return await this.writeFileViaCopy(dockerUri, content);

default:

throw new UnsupportedServerOSError();
}
};

return callWithTelemetryAndErrorHandling(
`containerFilesProvider.writeFile`,
async actionContext => {
actionContext.errorHandling.suppressDisplay = true; // Suppress display. VSCode already has a modal popup.
actionContext.errorHandling.rethrow = true; // Rethrow to hit the try/catch outside this block.

await method();
});
}

public delete(uri: vscode.Uri, options: { recursive: boolean; }): void | Thenable<void> {
Expand Down Expand Up @@ -229,4 +266,18 @@ export class ContainerFilesProvider extends vscode.Disposable implements vscode.

return Uint8Array.from(buffer);
}

private async writeFileViaCopy(dockerUri: DockerUri, content: Uint8Array): Promise<void> {
let containerOS = dockerUri.options?.containerOS;

if (containerOS === undefined) {
containerOS = await this.getContainerOS(dockerUri.containerId);
}

await this.dockerClientProvider().putContainerFile(
undefined, // context
dockerUri.containerId,
containerOS === 'windows' ? dockerUri.windowsPath : dockerUri.path,
Buffer.from(content));
}
}
4 changes: 1 addition & 3 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,9 +134,7 @@ export async function activateInternal(ctx: vscode.ExtensionContext, perfStats:
{
// While Windows containers aren't generally case-sensitive, Linux containers are and make up the overwhelming majority of running containers.
isCaseSensitive: true,

// TODO: Add support for editing container files (https://github.com/microsoft/vscode-docker/issues/2465)
isReadonly: true
isReadonly: false
})
);

Expand Down