Skip to content

Commit 37f1b84

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

File tree

2 files changed

+196
-1
lines changed

2 files changed

+196
-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: 99 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,87 @@ 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+
let nodes;
165+
try {
166+
nodes = await k8sClient.listNode();
167+
} catch (error) {
168+
throw new Error(
169+
`Failed to fetch nodes: ${
170+
error instanceof Error ? error.message : JSON.stringify(error)
171+
}`,
172+
);
173+
}
174+
175+
if (isError(nodes)) {
176+
throw new Error(`Failed to fetch nodes: ${nodes.error.message}`);
177+
}
178+
const nodeName = nodes.body.items.find((node) =>
179+
node.status.addresses.find(
180+
(address) => address.type === 'InternalIP' && address.address === IP,
181+
),
182+
)?.metadata.name;
183+
if (!nodeName) {
184+
throw new Error(`Failed to find node for IP ${IP}`);
185+
}
186+
// The map between hardwareDisk Type and StorageClassName
187+
// NVMe => SSD
188+
// the rest=> HDD
189+
const storageClassName =
190+
type === HardwareDiskType.NVMe ? 'ssd-ext4' : 'hdd-ext4';
191+
192+
const volume = await volumeClient.createMetalk8sV1alpha1Volume({
193+
apiVersion: 'storage.metalk8s.scality.com/v1alpha1',
194+
kind: 'Volume',
195+
metadata: {
196+
// It will be changed to Disk Serial Number in the future.
197+
name: `storage-data-${IP}-${devicePath}`,
198+
labels: {
199+
'xcore.scality.com/volume-type': 'data',
200+
},
201+
},
202+
spec: {
203+
nodeName,
204+
rawBlockDevice: { devicePath },
205+
storageClassName,
206+
},
207+
});
208+
if (isError(volume)) {
209+
throw new Error(
210+
`Failed to attach hardware volume: ${volume.error.message}`,
211+
);
212+
}
213+
214+
return {
215+
IP,
216+
devicePath,
217+
nodeName,
218+
volumeType: VolumeType.Hardware,
219+
volumeName: volume.metadata['name'],
220+
};
221+
};
124222
}

0 commit comments

Comments
 (0)