Skip to content

Commit f8c6485

Browse files
committed
ARTESCA-14896: Add attachHardwareVolume in Metalk8sLocalVolumeProvider
1 parent b442731 commit f8c6485

File tree

2 files changed

+193
-1
lines changed

2 files changed

+193
-1
lines changed

ui/src/services/k8s/Metalk8sLocalVolumeProvider.test.ts

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { CoreV1Api, CustomObjectsApi } from '@kubernetes/client-node';
22
import { updateApiServerConfig } from './api';
33
import Metalk8sLocalVolumeProvider, {
4+
HardwareDiskType,
45
VolumeType,
56
} from './Metalk8sLocalVolumeProvider';
67

@@ -20,6 +21,7 @@ describe('Metalk8sLocalVolumeProvider', () => {
2021
const mockCustomObjectsApi = {
2122
listClusterCustomObject: jest.fn(),
2223
deleteClusterCustomObject: jest.fn(),
24+
createClusterCustomObject: jest.fn(),
2325
} as unknown as CustomObjectsApi;
2426

2527
const mockCoreV1Api = {
@@ -227,4 +229,99 @@ describe('Metalk8sLocalVolumeProvider', () => {
227229
);
228230
});
229231
});
232+
233+
describe('attachHardwareVolume', () => {
234+
it('should attach hardware volume', async () => {
235+
//S
236+
(mockCoreV1Api.listNode as jest.Mock).mockResolvedValue({
237+
body: {
238+
apiVersion: 'v1',
239+
kind: 'NodeList',
240+
items: [
241+
{
242+
metadata: {
243+
name: 'test-node',
244+
},
245+
status: {
246+
addresses: [{ type: 'InternalIP', address: '192.168.1.100' }],
247+
},
248+
},
249+
],
250+
},
251+
});
252+
(
253+
mockCustomObjectsApi.createClusterCustomObject as jest.Mock
254+
).mockResolvedValue({
255+
metadata: {
256+
name: 'storage-data-192.168.1.100-/dev/sda',
257+
},
258+
});
259+
//E
260+
const result = await provider.attachHardwareVolume({
261+
IP: '192.168.1.100',
262+
devicePath: '/dev/sda',
263+
type: HardwareDiskType.NVMe,
264+
});
265+
//V
266+
expect(
267+
mockCustomObjectsApi.createClusterCustomObject,
268+
).toHaveBeenCalledWith(
269+
'storage.metalk8s.scality.com',
270+
'v1alpha1',
271+
'volumes',
272+
{
273+
apiVersion: 'storage.metalk8s.scality.com/v1alpha1',
274+
kind: 'Volume',
275+
metadata: {
276+
name: 'storage-data-192.168.1.100-/dev/sda',
277+
labels: {
278+
'xcore.scality.com/volume-type': 'data',
279+
},
280+
},
281+
spec: {
282+
nodeName: 'test-node',
283+
rawBlockDevice: { devicePath: '/dev/sda' },
284+
storageClassName: 'ssd-ext4',
285+
},
286+
},
287+
);
288+
expect(result).toEqual({
289+
IP: '192.168.1.100',
290+
devicePath: '/dev/sda',
291+
nodeName: 'test-node',
292+
volumeType: VolumeType.Hardware,
293+
volumeName: 'storage-data-192.168.1.100-/dev/sda',
294+
});
295+
});
296+
297+
it('should raise an error if volume creation fails', async () => {
298+
//S
299+
(
300+
mockCustomObjectsApi.createClusterCustomObject as jest.Mock
301+
).mockRejectedValue(new Error('Error'));
302+
//E+V
303+
await expect(
304+
provider.attachHardwareVolume({
305+
IP: '192.168.1.100',
306+
devicePath: '/dev/sda',
307+
type: HardwareDiskType.NVMe,
308+
}),
309+
).rejects.toThrow('Failed to attach hardware volume: Error');
310+
});
311+
312+
it('should raise an error if node retrieval fails', async () => {
313+
//S
314+
(mockCoreV1Api.listNode as jest.Mock).mockRejectedValue(
315+
new Error('Error'),
316+
);
317+
//E+V
318+
await expect(
319+
provider.attachHardwareVolume({
320+
IP: '192.168.1.100',
321+
devicePath: '/dev/sda',
322+
type: HardwareDiskType.NVMe,
323+
}),
324+
).rejects.toThrow('Failed to fetch nodes: Error');
325+
});
326+
});
230327
});

ui/src/services/k8s/Metalk8sLocalVolumeProvider.ts

Lines changed: 96 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,28 @@ export enum VolumeType {
1414
Virtual = 'Virtual',
1515
}
1616

17-
export type LocalPersistentVolume = V1PersistentVolume & {
17+
export enum HardwareDiskType {
18+
SATA = 'SATA',
19+
NVMe = 'NVMe',
20+
}
21+
22+
export type HardwareDisk = {
23+
IP: string;
24+
devicePath: string;
25+
type: HardwareDiskType;
26+
};
27+
28+
type LocalVolumeInfo = {
1829
IP: string;
1930
devicePath: string;
2031
nodeName: string;
2132
volumeType: VolumeType;
2233
};
2334

35+
export type LocalPersistentVolume = V1PersistentVolume & LocalVolumeInfo;
36+
37+
type LocalVolume = LocalVolumeInfo & { volumeName: string };
38+
2439
export default class Metalk8sLocalVolumeProvider {
2540
apiUrl: string;
2641
constructor(apiUrl: string, private getToken: () => Promise<string>) {
@@ -121,4 +136,84 @@ export default class Metalk8sLocalVolumeProvider {
121136
}
122137
}
123138
};
139+
140+
public attachHardwareVolume = async (
141+
hardwareDisk: HardwareDisk,
142+
): Promise<LocalVolume> => {
143+
const { IP, devicePath, type } = hardwareDisk;
144+
145+
const token = await this.getToken();
146+
const { coreV1, customObjects } = ApiK8s.updateApiServerConfig(
147+
this.apiUrl,
148+
token,
149+
);
150+
const volumeClient = new Metalk8sV1alpha1VolumeClient(customObjects);
151+
const k8sClient = coreV1;
152+
153+
if (isError(volumeClient)) {
154+
throw new Error(
155+
`Failed to create volume client: ${volumeClient.error.message}`,
156+
);
157+
}
158+
if (isError(k8sClient)) {
159+
throw new Error(
160+
`Failed to create k8s client: ${k8sClient.error.message}`,
161+
);
162+
}
163+
164+
const nodes = await k8sClient.listNode().catch((error) => {
165+
throw new Error(
166+
`Failed to fetch nodes: ${
167+
error instanceof Error ? error.message : JSON.stringify(error)
168+
}`,
169+
);
170+
});
171+
172+
if (isError(nodes)) {
173+
throw new Error(`Failed to fetch nodes: ${nodes.error.message}`);
174+
}
175+
const nodeName = nodes.body.items.find((node) =>
176+
node.status.addresses.find(
177+
(address) => address.type === 'InternalIP' && address.address === IP,
178+
),
179+
)?.metadata.name;
180+
if (!nodeName) {
181+
throw new Error(`Failed to find node for IP ${IP}`);
182+
}
183+
// The map between hardwareDisk Type and StorageClassName
184+
// NVMe => SSD
185+
// the rest=> HDD
186+
const storageClassName =
187+
type === HardwareDiskType.NVMe ? 'ssd-ext4' : 'hdd-ext4';
188+
189+
const volume = await volumeClient.createMetalk8sV1alpha1Volume({
190+
apiVersion: 'storage.metalk8s.scality.com/v1alpha1',
191+
kind: 'Volume',
192+
metadata: {
193+
// It will be changed to Disk Serial Number in the future.
194+
name: `storage-data-${IP}-${devicePath}`,
195+
labels: {
196+
'xcore.scality.com/volume-type': 'data',
197+
},
198+
},
199+
spec: {
200+
nodeName,
201+
rawBlockDevice: { devicePath },
202+
storageClassName,
203+
},
204+
});
205+
if (isError(volume)) {
206+
throw new Error(
207+
`Failed to attach hardware volume: ${volume.error.message}`,
208+
);
209+
}
210+
211+
return {
212+
IP,
213+
devicePath,
214+
nodeName,
215+
volumeType: VolumeType.Hardware,
216+
volumeName: volume.metadata['name'],
217+
};
218+
};
124219
}

0 commit comments

Comments
 (0)