Skip to content

Commit 28679ef

Browse files
authored
feat: simple no current context pages (#317)
Signed-off-by: Philippe Martin <[email protected]>
1 parent 040c600 commit 28679ef

18 files changed

+433
-56
lines changed

packages/common/src/channels.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import type { PortForwardApi } from './interface/port-forward-api';
2323
import type { SubscribeApi } from './interface/subscribe-api';
2424
import type { SystemApi } from './interface/system-api';
2525
import type { ActiveResourcesCountInfo } from './model/active-resources-count-info';
26+
import type { AvailableContextsInfo } from './model/available-contexts-info';
2627
import type { ContextsHealthsInfo } from './model/contexts-healths-info';
2728
import type { ContextsPermissionsInfo } from './model/contexts-permissions-info';
2829
import type { CurrentContextInfo } from './model/current-context-info';
@@ -50,6 +51,7 @@ export const CONTEXTS_HEALTHS = createRpcChannel<ContextsHealthsInfo>('ContextsH
5051
export const CONTEXTS_PERMISSIONS = createRpcChannel<ContextsPermissionsInfo>('ContextsPermissions');
5152
export const UPDATE_RESOURCE = createRpcChannel<UpdateResourceInfo>('UpdateResource');
5253
export const CURRENT_CONTEXT = createRpcChannel<CurrentContextInfo>('CurrentContext');
54+
export const AVAILABLE_CONTEXTS = createRpcChannel<AvailableContextsInfo>('AvailableContexts');
5355
export const RESOURCE_DETAILS = createRpcChannel<ResourceDetailsInfo>('ResourceDetailsInfo');
5456
export const RESOURCE_EVENTS = createRpcChannel<ResourceEventsInfo>('ResourceEvents');
5557
export const PORT_FORWARDS = createRpcChannel<PortForwardsInfo>('PortForwards');

packages/common/src/interface/contexts-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
export const ContextsApi = Symbol.for('ContextsApi');
2020

2121
export interface ContextsApi {
22+
setCurrentContext(contextName: string): Promise<void>;
2223
refreshContextState(contextName: string): Promise<void>;
2324
deleteObject(kind: string, name: string, namespace?: string): Promise<void>;
2425
deleteObjects(objects: { kind: string; name: string; namespace?: string }[]): Promise<void>;
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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+
export interface AvailableContextsInfo {
20+
contextNames: string[];
21+
}

packages/extension/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
"@kubernetes/client-node": "^1.3.0",
4747
"inversify": "^7.9.1",
4848
"reflect-metadata": "^0.2.2",
49-
"yaml": "^2.8.1"
49+
"yaml": "^2.8.1",
50+
"js-yaml": "^4.1.0"
5051
}
5152
}

packages/extension/src/dispatcher/_dispatcher-module.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { ResourceDetailsDispatcher } from './resource-details-dispatcher';
2828
import { ResourceEventsDispatcher } from './resource-events-dispatcher';
2929
import { PortForwardsDispatcher } from './port-forwards-dispatcher';
3030
import { EndpointsDispatcher } from './endpoints-dispatcher';
31+
import { AvailableContextsDispatcher } from './available-contexts-dispatcher';
3132

3233
const dispatchersModule = new ContainerModule(options => {
3334
options.bind<ActiveResourcesCountDispatcher>(ActiveResourcesCountDispatcher).toSelf().inSingletonScope();
@@ -45,6 +46,9 @@ const dispatchersModule = new ContainerModule(options => {
4546
options.bind<CurrentContextDispatcher>(CurrentContextDispatcher).toSelf().inSingletonScope();
4647
options.bind(DispatcherObject).toService(CurrentContextDispatcher);
4748

49+
options.bind<AvailableContextsDispatcher>(AvailableContextsDispatcher).toSelf().inSingletonScope();
50+
options.bind(DispatcherObject).toService(AvailableContextsDispatcher);
51+
4852
options.bind<UpdateResourceDispatcher>(UpdateResourceDispatcher).toSelf().inSingletonScope();
4953
options.bind(DispatcherObject).toService(UpdateResourceDispatcher);
5054

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
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 { inject, injectable } from 'inversify';
20+
import type { DispatcherObject } from './util/dispatcher-object';
21+
import { AbsDispatcherObjectImpl } from './util/dispatcher-object';
22+
import { AVAILABLE_CONTEXTS } from '/@common/channels';
23+
import { RpcExtension } from '/@common/rpc/rpc';
24+
import { AvailableContextsInfo } from '/@common/model/available-contexts-info';
25+
import { ContextsManager } from '/@/manager/contexts-manager';
26+
27+
@injectable()
28+
export class AvailableContextsDispatcher
29+
extends AbsDispatcherObjectImpl<void, AvailableContextsInfo>
30+
implements DispatcherObject<void>
31+
{
32+
constructor(
33+
@inject(RpcExtension) rpcExtension: RpcExtension,
34+
@inject(ContextsManager) private manager: ContextsManager,
35+
) {
36+
super(rpcExtension, AVAILABLE_CONTEXTS);
37+
}
38+
39+
getData(): AvailableContextsInfo {
40+
return {
41+
contextNames: this.manager.getContextsNames(),
42+
};
43+
}
44+
}

packages/extension/src/manager/contexts-manager.spec.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,9 @@
1818

1919
import type { Cluster, CoreV1Event, KubernetesObject, ObjectCache, V1Status } from '@kubernetes/client-node';
2020
import { ApiException, KubeConfig } from '@kubernetes/client-node';
21-
import type { Event } from '@podman-desktop/api';
21+
import type { Event, Uri } from '@podman-desktop/api';
2222
import { afterEach, assert, beforeEach, describe, expect, test, vi } from 'vitest';
23-
import { window } from '@podman-desktop/api';
23+
import { kubernetes, window } from '@podman-desktop/api';
2424

2525
import type { ContextHealthState } from './context-health-checker.js';
2626
import { ContextHealthChecker } from './context-health-checker.js';
@@ -39,6 +39,7 @@ import type {
3939
OfflineEvent,
4040
ResourceInformer,
4141
} from '/@/types/resource-informer.js';
42+
import { vol } from 'memfs';
4243

4344
const onCacheUpdatedMock = vi.fn<Event<CacheUpdatedEvent>>();
4445
const onOfflineMock = vi.fn<Event<OfflineEvent>>();
@@ -278,6 +279,8 @@ const kcWithNoCurrentContext = {
278279
],
279280
};
280281

282+
vi.mock('node:fs/promises');
283+
vi.mock('node:fs');
281284
vi.mock('./context-health-checker.js');
282285
vi.mock('./context-permissions-checker.js');
283286

@@ -287,6 +290,7 @@ const originalConsoleWarn = console.warn;
287290
const originalConsoleError = console.error;
288291

289292
beforeEach(() => {
293+
vol.reset();
290294
vi.clearAllMocks();
291295
kcWith2contexts = {
292296
contexts: [
@@ -405,6 +409,12 @@ describe('HealthChecker is built and start is called for each context the first
405409

406410
expect(permissionsStartMock).toHaveBeenCalledTimes(2);
407411
});
412+
413+
test('getContextsNames returns the correct contexts names', async () => {
414+
await manager.update(kc);
415+
const contextsNames = manager.getContextsNames();
416+
expect(contextsNames).toEqual(['context1', 'context2']);
417+
});
408418
});
409419

410420
describe('HealthChecker pass and PermissionsChecker resturns a value', async () => {
@@ -1398,6 +1408,28 @@ test('get currentContext', async () => {
13981408
expect(currentContext?.getKubeConfig().currentContext).toEqual('context1');
13991409
});
14001410

1411+
test('setCurrentContext sets the current context', async () => {
1412+
vol.fromJSON({
1413+
'/path/to/config': JSON.stringify(kcWith2contexts),
1414+
});
1415+
const kc = new KubeConfig();
1416+
kc.loadFromOptions(kcWith2contexts);
1417+
const manager = new TestContextsManager();
1418+
vi.spyOn(manager, 'startMonitoring').mockImplementation(async (): Promise<void> => {});
1419+
vi.spyOn(manager, 'stopMonitoring').mockImplementation((): void => {});
1420+
await manager.update(kc);
1421+
const currentContext = manager.currentContext;
1422+
expect(currentContext?.getKubeConfig().currentContext).toEqual('context1');
1423+
1424+
vi.mocked(kubernetes.getKubeconfig).mockReturnValue({
1425+
path: '/path/to/config',
1426+
} as Uri);
1427+
1428+
await manager.setCurrentContext('context2');
1429+
const content = vol.readFileSync('/path/to/config', 'utf-8');
1430+
expect(content).toContain('current-context: context2');
1431+
});
1432+
14011433
test('onCurrentContextChange is fired', async () => {
14021434
const kc = new KubeConfig();
14031435
kc.loadFromOptions(kcWith2contexts);

packages/extension/src/manager/contexts-manager.ts

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,8 @@ import {
3232
import type { ContextPermission } from '/@common/model/kubernetes-contexts-permissions.js';
3333
import type { ResourceCount } from '/@common/model/kubernetes-resource-count.js';
3434
import type { KubernetesTroubleshootingInformation } from '/@common/model/kubernetes-troubleshooting.js';
35-
import { window } from '@podman-desktop/api';
35+
import { kubernetes, window } from '@podman-desktop/api';
36+
import * as jsYaml from 'js-yaml';
3637

3738
import type { Event } from '/@/types/emitter.js';
3839
import { Emitter } from '/@/types/emitter.js';
@@ -72,6 +73,7 @@ import { TargetRef } from '/@common/model/target-ref.js';
7273
import { Endpoint } from '/@common/model/endpoint.js';
7374
import { V1Route } from '/@common/model/openshift-types.js';
7475
import { parseAllDocuments, stringify, type Tags } from 'yaml';
76+
import { writeFile } from 'node:fs/promises';
7577

7678
const HEALTH_CHECK_TIMEOUT_MS = 5_000;
7779
const DEFAULT_NAMESPACE = 'default';
@@ -108,6 +110,9 @@ export class ContextsManager {
108110
#onContextDelete = new Emitter<DispatcherEvent>();
109111
onContextDelete: Event<DispatcherEvent> = this.#onContextDelete.event;
110112

113+
#onContextAdd = new Emitter<DispatcherEvent>();
114+
onContextAdd: Event<DispatcherEvent> = this.#onContextAdd.event;
115+
111116
#onResourceUpdated = new Emitter<{ contextName: string; resourceName: string }>();
112117
onResourceUpdated: Event<{ contextName: string; resourceName: string }> = this.#onResourceUpdated.event;
113118

@@ -139,6 +144,7 @@ export class ContextsManager {
139144
this.#dispatcher.onUpdate(this.onUpdate.bind(this));
140145
this.#dispatcher.onDelete(this.onDelete.bind(this));
141146
this.#dispatcher.onDelete((state: DispatcherEvent) => this.#onContextDelete.fire(state));
147+
this.#dispatcher.onAdd((state: DispatcherEvent) => this.#onContextAdd.fire(state));
142148
this.#dispatcher.onCurrentChange(this.onCurrentChange.bind(this));
143149
}
144150

@@ -835,4 +841,16 @@ export class ContextsManager {
835841
}
836842
return tags;
837843
}
844+
845+
getContextsNames(): string[] {
846+
return this.#currentKubeConfig.contexts.map(ctx => ctx.name);
847+
}
848+
849+
async setCurrentContext(contextName: string): Promise<void> {
850+
this.#currentKubeConfig.currentContext = contextName;
851+
const jsonString = this.#currentKubeConfig.exportConfig();
852+
const yamlString = jsYaml.dump(JSON.parse(jsonString));
853+
const kubeconfigUri = kubernetes.getKubeconfig();
854+
await writeFile(kubeconfigUri.path, yamlString);
855+
}
838856
}

packages/extension/src/manager/contexts-states-dispatcher.spec.ts

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ import { ContextsHealthsDispatcher } from '/@/dispatcher/contexts-healths-dispat
3535
import { ContextsPermissionsDispatcher } from '/@/dispatcher/contexts-permissions-dispatcher.js';
3636
import {
3737
ACTIVE_RESOURCES_COUNT,
38+
AVAILABLE_CONTEXTS,
3839
CONTEXTS_HEALTHS,
3940
CONTEXTS_PERMISSIONS,
4041
CURRENT_CONTEXT,
@@ -51,6 +52,7 @@ const contextsManagerMock: ContextsManager = {
5152
onOfflineChange: vi.fn(),
5253
onContextPermissionResult: vi.fn(),
5354
onContextDelete: vi.fn(),
55+
onContextAdd: vi.fn(),
5456
getHealthCheckersStates: vi.fn(),
5557
getPermissions: vi.fn(),
5658
onResourceCountUpdated: vi.fn(),
@@ -143,10 +145,24 @@ test('ContextsStatesDispatcher should call updateHealthStates and updatePermissi
143145
vi.mocked(contextsManagerMock.onContextDelete).mockImplementation(f => f({} as DispatcherEvent) as IDisposable);
144146
dispatcher.init();
145147
await vi.waitFor(() => {
146-
expect(dispatcherSpy).toHaveBeenCalledTimes(2);
148+
expect(dispatcherSpy).toHaveBeenCalledTimes(3);
147149
});
148150
expect(dispatcherSpy).toHaveBeenCalledWith(CONTEXTS_HEALTHS);
149151
expect(dispatcherSpy).toHaveBeenCalledWith(CONTEXTS_PERMISSIONS);
152+
expect(dispatcherSpy).toHaveBeenCalledWith(AVAILABLE_CONTEXTS);
153+
});
154+
155+
test('ContextsStatesDispatcher should dispatchavailable contexts when onContextAdd event is fired', async () => {
156+
const dispatcherSpy = vi.spyOn(dispatcher, 'dispatch').mockResolvedValue();
157+
dispatcher.init();
158+
expect(dispatcherSpy).not.toHaveBeenCalled();
159+
160+
vi.mocked(contextsManagerMock.onContextAdd).mockImplementation(f => f({} as DispatcherEvent) as IDisposable);
161+
dispatcher.init();
162+
await vi.waitFor(() => {
163+
expect(dispatcherSpy).toHaveBeenCalledTimes(1);
164+
});
165+
expect(dispatcherSpy).toHaveBeenCalledWith(AVAILABLE_CONTEXTS);
150166
});
151167

152168
test('ContextsStatesDispatcher should call updateResource and updateActiveResourcesCount when onResourceUpdated event is fired', async () => {

packages/extension/src/manager/contexts-states-dispatcher.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ import { ContextsManager } from './contexts-manager.js';
2525
import { RpcChannel } from '/@common/rpc/rpc.js';
2626
import {
2727
ACTIVE_RESOURCES_COUNT,
28+
AVAILABLE_CONTEXTS,
2829
CONTEXTS_HEALTHS,
2930
CONTEXTS_PERMISSIONS,
3031
CURRENT_CONTEXT,
@@ -73,6 +74,10 @@ export class ContextsStatesDispatcher extends ChannelSubscriber implements Subsc
7374
this.manager.onContextDelete(async (_state: DispatcherEvent) => {
7475
await this.dispatch(CONTEXTS_HEALTHS);
7576
await this.dispatch(CONTEXTS_PERMISSIONS);
77+
await this.dispatch(AVAILABLE_CONTEXTS);
78+
});
79+
this.manager.onContextAdd(async (_state: DispatcherEvent) => {
80+
await this.dispatch(AVAILABLE_CONTEXTS);
7681
});
7782
this.manager.onResourceCountUpdated(async () => {
7883
await this.dispatch(RESOURCES_COUNT);

0 commit comments

Comments
 (0)