Skip to content

Commit 5aecf3d

Browse files
committed
ARTESCA-14623 // implement Metalk8sVolumeProvider
feat: implement K8SLocalVolumeProvider and remove Metalk8sLocalVolumeProvider feat: add K8SLocalVolumeProvider implementation and associated tests Add Metalk8sLocalVolumeProvider implementation and tests Enhance Metalk8sLocalVolumeProvider to support additional volume types and improve error handling in volume listing Remove Metalk8sLocalVolumeProvider entry from micro-app-configuration Add volumeType property to LocalPersistentVolume and refactor Metalk8sLocalVolumeProvider Refactor LocalPersistentVolume to use VolumeType enum for volumeType property
1 parent 20ce874 commit 5aecf3d

File tree

3 files changed

+254
-1
lines changed

3 files changed

+254
-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: 156 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,156 @@
1+
import { CoreV1Api, CustomObjectsApi } from '@kubernetes/client-node';
2+
import Metalk8sLocalVolumeProvider from './Metalk8sLocalVolumeProvider';
3+
import { Metalk8sV1alpha1VolumeClient } from './Metalk8sVolumeClient.generated';
4+
import { updateApiServerConfig } from './api';
5+
6+
jest.mock('../k8s/api', () => ({
7+
updateApiServerConfig: jest.fn(),
8+
}));
9+
10+
describe('Metalk8sLocalVolumeProvider', () => {
11+
let provider: Metalk8sLocalVolumeProvider;
12+
const mockUrl = 'mock-url';
13+
const mockToken = 'mock-token';
14+
15+
const mockCustomObjectsApi = {
16+
listClusterCustomObject: jest.fn(),
17+
} as unknown as CustomObjectsApi;
18+
19+
const mockVolumeClient = new Metalk8sV1alpha1VolumeClient(
20+
mockCustomObjectsApi,
21+
);
22+
23+
const mockCoreV1Api = {
24+
listNode: jest.fn(),
25+
listPersistentVolume: jest.fn(),
26+
} as unknown as CoreV1Api;
27+
28+
beforeEach(() => {
29+
(updateApiServerConfig as jest.Mock).mockReturnValue({
30+
coreV1: mockCoreV1Api,
31+
customObjects: mockCustomObjectsApi,
32+
});
33+
34+
provider = new Metalk8sLocalVolumeProvider(mockUrl, mockToken);
35+
provider.k8sClient = mockCoreV1Api;
36+
provider.volumeClient = mockVolumeClient;
37+
});
38+
39+
describe('listLocalPersistentVolumes', () => {
40+
it('should return local persistent volumes for a given node.', async () => {
41+
(mockCoreV1Api.listNode as jest.Mock).mockResolvedValue({
42+
body: {
43+
items: [
44+
{
45+
metadata: { name: 'test-node' },
46+
status: {
47+
addresses: [
48+
{ type: 'Hostname', address: 'test-node' },
49+
{ type: 'InternalIP', address: '192.168.1.100' },
50+
],
51+
},
52+
},
53+
],
54+
},
55+
});
56+
57+
(mockCoreV1Api.listPersistentVolume as jest.Mock).mockResolvedValue({
58+
body: {
59+
items: [
60+
{
61+
metadata: { name: 'test-volume' },
62+
spec: {},
63+
},
64+
],
65+
},
66+
});
67+
68+
(
69+
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
70+
).mockResolvedValue({
71+
body: {
72+
items: [
73+
{
74+
metadata: { name: 'test-volume' },
75+
spec: {
76+
nodeName: 'test-node',
77+
rawBlockDevice: { devicePath: '/dev/sda' },
78+
},
79+
},
80+
{
81+
metadata: { name: 'test-lvm' },
82+
spec: {
83+
nodeName: 'test-node',
84+
lvmLogicalVolume: { vgName: 'test-lvm', size: '10Gi' },
85+
},
86+
},
87+
{
88+
metadata: { name: 'test-sparseLoop' },
89+
spec: {
90+
nodeName: 'test-node',
91+
sparseLoopDevice: {
92+
size: '1Gi',
93+
},
94+
},
95+
},
96+
],
97+
},
98+
});
99+
100+
const volumes = await provider.listLocalPersistentVolumes('test-node');
101+
102+
expect(volumes).toHaveLength(3);
103+
expect(volumes[0]).toMatchObject({
104+
IP: '192.168.1.100',
105+
devicePath: '/dev/sda',
106+
nodeName: 'test-node',
107+
});
108+
expect(volumes[1]).toMatchObject({
109+
IP: '192.168.1.100',
110+
devicePath: 'test-lvm',
111+
nodeName: 'test-node',
112+
});
113+
expect(volumes[2]).toMatchObject({
114+
IP: '192.168.1.100',
115+
devicePath: 'test-sparseLoop',
116+
nodeName: 'test-node',
117+
});
118+
});
119+
120+
it('should raise an error if the node cannot be found', async () => {
121+
(mockCoreV1Api.listNode as jest.Mock).mockResolvedValue({
122+
body: { items: [] },
123+
});
124+
125+
await expect(
126+
provider.listLocalPersistentVolumes('non-existent-node'),
127+
).rejects.toThrow('Failed to find IP for node non-existent-node');
128+
});
129+
130+
it('should raise an error if volume retrieval fails', async () => {
131+
(mockCoreV1Api.listNode as jest.Mock).mockResolvedValue({
132+
body: {
133+
items: [
134+
{
135+
metadata: { name: 'test-node' },
136+
status: {
137+
addresses: [
138+
{ type: 'Hostname', address: 'test-node' },
139+
{ type: 'InternalIP', address: '192.168.1.100' },
140+
],
141+
},
142+
},
143+
],
144+
},
145+
});
146+
147+
(
148+
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
149+
).mockRejectedValue(new Error('Failed to fetch volumes'));
150+
151+
await expect(
152+
provider.listLocalPersistentVolumes('test-node'),
153+
).rejects.toThrow('Failed to fetch metalk8s volumes');
154+
});
155+
});
156+
});
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)