deploy: current vibn theia state
Made-with: Cursor
This commit is contained in:
10
packages/dev-container/.eslintrc.js
Normal file
10
packages/dev-container/.eslintrc.js
Normal file
@@ -0,0 +1,10 @@
|
||||
/** @type {import('eslint').Linter.Config} */
|
||||
module.exports = {
|
||||
extends: [
|
||||
'../../configs/build.eslintrc.json'
|
||||
],
|
||||
parserOptions: {
|
||||
tsconfigRootDir: __dirname,
|
||||
project: 'tsconfig.json'
|
||||
}
|
||||
};
|
||||
53
packages/dev-container/README.md
Normal file
53
packages/dev-container/README.md
Normal file
@@ -0,0 +1,53 @@
|
||||
<div align='center'>
|
||||
|
||||
<br />
|
||||
|
||||
<img src='https://raw.githubusercontent.com/eclipse-theia/theia/master/logo/theia.svg?sanitize=true' alt='theia-ext-logo' width='100px' />
|
||||
|
||||
<h2>ECLIPSE THEIA - DEV-CONTAINER EXTENSION</h2>
|
||||
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
## Description
|
||||
|
||||
The `@theia/dev-container` extension provides functionality to create, start and connect to development containers similiar to the
|
||||
[vscode Dev Containers extension](https://marketplace.visualstudio.com/items?itemName=ms-vscode-remote.remote-containers).
|
||||
|
||||
The full devcontainer.json Schema can be found [here](https://containers.dev/implementors/json_reference/).
|
||||
Currently not all of the configuration file properties are implemented. The following are implemented:
|
||||
|
||||
- name
|
||||
- Image
|
||||
- dockerfile/build.dockerfile
|
||||
- build.context
|
||||
- location
|
||||
- forwardPorts
|
||||
- mounts
|
||||
- containerEnv
|
||||
- remoteUser
|
||||
- shutdownAction
|
||||
- postCreateCommand
|
||||
- postStartCommand
|
||||
|
||||
see `main-container-creation-contributions.ts` for how to implementations or how to implement additional ones.
|
||||
|
||||
Additionally adds support for `composeUpArgs` devcontainer.json property to apply additional arguments for the `docker compose up` call.
|
||||
Usage: `"composeUpArgs": ["--force-recreate"]`
|
||||
|
||||
## Additional Information
|
||||
|
||||
- [API documentation for `@theia/dev-container`](https://eclipse-theia.github.io/theia/docs/next/modules/_theia_dev-container.html)
|
||||
- [Theia - GitHub](https://github.com/eclipse-theia/theia)
|
||||
- [Theia - Website](https://theia-ide.org/)
|
||||
|
||||
## License
|
||||
|
||||
- [Eclipse Public License 2.0](http://www.eclipse.org/legal/epl-2.0/)
|
||||
- [一 (Secondary) GNU General Public License, version 2 with the GNU Classpath Exception](https://projects.eclipse.org/license/secondary-gpl-2.0-cp)
|
||||
|
||||
## Trademark
|
||||
|
||||
"Theia" is a trademark of the Eclipse Foundation
|
||||
<https://www.eclipse.org/theia>
|
||||
55
packages/dev-container/package.json
Normal file
55
packages/dev-container/package.json
Normal file
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"name": "@theia/dev-container",
|
||||
"version": "1.68.0",
|
||||
"description": "Theia - Editor Preview Extension",
|
||||
"dependencies": {
|
||||
"@theia/core": "1.68.0",
|
||||
"@theia/output": "1.68.0",
|
||||
"@theia/remote": "1.68.0",
|
||||
"@theia/workspace": "1.68.0",
|
||||
"dockerode": "^4.0.2",
|
||||
"jsonc-parser": "^2.2.0",
|
||||
"uuid": "^8.0.0"
|
||||
},
|
||||
"publishConfig": {
|
||||
"access": "public"
|
||||
},
|
||||
"theiaExtensions": [
|
||||
{
|
||||
"frontendElectron": "lib/electron-browser/dev-container-frontend-module",
|
||||
"backendElectron": "lib/electron-node/dev-container-backend-module"
|
||||
}
|
||||
],
|
||||
"keywords": [
|
||||
"theia-extension"
|
||||
],
|
||||
"license": "EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/eclipse-theia/theia.git"
|
||||
},
|
||||
"bugs": {
|
||||
"url": "https://github.com/eclipse-theia/theia/issues"
|
||||
},
|
||||
"homepage": "https://github.com/eclipse-theia/theia",
|
||||
"files": [
|
||||
"lib",
|
||||
"src"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "theiaext build",
|
||||
"clean": "theiaext clean",
|
||||
"compile": "theiaext compile",
|
||||
"lint": "theiaext lint",
|
||||
"test": "theiaext test",
|
||||
"watch": "theiaext watch"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@theia/ext-scripts": "1.68.0",
|
||||
"@types/dockerode": "^3.3.23"
|
||||
},
|
||||
"nyc": {
|
||||
"extends": "../../configs/nyc.json"
|
||||
},
|
||||
"gitHead": "21358137e41342742707f660b8e222f940a27652"
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { createConnection } from 'net';
|
||||
import { stdin, argv, stdout } from 'process';
|
||||
|
||||
/**
|
||||
* this node.js Program is supposed to be executed by an docker exec session inside a docker container.
|
||||
* It uses a tty session to listen on stdin and send on stdout all communication with the theia backend running inside the container.
|
||||
*/
|
||||
|
||||
let backendPort: number | undefined = undefined;
|
||||
argv.slice(2).forEach(arg => {
|
||||
if (arg.startsWith('-target-port')) {
|
||||
backendPort = parseInt(arg.split('=')[1]);
|
||||
}
|
||||
});
|
||||
|
||||
if (!backendPort) {
|
||||
throw new Error('please start with -target-port={port number}');
|
||||
}
|
||||
if (stdin.isTTY) {
|
||||
stdin.setRawMode(true);
|
||||
}
|
||||
const connection = createConnection(backendPort, '0.0.0.0');
|
||||
|
||||
connection.pipe(stdout);
|
||||
stdin.pipe(connection);
|
||||
|
||||
connection.on('error', error => {
|
||||
console.error('connection error', error);
|
||||
});
|
||||
|
||||
connection.on('close', () => {
|
||||
console.log('connection closed');
|
||||
process.exit(0);
|
||||
});
|
||||
|
||||
// keep the process running
|
||||
setInterval(() => { }, 1 << 30);
|
||||
@@ -0,0 +1,158 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { AbstractRemoteRegistryContribution, RemoteRegistry } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
|
||||
import { DevContainerFile, LastContainerInfo, RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
|
||||
import { WorkspaceStorageService } from '@theia/workspace/lib/browser/workspace-storage-service';
|
||||
import { Command, MaybePromise, MessageService, nls, QuickInputService, URI } from '@theia/core';
|
||||
import { WorkspaceInput, WorkspaceOpenHandlerContribution, WorkspaceService } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { ContainerOutputProvider } from './container-output-provider';
|
||||
import { WorkspaceServer } from '@theia/workspace/lib/common';
|
||||
import { DEV_CONTAINER_PATH_QUERY, DEV_CONTAINER_WORKSPACE_SCHEME } from '../electron-common/dev-container-workspaces';
|
||||
import { RemotePreferences } from '@theia/remote/lib/electron-common/remote-preferences';
|
||||
|
||||
export namespace RemoteContainerCommands {
|
||||
export const REOPEN_IN_CONTAINER = Command.toLocalizedCommand({
|
||||
id: 'dev-container:reopen-in-container',
|
||||
label: 'Reopen in Container',
|
||||
category: 'Dev Container'
|
||||
}, 'theia/remote/dev-container/connect');
|
||||
}
|
||||
|
||||
const LAST_USED_CONTAINER = 'lastUsedContainer';
|
||||
@injectable()
|
||||
export class ContainerConnectionContribution extends AbstractRemoteRegistryContribution implements WorkspaceOpenHandlerContribution {
|
||||
|
||||
@inject(RemoteContainerConnectionProvider)
|
||||
protected readonly connectionProvider: RemoteContainerConnectionProvider;
|
||||
|
||||
@inject(RemotePreferences)
|
||||
protected readonly remotePreferences: RemotePreferences;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(WorkspaceStorageService)
|
||||
protected readonly workspaceStorageService: WorkspaceStorageService;
|
||||
|
||||
@inject(WorkspaceService)
|
||||
protected readonly workspaceService: WorkspaceService;
|
||||
|
||||
@inject(WorkspaceServer)
|
||||
protected readonly workspaceServer: WorkspaceServer;
|
||||
|
||||
@inject(QuickInputService)
|
||||
protected readonly quickInputService: QuickInputService;
|
||||
|
||||
@inject(ContainerOutputProvider)
|
||||
protected readonly containerOutputProvider: ContainerOutputProvider;
|
||||
|
||||
registerRemoteCommands(registry: RemoteRegistry): void {
|
||||
registry.registerCommand(RemoteContainerCommands.REOPEN_IN_CONTAINER, {
|
||||
execute: () => this.openInContainer()
|
||||
});
|
||||
}
|
||||
|
||||
canHandle(uri: URI): MaybePromise<boolean> {
|
||||
return uri.scheme === DEV_CONTAINER_WORKSPACE_SCHEME;
|
||||
}
|
||||
|
||||
async openWorkspace(uri: URI, options?: WorkspaceInput | undefined): Promise<void> {
|
||||
const filePath = new URLSearchParams(uri.query).get(DEV_CONTAINER_PATH_QUERY);
|
||||
|
||||
if (!filePath) {
|
||||
throw new Error('No devcontainer file specified for workspace');
|
||||
}
|
||||
|
||||
const devcontainerFiles = await this.connectionProvider.getDevContainerFiles(uri.path.toString());
|
||||
const devcontainerFile = devcontainerFiles.find(file => file.path === filePath);
|
||||
|
||||
if (!devcontainerFile) {
|
||||
throw new Error(`Devcontainer file at ${filePath} not found in workspace`);
|
||||
}
|
||||
|
||||
return this.doOpenInContainer(devcontainerFile, uri.path.toString());
|
||||
}
|
||||
|
||||
async getWorkspaceLabel(uri: URI): Promise<string | undefined> {
|
||||
const containerFilePath = new URLSearchParams(uri.query).get(DEV_CONTAINER_PATH_QUERY);
|
||||
if (!containerFilePath) {
|
||||
return;
|
||||
};
|
||||
const files = await this.connectionProvider.getDevContainerFiles(uri.path.toString());
|
||||
const devcontainerFile = files.find(file => file.path === containerFilePath);
|
||||
return `${uri.path.base} [Dev Container: ${devcontainerFile?.name}]`;
|
||||
}
|
||||
|
||||
async openInContainer(): Promise<void> {
|
||||
const devcontainerFile = await this.getOrSelectDevcontainerFile();
|
||||
if (!devcontainerFile) {
|
||||
return;
|
||||
}
|
||||
this.doOpenInContainer(devcontainerFile);
|
||||
}
|
||||
|
||||
async doOpenInContainer(devcontainerFile: DevContainerFile, workspacePath?: string): Promise<void> {
|
||||
const lastContainerInfoKey = `${LAST_USED_CONTAINER}:${devcontainerFile.path}`;
|
||||
const lastContainerInfo = await this.workspaceStorageService.getData<LastContainerInfo | undefined>(lastContainerInfoKey);
|
||||
|
||||
this.containerOutputProvider.openChannel();
|
||||
|
||||
const connectionResult = await this.connectionProvider.connectToContainer({
|
||||
nodeDownloadTemplate: this.remotePreferences['remote.nodeDownloadTemplate'],
|
||||
lastContainerInfo,
|
||||
devcontainerFile: devcontainerFile.path,
|
||||
workspacePath: workspacePath
|
||||
});
|
||||
|
||||
this.workspaceStorageService.setData<LastContainerInfo>(lastContainerInfoKey, {
|
||||
id: connectionResult.containerId,
|
||||
lastUsed: Date.now()
|
||||
});
|
||||
|
||||
this.workspaceServer.setMostRecentlyUsedWorkspace(
|
||||
`${DEV_CONTAINER_WORKSPACE_SCHEME}:${workspacePath ?? this.workspaceService.workspace?.resource.path}?${DEV_CONTAINER_PATH_QUERY}=${devcontainerFile.path}`);
|
||||
|
||||
this.openRemote(connectionResult.port, false, connectionResult.workspacePath);
|
||||
}
|
||||
|
||||
async getOrSelectDevcontainerFile(): Promise<DevContainerFile | undefined> {
|
||||
const workspace = this.workspaceService.workspace;
|
||||
if (!workspace) {
|
||||
return;
|
||||
}
|
||||
const devcontainerFiles = await this.connectionProvider.getDevContainerFiles(workspace.resource.path.toString());
|
||||
|
||||
if (devcontainerFiles.length === 1) {
|
||||
return devcontainerFiles[0];
|
||||
} else if (devcontainerFiles.length === 0) {
|
||||
// eslint-disable-next-line max-len
|
||||
this.messageService.error(nls.localize('theia/remote/dev-container/noDevcontainerFiles', 'No devcontainer.json files found in the workspace. Please ensure you have a .devcontainer directory with a devcontainer.json file.'));
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (await this.quickInputService.pick(devcontainerFiles.map(file => ({
|
||||
type: 'item',
|
||||
label: file.name,
|
||||
description: file.path,
|
||||
file: file,
|
||||
})), {
|
||||
title: nls.localize('theia/remote/dev-container/selectDevcontainer', 'Select a devcontainer.json file')
|
||||
}))?.file;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import { WindowTitleContribution } from '@theia/core/lib/browser/window/window-title-service';
|
||||
import { RemoteStatus, RemoteStatusService } from '@theia/remote/lib/electron-common/remote-status-service';
|
||||
import { FrontendApplicationContribution, LabelProviderContribution } from '@theia/core/lib/browser';
|
||||
import type { ContainerInspectInfo } from 'dockerode';
|
||||
import { RemoteContainerConnectionProvider } from '../electron-common/remote-container-connection-provider';
|
||||
import { PortForwardingService } from '@theia/remote/lib/electron-browser/port-forwarding/port-forwarding-service';
|
||||
import { DEV_CONTAINER_PATH_QUERY } from '../electron-common/dev-container-workspaces';
|
||||
import { URI } from '@theia/core';
|
||||
|
||||
@injectable()
|
||||
export class ContainerInfoContribution implements FrontendApplicationContribution, WindowTitleContribution, LabelProviderContribution {
|
||||
@inject(RemoteContainerConnectionProvider)
|
||||
protected readonly connectionProvider: RemoteContainerConnectionProvider;
|
||||
|
||||
@inject(PortForwardingService)
|
||||
protected readonly portForwardingService: PortForwardingService;
|
||||
|
||||
@inject(RemoteStatusService)
|
||||
protected readonly remoteStatusService: RemoteStatusService;
|
||||
|
||||
protected status: RemoteStatus | undefined;
|
||||
protected containerInfo: ContainerInspectInfo | undefined;
|
||||
protected containerFilePath: string | undefined;
|
||||
|
||||
async onStart(): Promise<void> {
|
||||
const containerPort = parseInt(new URLSearchParams(location.search).get('port') ?? '0');
|
||||
const containerInfo = await this.connectionProvider.getCurrentContainerInfo(containerPort);
|
||||
this.status = await this.remoteStatusService.getStatus(containerPort);
|
||||
|
||||
this.portForwardingService.forwardedPorts.push(...Object.entries(containerInfo?.NetworkSettings.Ports ?? {}).flatMap(([_, ports]) => (
|
||||
ports.map(port => ({
|
||||
editing: false,
|
||||
address: port.HostIp ?? '',
|
||||
localPort: parseInt(port.HostPort ?? '0'),
|
||||
origin: 'container'
|
||||
})))));
|
||||
}
|
||||
|
||||
enhanceTitle(title: string, parts: Map<string, string | undefined>): string {
|
||||
if (this.status && this.status.alive) {
|
||||
const devcontainerName = this.status.name;
|
||||
title = `${title} [Dev Container${devcontainerName ? ': ' + devcontainerName : ''}]`;
|
||||
}
|
||||
return title;
|
||||
}
|
||||
|
||||
canHandle(element: object): number {
|
||||
if ('query' in element) {
|
||||
let containerFilePath = new URLSearchParams((element as URI).query).get(DEV_CONTAINER_PATH_QUERY);
|
||||
if (containerFilePath) {
|
||||
if (containerFilePath.startsWith((element as URI).path.toString())) {
|
||||
containerFilePath = containerFilePath.replace((element as URI).path.toString(), '');
|
||||
}
|
||||
this.containerFilePath = containerFilePath;
|
||||
return 100;
|
||||
};
|
||||
return 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
getName(element: URI): string | undefined {
|
||||
const dir = new URI(this.containerFilePath).path.dir.base;
|
||||
return `${element.path.base} [Dev Container${dir && dir !== '.devcontainer' ? `: ${dir}` : ''}]`;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable, inject } from '@theia/core/shared/inversify';
|
||||
import { OutputChannel, OutputChannelManager } from '@theia/output/lib/browser/output-channel';
|
||||
|
||||
@injectable()
|
||||
export class ContainerOutputProvider implements ContainerOutputProvider {
|
||||
|
||||
@inject(OutputChannelManager)
|
||||
protected readonly outputChannelManager: OutputChannelManager;
|
||||
|
||||
protected currentChannel?: OutputChannel;
|
||||
|
||||
openChannel(): void {
|
||||
this.currentChannel = this.outputChannelManager.getChannel('Container');
|
||||
this.currentChannel.show();
|
||||
};
|
||||
|
||||
onRemoteOutput(output: string): void {
|
||||
this.currentChannel?.appendLine(output);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { RemoteRegistryContribution } from '@theia/remote/lib/electron-browser/remote-registry-contribution';
|
||||
import { RemoteContainerConnectionProvider, RemoteContainerConnectionProviderPath } from '../electron-common/remote-container-connection-provider';
|
||||
import { ContainerConnectionContribution } from './container-connection-contribution';
|
||||
import { ServiceConnectionProvider } from '@theia/core/lib/browser/messaging/service-connection-provider';
|
||||
import { ContainerOutputProvider } from './container-output-provider';
|
||||
import { ContainerInfoContribution } from './container-info-contribution';
|
||||
import { FrontendApplicationContribution, LabelProviderContribution } from '@theia/core/lib/browser';
|
||||
import { WorkspaceOpenHandlerContribution } from '@theia/workspace/lib/browser/workspace-service';
|
||||
import { WindowTitleContribution } from '@theia/core/lib/browser/window/window-title-service';
|
||||
|
||||
export default new ContainerModule(bind => {
|
||||
bind(ContainerConnectionContribution).toSelf().inSingletonScope();
|
||||
bind(RemoteRegistryContribution).toService(ContainerConnectionContribution);
|
||||
bind(WorkspaceOpenHandlerContribution).toService(ContainerConnectionContribution);
|
||||
|
||||
bind(ContainerOutputProvider).toSelf().inSingletonScope();
|
||||
|
||||
bind(RemoteContainerConnectionProvider).toDynamicValue(ctx => {
|
||||
const outputProvider = ctx.container.get(ContainerOutputProvider);
|
||||
return ServiceConnectionProvider.createLocalProxy<RemoteContainerConnectionProvider>(ctx.container, RemoteContainerConnectionProviderPath, outputProvider);
|
||||
}).inSingletonScope();
|
||||
|
||||
bind(ContainerInfoContribution).toSelf().inSingletonScope();
|
||||
bind(FrontendApplicationContribution).toService(ContainerInfoContribution);
|
||||
bind(WindowTitleContribution).toService(ContainerInfoContribution);
|
||||
bind(LabelProviderContribution).toService(ContainerInfoContribution);
|
||||
});
|
||||
@@ -0,0 +1,19 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export interface ContainerOutputProvider {
|
||||
onRemoteOutput(output: string): void;
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
export const DEV_CONTAINER_WORKSPACE_SCHEME = 'devcontainer';
|
||||
export const DEV_CONTAINER_PATH_QUERY = 'containerfile';
|
||||
@@ -0,0 +1,52 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
|
||||
import { RpcServer } from '@theia/core';
|
||||
import { ContainerOutputProvider } from './container-output-provider';
|
||||
import type { ContainerInspectInfo } from 'dockerode';
|
||||
|
||||
// *****************************************************************************
|
||||
export const RemoteContainerConnectionProviderPath = '/remote/container';
|
||||
|
||||
export const RemoteContainerConnectionProvider = Symbol('RemoteContainerConnectionProvider');
|
||||
|
||||
export interface ContainerConnectionOptions {
|
||||
nodeDownloadTemplate?: string;
|
||||
lastContainerInfo?: LastContainerInfo
|
||||
devcontainerFile: string;
|
||||
workspacePath?: string;
|
||||
}
|
||||
|
||||
export interface LastContainerInfo {
|
||||
id: string;
|
||||
lastUsed: number;
|
||||
}
|
||||
|
||||
export interface ContainerConnectionResult {
|
||||
port: string;
|
||||
workspacePath: string;
|
||||
containerId: string;
|
||||
}
|
||||
|
||||
export interface DevContainerFile {
|
||||
name: string;
|
||||
path: string;
|
||||
}
|
||||
|
||||
export interface RemoteContainerConnectionProvider extends RpcServer<ContainerOutputProvider> {
|
||||
connectToContainer(options: ContainerConnectionOptions): Promise<ContainerConnectionResult>;
|
||||
getDevContainerFiles(workspacePath: string): Promise<DevContainerFile[]>;
|
||||
getCurrentContainerInfo(port: number): Promise<ContainerInspectInfo | undefined>;
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContainerModule } from '@theia/core/shared/inversify';
|
||||
import { ConnectionContainerModule } from '@theia/core/lib/node/messaging/connection-container-module';
|
||||
import { DevContainerConnectionProvider } from './remote-container-connection-provider';
|
||||
import { RemoteContainerConnectionProvider, RemoteContainerConnectionProviderPath } from '../electron-common/remote-container-connection-provider';
|
||||
import { ContainerCreationContribution, DockerContainerService } from './docker-container-service';
|
||||
import { bindContributionProvider, ConnectionHandler, RpcConnectionHandler } from '@theia/core';
|
||||
import { registerContainerCreationContributions } from './devcontainer-contributions/main-container-creation-contributions';
|
||||
import { DevContainerFileService } from './dev-container-file-service';
|
||||
import { ContainerOutputProvider } from '../electron-common/container-output-provider';
|
||||
import { ExtensionsContribution, registerTheiaStartOptionsContributions, SettingsContribution } from './devcontainer-contributions/cli-enhancing-creation-contributions';
|
||||
import { RemoteCliContribution } from '@theia/core/lib/node/remote/remote-cli-contribution';
|
||||
import { ProfileFileModificationContribution } from './devcontainer-contributions/profile-file-modification-contribution';
|
||||
import { DevContainerWorkspaceHandler } from './dev-container-workspace-handler';
|
||||
import { WorkspaceHandlerContribution } from '@theia/workspace/lib/node/default-workspace-server';
|
||||
import { registerVariableResolverContributions, VariableResolverContribution } from './devcontainer-contributions/variable-resolver-contribution';
|
||||
import { DockerComposeService } from './docker-compose/compose-service';
|
||||
|
||||
export const remoteConnectionModule = ConnectionContainerModule.create(({ bind, bindBackendService }) => {
|
||||
bindContributionProvider(bind, ContainerCreationContribution);
|
||||
registerContainerCreationContributions(bind);
|
||||
registerTheiaStartOptionsContributions(bind);
|
||||
bindContributionProvider(bind, VariableResolverContribution);
|
||||
registerVariableResolverContributions(bind);
|
||||
bind(ProfileFileModificationContribution).toSelf().inSingletonScope();
|
||||
bind(ContainerCreationContribution).toService(ProfileFileModificationContribution);
|
||||
|
||||
bind(DevContainerConnectionProvider).toSelf().inSingletonScope();
|
||||
bind(RemoteContainerConnectionProvider).toService(DevContainerConnectionProvider);
|
||||
bind(ConnectionHandler).toDynamicValue(ctx =>
|
||||
new RpcConnectionHandler<ContainerOutputProvider>(RemoteContainerConnectionProviderPath, client => {
|
||||
const server = ctx.container.get<RemoteContainerConnectionProvider>(RemoteContainerConnectionProvider);
|
||||
server.setClient(client);
|
||||
client.onDidCloseConnection(() => server.dispose());
|
||||
return server;
|
||||
}));
|
||||
});
|
||||
|
||||
export default new ContainerModule((bind, unbind, isBound, rebind) => {
|
||||
bind(DockerContainerService).toSelf().inSingletonScope();
|
||||
bind(ConnectionContainerModule).toConstantValue(remoteConnectionModule);
|
||||
|
||||
bind(DockerComposeService).toSelf().inSingletonScope();
|
||||
|
||||
bind(DevContainerFileService).toSelf().inSingletonScope();
|
||||
|
||||
bind(ExtensionsContribution).toSelf().inSingletonScope();
|
||||
bind(SettingsContribution).toSelf().inSingletonScope();
|
||||
bind(RemoteCliContribution).toService(ExtensionsContribution);
|
||||
bind(RemoteCliContribution).toService(SettingsContribution);
|
||||
|
||||
bind(DevContainerWorkspaceHandler).toSelf().inSingletonScope();
|
||||
bind(WorkspaceHandlerContribution).toService(DevContainerWorkspaceHandler);
|
||||
});
|
||||
@@ -0,0 +1,102 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceServer } from '@theia/workspace/lib/common';
|
||||
import { DevContainerFile } from '../electron-common/remote-container-connection-provider';
|
||||
import { DevContainerConfiguration } from './devcontainer-file';
|
||||
import { parse } from 'jsonc-parser';
|
||||
import * as fs from '@theia/core/shared/fs-extra';
|
||||
import { ContributionProvider, Path, URI } from '@theia/core';
|
||||
import { VariableResolverContribution } from './devcontainer-contributions/variable-resolver-contribution';
|
||||
|
||||
const VARIABLE_REGEX = /^\$\{(.+?)(?::(.+))?\}$/;
|
||||
|
||||
@injectable()
|
||||
export class DevContainerFileService {
|
||||
|
||||
@inject(WorkspaceServer)
|
||||
protected readonly workspaceServer: WorkspaceServer;
|
||||
|
||||
@inject(ContributionProvider) @named(VariableResolverContribution)
|
||||
protected readonly variableResolverContributions: ContributionProvider<VariableResolverContribution>;
|
||||
|
||||
protected resolveVariable(value: string): string {
|
||||
const match = value.match(VARIABLE_REGEX);
|
||||
if (match) {
|
||||
const [, type, variable] = match;
|
||||
for (const contribution of this.variableResolverContributions.getContributions()) {
|
||||
if (contribution.canResolve(type)) {
|
||||
return contribution.resolve(variable ?? type);
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
protected resolveVariablesRecursively<T>(obj: T): T {
|
||||
if (typeof obj === 'string') {
|
||||
return this.resolveVariable(obj) as T;
|
||||
} else if (Array.isArray(obj)) {
|
||||
return obj.map(item => this.resolveVariablesRecursively(item)) as T;
|
||||
} else if (obj && typeof obj === 'object') {
|
||||
const newObj: Record<string, unknown> = {};
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
newObj[key] = this.resolveVariablesRecursively(value);
|
||||
}
|
||||
return newObj as T;
|
||||
}
|
||||
return obj;
|
||||
}
|
||||
|
||||
async getConfiguration(path: string): Promise<DevContainerConfiguration> {
|
||||
let configuration: DevContainerConfiguration = parse(await fs.readFile(path, 'utf-8').catch(() => '0')) as DevContainerConfiguration;
|
||||
if (!configuration) {
|
||||
throw new Error(`devcontainer file ${path} could not be parsed`);
|
||||
}
|
||||
|
||||
configuration = this.resolveVariablesRecursively(configuration);
|
||||
configuration.location = path;
|
||||
return configuration;
|
||||
}
|
||||
|
||||
async getAvailableFiles(workspace: string): Promise<DevContainerFile[]> {
|
||||
const devcontainerPath = new URI(workspace).path.join('.devcontainer').fsPath();
|
||||
|
||||
return (await this.searchForDevontainerJsonFiles(devcontainerPath, 1)).map(file => ({
|
||||
name: parse(fs.readFileSync(file, 'utf-8')).name ?? 'devcontainer',
|
||||
path: file
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
protected async searchForDevontainerJsonFiles(directory: string, depth: number): Promise<string[]> {
|
||||
if (depth < 0 || !await fs.pathExists(directory)) {
|
||||
return [];
|
||||
}
|
||||
const filesPaths = (await fs.readdir(directory)).map(file => new Path(directory).join(file).fsPath());
|
||||
|
||||
const devcontainerFiles = [];
|
||||
for (const file of filesPaths) {
|
||||
if (file.endsWith('devcontainer.json')) {
|
||||
devcontainerFiles.push(file);
|
||||
} else if ((await fs.stat(file)).isDirectory()) {
|
||||
devcontainerFiles.push(...await this.searchForDevontainerJsonFiles(file, depth - 1));
|
||||
}
|
||||
}
|
||||
return devcontainerFiles;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import { URI } from '@theia/core';
|
||||
import * as fs from '@theia/core/shared/fs-extra';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceHandlerContribution } from '@theia/workspace/lib/node/default-workspace-server';
|
||||
import { DEV_CONTAINER_PATH_QUERY, DEV_CONTAINER_WORKSPACE_SCHEME } from '../electron-common/dev-container-workspaces';
|
||||
|
||||
@injectable()
|
||||
export class DevContainerWorkspaceHandler implements WorkspaceHandlerContribution {
|
||||
|
||||
canHandle(uri: URI): boolean {
|
||||
return uri.scheme === DEV_CONTAINER_WORKSPACE_SCHEME;
|
||||
}
|
||||
|
||||
async workspaceStillExists(uri: URI): Promise<boolean> {
|
||||
const devcontainerFile = new URLSearchParams(uri.query).get(DEV_CONTAINER_PATH_QUERY);
|
||||
return await fs.pathExists(uri.path.fsPath()) && !!devcontainerFile && fs.pathExists(devcontainerFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { RemoteCliContext, RemoteCliContribution } from '@theia/core/lib/node/remote/remote-cli-contribution';
|
||||
import { ContainerCreationContribution } from '../docker-container-service';
|
||||
import * as Docker from 'dockerode';
|
||||
import { DevContainerConfiguration, } from '../devcontainer-file';
|
||||
import { injectable, interfaces } from '@theia/core/shared/inversify';
|
||||
|
||||
export function registerTheiaStartOptionsContributions(bind: interfaces.Bind): void {
|
||||
bind(ContainerCreationContribution).toService(ExtensionsContribution);
|
||||
bind(ContainerCreationContribution).toService(SettingsContribution);
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ExtensionsContribution implements RemoteCliContribution, ContainerCreationContribution {
|
||||
protected currentConfig: DevContainerConfiguration | undefined;
|
||||
|
||||
enhanceArgs(context: RemoteCliContext): string[] {
|
||||
if (!this.currentConfig) {
|
||||
return [];
|
||||
}
|
||||
const extensions = [
|
||||
...(this.currentConfig.extensions ?? []),
|
||||
...(this.currentConfig.customizations?.vscode?.extensions ?? [])
|
||||
];
|
||||
this.currentConfig = undefined;
|
||||
return extensions?.map(extension => `--install-plugin=${extension}`);
|
||||
}
|
||||
|
||||
async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration): Promise<void> {
|
||||
this.currentConfig = containerConfig;
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class SettingsContribution implements RemoteCliContribution, ContainerCreationContribution {
|
||||
protected currentConfig: DevContainerConfiguration | undefined;
|
||||
|
||||
enhanceArgs(context: RemoteCliContext): string[] {
|
||||
if (!this.currentConfig) {
|
||||
return [];
|
||||
}
|
||||
const settings = {
|
||||
...(this.currentConfig.settings ?? {}),
|
||||
...(this.currentConfig.customizations?.vscode?.settings ?? [])
|
||||
};
|
||||
this.currentConfig = undefined;
|
||||
return Object.entries(settings).map(([key, value]) => `--set-preference=${key}=${JSON.stringify(value)}`) ?? [];
|
||||
}
|
||||
|
||||
async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration): Promise<void> {
|
||||
this.currentConfig = containerConfig;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,211 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
import * as Docker from 'dockerode';
|
||||
import { inject, injectable, interfaces } from '@theia/core/shared/inversify';
|
||||
import { ContainerCreationContribution } from '../docker-container-service';
|
||||
import { DevContainerConfiguration, DockerfileContainer, ImageContainer, NonComposeContainerBase } from '../devcontainer-file';
|
||||
import { Path } from '@theia/core';
|
||||
import { ContainerOutputProvider } from '../../electron-common/container-output-provider';
|
||||
import * as fs from '@theia/core/shared/fs-extra';
|
||||
import { RemotePortForwardingProvider } from '@theia/remote/lib/electron-common/remote-port-forwarding-provider';
|
||||
import { RemoteDockerContainerConnection } from '../remote-container-connection-provider';
|
||||
|
||||
export function registerContainerCreationContributions(bind: interfaces.Bind): void {
|
||||
bind(ContainerCreationContribution).to(ImageFileContribution).inSingletonScope();
|
||||
bind(ContainerCreationContribution).to(DockerFileContribution).inSingletonScope();
|
||||
bind(ContainerCreationContribution).to(ForwardPortsContribution).inSingletonScope();
|
||||
bind(ContainerCreationContribution).to(MountsContribution).inSingletonScope();
|
||||
bind(ContainerCreationContribution).to(RemoteUserContribution).inSingletonScope();
|
||||
bind(ContainerCreationContribution).to(PostCreateCommandContribution).inSingletonScope();
|
||||
bind(ContainerCreationContribution).to(ContainerEnvContribution).inSingletonScope();
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ImageFileContribution implements ContainerCreationContribution {
|
||||
async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: ImageContainer,
|
||||
api: Docker, outputprovider: ContainerOutputProvider): Promise<void> {
|
||||
if (containerConfig.image) {
|
||||
const platform = process.platform;
|
||||
const arch = process.arch;
|
||||
const options = platform === 'darwin' && arch === 'arm64' ? { platform: 'amd64' } : {};
|
||||
await new Promise<void>((res, rej) => api.pull(containerConfig.image, options, (err, stream) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else if (stream === undefined) {
|
||||
rej('Stream is undefined');
|
||||
} else {
|
||||
api.modem.followProgress(stream!, (error, output) => error ?
|
||||
rej(error) :
|
||||
res(), progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress)));
|
||||
}
|
||||
}));
|
||||
createOptions.Image = containerConfig.image;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DockerFileContribution implements ContainerCreationContribution {
|
||||
async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DockerfileContainer,
|
||||
api: Docker, outputprovider: ContainerOutputProvider): Promise<void> {
|
||||
// check if dockerfile container
|
||||
if (containerConfig.dockerFile || containerConfig.build?.dockerfile) {
|
||||
const dockerfile = (containerConfig.dockerFile ?? containerConfig.build?.dockerfile) as string;
|
||||
const context = containerConfig.context ?? new Path(containerConfig.location as string).dir.fsPath();
|
||||
try {
|
||||
// ensure dockerfile exists
|
||||
await fs.lstat(new Path(context as string).join(dockerfile).fsPath());
|
||||
|
||||
const buildStream = await api.buildImage({
|
||||
context,
|
||||
src: [dockerfile],
|
||||
} as Docker.ImageBuildContext, {
|
||||
buildargs: containerConfig.build?.args
|
||||
});
|
||||
// TODO probably have some console windows showing the output of the build
|
||||
const imageId = await new Promise<string>((res, rej) => api.modem.followProgress(buildStream!, (err, outputs) => {
|
||||
if (err) {
|
||||
rej(err);
|
||||
} else {
|
||||
for (let i = outputs.length - 1; i >= 0; i--) {
|
||||
if (outputs[i].aux?.ID) {
|
||||
res(outputs[i].aux.ID);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}, progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress))));
|
||||
createOptions.Image = imageId;
|
||||
} catch (error) {
|
||||
outputprovider.onRemoteOutput(`could not build dockerfile "${dockerfile}" reason: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ForwardPortsContribution implements ContainerCreationContribution {
|
||||
|
||||
@inject(RemotePortForwardingProvider)
|
||||
protected readonly portForwardingProvider: RemotePortForwardingProvider;
|
||||
|
||||
async handlePostConnect(containerConfig: DevContainerConfiguration, connection: RemoteDockerContainerConnection): Promise<void> {
|
||||
if (!containerConfig.forwardPorts) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const forward of containerConfig.forwardPorts) {
|
||||
let port: number;
|
||||
let address: string | undefined;
|
||||
if (typeof forward === 'string') {
|
||||
const parts = forward.split(':');
|
||||
address = parts[0];
|
||||
port = parseInt(parts[1]);
|
||||
} else {
|
||||
port = forward;
|
||||
}
|
||||
|
||||
this.portForwardingProvider.forwardPort(connection.localPort, { port, address });
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class MountsContribution implements ContainerCreationContribution {
|
||||
async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration, api: Docker): Promise<void> {
|
||||
if (!containerConfig.mounts) {
|
||||
return;
|
||||
}
|
||||
|
||||
createOptions.HostConfig!.Mounts!.push(...(containerConfig as NonComposeContainerBase)?.mounts
|
||||
?.map(mount => typeof mount === 'string' ?
|
||||
this.parseMountString(mount) :
|
||||
{ Source: mount.source, Target: mount.target, Type: mount.type ?? 'bind' }) ?? []);
|
||||
}
|
||||
|
||||
parseMountString(mount: string): Docker.MountSettings {
|
||||
const parts = mount.split(',');
|
||||
return {
|
||||
Source: parts.find(part => part.startsWith('source=') || part.startsWith('src='))?.split('=')[1]!,
|
||||
Target: parts.find(part => part.startsWith('target=') || part.startsWith('dst='))?.split('=')[1]!,
|
||||
Type: (parts.find(part => part.startsWith('type='))?.split('=')[1] ?? 'bind') as Docker.MountType
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class RemoteUserContribution implements ContainerCreationContribution {
|
||||
async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration, api: Docker): Promise<void> {
|
||||
if (containerConfig.remoteUser) {
|
||||
createOptions.User = containerConfig.remoteUser;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class PostCreateCommandContribution implements ContainerCreationContribution {
|
||||
async handlePostCreate?(containerConfig: DevContainerConfiguration, container: Docker.Container, api: Docker, outputprovider: ContainerOutputProvider): Promise<void> {
|
||||
if (containerConfig.postCreateCommand) {
|
||||
const commands = typeof containerConfig.postCreateCommand === 'object' && !(containerConfig.postCreateCommand instanceof Array) ?
|
||||
Object.values(containerConfig.postCreateCommand) : [containerConfig.postCreateCommand];
|
||||
for (const command of commands) {
|
||||
try {
|
||||
let exec;
|
||||
if (command instanceof Array) {
|
||||
exec = await container.exec({ Cmd: command, AttachStderr: true, AttachStdout: true });
|
||||
|
||||
} else {
|
||||
exec = await container.exec({ Cmd: ['sh', '-c', command], AttachStderr: true, AttachStdout: true });
|
||||
}
|
||||
const stream = await exec.start({ Tty: true });
|
||||
stream.on('data', chunk => outputprovider.onRemoteOutput(chunk.toString()));
|
||||
} catch (error) {
|
||||
outputprovider.onRemoteOutput('could not execute postCreateCommand ' + JSON.stringify(command) + ' reason:' + error.message);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ContainerEnvContribution implements ContainerCreationContribution {
|
||||
async handleContainerCreation(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration): Promise<void> {
|
||||
if (containerConfig.containerEnv) {
|
||||
if (createOptions.Env === undefined) {
|
||||
createOptions.Env = [];
|
||||
}
|
||||
for (const [key, value] of Object.entries(containerConfig.containerEnv)) {
|
||||
createOptions.Env.push(`${key}=${value}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export namespace OutputHelper {
|
||||
export interface Progress {
|
||||
id?: string;
|
||||
stream: string;
|
||||
status?: string;
|
||||
progress?: string;
|
||||
}
|
||||
|
||||
export function parseProgress(progress: Progress): string {
|
||||
return progress.stream ?? progress.progress ?? progress.status ?? '';
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { DevContainerConfiguration } from '../devcontainer-file';
|
||||
import { ContainerCreationContribution } from '../docker-container-service';
|
||||
import * as Docker from 'dockerode';
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import { ContainerOutputProvider } from '../../electron-common/container-output-provider';
|
||||
|
||||
/**
|
||||
* this contribution changes the /etc/profile file so that it won't overwrite the PATH variable set by docker
|
||||
*/
|
||||
@injectable()
|
||||
export class ProfileFileModificationContribution implements ContainerCreationContribution {
|
||||
async handlePostCreate(containerConfig: DevContainerConfiguration, container: Docker.Container, api: Docker, outputprovider: ContainerOutputProvider): Promise<void> {
|
||||
const stream = await (await container.exec({
|
||||
Cmd: ['sh', '-c', 'sed -i \'s|PATH="\\([^"]*\\)"|PATH=${PATH:-"\\1"}|g\' /etc/profile'], User: 'root',
|
||||
AttachStderr: true, AttachStdout: true
|
||||
})).start({});
|
||||
stream.on('data', data => outputprovider.onRemoteOutput(data.toString()));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { inject, injectable, interfaces, LazyServiceIdentifier } from '@theia/core/shared/inversify';
|
||||
import { DockerContainerService } from '../docker-container-service';
|
||||
|
||||
export const VariableResolverContribution = Symbol('VariableResolverContribution');
|
||||
export interface VariableResolverContribution {
|
||||
canResolve(variable: string): boolean;
|
||||
resolve(variable: string): string;
|
||||
}
|
||||
|
||||
export function registerVariableResolverContributions(bind: interfaces.Bind): void {
|
||||
bind(VariableResolverContribution).to(LocalEnvVariableResolver).inSingletonScope();
|
||||
bind(VariableResolverContribution).to(ContainerIdResolver).inSingletonScope();
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class LocalEnvVariableResolver implements VariableResolverContribution {
|
||||
canResolve(type: string): boolean {
|
||||
console.log(`Resolving localEnv variable: ${type}`);
|
||||
return type === 'localEnv';
|
||||
}
|
||||
|
||||
resolve(variable: string): string {
|
||||
return process.env[variable] || '';
|
||||
}
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class ContainerIdResolver implements VariableResolverContribution {
|
||||
@inject(new LazyServiceIdentifier(() => DockerContainerService))
|
||||
protected readonly dockerContainerService: DockerContainerService;
|
||||
|
||||
canResolve(type: string): boolean {
|
||||
return type === 'devcontainerId' && !!this.dockerContainerService.container;
|
||||
}
|
||||
resolve(variable: string): string {
|
||||
return this.dockerContainerService.container?.id || variable;
|
||||
}
|
||||
}
|
||||
415
packages/dev-container/src/electron-node/devcontainer-file.ts
Normal file
415
packages/dev-container/src/electron-node/devcontainer-file.ts
Normal file
@@ -0,0 +1,415 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/**
|
||||
* Defines a dev container
|
||||
* type generated from https://containers.dev/implementors/json_schema/ and modified
|
||||
*/
|
||||
export type DevContainerConfiguration = (((DockerfileContainer | ImageContainer) & (NonComposeContainerBase)) | ComposeContainer) & DevContainerCommon & { location?: string };
|
||||
|
||||
export type DockerfileContainer = {
|
||||
/**
|
||||
* Docker build-related options.
|
||||
*/
|
||||
build: {
|
||||
/**
|
||||
* The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file.
|
||||
*/
|
||||
dockerfile: string
|
||||
/**
|
||||
* The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file.
|
||||
*/
|
||||
context?: string
|
||||
} & BuildOptions
|
||||
[k: string]: unknown
|
||||
} | {
|
||||
/**
|
||||
* The location of the Dockerfile that defines the contents of the container. The path is relative to the folder containing the `devcontainer.json` file.
|
||||
*/
|
||||
dockerFile: string
|
||||
/**
|
||||
* The location of the context folder for building the Docker image. The path is relative to the folder containing the `devcontainer.json` file.
|
||||
*/
|
||||
context?: string
|
||||
|
||||
/**
|
||||
* Docker build-related options.
|
||||
*/
|
||||
build?: {
|
||||
/**
|
||||
* Target stage in a multi-stage build.
|
||||
*/
|
||||
target?: string
|
||||
/**
|
||||
* Build arguments.
|
||||
*/
|
||||
args?: {
|
||||
[k: string]: string
|
||||
}
|
||||
/**
|
||||
* The image to consider as a cache. Use an array to specify multiple images.
|
||||
*/
|
||||
cacheFrom?: string | string[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
[k: string]: unknown
|
||||
};
|
||||
|
||||
export interface BuildOptions {
|
||||
/**
|
||||
* Target stage in a multi-stage build.
|
||||
*/
|
||||
target?: string
|
||||
/**
|
||||
* Build arguments.
|
||||
*/
|
||||
args?: {
|
||||
[k: string]: string
|
||||
}
|
||||
/**
|
||||
* The image to consider as a cache. Use an array to specify multiple images.
|
||||
*/
|
||||
cacheFrom?: string | string[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
export interface ImageContainer {
|
||||
/**
|
||||
* The docker image that will be used to create the container.
|
||||
*/
|
||||
image: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface NonComposeContainerBase {
|
||||
/**
|
||||
* Application ports that are exposed by the container. This can be a single port or an array of ports. Each port can be a number or a string. A number is mapped to
|
||||
* the same port on the host. A string is passed to Docker unchanged and can be used to map ports differently, e.g. '8000:8010'.
|
||||
*/
|
||||
appPort?: number | string | (number | string)[]
|
||||
/**
|
||||
* Container environment variables.
|
||||
*/
|
||||
containerEnv?: {
|
||||
[k: string]: string
|
||||
}
|
||||
/**
|
||||
* The user the container will be started with. The default is the user on the Docker image.
|
||||
*/
|
||||
containerUser?: string
|
||||
/**
|
||||
* Mount points to set up when creating the container. See Docker's documentation for the --mount option for the supported syntax.
|
||||
*/
|
||||
mounts?: (string | MountConfig)[]
|
||||
/**
|
||||
* The arguments required when starting in the container.
|
||||
*/
|
||||
runArgs?: string[]
|
||||
/**
|
||||
* Action to take when the user disconnects from the container in their editor. The default is to stop the container.
|
||||
*/
|
||||
shutdownAction?: 'none' | 'stopContainer'
|
||||
/**
|
||||
* Whether to overwrite the command specified in the image. The default is true.
|
||||
*/
|
||||
overrideCommand?: boolean
|
||||
/**
|
||||
* The path of the workspace folder inside the container.
|
||||
*/
|
||||
workspaceFolder?: string
|
||||
/**
|
||||
* The --mount parameter for docker run. The default is to mount the project folder at /workspaces/$project.
|
||||
*/
|
||||
workspaceMount?: string
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface ComposeContainer {
|
||||
/**
|
||||
* The name of the docker-compose file(s) used to start the services.
|
||||
*/
|
||||
dockerComposeFile: string | string[]
|
||||
/**
|
||||
* The service you want to work on. This is considered the primary container for your dev environment which your editor will connect to.
|
||||
*/
|
||||
service: string
|
||||
/**
|
||||
* An array of services that should be started and stopped.
|
||||
*/
|
||||
runServices?: string[]
|
||||
/**
|
||||
* The path of the workspace folder inside the container. This is typically the target path of a volume mount in the docker-compose.yml.
|
||||
*/
|
||||
workspaceFolder: string
|
||||
/**
|
||||
* Action to take when the user disconnects from the primary container in their editor. The default is to stop all of the compose containers.
|
||||
*/
|
||||
shutdownAction?: 'none' | 'stopCompose'
|
||||
/**
|
||||
* Whether to overwrite the command specified in the image. The default is false.
|
||||
*/
|
||||
overrideCommand?: boolean
|
||||
/**
|
||||
* Allows passing additional arguments to the 'docker compose up' command.
|
||||
*/
|
||||
composeUpArgs?: string[]
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface DevContainerCommon {
|
||||
/**
|
||||
* A name for the dev container which can be displayed to the user.
|
||||
*/
|
||||
name?: string
|
||||
/**
|
||||
* Features to add to the dev container.
|
||||
*/
|
||||
features?: {
|
||||
[k: string]: unknown
|
||||
}
|
||||
/**
|
||||
* Array consisting of the Feature id (without the semantic version) of Features in the order the user wants them to be installed.
|
||||
*/
|
||||
overrideFeatureInstallOrder?: string[]
|
||||
/**
|
||||
* Ports that are forwarded from the container to the local machine. Can be an integer port number, or a string of the format 'host:port_number'.
|
||||
*/
|
||||
forwardPorts?: (number | string)[]
|
||||
portsAttributes?: {
|
||||
/**
|
||||
* A port, range of ports (ex. '40000-55000'), or regular expression (ex. '.+\\/server.js').
|
||||
* For a port number or range, the attributes will apply to that port number or range of port numbers.
|
||||
* Attributes which use a regular expression will apply to ports whose associated process command line matches the expression.
|
||||
*
|
||||
* This interface was referenced by `undefined`'s JSON-Schema definition
|
||||
* via the `patternProperty` '(^\d+(-\d+)?$)|(.+)'.
|
||||
*/
|
||||
[k: string]: {
|
||||
/**
|
||||
* Defines the action that occurs when the port is discovered for automatic forwarding
|
||||
*/
|
||||
onAutoForward?:
|
||||
| 'notify'
|
||||
| 'openBrowser'
|
||||
| 'openBrowserOnce'
|
||||
| 'openPreview'
|
||||
| 'silent'
|
||||
| 'ignore'
|
||||
/**
|
||||
* Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.
|
||||
*/
|
||||
elevateIfNeeded?: boolean
|
||||
/**
|
||||
* Label that will be shown in the UI for this port.
|
||||
*/
|
||||
label?: string
|
||||
requireLocalPort?: boolean
|
||||
/**
|
||||
* The protocol to use when forwarding this port.
|
||||
*/
|
||||
protocol?: 'http' | 'https'
|
||||
[k: string]: unknown
|
||||
}
|
||||
}
|
||||
otherPortsAttributes?: {
|
||||
/**
|
||||
* Defines the action that occurs when the port is discovered for automatic forwarding
|
||||
*/
|
||||
onAutoForward?:
|
||||
| 'notify'
|
||||
| 'openBrowser'
|
||||
| 'openPreview'
|
||||
| 'silent'
|
||||
| 'ignore'
|
||||
/**
|
||||
* Automatically prompt for elevation (if needed) when this port is forwarded. Elevate is required if the local port is a privileged port.
|
||||
*/
|
||||
elevateIfNeeded?: boolean
|
||||
/**
|
||||
* Label that will be shown in the UI for this port.
|
||||
*/
|
||||
label?: string
|
||||
requireLocalPort?: boolean
|
||||
/**
|
||||
* The protocol to use when forwarding this port.
|
||||
*/
|
||||
protocol?: 'http' | 'https'
|
||||
}
|
||||
/**
|
||||
* Controls whether on Linux the container's user should be updated with the local user's UID and GID. On by default when opening from a local folder.
|
||||
*/
|
||||
updateRemoteUserUID?: boolean
|
||||
/**
|
||||
* Remote environment variables to set for processes spawned in the container including lifecycle scripts and any remote editor/IDE server process.
|
||||
*/
|
||||
remoteEnv?: {
|
||||
[k: string]: string | null
|
||||
}
|
||||
/**
|
||||
* The username to use for spawning processes in the container including lifecycle scripts and any remote editor/IDE server process.
|
||||
* The default is the same user as the container.
|
||||
*/
|
||||
remoteUser?: string
|
||||
|
||||
/**
|
||||
* extensions to install in the container at launch. The expeceted format is publisher.name[@version].
|
||||
* The default is no extensions being installed.
|
||||
*/
|
||||
extensions?: string[]
|
||||
|
||||
/**
|
||||
* settings to set in the container at launch in the settings.json. The expected format is key=value.
|
||||
* The default is no preferences being set.
|
||||
*/
|
||||
settings?: {
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
/**
|
||||
* A command to run locally before anything else. This command is run before 'onCreateCommand'.
|
||||
* If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.
|
||||
*/
|
||||
initializeCommand?: string | string[]
|
||||
/**
|
||||
* A command to run when creating the container. This command is run after 'initializeCommand' and before 'updateContentCommand'.
|
||||
* If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.
|
||||
*/
|
||||
onCreateCommand?:
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
[k: string]: string | string[]
|
||||
}
|
||||
/**
|
||||
* A command to run when creating the container and rerun when the workspace content was updated while creating the container.
|
||||
* This command is run after 'onCreateCommand' and before 'postCreateCommand'. If this is a single string, it will be run in a shell.
|
||||
* If this is an array of strings, it will be run as a single command without shell.
|
||||
*/
|
||||
updateContentCommand?:
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
[k: string]: string | string[]
|
||||
}
|
||||
/**
|
||||
* A command to run after creating the container. This command is run after 'updateContentCommand' and before 'postStartCommand'.
|
||||
* If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.
|
||||
*/
|
||||
postCreateCommand?:
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
[k: string]: string | string[]
|
||||
}
|
||||
/**
|
||||
* A command to run after starting the container. This command is run after 'postCreateCommand' and before 'postAttachCommand'.
|
||||
* If this is a single string, it will be run in a shell. If this is an array of strings, it will be run as a single command without shell.
|
||||
*/
|
||||
postStartCommand?:
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
[k: string]: string | string[]
|
||||
}
|
||||
/**
|
||||
* A command to run when attaching to the container. This command is run after 'postStartCommand'. If this is a single string, it will be run in a shell.
|
||||
* If this is an array of strings, it will be run as a single command without shell.
|
||||
*/
|
||||
postAttachCommand?:
|
||||
| string
|
||||
| string[]
|
||||
| {
|
||||
[k: string]: string | string[]
|
||||
}
|
||||
/**
|
||||
* The user command to wait for before continuing execution in the background while the UI is starting up. The default is 'updateContentCommand'.
|
||||
*/
|
||||
waitFor?:
|
||||
| 'initializeCommand'
|
||||
| 'onCreateCommand'
|
||||
| 'updateContentCommand'
|
||||
| 'postCreateCommand'
|
||||
| 'postStartCommand'
|
||||
/**
|
||||
* User environment probe to run. The default is 'loginInteractiveShell'.
|
||||
*/
|
||||
userEnvProbe?:
|
||||
| 'none'
|
||||
| 'loginShell'
|
||||
| 'loginInteractiveShell'
|
||||
| 'interactiveShell'
|
||||
/**
|
||||
* Host hardware requirements.
|
||||
*/
|
||||
hostRequirements?: {
|
||||
/**
|
||||
* Number of required CPUs.
|
||||
*/
|
||||
cpus?: number
|
||||
/**
|
||||
* Amount of required RAM in bytes. Supports units tb, gb, mb and kb.
|
||||
*/
|
||||
memory?: string
|
||||
/**
|
||||
* Amount of required disk space in bytes. Supports units tb, gb, mb and kb.
|
||||
*/
|
||||
storage?: string
|
||||
gpu?:
|
||||
| (true | false | 'optional')
|
||||
| {
|
||||
/**
|
||||
* Number of required cores.
|
||||
*/
|
||||
cores?: number
|
||||
/**
|
||||
* Amount of required RAM in bytes. Supports units tb, gb, mb and kb.
|
||||
*/
|
||||
memory?: string
|
||||
}
|
||||
[k: string]: unknown
|
||||
}
|
||||
/**
|
||||
* Tool-specific configuration. Each tool should use a JSON object subproperty with a unique name to group its customizations.
|
||||
*/
|
||||
customizations?: {
|
||||
[k: string]: unknown,
|
||||
vscode?: {
|
||||
/**
|
||||
* extensions to install in the container at launch. The expeceted format is publisher.name[@version].
|
||||
* The default is no extensions being installed.
|
||||
*/
|
||||
extensions?: string[],
|
||||
|
||||
/**
|
||||
* settings to set in the container at launch in the settings.json. The expected format is key=value.
|
||||
* The default is no preferences being set.
|
||||
*/
|
||||
settings?: {
|
||||
[k: string]: unknown
|
||||
}
|
||||
[k: string]: unknown
|
||||
}
|
||||
}
|
||||
additionalProperties?: {
|
||||
[k: string]: unknown
|
||||
}
|
||||
[k: string]: unknown
|
||||
}
|
||||
|
||||
export interface MountConfig {
|
||||
source: string,
|
||||
target: string,
|
||||
type: 'volume' | 'bind',
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2025 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { injectable } from '@theia/core/shared/inversify';
|
||||
import * as Docker from 'dockerode';
|
||||
import { ComposeContainer, DevContainerConfiguration } from '../devcontainer-file';
|
||||
import { ContainerOutputProvider } from '../../electron-common/container-output-provider';
|
||||
import { spawn } from 'child_process';
|
||||
import path = require('path');
|
||||
|
||||
@injectable()
|
||||
export class DockerComposeService {
|
||||
|
||||
async createContainers(
|
||||
devcontainerConfig: DevContainerConfiguration,
|
||||
containerCreateOptions: Docker.ContainerCreateOptions,
|
||||
outputProvider?: ContainerOutputProvider): Promise<string> {
|
||||
|
||||
if (!devcontainerConfig.dockerComposeFile || typeof devcontainerConfig.dockerComposeFile !== 'string') {
|
||||
throw new Error('dockerComposeFile is not defined in devcontainer configuration. Multiple files are not supported currently');
|
||||
}
|
||||
|
||||
const dockerComposeFilePath = resolveComposeFilePath(devcontainerConfig);
|
||||
|
||||
const composeUpArgs = Array.isArray(devcontainerConfig.composeUpArgs) ? devcontainerConfig.composeUpArgs : [];
|
||||
await this.executeComposeCommand(dockerComposeFilePath, 'up', ['--detach', ...composeUpArgs], outputProvider);
|
||||
|
||||
return (devcontainerConfig as ComposeContainer).service;
|
||||
}
|
||||
|
||||
protected executeComposeCommand(composeFilePath: string, command: string, args: string[], outputProvider?: ContainerOutputProvider): Promise<string> {
|
||||
return new Promise<string>((resolve, reject) => {
|
||||
const process = spawn('docker', ['compose', '-f', composeFilePath, command, ...args]);
|
||||
process.stdout.on('data', data => {
|
||||
outputProvider?.onRemoteOutput(data.toString());
|
||||
});
|
||||
process.stderr.on('data', data => {
|
||||
outputProvider?.onRemoteOutput(data.toString());
|
||||
});
|
||||
process.on('close', code => {
|
||||
outputProvider?.onRemoteOutput(`docker compose process exited with code ${code}`);
|
||||
if (code === 0) {
|
||||
resolve(''); // TODO return real container ids
|
||||
} else {
|
||||
reject(new Error(`docker compose process exited with code ${code}`));
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function resolveComposeFilePath(devcontainerConfig: DevContainerConfiguration): string {
|
||||
if (!devcontainerConfig.dockerComposeFile) {
|
||||
throw new Error('dockerComposeFile is not defined in devcontainer configuration.');
|
||||
}
|
||||
|
||||
if (typeof devcontainerConfig.dockerComposeFile !== 'string') {
|
||||
throw new Error('Multiple docker compose files are not supported currently.');
|
||||
}
|
||||
|
||||
if (path.isAbsolute(devcontainerConfig.dockerComposeFile)) {
|
||||
return devcontainerConfig.dockerComposeFile;
|
||||
} else {
|
||||
return path.resolve(path.dirname(devcontainerConfig.location!), devcontainerConfig.dockerComposeFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import { ContributionProvider, MaybePromise, URI } from '@theia/core';
|
||||
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
||||
import { WorkspaceServer } from '@theia/workspace/lib/common';
|
||||
import * as fs from '@theia/core/shared/fs-extra';
|
||||
import * as Docker from 'dockerode';
|
||||
import { ContainerConnectionOptions } from '../electron-common/remote-container-connection-provider';
|
||||
import { DevContainerConfiguration } from './devcontainer-file';
|
||||
import { DevContainerFileService } from './dev-container-file-service';
|
||||
import { ContainerOutputProvider } from '../electron-common/container-output-provider';
|
||||
import { RemoteDockerContainerConnection } from './remote-container-connection-provider';
|
||||
import { DockerComposeService } from './docker-compose/compose-service';
|
||||
|
||||
export const ContainerCreationContribution = Symbol('ContainerCreationContributions');
|
||||
|
||||
export interface ContainerCreationContribution {
|
||||
handleContainerCreation?(createOptions: Docker.ContainerCreateOptions,
|
||||
containerConfig: DevContainerConfiguration,
|
||||
api: Docker,
|
||||
outputProvider?: ContainerOutputProvider): MaybePromise<void>;
|
||||
|
||||
/**
|
||||
* executed after creating and starting the container
|
||||
*/
|
||||
handlePostCreate?(containerConfig: DevContainerConfiguration,
|
||||
container: Docker.Container,
|
||||
api: Docker,
|
||||
outputProvider?: ContainerOutputProvider): MaybePromise<void>;
|
||||
|
||||
/**
|
||||
* executed after a connection has been established with the container and theia has been setup
|
||||
*/
|
||||
handlePostConnect?(containerConfig: DevContainerConfiguration, connection: RemoteDockerContainerConnection,
|
||||
outputProvider?: ContainerOutputProvider): MaybePromise<void>;
|
||||
}
|
||||
|
||||
@injectable()
|
||||
export class DockerContainerService {
|
||||
|
||||
@inject(WorkspaceServer)
|
||||
protected readonly workspaceServer: WorkspaceServer;
|
||||
|
||||
@inject(ContributionProvider) @named(ContainerCreationContribution)
|
||||
protected readonly containerCreationContributions: ContributionProvider<ContainerCreationContribution>;
|
||||
|
||||
@inject(DevContainerFileService)
|
||||
protected readonly devContainerFileService: DevContainerFileService;
|
||||
|
||||
@inject(DockerComposeService)
|
||||
protected readonly dockerComposeService: DockerComposeService;
|
||||
|
||||
container: Docker.Container | undefined;
|
||||
|
||||
async getOrCreateContainer(docker: Docker, options: ContainerConnectionOptions, outputProvider?: ContainerOutputProvider): Promise<Docker.Container> {
|
||||
let container;
|
||||
|
||||
const workspace = new URI(options.workspacePath ?? await this.workspaceServer.getMostRecentlyUsedWorkspace());
|
||||
|
||||
if (options.lastContainerInfo && fs.statSync(options.devcontainerFile).mtimeMs < options.lastContainerInfo.lastUsed) {
|
||||
try {
|
||||
container = docker.getContainer(options.lastContainerInfo.id);
|
||||
if ((await container.inspect()).State.Running) {
|
||||
await container.restart();
|
||||
} else {
|
||||
await container.start();
|
||||
}
|
||||
} catch (e) {
|
||||
container = undefined;
|
||||
console.warn('DevContainer: could not find last used container');
|
||||
}
|
||||
}
|
||||
if (!container) {
|
||||
container = await this.buildContainer(docker, options.devcontainerFile, workspace, outputProvider);
|
||||
}
|
||||
this.container = container;
|
||||
return container;
|
||||
}
|
||||
|
||||
async postConnect(devcontainerFile: string, connection: RemoteDockerContainerConnection, outputProvider?: ContainerOutputProvider): Promise<void> {
|
||||
const devcontainerConfig = await this.devContainerFileService.getConfiguration(devcontainerFile);
|
||||
|
||||
for (const containerCreateContrib of this.containerCreationContributions.getContributions()) {
|
||||
await containerCreateContrib.handlePostConnect?.(devcontainerConfig, connection, outputProvider);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected async buildContainer(docker: Docker, devcontainerFile: string, workspace: URI, outputProvider?: ContainerOutputProvider): Promise<Docker.Container> {
|
||||
const devcontainerConfig = await this.devContainerFileService.getConfiguration(devcontainerFile);
|
||||
|
||||
if (!devcontainerConfig) {
|
||||
// TODO add ability for user to create new config
|
||||
throw new Error('No devcontainer.json');
|
||||
}
|
||||
|
||||
const containerCreateOptions: Docker.ContainerCreateOptions = {
|
||||
Tty: true,
|
||||
ExposedPorts: {},
|
||||
HostConfig: {
|
||||
PortBindings: {},
|
||||
Mounts: [{
|
||||
Source: workspace.path.toString(),
|
||||
Target: `/workspaces/${workspace.path.name}`,
|
||||
Type: 'bind'
|
||||
}],
|
||||
},
|
||||
};
|
||||
|
||||
for (const containerCreateContrib of this.containerCreationContributions.getContributions()) {
|
||||
await containerCreateContrib.handleContainerCreation?.(containerCreateOptions, devcontainerConfig, docker, outputProvider);
|
||||
}
|
||||
|
||||
let container: Docker.Container;
|
||||
|
||||
if (devcontainerConfig.dockerComposeFile) {
|
||||
const containerName = await this.dockerComposeService.createContainers(devcontainerConfig, containerCreateOptions, outputProvider);
|
||||
const services = await docker.listContainers({ filters: { label: [`com.docker.compose.service=${containerName}`] } });
|
||||
if (services.length === 0) {
|
||||
throw new Error(`No running container found for docker compose service ${containerName}`);
|
||||
}
|
||||
container = docker.getContainer(services[0].Id);
|
||||
} else {
|
||||
container = await docker.createContainer(containerCreateOptions);
|
||||
await container.start();
|
||||
}
|
||||
|
||||
for (const containerCreateContrib of this.containerCreationContributions.getContributions()) {
|
||||
await containerCreateContrib.handlePostCreate?.(devcontainerConfig, container, docker, outputProvider);
|
||||
}
|
||||
|
||||
return container;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,408 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2024 Typefox and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
import * as net from 'net';
|
||||
import {
|
||||
ContainerConnectionOptions, ContainerConnectionResult,
|
||||
DevContainerFile, RemoteContainerConnectionProvider
|
||||
} from '../electron-common/remote-container-connection-provider';
|
||||
import { RemoteConnection, RemoteExecOptions, RemoteExecResult, RemoteExecTester, RemoteStatusReport } from '@theia/remote/lib/electron-node/remote-types';
|
||||
import { RemoteSetupResult, RemoteSetupService } from '@theia/remote/lib/electron-node/setup/remote-setup-service';
|
||||
import { RemoteConnectionService } from '@theia/remote/lib/electron-node/remote-connection-service';
|
||||
import { RemoteProxyServerProvider } from '@theia/remote/lib/electron-node/remote-proxy-server-provider';
|
||||
import { Emitter, Event, generateUuid, MessageService, RpcServer, ILogger } from '@theia/core';
|
||||
import { Socket } from 'net';
|
||||
import { inject, injectable } from '@theia/core/shared/inversify';
|
||||
import * as Docker from 'dockerode';
|
||||
import { DockerContainerService } from './docker-container-service';
|
||||
import { Deferred } from '@theia/core/lib/common/promise-util';
|
||||
import { WriteStream } from 'tty';
|
||||
import { PassThrough } from 'stream';
|
||||
import { exec, execSync } from 'child_process';
|
||||
import { DevContainerFileService } from './dev-container-file-service';
|
||||
import { ContainerOutputProvider } from '../electron-common/container-output-provider';
|
||||
import { DevContainerConfiguration } from './devcontainer-file';
|
||||
import { resolveComposeFilePath } from './docker-compose/compose-service';
|
||||
|
||||
@injectable()
|
||||
export class DevContainerConnectionProvider implements RemoteContainerConnectionProvider, RpcServer<ContainerOutputProvider> {
|
||||
|
||||
@inject(RemoteConnectionService)
|
||||
protected readonly remoteConnectionService: RemoteConnectionService;
|
||||
|
||||
@inject(RemoteSetupService)
|
||||
protected readonly remoteSetup: RemoteSetupService;
|
||||
|
||||
@inject(MessageService)
|
||||
protected readonly messageService: MessageService;
|
||||
|
||||
@inject(RemoteProxyServerProvider)
|
||||
protected readonly serverProvider: RemoteProxyServerProvider;
|
||||
|
||||
@inject(DockerContainerService)
|
||||
protected readonly containerService: DockerContainerService;
|
||||
|
||||
@inject(DevContainerFileService)
|
||||
protected readonly devContainerFileService: DevContainerFileService;
|
||||
|
||||
@inject(RemoteConnectionService)
|
||||
protected readonly remoteService: RemoteConnectionService;
|
||||
|
||||
@inject(ILogger)
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
protected outputProvider: ContainerOutputProvider | undefined;
|
||||
|
||||
setClient(client: ContainerOutputProvider): void {
|
||||
this.outputProvider = client;
|
||||
}
|
||||
|
||||
async connectToContainer(options: ContainerConnectionOptions): Promise<ContainerConnectionResult> {
|
||||
const dockerOptions: Docker.DockerOptions = {};
|
||||
const dockerHost = process.env.DOCKER_HOST;
|
||||
|
||||
try {
|
||||
if (dockerHost) {
|
||||
const dockerHostURL = new URL(dockerHost);
|
||||
|
||||
if (dockerHostURL.protocol === 'unix:') {
|
||||
dockerOptions.socketPath = dockerHostURL.pathname;
|
||||
} else {
|
||||
if (dockerHostURL.protocol === 'http:') {
|
||||
dockerOptions.protocol = 'http';
|
||||
} else if (dockerHostURL.protocol === 'https:') {
|
||||
dockerOptions.protocol = 'https';
|
||||
} else if (dockerHostURL.protocol === 'ssh:') {
|
||||
dockerOptions.protocol = 'ssh';
|
||||
} else {
|
||||
dockerOptions.protocol = undefined;
|
||||
}
|
||||
dockerOptions.port = parseInt(dockerHostURL.port) || undefined;
|
||||
dockerOptions.username = dockerHostURL.username || undefined;
|
||||
}
|
||||
}
|
||||
} catch (_) {
|
||||
this.logger.warn(`Ignoring invalid DOCKER_HOST=${dockerHost}`);
|
||||
this.messageService.warn(`Ignoring invalid DOCKER_HOST=${dockerHost}`);
|
||||
}
|
||||
|
||||
const dockerConnection = new Docker(dockerOptions);
|
||||
const version = await dockerConnection.version()
|
||||
.catch(e => {
|
||||
console.error('Docker Error:', e);
|
||||
this.messageService.error('Docker Error: ' + e.message);
|
||||
});
|
||||
|
||||
if (!version) {
|
||||
this.messageService.error('Docker Daemon is not running');
|
||||
throw new Error('Docker is not running');
|
||||
}
|
||||
|
||||
// create container
|
||||
const progress = await this.messageService.showProgress({
|
||||
text: 'Creating container',
|
||||
});
|
||||
try {
|
||||
const container = await this.containerService.getOrCreateContainer(dockerConnection, options, this.outputProvider);
|
||||
const devContainerConfig = await this.devContainerFileService.getConfiguration(options.devcontainerFile);
|
||||
|
||||
// create actual connection
|
||||
const report: RemoteStatusReport = message => progress.report({ message });
|
||||
report('Connecting to remote system...');
|
||||
|
||||
const remote = await this.createContainerConnection(container, dockerConnection, devContainerConfig);
|
||||
const result = await this.remoteSetup.setup({
|
||||
connection: remote,
|
||||
report,
|
||||
nodeDownloadTemplate: options.nodeDownloadTemplate
|
||||
});
|
||||
remote.remoteSetupResult = result;
|
||||
|
||||
const registration = this.remoteConnectionService.register(remote);
|
||||
const server = await this.serverProvider.getProxyServer(socket => {
|
||||
remote.forwardOut(socket);
|
||||
});
|
||||
remote.onDidDisconnect(() => {
|
||||
server.close();
|
||||
registration.dispose();
|
||||
});
|
||||
const localPort = (server.address() as net.AddressInfo).port;
|
||||
remote.localPort = localPort;
|
||||
|
||||
await this.containerService.postConnect(options.devcontainerFile, remote, this.outputProvider);
|
||||
|
||||
return {
|
||||
containerId: container.id,
|
||||
workspacePath: (await container.inspect()).Mounts[0].Destination,
|
||||
port: localPort.toString(),
|
||||
};
|
||||
} catch (e) {
|
||||
this.messageService.error(e.message);
|
||||
console.error(e);
|
||||
throw e;
|
||||
} finally {
|
||||
progress.cancel();
|
||||
}
|
||||
}
|
||||
|
||||
getDevContainerFiles(workspacePath: string): Promise<DevContainerFile[]> {
|
||||
return this.devContainerFileService.getAvailableFiles(workspacePath);
|
||||
}
|
||||
|
||||
async createContainerConnection(container: Docker.Container, docker: Docker, config: DevContainerConfiguration): Promise<RemoteDockerContainerConnection> {
|
||||
return Promise.resolve(new RemoteDockerContainerConnection({
|
||||
id: generateUuid(),
|
||||
name: config.name ?? 'dev-container',
|
||||
type: 'Dev Container',
|
||||
docker,
|
||||
container,
|
||||
config,
|
||||
logger: this.logger
|
||||
}));
|
||||
}
|
||||
|
||||
async getCurrentContainerInfo(port: number): Promise<Docker.ContainerInspectInfo | undefined> {
|
||||
const connection = this.remoteConnectionService.getConnectionFromPort(port);
|
||||
if (!connection || !(connection instanceof RemoteDockerContainerConnection)) {
|
||||
return undefined;
|
||||
}
|
||||
return connection.container.inspect();
|
||||
}
|
||||
|
||||
dispose(): void {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export interface RemoteContainerConnectionOptions {
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
docker: Docker;
|
||||
container: Docker.Container;
|
||||
config: DevContainerConfiguration;
|
||||
logger: ILogger;
|
||||
}
|
||||
|
||||
interface ContainerTerminalSession {
|
||||
execution: Docker.Exec,
|
||||
stdout: WriteStream,
|
||||
stderr: WriteStream,
|
||||
executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>;
|
||||
}
|
||||
|
||||
interface ContainerTerminalSession {
|
||||
execution: Docker.Exec,
|
||||
stdout: WriteStream,
|
||||
stderr: WriteStream,
|
||||
executeCommand(cmd: string, args?: string[]): Promise<{ stdout: string, stderr: string }>;
|
||||
}
|
||||
|
||||
export class RemoteDockerContainerConnection implements RemoteConnection {
|
||||
|
||||
id: string;
|
||||
name: string;
|
||||
type: string;
|
||||
localPort: number;
|
||||
remotePort: number;
|
||||
|
||||
docker: Docker;
|
||||
container: Docker.Container;
|
||||
|
||||
remoteSetupResult: RemoteSetupResult;
|
||||
|
||||
protected readonly logger: ILogger;
|
||||
|
||||
protected config: DevContainerConfiguration;
|
||||
|
||||
protected activeTerminalSession: ContainerTerminalSession | undefined;
|
||||
|
||||
protected readonly onDidDisconnectEmitter = new Emitter<void>();
|
||||
onDidDisconnect: Event<void> = this.onDidDisconnectEmitter.event;
|
||||
|
||||
constructor(options: RemoteContainerConnectionOptions) {
|
||||
this.id = options.id;
|
||||
this.type = options.type;
|
||||
this.name = options.name;
|
||||
|
||||
this.docker = options.docker;
|
||||
this.container = options.container;
|
||||
|
||||
this.config = options.config;
|
||||
|
||||
this.docker.getEvents({ filters: { container: [this.container.id], event: ['stop'] } }).then(stream => {
|
||||
stream.on('data', () => this.onDidDisconnectEmitter.fire());
|
||||
});
|
||||
|
||||
this.logger = options.logger;
|
||||
}
|
||||
|
||||
async forwardOut(socket: Socket, port?: number): Promise<void> {
|
||||
const node = `${this.remoteSetupResult.nodeDirectory}/bin/node`;
|
||||
const devContainerServer = `${this.remoteSetupResult.applicationDirectory}/backend/dev-container-server.js`;
|
||||
try {
|
||||
const ttySession = await this.container.exec({
|
||||
Cmd: ['sh', '-c', `${node} ${devContainerServer} -target-port=${port ?? this.remotePort}`],
|
||||
AttachStdin: true, AttachStdout: true, AttachStderr: true
|
||||
});
|
||||
|
||||
const stream = await ttySession.start({ hijack: true, stdin: true });
|
||||
|
||||
socket.pipe(stream);
|
||||
ttySession.modem.demuxStream(stream, socket, socket);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
async exec(cmd: string, args?: string[], options?: RemoteExecOptions): Promise<RemoteExecResult> {
|
||||
// return (await this.getOrCreateTerminalSession()).executeCommand(cmd, args);
|
||||
const deferred = new Deferred<RemoteExecResult>();
|
||||
try {
|
||||
// TODO add windows container support
|
||||
const execution = await this.container.exec({ Cmd: ['sh', '-c', `${cmd} ${args?.join(' ') ?? ''}`], AttachStdout: true, AttachStderr: true });
|
||||
let stdoutBuffer = '';
|
||||
let stderrBuffer = '';
|
||||
const stream = await execution?.start({});
|
||||
const stdout = new PassThrough();
|
||||
stdout.on('data', (chunk: Buffer) => {
|
||||
stdoutBuffer += chunk.toString();
|
||||
});
|
||||
const stderr = new PassThrough();
|
||||
stderr.on('data', (chunk: Buffer) => {
|
||||
stderrBuffer += chunk.toString();
|
||||
});
|
||||
execution.modem.demuxStream(stream, stdout, stderr);
|
||||
stream?.addListener('close', () => deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer }));
|
||||
} catch (e) {
|
||||
deferred.reject(e);
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
async execPartial(cmd: string, tester: RemoteExecTester, args?: string[], options?: RemoteExecOptions): Promise<RemoteExecResult> {
|
||||
const deferred = new Deferred<RemoteExecResult>();
|
||||
try {
|
||||
// TODO add windows container support
|
||||
const execution = await this.container.exec({ Cmd: ['sh', '-c', `${cmd} ${args?.join(' ') ?? ''}`], AttachStdout: true, AttachStderr: true });
|
||||
let stdoutBuffer = '';
|
||||
let stderrBuffer = '';
|
||||
const stream = await execution?.start({});
|
||||
stream.on('close', () => {
|
||||
if (deferred.state === 'unresolved') {
|
||||
deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
|
||||
}
|
||||
});
|
||||
const stdout = new PassThrough();
|
||||
stdout.on('data', (data: Buffer) => {
|
||||
this.logger.debug('REMOTE STDOUT:', data.toString());
|
||||
if (deferred.state === 'unresolved') {
|
||||
stdoutBuffer += data.toString();
|
||||
|
||||
if (tester(stdoutBuffer, stderrBuffer)) {
|
||||
deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
|
||||
}
|
||||
}
|
||||
});
|
||||
const stderr = new PassThrough();
|
||||
stderr.on('data', (data: Buffer) => {
|
||||
this.logger.debug('REMOTE STDERR:', data.toString());
|
||||
if (deferred.state === 'unresolved') {
|
||||
stderrBuffer += data.toString();
|
||||
|
||||
if (tester(stdoutBuffer, stderrBuffer)) {
|
||||
deferred.resolve({ stdout: stdoutBuffer, stderr: stderrBuffer });
|
||||
}
|
||||
}
|
||||
});
|
||||
execution.modem.demuxStream(stream, stdout, stderr);
|
||||
} catch (e) {
|
||||
deferred.reject(e);
|
||||
}
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
getDockerHost(): string {
|
||||
const dockerHost = process.env.DOCKER_HOST;
|
||||
let remoteHost = '';
|
||||
try {
|
||||
if (dockerHost) {
|
||||
const dockerHostURL = new URL(dockerHost);
|
||||
if (dockerHostURL.protocol === 'http:' || dockerHostURL.protocol === 'https:') {
|
||||
dockerHostURL.protocol = 'tcp:';
|
||||
}
|
||||
remoteHost = `-H ${dockerHostURL.href} `;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
|
||||
return remoteHost;
|
||||
}
|
||||
|
||||
async copy(localPath: string | Buffer | NodeJS.ReadableStream, remotePath: string): Promise<void> {
|
||||
const deferred = new Deferred<void>();
|
||||
const remoteHost = this.getDockerHost();
|
||||
|
||||
const subprocess = exec(`docker ${remoteHost}cp -a ${localPath.toString()} ${this.container.id}:${remotePath}`);
|
||||
|
||||
let stderr = '';
|
||||
subprocess.stderr?.on('data', data => {
|
||||
stderr += data.toString();
|
||||
});
|
||||
subprocess.on('close', code => {
|
||||
if (code === 0) {
|
||||
deferred.resolve();
|
||||
} else {
|
||||
deferred.reject(stderr);
|
||||
}
|
||||
});
|
||||
return deferred.promise;
|
||||
}
|
||||
|
||||
disposeSync(): void {
|
||||
// cant use dockerrode here since this needs to happen on one tick
|
||||
this.shutdownContainer(true);
|
||||
}
|
||||
|
||||
async dispose(): Promise<void> {
|
||||
await this.shutdownContainer(false);
|
||||
}
|
||||
|
||||
protected async shutdownContainer(sync: boolean): Promise<unknown> {
|
||||
const remoteHost = this.getDockerHost();
|
||||
|
||||
const shutdownAction = this.config.shutdownAction ?? this.config.dockerComposeFile ? 'stopCompose' : 'stopContainer';
|
||||
|
||||
if (shutdownAction === 'stopContainer') {
|
||||
return sync ? execSync(`docker ${remoteHost}stop ${this.container.id}`) : this.container.stop();
|
||||
} else if (shutdownAction === 'stopCompose') {
|
||||
const composeFilePath = resolveComposeFilePath(this.config);
|
||||
return sync ? execSync(`docker ${remoteHost}compose -f ${composeFilePath} stop`) :
|
||||
new Promise<void>((res, rej) => exec(`docker ${remoteHost}compose -f ${composeFilePath} stop`, err => {
|
||||
if (err) {
|
||||
console.error(err);
|
||||
rej(err);
|
||||
} else {
|
||||
res();
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
28
packages/dev-container/src/package.spec.ts
Normal file
28
packages/dev-container/src/package.spec.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// *****************************************************************************
|
||||
// Copyright (C) 2017 Ericsson and others.
|
||||
//
|
||||
// This program and the accompanying materials are made available under the
|
||||
// terms of the Eclipse Public License v. 2.0 which is available at
|
||||
// http://www.eclipse.org/legal/epl-2.0.
|
||||
//
|
||||
// This Source Code may also be made available under the following Secondary
|
||||
// Licenses when the conditions for such availability set forth in the Eclipse
|
||||
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
||||
// with the GNU Classpath Exception which is available at
|
||||
// https://www.gnu.org/software/classpath/license.html.
|
||||
//
|
||||
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
||||
// *****************************************************************************
|
||||
|
||||
/* note: this bogus test file is required so that
|
||||
we are able to run mocha unit tests on this
|
||||
package, without having any actual unit tests in it.
|
||||
This way a coverage report will be generated,
|
||||
showing 0% coverage, instead of no report.
|
||||
This file can be removed once we have real unit
|
||||
tests in place. */
|
||||
|
||||
describe('dev-container package', () => {
|
||||
|
||||
it('support code coverage statistics', () => true);
|
||||
});
|
||||
25
packages/dev-container/tsconfig.json
Normal file
25
packages/dev-container/tsconfig.json
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
"extends": "../../configs/base.tsconfig",
|
||||
"compilerOptions": {
|
||||
"composite": true,
|
||||
"rootDir": "src",
|
||||
"outDir": "lib"
|
||||
},
|
||||
"include": [
|
||||
"src"
|
||||
],
|
||||
"references": [
|
||||
{
|
||||
"path": "../core"
|
||||
},
|
||||
{
|
||||
"path": "../output"
|
||||
},
|
||||
{
|
||||
"path": "../remote"
|
||||
},
|
||||
{
|
||||
"path": "../workspace"
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user