Skip to content

Commit 07eefee

Browse files
committed
Merge branch 'feature/ARTESCA-14896-add-attachHardwareVolume-method-in-Metalk8sKubernetesLocalVolumeProvider' into tmp/octopus/w/130.0/feature/ARTESCA-14896-add-attachHardwareVolume-method-in-Metalk8sKubernetesLocalVolumeProvider
2 parents bb822f1 + f6e36e6 commit 07eefee

File tree

2 files changed

+153
-0
lines changed

2 files changed

+153
-0
lines changed

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

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { CoreV1Api, CustomObjectsApi } from '@kubernetes/client-node';
22
import Metalk8sLocalVolumeProvider, {
3+
HardwareDiskType,
34
VolumeType,
45
} from './Metalk8sLocalVolumeProvider';
56
import { updateApiServerConfig } from './api';
@@ -221,4 +222,94 @@ describe('Metalk8sLocalVolumeProvider', () => {
221222
);
222223
});
223224
});
225+
226+
describe('attachHardwareVolumes', () => {
227+
it('should attach hardware volumes', async () => {
228+
//S
229+
(mockCoreV1Api.listNode as jest.Mock).mockResolvedValue({
230+
body: {
231+
apiVersion: 'v1',
232+
kind: 'NodeList',
233+
items: [
234+
{
235+
metadata: {
236+
name: 'test-node',
237+
},
238+
status: {
239+
addresses: [{ type: 'InternalIP', address: '192.168.1.100' }],
240+
},
241+
},
242+
],
243+
},
244+
});
245+
(
246+
mockVolumeClient.createMetalk8sV1alpha1Volume as jest.Mock
247+
).mockResolvedValue({
248+
metadata: {
249+
name: 'storage-data-192.168.1.100-/dev/sda',
250+
},
251+
});
252+
//E
253+
const result = await provider.attachHardwareVolumes([
254+
{
255+
IP: '192.168.1.100',
256+
devicePath: '/dev/sda',
257+
type: HardwareDiskType.NVMe,
258+
},
259+
]);
260+
//V
261+
expect(
262+
mockVolumeClient.createMetalk8sV1alpha1Volume,
263+
).toHaveBeenCalledWith({
264+
apiVersion: 'storage.metalk8s.scality.com/v1alpha1',
265+
kind: 'Volume',
266+
metadata: {
267+
name: 'storage-data-192.168.1.100-/dev/sda',
268+
labels: {
269+
'xcore.scality.com/volume-type': 'data',
270+
},
271+
},
272+
spec: {
273+
nodeName: 'test-node',
274+
rawBlockDevice: { devicePath: '/dev/sda' },
275+
storageClassName: 'ssd-ext4',
276+
},
277+
});
278+
expect(result).toEqual(['storage-data-192.168.1.100-/dev/sda']);
279+
});
280+
281+
it('should raise an error if volume creation fails', async () => {
282+
//S
283+
(
284+
mockVolumeClient.createMetalk8sV1alpha1Volume as jest.Mock
285+
).mockRejectedValue(new Error('Failed to create volume'));
286+
//E+V
287+
await expect(
288+
provider.attachHardwareVolumes([
289+
{
290+
IP: '192.168.1.100',
291+
devicePath: '/dev/sda',
292+
type: HardwareDiskType.NVMe,
293+
},
294+
]),
295+
).rejects.toThrow('Failed to create volume');
296+
});
297+
298+
it('should raise an error if node retrieval fails', async () => {
299+
//S
300+
(mockCoreV1Api.listNode as jest.Mock).mockRejectedValue(
301+
new Error('Failed to fetch nodes'),
302+
);
303+
//E+V
304+
await expect(
305+
provider.attachHardwareVolumes([
306+
{
307+
IP: '192.168.1.100',
308+
devicePath: '/dev/sda',
309+
type: HardwareDiskType.NVMe,
310+
},
311+
]),
312+
).rejects.toThrow('Failed to fetch nodes');
313+
});
314+
});
224315
});

ui/src/services/k8s/Metalk8sLocalVolumeProvider.ts

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,17 @@ export enum VolumeType {
1414
Virtual = 'Virtual',
1515
}
1616

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+
1728
export type LocalPersistentVolume = V1PersistentVolume & {
1829
IP: string;
1930
devicePath: string;
@@ -100,4 +111,55 @@ export default class Metalk8sLocalVolumeProvider {
100111
}
101112
}
102113
};
114+
115+
public attachHardwareVolumes = async (
116+
hardwareDisks: HardwareDisk[],
117+
): Promise<string[]> => {
118+
const nodes = await this.k8sClient.listNode();
119+
if (isError(nodes)) {
120+
throw new Error(`Failed to fetch nodes: ${nodes.error}`);
121+
}
122+
123+
const volumeNames: string[] = [];
124+
125+
for (const hardwareDisk of hardwareDisks) {
126+
const { IP, devicePath, type } = hardwareDisk;
127+
128+
const nodeName = nodes.body.items.find((node) =>
129+
node.status.addresses.find(
130+
(address) => address.type === 'InternalIP' && address.address === IP,
131+
),
132+
)?.metadata.name;
133+
if (!nodeName) {
134+
throw new Error(`Failed to find node for IP ${IP}`);
135+
}
136+
// The map between hardwareDisk Type and StorageClassName
137+
// NVMe => SSD
138+
// the rest=> HDD
139+
const storageClassName =
140+
type === HardwareDiskType.NVMe ? 'ssd-ext4' : 'hdd-ext4';
141+
142+
const volume = await this.volumeClient.createMetalk8sV1alpha1Volume({
143+
apiVersion: 'storage.metalk8s.scality.com/v1alpha1',
144+
kind: 'Volume',
145+
metadata: {
146+
// It will be changed to Disk Serial Number in the future.
147+
name: `storage-data-${IP}-${devicePath}`,
148+
labels: {
149+
'xcore.scality.com/volume-type': 'data',
150+
},
151+
},
152+
spec: {
153+
nodeName,
154+
rawBlockDevice: { devicePath },
155+
storageClassName,
156+
},
157+
});
158+
if (isError(volume)) {
159+
throw new Error(`Failed to create MetalK8s volume: ${volume.error}`);
160+
}
161+
volumeNames.push(volume.metadata['name']);
162+
}
163+
return volumeNames;
164+
};
103165
}

0 commit comments

Comments
 (0)