Skip to content

Commit 8bf7b3d

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

File tree

2 files changed

+171
-7
lines changed

2 files changed

+171
-7
lines changed

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

Lines changed: 122 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ 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';
77

88
jest.mock('../k8s/api', () => ({
99
updateApiServerConfig: jest.fn(),
@@ -18,13 +18,18 @@ describe('Metalk8sLocalVolumeProvider', () => {
1818
listClusterCustomObject: jest.fn(),
1919
} as unknown as CustomObjectsApi;
2020

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

2529
const mockCoreV1Api = {
2630
listNode: jest.fn(),
2731
listPersistentVolume: jest.fn(),
32+
deletePersistentVolume: jest.fn(),
2833
} as unknown as CoreV1Api;
2934

3035
beforeEach(() => {
@@ -68,7 +73,7 @@ describe('Metalk8sLocalVolumeProvider', () => {
6873
});
6974

7075
(
71-
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
76+
mockVolumeClient.getMetalk8sV1alpha1VolumeList as jest.Mock
7277
).mockResolvedValue({
7378
body: {
7479
items: [
@@ -150,12 +155,122 @@ describe('Metalk8sLocalVolumeProvider', () => {
150155
});
151156

152157
(
153-
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
158+
mockVolumeClient.getMetalk8sV1alpha1VolumeList as jest.Mock
154159
).mockRejectedValue(new Error('Failed to fetch volumes'));
155160

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

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)