Skip to content

Commit 46e6c0b

Browse files
authored
chore: move docker cli context select implem from core to docker extension (podman-desktop#10526)
* chore: move docker cli context select implem from core to docker extension fixes podman-desktop#10517 Signed-off-by: Florent Benoit <[email protected]>
1 parent 8b8494e commit 46e6c0b

13 files changed

+285
-267
lines changed

extensions/docker/package.json

+22
Original file line numberDiff line numberDiff line change
@@ -25,5 +25,27 @@
2525
"mkdirp": "^3.0.1",
2626
"vite": "^6.0.7",
2727
"vitest": "^2.1.6"
28+
},
29+
"contributes": {
30+
"commands": [
31+
{
32+
"command": "docker.cli.context.onChange",
33+
"title": "Callback for Docker CLI context change",
34+
"category": "Docker",
35+
"enablement": "false"
36+
}
37+
],
38+
"configuration": {
39+
"title": "Docker",
40+
"properties": {
41+
"docker.cli.context": {
42+
"type": "string",
43+
"enum": [],
44+
"markdownDescription": "Select the active Docker CLI context:",
45+
"group": "podman-desktop.docker",
46+
"scope": "DockerCompatibility"
47+
}
48+
}
49+
}
2850
}
2951
}

extensions/docker/src/docker-api.ts

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
/**********************************************************************
2+
* Copyright (C) 2025 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
// handle the context information of the docker contexts
20+
// https://docs.docker.com/engine/manage-resources/contexts/
21+
export interface DockerContextInfo {
22+
name: string;
23+
isCurrentContext: boolean;
24+
metadata: {
25+
description: string;
26+
};
27+
endpoints: {
28+
docker: {
29+
host: string;
30+
};
31+
};
32+
}
33+
34+
export const WINDOWS_NPIPE = '//./pipe/docker_engine';
35+
export const UNIX_SOCKET_PATH = '/var/run/docker.sock';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
/**********************************************************************
2+
* Copyright (C) 2024-2025 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import type { Configuration } from '@podman-desktop/api';
20+
import { configuration, context } from '@podman-desktop/api';
21+
import { beforeEach, expect, test, vi } from 'vitest';
22+
23+
import { DockerCompatibilitySetup } from './docker-compatibility-setup.js';
24+
import type { DockerContextHandler } from './docker-context-handler.js';
25+
26+
vi.mock('@podman-desktop/api', async () => {
27+
return {
28+
context: {
29+
setValue: vi.fn(),
30+
},
31+
configuration: {
32+
onDidChangeConfiguration: vi.fn(),
33+
getConfiguration: vi.fn(),
34+
},
35+
env: {
36+
isLinux: false,
37+
isWindows: false,
38+
isMac: false,
39+
},
40+
};
41+
});
42+
43+
const dockerContextHandler = {
44+
listContexts: vi.fn(),
45+
switchContext: vi.fn(),
46+
} as unknown as DockerContextHandler;
47+
48+
let dockerCompatibilitySetup: DockerCompatibilitySetup;
49+
50+
beforeEach(() => {
51+
vi.resetAllMocks();
52+
dockerCompatibilitySetup = new DockerCompatibilitySetup(dockerContextHandler);
53+
});
54+
55+
test('check sending the docker cli contexts as context.setValue', async () => {
56+
// return a list of 2 contexts, second one being the current one
57+
vi.mocked(dockerContextHandler.listContexts).mockResolvedValue([
58+
{
59+
name: 'context1',
60+
metadata: {
61+
description: 'description1',
62+
},
63+
endpoints: {
64+
docker: {
65+
host: 'host1',
66+
},
67+
},
68+
isCurrentContext: false,
69+
},
70+
{
71+
name: 'context2',
72+
metadata: {
73+
description: 'description2',
74+
},
75+
endpoints: {
76+
docker: {
77+
host: 'host2',
78+
},
79+
},
80+
isCurrentContext: true,
81+
},
82+
]);
83+
84+
await dockerCompatibilitySetup.init();
85+
86+
// check we called listContexts
87+
expect(dockerContextHandler.listContexts).toHaveBeenCalled();
88+
89+
// check we called setValue with the expected values
90+
expect(context.setValue).toHaveBeenCalledWith(
91+
'docker.cli.context',
92+
[
93+
{
94+
label: 'context1 (host1)',
95+
selected: false,
96+
value: 'context1',
97+
},
98+
{
99+
label: 'context2 (host2)',
100+
selected: true,
101+
value: 'context2',
102+
},
103+
],
104+
'DockerCompatibility',
105+
);
106+
});
107+
108+
test('check set the context when configuration change', async () => {
109+
// empty list of context
110+
vi.mocked(dockerContextHandler.listContexts).mockResolvedValue([]);
111+
112+
await dockerCompatibilitySetup.init();
113+
114+
// capture the callback sent to onDidChangeConfiguration
115+
const callback = vi.mocked(configuration.onDidChangeConfiguration).mock.calls[0][0];
116+
117+
// mock configuration.getConfiguration
118+
vi.mocked(configuration.getConfiguration).mockReturnValue({
119+
get: vi.fn(() => 'context1'),
120+
} as unknown as Configuration);
121+
122+
// mock switchContext
123+
vi.mocked(dockerContextHandler.switchContext).mockResolvedValue();
124+
125+
// call the callback
126+
callback({ affectsConfiguration: vi.fn(() => true) });
127+
128+
// check we called switchContext
129+
expect(dockerContextHandler.switchContext).toHaveBeenCalledWith('context1');
130+
});
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
/**********************************************************************
2+
* Copyright (C) 2025 Red Hat, Inc.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*
16+
* SPDX-License-Identifier: Apache-2.0
17+
***********************************************************************/
18+
19+
import { configuration, context } from '@podman-desktop/api';
20+
21+
import type { DockerContextHandler } from './docker-context-handler';
22+
23+
// setup the management of the docker contexts
24+
// registering the DockerCompatibility configuration
25+
export class DockerCompatibilitySetup {
26+
#dockerContextHandler: DockerContextHandler;
27+
28+
constructor(dockerContextHandler: DockerContextHandler) {
29+
this.#dockerContextHandler = dockerContextHandler;
30+
}
31+
32+
async init(): Promise<void> {
33+
// get the current contexts
34+
const currentContexts = await this.#dockerContextHandler.listContexts();
35+
36+
// define the enum list
37+
const contextEnumItems = currentContexts.map(context => {
38+
return {
39+
label: `${context.name} (${context.endpoints.docker.host})`,
40+
value: context.name,
41+
selected: context.isCurrentContext,
42+
};
43+
});
44+
context.setValue('docker.cli.context', contextEnumItems, 'DockerCompatibility');
45+
46+
// track the changes operated by the user
47+
configuration.onDidChangeConfiguration(event => {
48+
if (event.affectsConfiguration('docker.cli.context')) {
49+
// get the value
50+
const value = configuration.getConfiguration('docker.cli', 'DockerCompatibility');
51+
const contextName = value.get<string>('context');
52+
53+
if (contextName) {
54+
this.#dockerContextHandler.switchContext(contextName).catch((error: unknown) => {
55+
console.error('Error switching docker context', error);
56+
});
57+
}
58+
}
59+
});
60+
}
61+
}

packages/main/src/plugin/docker/docker-context-handler.spec.ts extensions/docker/src/docker-context-handler.spec.ts

+14-4
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**********************************************************************
2-
* Copyright (C) 2024 Red Hat, Inc.
2+
* Copyright (C) 2024-2025 Red Hat, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -20,13 +20,13 @@ import * as fs from 'node:fs';
2020
import { homedir } from 'node:os';
2121
import { join } from 'node:path';
2222

23+
import { env } from '@podman-desktop/api';
2324
import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2425

25-
import * as util from '../../util.js';
2626
import type { DockerContextParsingInfo } from './docker-context-handler.js';
2727
import { DockerContextHandler } from './docker-context-handler.js';
2828

29-
export class TestDockerContextHandler extends DockerContextHandler {
29+
class TestDockerContextHandler extends DockerContextHandler {
3030
override getDockerConfigPath(): string {
3131
return super.getDockerConfigPath();
3232
}
@@ -43,6 +43,16 @@ export class TestDockerContextHandler extends DockerContextHandler {
4343
// mock exists sync
4444
vi.mock('node:fs');
4545

46+
vi.mock('@podman-desktop/api', async () => {
47+
return {
48+
env: {
49+
isLinux: false,
50+
isWindows: false,
51+
isMac: false,
52+
},
53+
};
54+
});
55+
4656
const originalConsoleError = console.error;
4757
let dockerContextHandler: TestDockerContextHandler;
4858

@@ -174,7 +184,7 @@ describe('getContexts', () => {
174184

175185
test('check default on Windows', async () => {
176186
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
177-
vi.spyOn(util, 'isWindows').mockImplementation(() => true);
187+
vi.mocked(env).isWindows = true;
178188

179189
const contexts = await dockerContextHandler.getContexts();
180190

packages/main/src/plugin/docker/docker-context-handler.ts extensions/docker/src/docker-context-handler.ts

+9-10
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**********************************************************************
2-
* Copyright (C) 2024 Red Hat, Inc.
2+
* Copyright (C) 2024-2025 Red Hat, Inc.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -21,10 +21,9 @@ import { existsSync, promises } from 'node:fs';
2121
import { homedir } from 'node:os';
2222
import { join } from 'node:path';
2323

24-
import { isWindows } from '/@/util.js';
25-
import type { DockerContextInfo } from '/@api/docker-compatibility-info.js';
24+
import { env } from '@podman-desktop/api';
2625

27-
import { DockerCompatibility } from './docker-compatibility.js';
26+
import { type DockerContextInfo, UNIX_SOCKET_PATH, WINDOWS_NPIPE } from './docker-api.js';
2827

2928
// omit current context as it is coming from another source
3029
// disabling the rule as we're not only extending the interface but omitting one field
@@ -41,7 +40,7 @@ export class DockerContextHandler {
4140
}
4241

4342
protected async getCurrentContext(): Promise<string> {
44-
let currentContext: string = 'default';
43+
let currentContext = 'default';
4544

4645
// if $HOME/.docker/config.json exists, read it and get the current context
4746
const dockerConfigExists = existsSync(this.getDockerConfigPath());
@@ -66,8 +65,8 @@ export class DockerContextHandler {
6665
protected async getContexts(): Promise<DockerContextParsingInfo[]> {
6766
const contexts: DockerContextParsingInfo[] = [];
6867

69-
const defaultHostForWindows = `npipe://${DockerCompatibility.WINDOWS_NPIPE}`;
70-
const defaultHostForMacOrLinux = `unix://${DockerCompatibility.UNIX_SOCKET_PATH}`;
68+
const defaultHostForWindows = `npipe://${WINDOWS_NPIPE}`;
69+
const defaultHostForMacOrLinux = `unix://${UNIX_SOCKET_PATH}`;
7170

7271
// adds the default context
7372
contexts.push({
@@ -77,7 +76,7 @@ export class DockerContextHandler {
7776
},
7877
endpoints: {
7978
docker: {
80-
host: isWindows() ? defaultHostForWindows : defaultHostForMacOrLinux,
79+
host: env.isWindows ? defaultHostForWindows : defaultHostForMacOrLinux,
8180
},
8281
},
8382
});
@@ -166,15 +165,15 @@ export class DockerContextHandler {
166165
// now, write the context name to the ~/.docker/config.json file
167166
// read current content
168167
const content = await promises.readFile(this.getDockerConfigPath(), 'utf-8');
169-
let config;
168+
let config: { currentContext?: string };
170169
try {
171170
config = JSON.parse(content);
172171
} catch (error: unknown) {
173172
throw new Error(`Error parsing docker config file: ${String(error)}`);
174173
}
175174
// update the current context or drop the field if it is the default context
176175
if (contextName === 'default') {
177-
delete config.currentContext;
176+
config.currentContext = undefined;
178177
} else {
179178
config.currentContext = contextName;
180179
}

0 commit comments

Comments
 (0)