Skip to content

Commit b62d812

Browse files
committed
Merge branch 'improvement/ARTESCA-14623' into tmp/octopus/w/130.0/improvement/ARTESCA-14623
2 parents 104c1b8 + 0d3cc60 commit b62d812

File tree

3 files changed

+259
-1
lines changed

3 files changed

+259
-1
lines changed

ui/rspack.config.ts

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ const config: Configuration = {
117117
'./platformLibrary': './src/services/platformlibrary/k8s.ts',
118118
'./AlertsNavbarUpdater':
119119
'./src/components/AlertNavbarUpdaterComponent.tsx',
120+
'./Metalk8sLocalVolumeProvider':
121+
'./src/services/k8s/Metalk8sLocalVolumeProvider.ts',
120122
},
121123
remotes: !isProduction
122124
? {
@@ -148,7 +150,19 @@ const config: Configuration = {
148150
},
149151
}),
150152
new rspack.CopyRspackPlugin({
151-
patterns: [{ from: 'public' }],
153+
patterns: [
154+
{ from: 'public' },
155+
{
156+
from: path.resolve(__dirname, 'build/static/js/@mf-types.zip'),
157+
to: '@mf-types.zip',
158+
noErrorOnMissing: true,
159+
},
160+
{
161+
from: path.resolve(__dirname, 'build/static/js/@mf-types.d.ts'),
162+
to: '@mf-types.d.ts',
163+
noErrorOnMissing: true,
164+
},
165+
],
152166
}),
153167
new rspack.DefinePlugin({
154168
NODE_ENV: process.env.NODE_ENV,
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
import { CoreV1Api, CustomObjectsApi } from '@kubernetes/client-node';
2+
import Metalk8sLocalVolumeProvider, {
3+
VolumeType,
4+
} from './Metalk8sLocalVolumeProvider';
5+
import { Metalk8sV1alpha1VolumeClient } from './Metalk8sVolumeClient.generated';
6+
import { updateApiServerConfig } from './api';
7+
8+
jest.mock('../k8s/api', () => ({
9+
updateApiServerConfig: jest.fn(),
10+
}));
11+
12+
describe('Metalk8sLocalVolumeProvider', () => {
13+
let provider: Metalk8sLocalVolumeProvider;
14+
const mockUrl = 'mock-url';
15+
const mockToken = 'mock-token';
16+
17+
const mockCustomObjectsApi = {
18+
listClusterCustomObject: jest.fn(),
19+
} as unknown as CustomObjectsApi;
20+
21+
const mockVolumeClient = new Metalk8sV1alpha1VolumeClient(
22+
mockCustomObjectsApi,
23+
);
24+
25+
const mockCoreV1Api = {
26+
listNode: jest.fn(),
27+
listPersistentVolume: jest.fn(),
28+
} as unknown as CoreV1Api;
29+
30+
beforeEach(() => {
31+
(updateApiServerConfig as jest.Mock).mockReturnValue({
32+
coreV1: mockCoreV1Api,
33+
customObjects: mockCustomObjectsApi,
34+
});
35+
36+
provider = new Metalk8sLocalVolumeProvider(mockUrl, mockToken);
37+
provider.k8sClient = mockCoreV1Api;
38+
provider.volumeClient = mockVolumeClient;
39+
});
40+
41+
describe('listLocalPersistentVolumes', () => {
42+
it('should return local persistent volumes for a given node.', async () => {
43+
(mockCoreV1Api.listNode as jest.Mock).mockResolvedValue({
44+
body: {
45+
items: [
46+
{
47+
metadata: { name: 'test-node' },
48+
status: {
49+
addresses: [
50+
{ type: 'Hostname', address: 'test-node' },
51+
{ type: 'InternalIP', address: '192.168.1.100' },
52+
],
53+
},
54+
},
55+
],
56+
},
57+
});
58+
59+
(mockCoreV1Api.listPersistentVolume as jest.Mock).mockResolvedValue({
60+
body: {
61+
items: [
62+
{
63+
metadata: { name: 'test-volume' },
64+
spec: {},
65+
},
66+
],
67+
},
68+
});
69+
70+
(
71+
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
72+
).mockResolvedValue({
73+
body: {
74+
items: [
75+
{
76+
metadata: { name: 'test-volume' },
77+
spec: {
78+
nodeName: 'test-node',
79+
rawBlockDevice: { devicePath: '/dev/sda' },
80+
},
81+
},
82+
{
83+
metadata: { name: 'test-lvm' },
84+
spec: {
85+
nodeName: 'test-node',
86+
lvmLogicalVolume: { vgName: 'test-lvm', size: '10Gi' },
87+
},
88+
},
89+
{
90+
metadata: { name: 'test-sparseLoop' },
91+
spec: {
92+
nodeName: 'test-node',
93+
sparseLoopDevice: {
94+
size: '1Gi',
95+
},
96+
},
97+
},
98+
],
99+
},
100+
});
101+
102+
const volumes = await provider.listLocalPersistentVolumes('test-node');
103+
104+
expect(volumes).toHaveLength(3);
105+
expect(volumes[0]).toMatchObject({
106+
IP: '192.168.1.100',
107+
devicePath: '/dev/sda',
108+
nodeName: 'test-node',
109+
volumeType: VolumeType.Hardware,
110+
});
111+
expect(volumes[1]).toMatchObject({
112+
IP: '192.168.1.100',
113+
devicePath: 'test-lvm',
114+
nodeName: 'test-node',
115+
volumeType: VolumeType.Virtual,
116+
});
117+
expect(volumes[2]).toMatchObject({
118+
IP: '192.168.1.100',
119+
devicePath: 'test-sparseLoop',
120+
nodeName: 'test-node',
121+
volumeType: VolumeType.Virtual,
122+
});
123+
});
124+
125+
it('should raise an error if the node cannot be found', async () => {
126+
(mockCoreV1Api.listNode as jest.Mock).mockResolvedValue({
127+
body: { items: [] },
128+
});
129+
130+
await expect(
131+
provider.listLocalPersistentVolumes('non-existent-node'),
132+
).rejects.toThrow('Failed to find IP for node non-existent-node');
133+
});
134+
135+
it('should raise an error if volume retrieval fails', async () => {
136+
(mockCoreV1Api.listNode as jest.Mock).mockResolvedValue({
137+
body: {
138+
items: [
139+
{
140+
metadata: { name: 'test-node' },
141+
status: {
142+
addresses: [
143+
{ type: 'Hostname', address: 'test-node' },
144+
{ type: 'InternalIP', address: '192.168.1.100' },
145+
],
146+
},
147+
},
148+
],
149+
},
150+
});
151+
152+
(
153+
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
154+
).mockRejectedValue(new Error('Failed to fetch volumes'));
155+
156+
await expect(
157+
provider.listLocalPersistentVolumes('test-node'),
158+
).rejects.toThrow('Failed to fetch metalk8s volumes');
159+
});
160+
});
161+
});
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
import { CoreV1Api, V1PersistentVolume } from '@kubernetes/client-node';
2+
import * as ApiK8s from './api';
3+
import {
4+
Metalk8sV1alpha1VolumeClient,
5+
Result,
6+
} from './Metalk8sVolumeClient.generated';
7+
8+
function isError<T>(result: Result<T>): result is { error: any } {
9+
return (result as { error: any }).error !== undefined;
10+
}
11+
12+
export enum VolumeType {
13+
Hardware = 'Hardware',
14+
Virtual = 'Virtual',
15+
}
16+
17+
export type LocalPersistentVolume = V1PersistentVolume & {
18+
IP: string;
19+
devicePath: string;
20+
nodeName: string;
21+
volumeType: VolumeType;
22+
};
23+
24+
export default class Metalk8sLocalVolumeProvider {
25+
volumeClient: Metalk8sV1alpha1VolumeClient;
26+
k8sClient: CoreV1Api;
27+
constructor(url: string, token: string) {
28+
const { coreV1, customObjects } = ApiK8s.updateApiServerConfig(url, token);
29+
this.volumeClient = new Metalk8sV1alpha1VolumeClient(customObjects);
30+
this.k8sClient = coreV1;
31+
}
32+
public listLocalPersistentVolumes = async (
33+
serverName: string,
34+
): Promise<LocalPersistentVolume[]> => {
35+
try {
36+
const nodes = await this.k8sClient.listNode();
37+
const nodeIP = nodes.body.items
38+
.find((node) => node.metadata.name === serverName)
39+
?.status.addresses.find((address) => address.type === 'InternalIP');
40+
41+
if (!nodeIP) {
42+
throw new Error(`Failed to find IP for node ${serverName}`);
43+
}
44+
45+
const volumes = await this.volumeClient.getMetalk8sV1alpha1VolumeList();
46+
47+
if (!isError(volumes)) {
48+
const nodeVolumes = volumes.body.items.filter(
49+
(volume) => volume.spec.nodeName === serverName,
50+
);
51+
const pv = await this.k8sClient.listPersistentVolume();
52+
53+
const localPv = nodeVolumes.reduce((acc, item) => {
54+
const isLocalPv = pv.body.items.find(
55+
(p) => p.metadata.name === item.metadata['name'],
56+
);
57+
58+
return [
59+
...acc,
60+
{
61+
...isLocalPv,
62+
IP: nodeIP.address,
63+
devicePath:
64+
item.spec?.rawBlockDevice?.devicePath || item.metadata['name'],
65+
nodeName: item.spec.nodeName,
66+
volumeType: item.spec.rawBlockDevice
67+
? VolumeType.Hardware
68+
: VolumeType.Virtual,
69+
},
70+
];
71+
}, [] as LocalPersistentVolume[]);
72+
73+
return localPv;
74+
} else {
75+
throw new Error(`Failed to fetch metalk8s volumes: ${volumes.error}`);
76+
}
77+
} catch (error) {
78+
throw new Error(
79+
`Failed to fetch local persistent volumes: ${error.message}`,
80+
);
81+
}
82+
};
83+
}

0 commit comments

Comments
 (0)