@theia/dev-container 1.72.0-next.59 → 1.72.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/electron-browser/container-connection-contribution.d.ts +5 -2
- package/lib/electron-browser/container-connection-contribution.d.ts.map +1 -1
- package/lib/electron-browser/container-connection-contribution.js +130 -30
- package/lib/electron-browser/container-connection-contribution.js.map +1 -1
- package/lib/electron-browser/container-output-provider.d.ts.map +1 -1
- package/lib/electron-browser/container-output-provider.js +3 -1
- package/lib/electron-browser/container-output-provider.js.map +1 -1
- package/lib/electron-browser/dev-container-frontend-module.d.ts.map +1 -1
- package/lib/electron-browser/dev-container-frontend-module.js +5 -0
- package/lib/electron-browser/dev-container-frontend-module.js.map +1 -1
- package/lib/electron-browser/dev-container-startup-contribution.d.ts +15 -0
- package/lib/electron-browser/dev-container-startup-contribution.d.ts.map +1 -0
- package/lib/electron-browser/dev-container-startup-contribution.js +94 -0
- package/lib/electron-browser/dev-container-startup-contribution.js.map +1 -0
- package/lib/electron-common/dev-container-preferences.d.ts +12 -0
- package/lib/electron-common/dev-container-preferences.d.ts.map +1 -0
- package/lib/electron-common/dev-container-preferences.js +44 -0
- package/lib/electron-common/dev-container-preferences.js.map +1 -0
- package/lib/electron-common/remote-container-connection-provider.d.ts +20 -1
- package/lib/electron-common/remote-container-connection-provider.d.ts.map +1 -1
- package/lib/electron-node/dev-container-backend-module.d.ts.map +1 -1
- package/lib/electron-node/dev-container-backend-module.js +4 -0
- package/lib/electron-node/dev-container-backend-module.js.map +1 -1
- package/lib/electron-node/dev-container-cli-contribution.d.ts +19 -0
- package/lib/electron-node/dev-container-cli-contribution.d.ts.map +1 -0
- package/lib/electron-node/dev-container-cli-contribution.js +66 -0
- package/lib/electron-node/dev-container-cli-contribution.js.map +1 -0
- package/lib/electron-node/dev-container-cli-contribution.spec.d.ts +2 -0
- package/lib/electron-node/dev-container-cli-contribution.spec.d.ts.map +1 -0
- package/lib/electron-node/dev-container-cli-contribution.spec.js +91 -0
- package/lib/electron-node/dev-container-cli-contribution.spec.js.map +1 -0
- package/lib/electron-node/dev-container-file-service.d.ts +4 -4
- package/lib/electron-node/dev-container-file-service.d.ts.map +1 -1
- package/lib/electron-node/dev-container-file-service.js +9 -9
- package/lib/electron-node/dev-container-file-service.js.map +1 -1
- package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts +6 -2
- package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.d.ts.map +1 -1
- package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js +24 -4
- package/lib/electron-node/devcontainer-contributions/main-container-creation-contributions.js.map +1 -1
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.d.ts +7 -6
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.d.ts.map +1 -1
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js +4 -9
- package/lib/electron-node/devcontainer-contributions/variable-resolver-contribution.js.map +1 -1
- package/lib/electron-node/devcontainer-util.d.ts +19 -0
- package/lib/electron-node/devcontainer-util.d.ts.map +1 -0
- package/lib/electron-node/devcontainer-util.js +48 -0
- package/lib/electron-node/devcontainer-util.js.map +1 -0
- package/lib/electron-node/devcontainer-util.spec.d.ts +2 -0
- package/lib/electron-node/devcontainer-util.spec.d.ts.map +1 -0
- package/lib/electron-node/devcontainer-util.spec.js +128 -0
- package/lib/electron-node/devcontainer-util.spec.js.map +1 -0
- package/lib/electron-node/docker-container-service.d.ts +3 -3
- package/lib/electron-node/docker-container-service.d.ts.map +1 -1
- package/lib/electron-node/docker-container-service.js +3 -4
- package/lib/electron-node/docker-container-service.js.map +1 -1
- package/lib/electron-node/remote-container-connection-provider.d.ts +27 -66
- package/lib/electron-node/remote-container-connection-provider.d.ts.map +1 -1
- package/lib/electron-node/remote-container-connection-provider.js +269 -311
- package/lib/electron-node/remote-container-connection-provider.js.map +1 -1
- package/lib/electron-node/remote-docker-container-connection.d.ts +50 -0
- package/lib/electron-node/remote-docker-container-connection.d.ts.map +1 -0
- package/lib/electron-node/remote-docker-container-connection.js +239 -0
- package/lib/electron-node/remote-docker-container-connection.js.map +1 -0
- package/lib/electron-node/remote-docker-container-connection.spec.d.ts +2 -0
- package/lib/electron-node/remote-docker-container-connection.spec.d.ts.map +1 -0
- package/lib/electron-node/remote-docker-container-connection.spec.js +217 -0
- package/lib/electron-node/remote-docker-container-connection.spec.js.map +1 -0
- package/package.json +7 -7
- package/src/electron-browser/container-connection-contribution.ts +155 -38
- package/src/electron-browser/container-output-provider.ts +3 -1
- package/src/electron-browser/dev-container-frontend-module.ts +6 -0
- package/src/electron-browser/dev-container-startup-contribution.ts +99 -0
- package/src/electron-common/dev-container-preferences.ts +53 -0
- package/src/electron-common/remote-container-connection-provider.ts +23 -1
- package/src/electron-node/dev-container-backend-module.ts +5 -0
- package/src/electron-node/dev-container-cli-contribution.spec.ts +106 -0
- package/src/electron-node/dev-container-cli-contribution.ts +68 -0
- package/src/electron-node/dev-container-file-service.ts +10 -10
- package/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts +29 -5
- package/src/electron-node/devcontainer-contributions/variable-resolver-contribution.ts +11 -11
- package/src/electron-node/devcontainer-util.spec.ts +154 -0
- package/src/electron-node/devcontainer-util.ts +49 -0
- package/src/electron-node/docker-container-service.ts +6 -7
- package/src/electron-node/remote-container-connection-provider.ts +274 -366
- package/src/electron-node/{remote-container-connection-provider.spec.ts → remote-docker-container-connection.spec.ts} +105 -4
- package/src/electron-node/remote-docker-container-connection.ts +290 -0
- package/lib/electron-node/remote-container-connection-provider.spec.d.ts +0 -2
- package/lib/electron-node/remote-container-connection-provider.spec.d.ts.map +0 -1
- package/lib/electron-node/remote-container-connection-provider.spec.js +0 -131
- package/lib/electron-node/remote-container-connection-provider.spec.js.map +0 -1
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { CliContribution } from '@theia/core/lib/node';
|
|
18
|
+
import { injectable } from '@theia/core/shared/inversify';
|
|
19
|
+
import { Arguments, Argv } from '@theia/core/shared/yargs';
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Handles the `--attach-container` CLI argument and stashes the value.
|
|
23
|
+
*/
|
|
24
|
+
@injectable()
|
|
25
|
+
export class DevContainerCliContribution implements CliContribution {
|
|
26
|
+
|
|
27
|
+
protected attachContainerId: string | undefined;
|
|
28
|
+
|
|
29
|
+
protected scanForDevJson: boolean = true;
|
|
30
|
+
|
|
31
|
+
configure(conf: Argv): void {
|
|
32
|
+
conf.option('attach-container', {
|
|
33
|
+
description: 'Attach to a running Docker container by ID or name on startup',
|
|
34
|
+
type: 'string'
|
|
35
|
+
});
|
|
36
|
+
conf.option('dev-json', {
|
|
37
|
+
description: 'Scan for and apply devcontainer.json when attaching to a container (use --no-dev-json to skip)',
|
|
38
|
+
type: 'boolean',
|
|
39
|
+
default: true
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
setArguments(args: Arguments): void {
|
|
44
|
+
if (args['attach-container'] !== undefined) {
|
|
45
|
+
const id = String(args['attach-container']).trim();
|
|
46
|
+
this.attachContainerId = id.length > 0 ? id : undefined;
|
|
47
|
+
}
|
|
48
|
+
this.scanForDevJson = args['dev-json'] !== false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getAttachContainerId(): string | undefined {
|
|
52
|
+
return this.attachContainerId;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Returns and clears the attach container ID so the startup attach flow
|
|
57
|
+
* runs at most once (the window reloads after connecting to the remote).
|
|
58
|
+
*/
|
|
59
|
+
consumeAttachContainerId(): string | undefined {
|
|
60
|
+
const id = this.attachContainerId;
|
|
61
|
+
this.attachContainerId = undefined;
|
|
62
|
+
return id;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
shouldScanForDevJson(): boolean {
|
|
66
|
+
return this.scanForDevJson;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
@@ -21,7 +21,7 @@ import { DevContainerConfiguration } from './devcontainer-file';
|
|
|
21
21
|
import { parse } from 'jsonc-parser';
|
|
22
22
|
import * as fs from '@theia/core/shared/fs-extra';
|
|
23
23
|
import { ContributionProvider, Path, URI } from '@theia/core';
|
|
24
|
-
import { VariableResolverContribution } from './devcontainer-contributions/variable-resolver-contribution';
|
|
24
|
+
import { VariableContext, VariableResolverContribution } from './devcontainer-contributions/variable-resolver-contribution';
|
|
25
25
|
|
|
26
26
|
const VARIABLE_REGEX = /\$\{(.+?)(?::(.+?))?\}/g;
|
|
27
27
|
|
|
@@ -34,39 +34,39 @@ export class DevContainerFileService {
|
|
|
34
34
|
@inject(ContributionProvider) @named(VariableResolverContribution)
|
|
35
35
|
protected readonly variableResolverContributions: ContributionProvider<VariableResolverContribution>;
|
|
36
36
|
|
|
37
|
-
protected resolveVariable(value: string): string {
|
|
37
|
+
protected resolveVariable(value: string, context?: VariableContext): string {
|
|
38
38
|
return value.replace(VARIABLE_REGEX, (match, type, variable) => {
|
|
39
39
|
for (const contribution of this.variableResolverContributions.getContributions()) {
|
|
40
|
-
if (contribution.canResolve(type)) {
|
|
41
|
-
return contribution.resolve(variable ?? type);
|
|
40
|
+
if (contribution.canResolve(type, context)) {
|
|
41
|
+
return contribution.resolve(variable ?? type, context);
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
44
|
return match;
|
|
45
45
|
});
|
|
46
46
|
}
|
|
47
47
|
|
|
48
|
-
protected resolveVariablesRecursively<T>(obj: T): T {
|
|
48
|
+
protected resolveVariablesRecursively<T>(obj: T, context?: VariableContext): T {
|
|
49
49
|
if (typeof obj === 'string') {
|
|
50
|
-
return this.resolveVariable(obj) as T;
|
|
50
|
+
return this.resolveVariable(obj, context) as T;
|
|
51
51
|
} else if (Array.isArray(obj)) {
|
|
52
|
-
return obj.map(item => this.resolveVariablesRecursively(item)) as T;
|
|
52
|
+
return obj.map(item => this.resolveVariablesRecursively(item, context)) as T;
|
|
53
53
|
} else if (obj && typeof obj === 'object') {
|
|
54
54
|
const newObj: Record<string, unknown> = {};
|
|
55
55
|
for (const [key, value] of Object.entries(obj)) {
|
|
56
|
-
newObj[key] = this.resolveVariablesRecursively(value);
|
|
56
|
+
newObj[key] = this.resolveVariablesRecursively(value, context);
|
|
57
57
|
}
|
|
58
58
|
return newObj as T;
|
|
59
59
|
}
|
|
60
60
|
return obj;
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
async getConfiguration(path: string): Promise<DevContainerConfiguration> {
|
|
63
|
+
async getConfiguration(path: string, context?: VariableContext): Promise<DevContainerConfiguration> {
|
|
64
64
|
let configuration: DevContainerConfiguration = parse(await fs.readFile(path, 'utf-8').catch(() => '0')) as DevContainerConfiguration;
|
|
65
65
|
if (!configuration) {
|
|
66
66
|
throw new Error(`devcontainer file ${path} could not be parsed`);
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
configuration = this.resolveVariablesRecursively(configuration);
|
|
69
|
+
configuration = this.resolveVariablesRecursively(configuration, context);
|
|
70
70
|
configuration.location = path;
|
|
71
71
|
return configuration;
|
|
72
72
|
}
|
package/src/electron-node/devcontainer-contributions/main-container-creation-contributions.ts
CHANGED
|
@@ -24,7 +24,7 @@ import * as os from 'os';
|
|
|
24
24
|
import * as path from 'path';
|
|
25
25
|
import * as cp from 'child_process';
|
|
26
26
|
import { ForwardedPort, RemotePortForwardingProvider } from '@theia/remote/lib/electron-common/remote-port-forwarding-provider';
|
|
27
|
-
import { RemoteDockerContainerConnection } from '../remote-container-connection
|
|
27
|
+
import { RemoteDockerContainerConnection } from '../remote-docker-container-connection';
|
|
28
28
|
import { WorkspaceCreationContribution } from './workspace-creation-contribution';
|
|
29
29
|
import { parseWorkspaceMount } from '../dockerode-utils';
|
|
30
30
|
|
|
@@ -49,6 +49,7 @@ export class ImageFileContribution implements ContainerCreationContribution {
|
|
|
49
49
|
const platform = process.platform;
|
|
50
50
|
const arch = process.arch;
|
|
51
51
|
const options = platform === 'darwin' && arch === 'arm64' ? { platform: 'amd64' } : {};
|
|
52
|
+
const dedup = OutputHelper.createDedupContext();
|
|
52
53
|
await new Promise<void>((res, rej) => api.pull(containerConfig.image, options, (err, stream) => {
|
|
53
54
|
if (err) {
|
|
54
55
|
rej(err);
|
|
@@ -57,7 +58,7 @@ export class ImageFileContribution implements ContainerCreationContribution {
|
|
|
57
58
|
} else {
|
|
58
59
|
api.modem.followProgress(stream!, (error, output) => error ?
|
|
59
60
|
rej(error) :
|
|
60
|
-
res(), progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress)));
|
|
61
|
+
res(), progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress, dedup)));
|
|
61
62
|
}
|
|
62
63
|
}));
|
|
63
64
|
createOptions.Image = containerConfig.image;
|
|
@@ -84,6 +85,7 @@ export class DockerFileContribution implements ContainerCreationContribution {
|
|
|
84
85
|
buildargs: containerConfig.build?.args
|
|
85
86
|
});
|
|
86
87
|
// TODO probably have some console windows showing the output of the build
|
|
88
|
+
const dedup = OutputHelper.createDedupContext();
|
|
87
89
|
const imageId = await new Promise<string>((res, rej) => api.modem.followProgress(buildStream!, (err, outputs) => {
|
|
88
90
|
if (err) {
|
|
89
91
|
rej(err);
|
|
@@ -95,7 +97,7 @@ export class DockerFileContribution implements ContainerCreationContribution {
|
|
|
95
97
|
}
|
|
96
98
|
}
|
|
97
99
|
}
|
|
98
|
-
}, progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress))));
|
|
100
|
+
}, progress => outputprovider.onRemoteOutput(OutputHelper.parseProgress(progress, dedup))));
|
|
99
101
|
createOptions.Image = imageId;
|
|
100
102
|
} catch (error) {
|
|
101
103
|
outputprovider.onRemoteOutput(`Could not build dockerfile "${dockerfile}": ${error.message}`);
|
|
@@ -517,7 +519,29 @@ export namespace OutputHelper {
|
|
|
517
519
|
progress?: string;
|
|
518
520
|
}
|
|
519
521
|
|
|
520
|
-
export
|
|
521
|
-
|
|
522
|
+
export interface DedupContext {
|
|
523
|
+
lastMessage: string;
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
export function createDedupContext(): DedupContext {
|
|
527
|
+
return { lastMessage: '' };
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
export function parseProgress(progress: Progress, dedup?: DedupContext): string {
|
|
531
|
+
if (progress.stream) {
|
|
532
|
+
if (dedup) {
|
|
533
|
+
dedup.lastMessage = '';
|
|
534
|
+
}
|
|
535
|
+
return progress.stream;
|
|
536
|
+
}
|
|
537
|
+
const message = progress.progress ?? progress.status ?? '';
|
|
538
|
+
const formatted = progress.id ? `${progress.id}: ${message}` : message;
|
|
539
|
+
if (dedup && formatted === dedup.lastMessage) {
|
|
540
|
+
return '';
|
|
541
|
+
}
|
|
542
|
+
if (dedup) {
|
|
543
|
+
dedup.lastMessage = formatted;
|
|
544
|
+
}
|
|
545
|
+
return formatted;
|
|
522
546
|
}
|
|
523
547
|
}
|
|
@@ -14,13 +14,16 @@
|
|
|
14
14
|
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
|
-
import {
|
|
18
|
-
|
|
17
|
+
import { injectable, interfaces } from '@theia/core/shared/inversify';
|
|
18
|
+
|
|
19
|
+
export interface VariableContext {
|
|
20
|
+
containerId?: string;
|
|
21
|
+
}
|
|
19
22
|
|
|
20
23
|
export const VariableResolverContribution = Symbol('VariableResolverContribution');
|
|
21
24
|
export interface VariableResolverContribution {
|
|
22
|
-
canResolve(variable: string): boolean;
|
|
23
|
-
resolve(variable: string): string;
|
|
25
|
+
canResolve(variable: string, context?: VariableContext): boolean;
|
|
26
|
+
resolve(variable: string, context?: VariableContext): string;
|
|
24
27
|
}
|
|
25
28
|
|
|
26
29
|
export function registerVariableResolverContributions(bind: interfaces.Bind): void {
|
|
@@ -41,13 +44,10 @@ export class LocalEnvVariableResolver implements VariableResolverContribution {
|
|
|
41
44
|
|
|
42
45
|
@injectable()
|
|
43
46
|
export class ContainerIdResolver implements VariableResolverContribution {
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
canResolve(type: string): boolean {
|
|
48
|
-
return type === 'devcontainerId' && !!this.dockerContainerService.container;
|
|
47
|
+
canResolve(type: string, context?: VariableContext): boolean {
|
|
48
|
+
return type === 'devcontainerId' && !!context?.containerId;
|
|
49
49
|
}
|
|
50
|
-
resolve(
|
|
51
|
-
return
|
|
50
|
+
resolve(_variable: string, context?: VariableContext): string {
|
|
51
|
+
return context?.containerId ?? _variable;
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import { expect } from 'chai';
|
|
18
|
+
import { getWorkspaceMounts, inferWorkspacePath, isWorkspaceMount, MountInfo } from './devcontainer-util';
|
|
19
|
+
import type { ContainerInspectInfo } from 'dockerode';
|
|
20
|
+
|
|
21
|
+
function createContainerInfo(
|
|
22
|
+
mounts: Array<{ Destination: string; Type?: string }>,
|
|
23
|
+
workingDir?: string
|
|
24
|
+
): ContainerInspectInfo {
|
|
25
|
+
return {
|
|
26
|
+
Mounts: mounts.map(m => ({ ...m, Type: m.Type ?? 'bind' })),
|
|
27
|
+
Config: { WorkingDir: workingDir ?? '' }
|
|
28
|
+
} as unknown as ContainerInspectInfo;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('inferWorkspacePath', () => {
|
|
32
|
+
|
|
33
|
+
it('should return workspace mount destination', () => {
|
|
34
|
+
const info = createContainerInfo([{ Destination: '/workspaces/project' }]);
|
|
35
|
+
expect(inferWorkspacePath(info)).to.equal('/workspaces/project');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('should skip .ssh mount', () => {
|
|
39
|
+
const info = createContainerInfo([
|
|
40
|
+
{ Destination: '/root/.ssh' },
|
|
41
|
+
{ Destination: '/workspaces/project' }
|
|
42
|
+
]);
|
|
43
|
+
expect(inferWorkspacePath(info)).to.equal('/workspaces/project');
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('should skip .gitconfig mount', () => {
|
|
47
|
+
const info = createContainerInfo([
|
|
48
|
+
{ Destination: '/home/user/.gitconfig' },
|
|
49
|
+
{ Destination: '/workspaces/project' }
|
|
50
|
+
]);
|
|
51
|
+
expect(inferWorkspacePath(info)).to.equal('/workspaces/project');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it('should skip /tmp/host_gitconfig mount', () => {
|
|
55
|
+
const info = createContainerInfo([
|
|
56
|
+
{ Destination: '/tmp/host_gitconfig' },
|
|
57
|
+
{ Destination: '/workspaces/project' }
|
|
58
|
+
]);
|
|
59
|
+
expect(inferWorkspacePath(info)).to.equal('/workspaces/project');
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
it('should skip all injected mounts and find workspace', () => {
|
|
63
|
+
const info = createContainerInfo([
|
|
64
|
+
{ Destination: '/root/.ssh' },
|
|
65
|
+
{ Destination: '/tmp/host_gitconfig' },
|
|
66
|
+
{ Destination: '/workspace' }
|
|
67
|
+
]);
|
|
68
|
+
expect(inferWorkspacePath(info)).to.equal('/workspace');
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it('should fall back to WorkingDir when no suitable mounts', () => {
|
|
72
|
+
const info = createContainerInfo([{ Destination: '/root/.ssh' }], '/app');
|
|
73
|
+
expect(inferWorkspacePath(info)).to.equal('/app');
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('should fall back to / when no mounts and no WorkingDir', () => {
|
|
77
|
+
const info = createContainerInfo([], '');
|
|
78
|
+
expect(inferWorkspacePath(info)).to.equal('/');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should fall back to / when mounts is empty', () => {
|
|
82
|
+
const info = createContainerInfo([]);
|
|
83
|
+
expect(inferWorkspacePath(info)).to.equal('/');
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it('should skip non-bind mounts', () => {
|
|
87
|
+
const info = createContainerInfo([
|
|
88
|
+
{ Destination: '/data', Type: 'volume' },
|
|
89
|
+
{ Destination: '/workspaces/project' }
|
|
90
|
+
]);
|
|
91
|
+
expect(inferWorkspacePath(info)).to.equal('/workspaces/project');
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
describe('isWorkspaceMount', () => {
|
|
96
|
+
|
|
97
|
+
function mount(destination: string, type: string = 'bind'): MountInfo {
|
|
98
|
+
return { Destination: destination, Type: type } as MountInfo;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
it('should accept a normal bind mount', () => {
|
|
102
|
+
expect(isWorkspaceMount(mount('/workspaces/project'))).to.be.true;
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should reject .ssh mounts', () => {
|
|
106
|
+
expect(isWorkspaceMount(mount('/root/.ssh'))).to.be.false;
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it('should reject .gitconfig mounts', () => {
|
|
110
|
+
expect(isWorkspaceMount(mount('/home/user/.gitconfig'))).to.be.false;
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it('should reject /tmp/host_gitconfig', () => {
|
|
114
|
+
expect(isWorkspaceMount(mount('/tmp/host_gitconfig'))).to.be.false;
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('should reject volume mounts', () => {
|
|
118
|
+
expect(isWorkspaceMount(mount('/workspaces/project', 'volume'))).to.be.false;
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should reject tmpfs mounts', () => {
|
|
122
|
+
expect(isWorkspaceMount(mount('/workspaces/project', 'tmpfs'))).to.be.false;
|
|
123
|
+
});
|
|
124
|
+
});
|
|
125
|
+
|
|
126
|
+
describe('getWorkspaceMounts', () => {
|
|
127
|
+
|
|
128
|
+
function mount(destination: string, type: string = 'bind'): MountInfo {
|
|
129
|
+
return { Destination: destination, Type: type } as MountInfo;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
it('should filter out injected mounts and return workspace mounts', () => {
|
|
133
|
+
const mounts = [
|
|
134
|
+
mount('/root/.ssh'),
|
|
135
|
+
mount('/workspaces/project'),
|
|
136
|
+
mount('/tmp/host_gitconfig'),
|
|
137
|
+
mount('/app'),
|
|
138
|
+
];
|
|
139
|
+
const result = getWorkspaceMounts(mounts);
|
|
140
|
+
expect(result.map(m => m.Destination)).to.deep.equal(['/workspaces/project', '/app']);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it('should return empty array when all mounts are injected', () => {
|
|
144
|
+
const mounts = [
|
|
145
|
+
mount('/root/.ssh'),
|
|
146
|
+
mount('/home/user/.gitconfig'),
|
|
147
|
+
];
|
|
148
|
+
expect(getWorkspaceMounts(mounts)).to.deep.equal([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('should return empty array for empty input', () => {
|
|
152
|
+
expect(getWorkspaceMounts([])).to.deep.equal([]);
|
|
153
|
+
});
|
|
154
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// *****************************************************************************
|
|
2
|
+
// Copyright (C) 2026 EclipseSource GmbH and others.
|
|
3
|
+
//
|
|
4
|
+
// This program and the accompanying materials are made available under the
|
|
5
|
+
// terms of the Eclipse Public License v. 2.0 which is available at
|
|
6
|
+
// http://www.eclipse.org/legal/epl-2.0.
|
|
7
|
+
//
|
|
8
|
+
// This Source Code may also be made available under the following Secondary
|
|
9
|
+
// Licenses when the conditions for such availability set forth in the Eclipse
|
|
10
|
+
// Public License v. 2.0 are satisfied: GNU General Public License, version 2
|
|
11
|
+
// with the GNU Classpath Exception which is available at
|
|
12
|
+
// https://www.gnu.org/software/classpath/license.html.
|
|
13
|
+
//
|
|
14
|
+
// SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0
|
|
15
|
+
// *****************************************************************************
|
|
16
|
+
|
|
17
|
+
import type { ContainerInspectInfo } from 'dockerode';
|
|
18
|
+
|
|
19
|
+
export type MountInfo = ContainerInspectInfo['Mounts'][number];
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns true for bind mounts that are not injected by HostConfigSharingContribution
|
|
23
|
+
* (SSH dir, gitconfig). Shared by {@link inferWorkspacePath} and
|
|
24
|
+
* {@link getWorkspaceMounts} to keep the filtering logic in one place.
|
|
25
|
+
*/
|
|
26
|
+
export function isWorkspaceMount(mount: MountInfo): boolean {
|
|
27
|
+
return mount.Type === 'bind'
|
|
28
|
+
&& !!mount.Destination
|
|
29
|
+
&& !mount.Destination.endsWith('/.ssh')
|
|
30
|
+
&& !mount.Destination.endsWith('/.gitconfig')
|
|
31
|
+
&& mount.Destination !== '/tmp/host_gitconfig';
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Returns all bind mounts that look like workspace mounts (filtering out
|
|
36
|
+
* injected SSH/gitconfig mounts).
|
|
37
|
+
*/
|
|
38
|
+
export function getWorkspaceMounts(mounts: MountInfo[]): MountInfo[] {
|
|
39
|
+
return mounts.filter(isWorkspaceMount);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Infers a workspace path from container inspect info.
|
|
44
|
+
* Takes the first workspace-relevant bind mount, falls back to WorkingDir, then `/`.
|
|
45
|
+
*/
|
|
46
|
+
export function inferWorkspacePath(containerInfo: ContainerInspectInfo): string {
|
|
47
|
+
const workspaceMount = containerInfo.Mounts.find(isWorkspaceMount);
|
|
48
|
+
return (workspaceMount?.Destination ?? containerInfo.Config.WorkingDir) || '/';
|
|
49
|
+
}
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// *****************************************************************************
|
|
16
16
|
|
|
17
17
|
import { ContributionProvider, MaybePromise, URI } from '@theia/core';
|
|
18
|
+
import { VariableContext } from './devcontainer-contributions/variable-resolver-contribution';
|
|
18
19
|
import { inject, injectable, named } from '@theia/core/shared/inversify';
|
|
19
20
|
import { WorkspaceServer } from '@theia/workspace/lib/common';
|
|
20
21
|
import * as fs from '@theia/core/shared/fs-extra';
|
|
@@ -23,7 +24,7 @@ import { ContainerConnectionOptions } from '../electron-common/remote-container-
|
|
|
23
24
|
import { DevContainerConfiguration, NonComposeContainerBase } from './devcontainer-file';
|
|
24
25
|
import { DevContainerFileService } from './dev-container-file-service';
|
|
25
26
|
import { ContainerOutputProvider } from '../electron-common/container-output-provider';
|
|
26
|
-
import { RemoteDockerContainerConnection } from './remote-container-connection
|
|
27
|
+
import { RemoteDockerContainerConnection } from './remote-docker-container-connection';
|
|
27
28
|
import { DockerComposeService } from './docker-compose/compose-service';
|
|
28
29
|
|
|
29
30
|
export const ContainerCreationContribution = Symbol('ContainerCreationContributions');
|
|
@@ -64,14 +65,12 @@ export class DockerContainerService {
|
|
|
64
65
|
@inject(DockerComposeService)
|
|
65
66
|
protected readonly dockerComposeService: DockerComposeService;
|
|
66
67
|
|
|
67
|
-
container: Docker.Container | undefined;
|
|
68
|
-
|
|
69
68
|
async getOrCreateContainer(docker: Docker, options: ContainerConnectionOptions, outputProvider?: ContainerOutputProvider): Promise<Docker.Container> {
|
|
70
69
|
let container;
|
|
71
70
|
|
|
72
71
|
const workspace = new URI(options.workspacePath ?? await this.workspaceServer.getMostRecentlyUsedWorkspace());
|
|
73
72
|
|
|
74
|
-
if (options.lastContainerInfo && fs.statSync(options.devcontainerFile).mtimeMs < options.lastContainerInfo.lastUsed) {
|
|
73
|
+
if (options.lastContainerInfo && fs.pathExistsSync(options.devcontainerFile) && fs.statSync(options.devcontainerFile).mtimeMs < options.lastContainerInfo.lastUsed) {
|
|
75
74
|
try {
|
|
76
75
|
container = docker.getContainer(options.lastContainerInfo.id);
|
|
77
76
|
if ((await container.inspect()).State.Running) {
|
|
@@ -87,12 +86,12 @@ export class DockerContainerService {
|
|
|
87
86
|
if (!container) {
|
|
88
87
|
container = await this.buildContainer(docker, options.devcontainerFile, workspace, outputProvider);
|
|
89
88
|
}
|
|
90
|
-
this.container = container;
|
|
91
89
|
return container;
|
|
92
90
|
}
|
|
93
91
|
|
|
94
|
-
async postConnect(devcontainerFile: string, connection: RemoteDockerContainerConnection, outputProvider?: ContainerOutputProvider
|
|
95
|
-
|
|
92
|
+
async postConnect(devcontainerFile: string, connection: RemoteDockerContainerConnection, outputProvider?: ContainerOutputProvider,
|
|
93
|
+
context?: VariableContext): Promise<void> {
|
|
94
|
+
const devcontainerConfig = await this.devContainerFileService.getConfiguration(devcontainerFile, context);
|
|
96
95
|
|
|
97
96
|
for (const containerCreateContrib of this.containerCreationContributions.getContributions()) {
|
|
98
97
|
await containerCreateContrib.handlePostConnect?.(devcontainerConfig, connection, outputProvider);
|