Skip to content

Commit c14c4df

Browse files
committed
Merge branch 'feature/ARTESCA-14924-wait-for-volume-tobe-provisioned' into q/129.0
2 parents 6e5e8fd + 28e4b2e commit c14c4df

File tree

2 files changed

+156
-59
lines changed

2 files changed

+156
-59
lines changed

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

Lines changed: 87 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ describe('Metalk8sLocalVolumeProvider', () => {
2020
const mockCustomObjectsApi = {
2121
listClusterCustomObject: jest.fn(),
2222
deleteClusterCustomObject: jest.fn(),
23+
getClusterCustomObject: jest.fn(),
2324
} as unknown as CustomObjectsApi;
2425

2526
const mockCoreV1Api = {
@@ -160,31 +161,22 @@ describe('Metalk8sLocalVolumeProvider', () => {
160161
});
161162
});
162163

163-
describe('detachVolumes', () => {
164-
it('should detach hardware volumes and virtual volumes', async () => {
164+
describe('detachVolume', () => {
165+
it('should detach volume', async () => {
165166
//S
166167
(
167168
mockCustomObjectsApi.deleteClusterCustomObject as jest.Mock
168169
).mockResolvedValue({
169170
body: {},
170171
});
171172
//E
172-
await provider.detachVolumes([
173-
{
174-
IP: '192.168.1.100',
175-
devicePath: '/dev/sda',
176-
volumeType: VolumeType.Hardware,
177-
nodeName: 'test-node',
178-
metadata: { name: 'test-volume' },
179-
},
180-
{
181-
IP: '192.168.1.100',
182-
devicePath: 'test-lvm',
183-
volumeType: VolumeType.Virtual,
184-
nodeName: 'test-node',
185-
metadata: { name: 'test-lvm' },
186-
},
187-
]);
173+
await provider.detachVolume({
174+
IP: '192.168.1.100',
175+
devicePath: '/dev/sda',
176+
volumeType: VolumeType.Hardware,
177+
nodeName: 'test-node',
178+
metadata: { name: 'test-volume' },
179+
});
188180
//V
189181
expect(
190182
mockCustomObjectsApi.deleteClusterCustomObject,
@@ -195,15 +187,6 @@ describe('Metalk8sLocalVolumeProvider', () => {
195187
'test-volume',
196188
{},
197189
);
198-
expect(
199-
mockCustomObjectsApi.deleteClusterCustomObject,
200-
).toHaveBeenCalledWith(
201-
MOCK_GROUP,
202-
MOCK_VERSION,
203-
MOCK_PLURAL,
204-
'test-lvm',
205-
{},
206-
);
207190
});
208191

209192
it('should raise an error if metalk8s volume deletion fails', async () => {
@@ -213,18 +196,86 @@ describe('Metalk8sLocalVolumeProvider', () => {
213196
).mockRejectedValue(new Error('Failed to delete metalk8s volume'));
214197
//E+V
215198
await expect(
216-
provider.detachVolumes([
217-
{
218-
IP: '192.168.1.100',
219-
devicePath: '/dev/sda',
220-
volumeType: VolumeType.Hardware,
221-
nodeName: 'test-node',
222-
metadata: { name: 'test-volume' },
223-
},
224-
]),
199+
provider.detachVolume({
200+
IP: '192.168.1.100',
201+
devicePath: '/dev/sda',
202+
volumeType: VolumeType.Hardware,
203+
nodeName: 'test-node',
204+
metadata: { name: 'test-volume' },
205+
}),
225206
).rejects.toThrow(
226207
'Failed to delete MetalK8s volume test-volume: Failed to delete metalk8s volume',
227208
);
228209
});
229210
});
211+
212+
describe('isVolumeProvisioned', () => {
213+
it('should return false if the volume is not yet provisioned', async () => {
214+
//S
215+
(
216+
mockCustomObjectsApi.getClusterCustomObject as jest.Mock
217+
).mockResolvedValue({
218+
status: { conditions: [{ type: 'Ready', status: 'Unknown' }] },
219+
});
220+
221+
//E
222+
const result = await provider.isVolumeProvisioned({
223+
IP: '192.168.1.100',
224+
devicePath: '/dev/sda',
225+
volumeType: VolumeType.Hardware,
226+
nodeName: 'test-node',
227+
volumeName: 'test-volume',
228+
});
229+
//V
230+
expect(result).toBe(false);
231+
});
232+
233+
it('should return true if the volume is provisioned', async () => {
234+
//S
235+
(
236+
mockCustomObjectsApi.getClusterCustomObject as jest.Mock
237+
).mockResolvedValue({
238+
status: { conditions: [{ type: 'Ready', status: 'True' }] },
239+
});
240+
//E
241+
const result = await provider.isVolumeProvisioned({
242+
IP: '192.168.1.100',
243+
devicePath: '/dev/sda',
244+
volumeType: VolumeType.Hardware,
245+
nodeName: 'test-node',
246+
volumeName: 'test-volume',
247+
});
248+
//V
249+
expect(result).toBe(true);
250+
});
251+
252+
it('should raise an error if the volume is failed to provisioned', async () => {
253+
//S
254+
(
255+
mockCustomObjectsApi.getClusterCustomObject as jest.Mock
256+
).mockResolvedValue({
257+
status: {
258+
conditions: [
259+
{
260+
type: 'Ready',
261+
status: 'False',
262+
reason: 'Volume is not provisioned',
263+
},
264+
],
265+
},
266+
});
267+
//E+V
268+
await expect(
269+
provider.isVolumeProvisioned({
270+
IP: '192.168.1.100',
271+
devicePath: '/dev/sda',
272+
volumeType: VolumeType.Hardware,
273+
nodeName: 'test-node',
274+
volumeName: 'test-volume',
275+
}),
276+
).rejects.toThrow(
277+
'Volume test-volume failed to provisioned: Volume is not provisioned',
278+
);
279+
});
280+
});
230281
});

ui/src/services/k8s/Metalk8sLocalVolumeProvider.ts

Lines changed: 69 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,14 @@ export type LocalPersistentVolume = V1PersistentVolume & {
2121
volumeType: VolumeType;
2222
};
2323

24+
type LocalVolume = {
25+
IP: string;
26+
devicePath: string;
27+
nodeName: string;
28+
volumeType: VolumeType;
29+
volumeName: string;
30+
};
31+
2432
export default class Metalk8sLocalVolumeProvider {
2533
apiUrl: string;
2634
constructor(apiUrl: string, private getToken: () => Promise<string>) {
@@ -88,37 +96,75 @@ export default class Metalk8sLocalVolumeProvider {
8896
};
8997

9098
// Since we don't have unique Serial Number for the disks, we need to retrieve the Volume Name from the PV.
91-
public detachVolumes = async (
92-
localPVs: LocalPersistentVolume[],
99+
public detachVolume = async (
100+
localPV: LocalPersistentVolume,
93101
): Promise<void> => {
94102
// The volume name is the same as the PV name
95-
const volumeNames = localPVs.map((localPV) => localPV.metadata.name);
103+
const volumeName = localPV.metadata.name;
104+
const token = await this.getToken();
105+
const { customObjects } = ApiK8s.updateApiServerConfig(this.apiUrl, token);
106+
const volumeClient = new Metalk8sV1alpha1VolumeClient(customObjects);
96107

97-
for (const volumeName of volumeNames) {
98-
const token = await this.getToken();
99-
const { customObjects } = ApiK8s.updateApiServerConfig(
100-
this.apiUrl,
101-
token,
108+
try {
109+
const deleteVolume = await volumeClient.deleteMetalk8sV1alpha1Volume(
110+
volumeName,
102111
);
103-
const volumeClient = new Metalk8sV1alpha1VolumeClient(customObjects);
104-
105-
try {
106-
const deleteVolume = await volumeClient.deleteMetalk8sV1alpha1Volume(
107-
volumeName,
108-
);
109112

110-
if (isError(deleteVolume)) {
111-
throw new Error(
112-
`Failed to delete MetalK8s volume ${volumeName}: ${deleteVolume.error.message}`,
113-
);
114-
}
115-
} catch (error) {
113+
if (isError(deleteVolume)) {
116114
throw new Error(
117-
`Failed to delete MetalK8s volume ${volumeName}: ${
118-
error instanceof Error ? error.message : JSON.stringify(error)
119-
}`,
115+
`Failed to delete MetalK8s volume ${volumeName}: ${deleteVolume.error.message}`,
120116
);
121117
}
118+
} catch (error) {
119+
throw new Error(
120+
`Failed to delete MetalK8s volume ${volumeName}: ${
121+
error instanceof Error ? error.message : JSON.stringify(error)
122+
}`,
123+
);
122124
}
123125
};
126+
127+
public isVolumeProvisioned = async (
128+
localVolume: LocalVolume,
129+
): Promise<boolean> => {
130+
const volumeName = localVolume.volumeName;
131+
132+
const token = await this.getToken();
133+
const { customObjects } = ApiK8s.updateApiServerConfig(this.apiUrl, token);
134+
const volumeClient = new Metalk8sV1alpha1VolumeClient(customObjects);
135+
136+
if (isError(volumeClient)) {
137+
throw new Error('Failed to create volume client');
138+
}
139+
140+
const volume = await volumeClient.getMetalk8sV1alpha1Volume(volumeName);
141+
142+
if (isError(volume)) {
143+
throw new Error(`Failed to get volume ${volumeName}: ${volume.error}`);
144+
}
145+
146+
const volumeStatus = volume.status?.conditions?.find(
147+
(condition) => condition.type === 'Ready',
148+
);
149+
150+
if (!volumeStatus) {
151+
return false;
152+
}
153+
154+
if (volumeStatus?.status === 'Unknown') {
155+
return false;
156+
}
157+
158+
if (volumeStatus?.status === 'False') {
159+
throw new Error(
160+
`Volume ${volumeName} failed to provisioned: ${volumeStatus.reason} `,
161+
);
162+
}
163+
164+
if (volumeStatus?.status === 'True') {
165+
return true;
166+
}
167+
168+
return false;
169+
};
124170
}

0 commit comments

Comments
 (0)