Skip to content

Commit 7576b09

Browse files
committed
ARTESCA-14895: Add detach volumes method in Metalk8sLocalvolumeProvider
1 parent 906a0e7 commit 7576b09

File tree

2 files changed

+154
-7
lines changed

2 files changed

+154
-7
lines changed

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

Lines changed: 105 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ import { CoreV1Api, CustomObjectsApi } from '@kubernetes/client-node';
22
import Metalk8sLocalVolumeProvider, {
33
VolumeType,
44
} from './Metalk8sLocalVolumeProvider';
5-
import { Metalk8sV1alpha1VolumeClient } from './Metalk8sVolumeClient.generated';
65
import { updateApiServerConfig } from './api';
6+
import { Metalk8sV1alpha1VolumeClient } from './Metalk8sVolumeClient.generated';
7+
8+
const HARDWARE_DEVICEPATH = '/dev/sda';
79

810
jest.mock('../k8s/api', () => ({
911
updateApiServerConfig: jest.fn(),
@@ -18,13 +20,18 @@ describe('Metalk8sLocalVolumeProvider', () => {
1820
listClusterCustomObject: jest.fn(),
1921
} as unknown as CustomObjectsApi;
2022

21-
const mockVolumeClient = new Metalk8sV1alpha1VolumeClient(
22-
mockCustomObjectsApi,
23-
);
23+
const mockVolumeClient = {
24+
deleteMetalk8sV1alpha1Volume: jest.fn().mockResolvedValue({ body: {} }),
25+
getMetalk8sV1alpha1VolumeList: jest.fn(),
26+
getMetalk8sV1alpha1Volume: jest.fn(),
27+
createMetalk8sV1alpha1Volume: jest.fn(),
28+
patchMetalk8sV1alpha1Volume: jest.fn(),
29+
} as unknown as Metalk8sV1alpha1VolumeClient;
2430

2531
const mockCoreV1Api = {
2632
listNode: jest.fn(),
2733
listPersistentVolume: jest.fn(),
34+
deletePersistentVolume: jest.fn(),
2835
} as unknown as CoreV1Api;
2936

3037
beforeEach(() => {
@@ -68,7 +75,7 @@ describe('Metalk8sLocalVolumeProvider', () => {
6875
});
6976

7077
(
71-
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
78+
mockVolumeClient.getMetalk8sV1alpha1VolumeList as jest.Mock
7279
).mockResolvedValue({
7380
body: {
7481
items: [
@@ -150,12 +157,103 @@ describe('Metalk8sLocalVolumeProvider', () => {
150157
});
151158

152159
(
153-
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
160+
mockVolumeClient.getMetalk8sV1alpha1VolumeList as jest.Mock
154161
).mockRejectedValue(new Error('Failed to fetch volumes'));
155162

156163
await expect(
157164
provider.listLocalPersistentVolumes('test-node'),
158-
).rejects.toThrow('Failed to fetch metalk8s volumes');
165+
).rejects.toThrow(
166+
'Failed to fetch local persistent volumes: Failed to fetch volumes',
167+
);
168+
});
169+
});
170+
171+
describe('detachVolumes', () => {
172+
it('should detach volumes', async () => {
173+
//S
174+
(
175+
mockVolumeClient.deleteMetalk8sV1alpha1Volume as jest.Mock
176+
).mockResolvedValue({
177+
body: {},
178+
});
179+
(mockCoreV1Api.deletePersistentVolume as jest.Mock).mockResolvedValue({
180+
body: {},
181+
});
182+
(
183+
mockVolumeClient.getMetalk8sV1alpha1VolumeList as jest.Mock
184+
).mockResolvedValue({
185+
body: {
186+
items: [
187+
{
188+
metadata: { name: 'test-volume' },
189+
spec: {
190+
nodeName: 'test-node',
191+
rawBlockDevice: { devicePath: HARDWARE_DEVICEPATH },
192+
},
193+
},
194+
],
195+
},
196+
});
197+
198+
//E
199+
await provider.detachVolumes([
200+
{
201+
IP: '192.168.1.100',
202+
devicePath: HARDWARE_DEVICEPATH,
203+
volumeType: VolumeType.Hardware,
204+
nodeName: 'test-node',
205+
},
206+
]);
207+
//V
208+
expect(mockCoreV1Api.deletePersistentVolume).toHaveBeenCalledWith(
209+
'test-volume',
210+
);
211+
expect(
212+
mockVolumeClient.deleteMetalk8sV1alpha1Volume,
213+
).toHaveBeenCalledWith('test-volume');
214+
});
215+
216+
it('should raise an error if pv deletion fails', async () => {
217+
//S
218+
(mockCoreV1Api.deletePersistentVolume as jest.Mock).mockRejectedValue(
219+
new Error('Failed to delete volume'),
220+
);
221+
//E+V
222+
await expect(
223+
provider.detachVolumes([
224+
{
225+
IP: '192.168.1.100',
226+
devicePath: HARDWARE_DEVICEPATH,
227+
volumeType: VolumeType.Hardware,
228+
nodeName: 'test-node',
229+
},
230+
]),
231+
).rejects.toThrow(
232+
'Failed to delete PV test-volume: Failed to delete volume',
233+
);
234+
});
235+
236+
it('should raise an error if metalk8s volume deletion fails', async () => {
237+
//S
238+
(mockCoreV1Api.deletePersistentVolume as jest.Mock).mockResolvedValue({
239+
body: {},
240+
});
241+
(
242+
mockVolumeClient.deleteMetalk8sV1alpha1Volume as jest.Mock
243+
).mockRejectedValue(new Error('Failed to delete metalk8s volume'));
244+
//E+V
245+
await expect(
246+
provider.detachVolumes([
247+
{
248+
IP: '192.168.1.100',
249+
devicePath: HARDWARE_DEVICEPATH,
250+
volumeType: VolumeType.Hardware,
251+
nodeName: 'test-node',
252+
},
253+
]),
254+
).rejects.toThrow(
255+
'Failed to delete Metalk8s volume test-volume: Failed to delete metalk8s volume',
256+
);
159257
});
160258
});
161259
});

ui/src/services/k8s/Metalk8sLocalVolumeProvider.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,4 +80,53 @@ export default class Metalk8sLocalVolumeProvider {
8080
);
8181
}
8282
};
83+
84+
// Since we don't have unique Serial Number for the disks, we need to retrieve the Volume Name from the PV.
85+
public detachVolumes = async (
86+
localPVs: LocalPersistentVolume[],
87+
): Promise<void> => {
88+
const volumeNames = [];
89+
for (const localPV of localPVs) {
90+
const volumes = await this.volumeClient.getMetalk8sV1alpha1VolumeList();
91+
if (isError(volumes)) {
92+
throw new Error(`Failed to fetch metalk8s volumes: ${volumes.error}`);
93+
}
94+
const volumeName = volumes.body.items.find(
95+
(volume) =>
96+
volume.spec.nodeName === localPV.nodeName &&
97+
(localPV.volumeType === VolumeType.Hardware
98+
? volume.spec.rawBlockDevice?.devicePath === localPV.devicePath
99+
: volume.metadata['name'] === localPV.devicePath),
100+
)?.metadata['name'];
101+
if (!volumeName) {
102+
throw new Error(
103+
`Failed to find Volume for devicePath ${localPV.devicePath}`,
104+
);
105+
}
106+
volumeNames.push(volumeName);
107+
}
108+
109+
for (const volumeName of volumeNames) {
110+
try {
111+
await this.k8sClient.deletePersistentVolume(volumeName);
112+
} catch (error) {
113+
throw new Error(
114+
`Failed to delete PV ${volumeName}: ${
115+
error instanceof Error ? error.message : JSON.stringify(error)
116+
}`,
117+
);
118+
}
119+
}
120+
for (const volumeName of volumeNames) {
121+
try {
122+
await this.volumeClient.deleteMetalk8sV1alpha1Volume(volumeName);
123+
} catch (error) {
124+
throw new Error(
125+
`Failed to delete Metalk8s volume ${volumeName}: ${
126+
error instanceof Error ? error.message : JSON.stringify(error)
127+
}`,
128+
);
129+
}
130+
}
131+
};
83132
}

0 commit comments

Comments
 (0)