Skip to content

Commit

Permalink
chore: move docker cli context select implem from core to docker exte…
Browse files Browse the repository at this point in the history
…nsion (#10526)

* chore: move docker cli context select implem from core to docker extension

fixes podman-desktop/podman-desktop#10517
Signed-off-by: Florent Benoit <[email protected]>
  • Loading branch information
benoitf authored Jan 7, 2025
1 parent 8b8494e commit 46e6c0b
Show file tree
Hide file tree
Showing 13 changed files with 285 additions and 267 deletions.
22 changes: 22 additions & 0 deletions extensions/docker/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,27 @@
"mkdirp": "^3.0.1",
"vite": "^6.0.7",
"vitest": "^2.1.6"
},
"contributes": {
"commands": [
{
"command": "docker.cli.context.onChange",
"title": "Callback for Docker CLI context change",
"category": "Docker",
"enablement": "false"
}
],
"configuration": {
"title": "Docker",
"properties": {
"docker.cli.context": {
"type": "string",
"enum": [],
"markdownDescription": "Select the active Docker CLI context:",
"group": "podman-desktop.docker",
"scope": "DockerCompatibility"
}
}
}
}
}
35 changes: 35 additions & 0 deletions extensions/docker/src/docker-api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**********************************************************************
* Copyright (C) 2025 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

// handle the context information of the docker contexts
// https://docs.docker.com/engine/manage-resources/contexts/
export interface DockerContextInfo {
name: string;
isCurrentContext: boolean;
metadata: {
description: string;
};
endpoints: {
docker: {
host: string;
};
};
}

export const WINDOWS_NPIPE = '//./pipe/docker_engine';
export const UNIX_SOCKET_PATH = '/var/run/docker.sock';
130 changes: 130 additions & 0 deletions extensions/docker/src/docker-compatibility-setup.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
/**********************************************************************
* Copyright (C) 2024-2025 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import type { Configuration } from '@podman-desktop/api';
import { configuration, context } from '@podman-desktop/api';
import { beforeEach, expect, test, vi } from 'vitest';

import { DockerCompatibilitySetup } from './docker-compatibility-setup.js';
import type { DockerContextHandler } from './docker-context-handler.js';

vi.mock('@podman-desktop/api', async () => {
return {
context: {
setValue: vi.fn(),
},
configuration: {
onDidChangeConfiguration: vi.fn(),
getConfiguration: vi.fn(),
},
env: {
isLinux: false,
isWindows: false,
isMac: false,
},
};
});

const dockerContextHandler = {
listContexts: vi.fn(),
switchContext: vi.fn(),
} as unknown as DockerContextHandler;

let dockerCompatibilitySetup: DockerCompatibilitySetup;

beforeEach(() => {
vi.resetAllMocks();
dockerCompatibilitySetup = new DockerCompatibilitySetup(dockerContextHandler);
});

test('check sending the docker cli contexts as context.setValue', async () => {
// return a list of 2 contexts, second one being the current one
vi.mocked(dockerContextHandler.listContexts).mockResolvedValue([
{
name: 'context1',
metadata: {
description: 'description1',
},
endpoints: {
docker: {
host: 'host1',
},
},
isCurrentContext: false,
},
{
name: 'context2',
metadata: {
description: 'description2',
},
endpoints: {
docker: {
host: 'host2',
},
},
isCurrentContext: true,
},
]);

await dockerCompatibilitySetup.init();

// check we called listContexts
expect(dockerContextHandler.listContexts).toHaveBeenCalled();

// check we called setValue with the expected values
expect(context.setValue).toHaveBeenCalledWith(
'docker.cli.context',
[
{
label: 'context1 (host1)',
selected: false,
value: 'context1',
},
{
label: 'context2 (host2)',
selected: true,
value: 'context2',
},
],
'DockerCompatibility',
);
});

test('check set the context when configuration change', async () => {
// empty list of context
vi.mocked(dockerContextHandler.listContexts).mockResolvedValue([]);

await dockerCompatibilitySetup.init();

// capture the callback sent to onDidChangeConfiguration
const callback = vi.mocked(configuration.onDidChangeConfiguration).mock.calls[0][0];

// mock configuration.getConfiguration
vi.mocked(configuration.getConfiguration).mockReturnValue({
get: vi.fn(() => 'context1'),
} as unknown as Configuration);

// mock switchContext
vi.mocked(dockerContextHandler.switchContext).mockResolvedValue();

// call the callback
callback({ affectsConfiguration: vi.fn(() => true) });

// check we called switchContext
expect(dockerContextHandler.switchContext).toHaveBeenCalledWith('context1');
});
61 changes: 61 additions & 0 deletions extensions/docker/src/docker-compatibility-setup.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
/**********************************************************************
* Copyright (C) 2025 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-License-Identifier: Apache-2.0
***********************************************************************/

import { configuration, context } from '@podman-desktop/api';

import type { DockerContextHandler } from './docker-context-handler';

// setup the management of the docker contexts
// registering the DockerCompatibility configuration
export class DockerCompatibilitySetup {
#dockerContextHandler: DockerContextHandler;

constructor(dockerContextHandler: DockerContextHandler) {
this.#dockerContextHandler = dockerContextHandler;
}

async init(): Promise<void> {
// get the current contexts
const currentContexts = await this.#dockerContextHandler.listContexts();

// define the enum list
const contextEnumItems = currentContexts.map(context => {
return {
label: `${context.name} (${context.endpoints.docker.host})`,
value: context.name,
selected: context.isCurrentContext,
};
});
context.setValue('docker.cli.context', contextEnumItems, 'DockerCompatibility');

// track the changes operated by the user
configuration.onDidChangeConfiguration(event => {
if (event.affectsConfiguration('docker.cli.context')) {
// get the value
const value = configuration.getConfiguration('docker.cli', 'DockerCompatibility');
const contextName = value.get<string>('context');

if (contextName) {
this.#dockerContextHandler.switchContext(contextName).catch((error: unknown) => {
console.error('Error switching docker context', error);
});
}
}
});
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
/**********************************************************************
* Copyright (C) 2024 Red Hat, Inc.
* Copyright (C) 2024-2025 Red Hat, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand All @@ -20,13 +20,13 @@ import * as fs from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';

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

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

export class TestDockerContextHandler extends DockerContextHandler {
class TestDockerContextHandler extends DockerContextHandler {
override getDockerConfigPath(): string {
return super.getDockerConfigPath();
}
Expand All @@ -43,6 +43,16 @@ export class TestDockerContextHandler extends DockerContextHandler {
// mock exists sync
vi.mock('node:fs');

vi.mock('@podman-desktop/api', async () => {
return {
env: {
isLinux: false,
isWindows: false,
isMac: false,
},
};
});

const originalConsoleError = console.error;
let dockerContextHandler: TestDockerContextHandler;

Expand Down Expand Up @@ -174,7 +184,7 @@ describe('getContexts', () => {

test('check default on Windows', async () => {
vi.spyOn(fs, 'existsSync').mockReturnValue(false);
vi.spyOn(util, 'isWindows').mockImplementation(() => true);
vi.mocked(env).isWindows = true;

const contexts = await dockerContextHandler.getContexts();

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

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

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

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

protected async getCurrentContext(): Promise<string> {
let currentContext: string = 'default';
let currentContext = 'default';

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

const defaultHostForWindows = `npipe://${DockerCompatibility.WINDOWS_NPIPE}`;
const defaultHostForMacOrLinux = `unix://${DockerCompatibility.UNIX_SOCKET_PATH}`;
const defaultHostForWindows = `npipe://${WINDOWS_NPIPE}`;
const defaultHostForMacOrLinux = `unix://${UNIX_SOCKET_PATH}`;

// adds the default context
contexts.push({
Expand All @@ -77,7 +76,7 @@ export class DockerContextHandler {
},
endpoints: {
docker: {
host: isWindows() ? defaultHostForWindows : defaultHostForMacOrLinux,
host: env.isWindows ? defaultHostForWindows : defaultHostForMacOrLinux,
},
},
});
Expand Down Expand Up @@ -166,15 +165,15 @@ export class DockerContextHandler {
// now, write the context name to the ~/.docker/config.json file
// read current content
const content = await promises.readFile(this.getDockerConfigPath(), 'utf-8');
let config;
let config: { currentContext?: string };
try {
config = JSON.parse(content);
} catch (error: unknown) {
throw new Error(`Error parsing docker config file: ${String(error)}`);
}
// update the current context or drop the field if it is the default context
if (contextName === 'default') {
delete config.currentContext;
config.currentContext = undefined;
} else {
config.currentContext = contextName;
}
Expand Down
Loading

0 comments on commit 46e6c0b

Please sign in to comment.