Skip to content

Commit 5d9cde3

Browse files
authored
feat: start and listen monitoring (#10)
Signed-off-by: Philippe Martin <[email protected]>
1 parent f7d7aef commit 5d9cde3

File tree

9 files changed

+229
-26
lines changed

9 files changed

+229
-26
lines changed

__mocks__/@podman-desktop/api.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
***********************************************************************/
1818
import { vi } from 'vitest';
1919
import { EventEmitter } from 'node:events';
20-
import { parse } from 'node:path';
2120

2221
/**
2322
* Mock the extension API for vitest.
@@ -75,6 +74,10 @@ const plugin = {
7574
process: {
7675
exec: vi.fn(),
7776
},
77+
kubernetes: {
78+
onDidUpdateKubeconfig: vi.fn(),
79+
getKubeconfig: vi.fn(),
80+
}
7881
};
7982

8083
module.exports = plugin;
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const { fs } = require('memfs')
2+
module.exports = fs
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
const { fs } = require('memfs')
2+
module.exports = fs.promises

packages/extension/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
"eslint-plugin-no-null": "^1.0.2",
3535
"eslint-plugin-redundant-undefined": "^1.0.0",
3636
"eslint-plugin-sonarjs": "^3.0.2",
37+
"memfs": "^4.17.2",
3738
"prettier": "^3.6.1",
3839
"typescript": "5.8.3",
3940
"vite": "^7.0",

packages/extension/src/dashboard-extension.spec.ts

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -17,42 +17,101 @@
1717
***********************************************************************/
1818

1919
import type { WebviewPanel, ExtensionContext } from '@podman-desktop/api';
20-
import { Uri, window } from '@podman-desktop/api';
21-
import { beforeEach, test, vi } from 'vitest';
20+
import { kubernetes, Uri, window } from '@podman-desktop/api';
21+
import { assert, beforeEach, describe, expect, test, vi } from 'vitest';
2222
import { DashboardExtension } from './dashboard-extension';
23+
import { vol } from 'memfs';
2324

24-
import { readFile } from 'node:fs/promises';
25+
import type { ContextsManager } from './manager/contexts-manager';
26+
import type { ContextsStatesDispatcher } from './manager/contexts-states-dispatcher';
2527

2628
let extensionContextMock: ExtensionContext;
2729
let dashboardExtension: DashboardExtension;
30+
let contextsManagerMock: ContextsManager;
31+
let contextsStatesDispatcher: ContextsStatesDispatcher;
2832

2933
vi.mock(import('node:fs'));
3034
vi.mock(import('node:fs/promises'));
35+
vi.mock(import('@kubernetes/client-node'));
3136

3237
beforeEach(() => {
3338
vi.restoreAllMocks();
3439
vi.resetAllMocks();
40+
vol.reset();
3541

3642
vi.mocked(window.createWebviewPanel).mockReturnValue({
3743
webview: {
3844
html: '',
3945
onDidReceiveMessage: vi.fn(),
4046
},
4147
} as unknown as WebviewPanel);
42-
vi.mocked(Uri.joinPath).mockReturnValue({ fsPath: '' } as unknown as Uri);
43-
vi.mocked(readFile).mockResolvedValue('<html></html>');
48+
vi.mocked(Uri.joinPath).mockReturnValue({ fsPath: '/path/to/extension/index.html' } as unknown as Uri);
4449
// Create a mock for the ExtensionContext
4550
extensionContextMock = {
4651
subscriptions: [],
4752
} as unknown as ExtensionContext;
48-
dashboardExtension = new DashboardExtension(extensionContextMock);
53+
// Create a mock for the contextsManager
54+
contextsManagerMock = {
55+
update: vi.fn(),
56+
} as unknown as ContextsManager;
57+
contextsStatesDispatcher = {
58+
init: vi.fn(),
59+
} as unknown as ContextsStatesDispatcher;
60+
dashboardExtension = new DashboardExtension(extensionContextMock, contextsManagerMock, contextsStatesDispatcher);
61+
vi.mocked(kubernetes.getKubeconfig).mockReturnValue({
62+
path: '/path/to/kube/config',
63+
} as Uri);
4964
});
5065

51-
test('should activate correctly', async () => {
52-
await dashboardExtension.activate();
66+
describe('a kubeconfig file is not present', () => {
67+
beforeEach(() => {
68+
vol.fromJSON({
69+
'/path/to/extension/index.html': '<html></html>',
70+
});
71+
});
72+
73+
test('should activate correctly and calls contextsManager every time the kubeconfig file changes', async () => {
74+
await dashboardExtension.activate();
75+
expect(contextsManagerMock.update).not.toHaveBeenCalled();
76+
77+
const callback = vi.mocked(kubernetes.onDidUpdateKubeconfig).mock.lastCall?.[0];
78+
assert(callback);
79+
vi.mocked(contextsManagerMock.update).mockClear();
80+
callback({ type: 'UPDATE', location: { path: '/path/to/kube/config' } as Uri });
81+
expect(contextsManagerMock.update).toHaveBeenCalledOnce();
82+
83+
expect(contextsStatesDispatcher.init).toHaveBeenCalledOnce();
84+
});
85+
86+
test('should deactivate correctly', async () => {
87+
await dashboardExtension.activate();
88+
await dashboardExtension.deactivate();
89+
});
5390
});
5491

55-
test('should deactivate correctly', async () => {
56-
await dashboardExtension.activate();
57-
await dashboardExtension.deactivate();
92+
describe('a kubeconfig file is present', () => {
93+
beforeEach(() => {
94+
vol.fromJSON({
95+
'/path/to/extension/index.html': '<html></html>',
96+
'/path/to/kube/config': '{}',
97+
});
98+
});
99+
100+
test('should activate correctly and calls contextsManager every time the kubeconfig file changes', async () => {
101+
await dashboardExtension.activate();
102+
expect(contextsManagerMock.update).toHaveBeenCalledOnce();
103+
104+
const callback = vi.mocked(kubernetes.onDidUpdateKubeconfig).mock.lastCall?.[0];
105+
assert(callback);
106+
vi.mocked(contextsManagerMock.update).mockClear();
107+
callback({ type: 'UPDATE', location: { path: '/path/to/kube/config' } as Uri });
108+
expect(contextsManagerMock.update).toHaveBeenCalledOnce();
109+
110+
expect(contextsStatesDispatcher.init).toHaveBeenCalledOnce();
111+
});
112+
113+
test('should deactivate correctly', async () => {
114+
await dashboardExtension.activate();
115+
await dashboardExtension.deactivate();
116+
});
58117
});

packages/extension/src/dashboard-extension.ts

Lines changed: 65 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,55 @@
1616
* SPDX-License-Identifier: Apache-2.0
1717
***********************************************************************/
1818

19-
import { Uri, window, type ExtensionContext } from '@podman-desktop/api';
19+
import type { WebviewPanel, ExtensionContext, KubeconfigUpdateEvent } from '@podman-desktop/api';
20+
import { kubernetes, Uri, window } from '@podman-desktop/api';
2021

2122
import { RpcExtension } from '/@common/rpc/rpc';
2223

2324
import { readFile } from 'node:fs/promises';
25+
import type { ContextsManager } from './manager/contexts-manager';
26+
import { existsSync } from 'node:fs';
27+
import { KubeConfig } from '@kubernetes/client-node';
28+
import type { ContextsStatesDispatcher } from './manager/contexts-states-dispatcher';
2429

2530
export class DashboardExtension {
2631
#extensionContext: ExtensionContext;
27-
28-
constructor(readonly extensionContext: ExtensionContext) {
32+
#contextsManager: ContextsManager;
33+
#contextsStatesDispatcher: ContextsStatesDispatcher;
34+
35+
constructor(
36+
readonly extensionContext: ExtensionContext,
37+
readonly contextManager: ContextsManager,
38+
readonly contextsStatesDispatcher: ContextsStatesDispatcher,
39+
) {
2940
this.#extensionContext = extensionContext;
41+
this.#contextsManager = contextManager;
42+
this.#contextsStatesDispatcher = contextsStatesDispatcher;
3043
}
3144

3245
async activate(): Promise<void> {
46+
const panel = await this.createWebview();
47+
48+
// Register webview communication for this webview
49+
const rpcExtension = new RpcExtension(panel.webview);
50+
rpcExtension.init();
51+
this.#extensionContext.subscriptions.push(rpcExtension);
52+
53+
const now = performance.now();
54+
55+
const afterFirst = performance.now();
56+
57+
console.log('activation time:', afterFirst - now);
58+
59+
await this.listenMonitoring();
60+
await this.startMonitoring();
61+
}
62+
63+
async deactivate(): Promise<void> {
64+
console.log('deactivating Kubernetes Dashboard extension');
65+
}
66+
67+
private async createWebview(): Promise<WebviewPanel> {
3368
const panel = window.createWebviewPanel('kubernetes-dashboard', 'Kubernetes', {
3469
localResourceRoots: [Uri.joinPath(this.#extensionContext.extensionUri, 'media')],
3570
});
@@ -70,19 +105,37 @@ export class DashboardExtension {
70105
// Update the webview panel with the new index.html file with corrected links.
71106
panel.webview.html = indexHtml;
72107

73-
// Register webview communication for this webview
74-
const rpcExtension = new RpcExtension(panel.webview);
75-
rpcExtension.init();
76-
this.#extensionContext.subscriptions.push(rpcExtension);
108+
return panel;
109+
}
77110

78-
const now = performance.now();
111+
private async listenMonitoring(): Promise<void> {
112+
this.#contextsStatesDispatcher.init();
113+
}
79114

80-
const afterFirst = performance.now();
115+
private async startMonitoring(): Promise<void> {
116+
this.#extensionContext.subscriptions.push(this.#contextsManager);
81117

82-
console.log('activation time:', afterFirst - now);
118+
const kubeconfigWatcher = kubernetes.onDidUpdateKubeconfig(this.onKubeconfigUpdate.bind(this));
119+
this.#extensionContext.subscriptions.push(kubeconfigWatcher);
120+
121+
// initial state is not sent by watcher, let's get it explicitely
122+
const kubeconfig = kubernetes.getKubeconfig();
123+
if (existsSync(kubeconfig.path)) {
124+
await this.onKubeconfigUpdate({
125+
location: kubeconfig,
126+
type: 'CREATE',
127+
});
128+
}
83129
}
84130

85-
async deactivate(): Promise<void> {
86-
console.log('deactivating Kubernetes Dashboard extension');
131+
private async onKubeconfigUpdate(event: KubeconfigUpdateEvent): Promise<void> {
132+
if (event.type === 'DELETE') {
133+
// update with an empty KubeConfig
134+
await this.#contextsManager.update(new KubeConfig());
135+
return;
136+
}
137+
const kubeConfig = new KubeConfig();
138+
kubeConfig.loadFromFile(event.location.path);
139+
await this.#contextsManager.update(kubeConfig);
87140
}
88141
}

packages/extension/src/main.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,12 +19,21 @@
1919
import type { ExtensionContext } from '@podman-desktop/api';
2020

2121
import { DashboardExtension } from './dashboard-extension';
22+
import { ContextsManager } from './manager/contexts-manager';
23+
import { ContextsStatesDispatcher } from './manager/contexts-states-dispatcher';
2224

2325
let dashboardExtension: DashboardExtension | undefined;
2426

2527
// Initialize the activation of the extension.
2628
export async function activate(extensionContext: ExtensionContext): Promise<void> {
27-
dashboardExtension ??= new DashboardExtension(extensionContext);
29+
const contextsManager = new ContextsManager();
30+
const apiSender = {
31+
send: (channel: string, data?: unknown): void => {
32+
console.log(`==> recv data "${data}" on channel ${channel}`);
33+
},
34+
};
35+
const contextsStatesDispatcher = new ContextsStatesDispatcher(contextsManager, apiSender);
36+
dashboardExtension ??= new DashboardExtension(extensionContext, contextsManager, contextsStatesDispatcher);
2837

2938
await dashboardExtension.activate();
3039
}

0 commit comments

Comments
 (0)