Skip to content

Commit 66a55de

Browse files
committed
feat: add contexts-state-dispatcher
Signed-off-by: Philippe Martin <[email protected]>
1 parent abb0325 commit 66a55de

File tree

4 files changed

+469
-0
lines changed

4 files changed

+469
-0
lines changed
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
/**********************************************************************
2+
* Copyright (C) 2022-2024 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 type ApiSenderType = {
20+
send: (channel: string, data?: unknown) => void;
21+
};
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
/**********************************************************************
2+
* Copyright (C) 2024 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 ContextHealth {
20+
contextName: string;
21+
// is the health of the cluster being checked?
22+
checking: boolean;
23+
// was the health check successful?
24+
reachable: boolean;
25+
// is one of the informers marked offline (disconnect after being connected, the cache still being populated)
26+
offline: boolean;
27+
// description in case of error (other than health check)
28+
// currently detected errors:
29+
// - user.exec.command not found
30+
errorMessage?: string;
31+
}
Lines changed: 308 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,308 @@
1+
/**********************************************************************
2+
* Copyright (C) 2024 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 { expect, test, vi } from 'vitest';
20+
21+
import type { IDisposable } from '../types/disposable.js';
22+
import type { ContextPermission } from '/@common/model/kubernetes-contexts-permissions.js';
23+
24+
import type { ApiSenderType } from '/@common/model/api-sender.js';
25+
import type { ContextHealthState } from './context-health-checker.js';
26+
import type { ContextPermissionResult } from './context-permissions-checker.js';
27+
import type { DispatcherEvent } from './contexts-dispatcher.js';
28+
import type { ContextsManager } from './contexts-manager.js';
29+
import { ContextsStatesDispatcher } from './contexts-states-dispatcher.js';
30+
import type { KubeConfigSingleContext } from '../types/kubeconfig-single-context.js';
31+
32+
test('ContextsStatesDispatcher should call updateHealthStates when onContextHealthStateChange event is fired', () => {
33+
const manager: ContextsManager = {
34+
onContextHealthStateChange: vi.fn(),
35+
onOfflineChange: vi.fn(),
36+
onContextPermissionResult: vi.fn(),
37+
onContextDelete: vi.fn(),
38+
getHealthCheckersStates: vi.fn(),
39+
getPermissions: vi.fn(),
40+
onResourceCountUpdated: vi.fn(),
41+
onResourceUpdated: vi.fn(),
42+
isContextOffline: vi.fn(),
43+
} as unknown as ContextsManager;
44+
const apiSender: ApiSenderType = {
45+
send: vi.fn(),
46+
} as unknown as ApiSenderType;
47+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
48+
const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates');
49+
const updatePermissionsSpy = vi.spyOn(dispatcher, 'updatePermissions');
50+
dispatcher.init();
51+
expect(updateHealthStatesSpy).not.toHaveBeenCalled();
52+
expect(updatePermissionsSpy).not.toHaveBeenCalled();
53+
54+
vi.mocked(manager.onContextHealthStateChange).mockImplementation(f => f({} as ContextHealthState) as IDisposable);
55+
vi.mocked(manager.getHealthCheckersStates).mockReturnValue(new Map<string, ContextHealthState>());
56+
dispatcher.init();
57+
expect(updateHealthStatesSpy).toHaveBeenCalled();
58+
expect(updatePermissionsSpy).not.toHaveBeenCalled();
59+
});
60+
61+
test('ContextsStatesDispatcher should call updateHealthStates, updateResourcesCount and updateActiveResourcesCount when onOfflineChange event is fired', () => {
62+
const manager: ContextsManager = {
63+
onContextHealthStateChange: vi.fn(),
64+
onOfflineChange: vi.fn(),
65+
onContextPermissionResult: vi.fn(),
66+
onContextDelete: vi.fn(),
67+
getHealthCheckersStates: vi.fn(),
68+
getPermissions: vi.fn(),
69+
onResourceCountUpdated: vi.fn(),
70+
onResourceUpdated: vi.fn(),
71+
isContextOffline: vi.fn(),
72+
} as unknown as ContextsManager;
73+
const apiSender: ApiSenderType = {
74+
send: vi.fn(),
75+
} as unknown as ApiSenderType;
76+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
77+
const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates');
78+
const updateResourcesCountSpy = vi.spyOn(dispatcher, 'updateResourcesCount');
79+
const updateActiveResourcesCountSpy = vi.spyOn(dispatcher, 'updateActiveResourcesCount');
80+
dispatcher.init();
81+
expect(updateHealthStatesSpy).not.toHaveBeenCalled();
82+
expect(updateResourcesCountSpy).not.toHaveBeenCalled();
83+
expect(updateActiveResourcesCountSpy).not.toHaveBeenCalled();
84+
85+
vi.mocked(manager.onOfflineChange).mockImplementation(f => f() as IDisposable);
86+
vi.mocked(manager.getHealthCheckersStates).mockReturnValue(new Map<string, ContextHealthState>());
87+
dispatcher.init();
88+
expect(updateHealthStatesSpy).toHaveBeenCalled();
89+
expect(updateResourcesCountSpy).toHaveBeenCalled();
90+
expect(updateActiveResourcesCountSpy).toHaveBeenCalled();
91+
});
92+
93+
test('ContextsStatesDispatcher should call updatePermissions when onContextPermissionResult event is fired', () => {
94+
const manager: ContextsManager = {
95+
onContextHealthStateChange: vi.fn(),
96+
onOfflineChange: vi.fn(),
97+
onContextPermissionResult: vi.fn(),
98+
onContextDelete: vi.fn(),
99+
getHealthCheckersStates: vi.fn(),
100+
getPermissions: vi.fn(),
101+
onResourceCountUpdated: vi.fn(),
102+
onResourceUpdated: vi.fn(),
103+
isContextOffline: vi.fn(),
104+
} as unknown as ContextsManager;
105+
const apiSender: ApiSenderType = {
106+
send: vi.fn(),
107+
} as unknown as ApiSenderType;
108+
vi.mocked(manager.getPermissions).mockReturnValue([]);
109+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
110+
const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates');
111+
const updatePermissionsSpy = vi.spyOn(dispatcher, 'updatePermissions');
112+
dispatcher.init();
113+
expect(updateHealthStatesSpy).not.toHaveBeenCalled();
114+
expect(updatePermissionsSpy).not.toHaveBeenCalled();
115+
116+
vi.mocked(manager.onContextPermissionResult).mockImplementation(f => f({} as ContextPermissionResult) as IDisposable);
117+
dispatcher.init();
118+
expect(updateHealthStatesSpy).not.toHaveBeenCalled();
119+
expect(updatePermissionsSpy).toHaveBeenCalled();
120+
});
121+
122+
test('ContextsStatesDispatcher should call updateHealthStates and updatePermissions when onContextDelete event is fired', () => {
123+
const manager: ContextsManager = {
124+
onContextHealthStateChange: vi.fn(),
125+
onOfflineChange: vi.fn(),
126+
onContextPermissionResult: vi.fn(),
127+
onContextDelete: vi.fn(),
128+
getHealthCheckersStates: vi.fn(),
129+
getPermissions: vi.fn(),
130+
onResourceCountUpdated: vi.fn(),
131+
onResourceUpdated: vi.fn(),
132+
isContextOffline: vi.fn(),
133+
} as unknown as ContextsManager;
134+
const apiSender: ApiSenderType = {
135+
send: vi.fn(),
136+
} as unknown as ApiSenderType;
137+
vi.mocked(manager.getPermissions).mockReturnValue([]);
138+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
139+
const updateHealthStatesSpy = vi.spyOn(dispatcher, 'updateHealthStates');
140+
const updatePermissionsSpy = vi.spyOn(dispatcher, 'updatePermissions');
141+
vi.mocked(manager.getHealthCheckersStates).mockReturnValue(new Map<string, ContextHealthState>());
142+
dispatcher.init();
143+
expect(updateHealthStatesSpy).not.toHaveBeenCalled();
144+
expect(updatePermissionsSpy).not.toHaveBeenCalled();
145+
146+
vi.mocked(manager.onContextDelete).mockImplementation(f => f({} as DispatcherEvent) as IDisposable);
147+
dispatcher.init();
148+
expect(updateHealthStatesSpy).toHaveBeenCalled();
149+
expect(updatePermissionsSpy).toHaveBeenCalled();
150+
});
151+
152+
test('ContextsStatesDispatcher should call updateResource and updateActiveResourcesCount when onResourceUpdated event is fired', () => {
153+
const manager: ContextsManager = {
154+
onContextHealthStateChange: vi.fn(),
155+
onOfflineChange: vi.fn(),
156+
onContextPermissionResult: vi.fn(),
157+
onContextDelete: vi.fn(),
158+
getHealthCheckersStates: vi.fn(),
159+
getPermissions: vi.fn(),
160+
onResourceCountUpdated: vi.fn(),
161+
onResourceUpdated: vi.fn(),
162+
isContextOffline: vi.fn(),
163+
} as unknown as ContextsManager;
164+
const apiSender: ApiSenderType = {
165+
send: vi.fn(),
166+
} as unknown as ApiSenderType;
167+
vi.mocked(manager.getPermissions).mockReturnValue([]);
168+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
169+
const updateResourceSpy = vi.spyOn(dispatcher, 'updateResource');
170+
const updateActiveResourcesCountSpy = vi.spyOn(dispatcher, 'updateActiveResourcesCount');
171+
vi.mocked(manager.getHealthCheckersStates).mockReturnValue(new Map<string, ContextHealthState>());
172+
dispatcher.init();
173+
expect(updateResourceSpy).not.toHaveBeenCalled();
174+
expect(updateActiveResourcesCountSpy).not.toHaveBeenCalled();
175+
176+
vi.mocked(manager.onResourceUpdated).mockImplementation(
177+
f => f({} as { contextName: string; resourceName: string }) as IDisposable,
178+
);
179+
dispatcher.init();
180+
expect(updateResourceSpy).toHaveBeenCalled();
181+
expect(updateActiveResourcesCountSpy).toHaveBeenCalled();
182+
});
183+
184+
test('getContextsHealths should return the values of the map returned by manager.getHealthCheckersStates without kubeConfig', () => {
185+
const manager: ContextsManager = {
186+
onContextHealthStateChange: vi.fn(),
187+
onOfflineChange: vi.fn(),
188+
onContextPermissionResult: vi.fn(),
189+
onContextDelete: vi.fn(),
190+
getHealthCheckersStates: vi.fn(),
191+
getPermissions: vi.fn(),
192+
isContextOffline: vi.fn(),
193+
} as unknown as ContextsManager;
194+
const apiSender: ApiSenderType = {
195+
send: vi.fn(),
196+
} as unknown as ApiSenderType;
197+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
198+
const context1State = {
199+
contextName: 'context1',
200+
checking: true,
201+
reachable: false,
202+
};
203+
const context2State = {
204+
contextName: 'context2',
205+
checking: false,
206+
reachable: true,
207+
};
208+
const context3State = {
209+
contextName: 'context3',
210+
checking: false,
211+
reachable: false,
212+
errorMessage: 'an error',
213+
};
214+
const value = new Map<string, ContextHealthState>([
215+
['context1', { ...context1State, kubeConfig: {} as unknown as KubeConfigSingleContext }],
216+
['context2', { ...context2State, kubeConfig: {} as unknown as KubeConfigSingleContext }],
217+
['context3', { ...context3State, kubeConfig: {} as unknown as KubeConfigSingleContext }],
218+
]);
219+
vi.mocked(manager.getHealthCheckersStates).mockReturnValue(value);
220+
const result = dispatcher.getContextsHealths();
221+
expect(result).toEqual([context1State, context2State, context3State]);
222+
});
223+
224+
test('updateHealthStates should call apiSender.send with kubernetes-contexts-healths', () => {
225+
const manager: ContextsManager = {
226+
onContextHealthStateChange: vi.fn(),
227+
onContextPermissionResult: vi.fn(),
228+
onContextDelete: vi.fn(),
229+
getHealthCheckersStates: vi.fn(),
230+
getPermissions: vi.fn(),
231+
} as unknown as ContextsManager;
232+
const apiSender: ApiSenderType = {
233+
send: vi.fn(),
234+
} as unknown as ApiSenderType;
235+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
236+
vi.spyOn(dispatcher, 'getContextsHealths').mockReturnValue([]);
237+
dispatcher.updateHealthStates();
238+
expect(apiSender.send).toHaveBeenCalledWith('kubernetes-contexts-healths');
239+
});
240+
241+
test('getContextsPermissions should return the values as an array', () => {
242+
const manager: ContextsManager = {
243+
getPermissions: vi.fn(),
244+
} as unknown as ContextsManager;
245+
const apiSender: ApiSenderType = {
246+
send: vi.fn(),
247+
} as unknown as ApiSenderType;
248+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
249+
const value: ContextPermission[] = [
250+
{
251+
contextName: 'context1',
252+
resourceName: 'resource1',
253+
permitted: true,
254+
reason: 'ok',
255+
},
256+
{
257+
contextName: 'context1',
258+
resourceName: 'resource2',
259+
permitted: false,
260+
reason: 'nok',
261+
},
262+
{
263+
contextName: 'context2',
264+
resourceName: 'resource1',
265+
permitted: false,
266+
reason: 'nok',
267+
},
268+
{
269+
contextName: 'context2',
270+
resourceName: 'resource2',
271+
permitted: true,
272+
reason: 'ok',
273+
},
274+
];
275+
vi.mocked(manager.getPermissions).mockReturnValue(value);
276+
const result = dispatcher.getContextsPermissions();
277+
expect(result).toEqual(value);
278+
});
279+
280+
test('updatePermissions should call apiSender.send with kubernetes-contexts-permissions', () => {
281+
const manager: ContextsManager = {} as ContextsManager;
282+
const apiSender: ApiSenderType = {
283+
send: vi.fn(),
284+
} as unknown as ApiSenderType;
285+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
286+
dispatcher.updatePermissions();
287+
expect(vi.mocked(apiSender.send)).toHaveBeenCalledWith('kubernetes-contexts-permissions');
288+
});
289+
290+
test('updateResourcesCount should call apiSender.send with kubernetes-resources-count', () => {
291+
const manager: ContextsManager = {} as ContextsManager;
292+
const apiSender: ApiSenderType = {
293+
send: vi.fn(),
294+
} as unknown as ApiSenderType;
295+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
296+
dispatcher.updateResourcesCount();
297+
expect(vi.mocked(apiSender.send)).toHaveBeenCalledWith('kubernetes-resources-count');
298+
});
299+
300+
test('updateResource should call apiSender.send with kubernetes-`resource-name`', () => {
301+
const manager: ContextsManager = {} as ContextsManager;
302+
const apiSender: ApiSenderType = {
303+
send: vi.fn(),
304+
} as unknown as ApiSenderType;
305+
const dispatcher = new ContextsStatesDispatcher(manager, apiSender);
306+
dispatcher.updateResource('resource1');
307+
expect(vi.mocked(apiSender.send)).toHaveBeenCalledWith('kubernetes-update-resource1');
308+
});

0 commit comments

Comments
 (0)