Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
123 changes: 87 additions & 36 deletions ui/src/services/k8s/Metalk8sLocalVolumeProvider.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ describe('Metalk8sLocalVolumeProvider', () => {
const mockCustomObjectsApi = {
listClusterCustomObject: jest.fn(),
deleteClusterCustomObject: jest.fn(),
getClusterCustomObject: jest.fn(),
} as unknown as CustomObjectsApi;

const mockCoreV1Api = {
Expand Down Expand Up @@ -160,31 +161,22 @@ describe('Metalk8sLocalVolumeProvider', () => {
});
});

describe('detachVolumes', () => {
it('should detach hardware volumes and virtual volumes', async () => {
describe('detachVolume', () => {
it('should detach volume', async () => {
//S
(
mockCustomObjectsApi.deleteClusterCustomObject as jest.Mock
).mockResolvedValue({
body: {},
});
//E
await provider.detachVolumes([
{
IP: '192.168.1.100',
devicePath: '/dev/sda',
volumeType: VolumeType.Hardware,
nodeName: 'test-node',
metadata: { name: 'test-volume' },
},
{
IP: '192.168.1.100',
devicePath: 'test-lvm',
volumeType: VolumeType.Virtual,
nodeName: 'test-node',
metadata: { name: 'test-lvm' },
},
]);
await provider.detachVolume({
IP: '192.168.1.100',
devicePath: '/dev/sda',
volumeType: VolumeType.Hardware,
nodeName: 'test-node',
metadata: { name: 'test-volume' },
});
//V
expect(
mockCustomObjectsApi.deleteClusterCustomObject,
Expand All @@ -195,15 +187,6 @@ describe('Metalk8sLocalVolumeProvider', () => {
'test-volume',
{},
);
expect(
mockCustomObjectsApi.deleteClusterCustomObject,
).toHaveBeenCalledWith(
MOCK_GROUP,
MOCK_VERSION,
MOCK_PLURAL,
'test-lvm',
{},
);
});

it('should raise an error if metalk8s volume deletion fails', async () => {
Expand All @@ -213,18 +196,86 @@ describe('Metalk8sLocalVolumeProvider', () => {
).mockRejectedValue(new Error('Failed to delete metalk8s volume'));
//E+V
await expect(
provider.detachVolumes([
{
IP: '192.168.1.100',
devicePath: '/dev/sda',
volumeType: VolumeType.Hardware,
nodeName: 'test-node',
metadata: { name: 'test-volume' },
},
]),
provider.detachVolume({
IP: '192.168.1.100',
devicePath: '/dev/sda',
volumeType: VolumeType.Hardware,
nodeName: 'test-node',
metadata: { name: 'test-volume' },
}),
).rejects.toThrow(
'Failed to delete MetalK8s volume test-volume: Failed to delete metalk8s volume',
);
});
});

describe('isVolumeProvisioned', () => {
it('should return false if the volume is not yet provisioned', async () => {
//S
(
mockCustomObjectsApi.getClusterCustomObject as jest.Mock
).mockResolvedValue({
status: { conditions: [{ type: 'Ready', status: 'Unknown' }] },
});

//E
const result = await provider.isVolumeProvisioned({
IP: '192.168.1.100',
devicePath: '/dev/sda',
volumeType: VolumeType.Hardware,
nodeName: 'test-node',
volumeName: 'test-volume',
});
//V
expect(result).toBe(false);
});

it('should return true if the volume is provisioned', async () => {
//S
(
mockCustomObjectsApi.getClusterCustomObject as jest.Mock
).mockResolvedValue({
status: { conditions: [{ type: 'Ready', status: 'True' }] },
});
//E
const result = await provider.isVolumeProvisioned({
IP: '192.168.1.100',
devicePath: '/dev/sda',
volumeType: VolumeType.Hardware,
nodeName: 'test-node',
volumeName: 'test-volume',
});
//V
expect(result).toBe(true);
});

it('should raise an error if the volume is failed to provisioned', async () => {
//S
(
mockCustomObjectsApi.getClusterCustomObject as jest.Mock
).mockResolvedValue({
status: {
conditions: [
{
type: 'Ready',
status: 'False',
reason: 'Volume is not provisioned',
},
],
},
});
//E+V
await expect(
provider.isVolumeProvisioned({
IP: '192.168.1.100',
devicePath: '/dev/sda',
volumeType: VolumeType.Hardware,
nodeName: 'test-node',
volumeName: 'test-volume',
}),
).rejects.toThrow(
'Volume test-volume failed to provisioned: Volume is not provisioned',
);
});
});
});
92 changes: 69 additions & 23 deletions ui/src/services/k8s/Metalk8sLocalVolumeProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ export type LocalPersistentVolume = V1PersistentVolume & {
volumeType: VolumeType;
};

type LocalVolume = {
IP: string;
devicePath: string;
nodeName: string;
volumeType: VolumeType;
volumeName: string;
};

export default class Metalk8sLocalVolumeProvider {
apiUrl: string;
constructor(apiUrl: string, private getToken: () => Promise<string>) {
Expand Down Expand Up @@ -88,37 +96,75 @@ export default class Metalk8sLocalVolumeProvider {
};

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

for (const volumeName of volumeNames) {
const token = await this.getToken();
const { customObjects } = ApiK8s.updateApiServerConfig(
this.apiUrl,
token,
try {
const deleteVolume = await volumeClient.deleteMetalk8sV1alpha1Volume(
volumeName,
);
const volumeClient = new Metalk8sV1alpha1VolumeClient(customObjects);

try {
const deleteVolume = await volumeClient.deleteMetalk8sV1alpha1Volume(
volumeName,
);

if (isError(deleteVolume)) {
throw new Error(
`Failed to delete MetalK8s volume ${volumeName}: ${deleteVolume.error.message}`,
);
}
} catch (error) {
if (isError(deleteVolume)) {
throw new Error(
`Failed to delete MetalK8s volume ${volumeName}: ${
error instanceof Error ? error.message : JSON.stringify(error)
}`,
`Failed to delete MetalK8s volume ${volumeName}: ${deleteVolume.error.message}`,
);
}
} catch (error) {
throw new Error(
`Failed to delete MetalK8s volume ${volumeName}: ${
error instanceof Error ? error.message : JSON.stringify(error)
}`,
);
}
};

public isVolumeProvisioned = async (
localVolume: LocalVolume,
): Promise<boolean> => {
const volumeName = localVolume.volumeName;

const token = await this.getToken();
const { customObjects } = ApiK8s.updateApiServerConfig(this.apiUrl, token);
const volumeClient = new Metalk8sV1alpha1VolumeClient(customObjects);

if (isError(volumeClient)) {
throw new Error('Failed to create volume client');
}

const volume = await volumeClient.getMetalk8sV1alpha1Volume(volumeName);

if (isError(volume)) {
throw new Error(`Failed to get volume ${volumeName}: ${volume.error}`);
}

const volumeStatus = volume.status?.conditions?.find(
(condition) => condition.type === 'Ready',
);

if (!volumeStatus) {
return false;
}

if (volumeStatus?.status === 'Unknown') {
return false;
}

if (volumeStatus?.status === 'False') {
throw new Error(
`Volume ${volumeName} failed to provisioned: ${volumeStatus.reason} `,
);
}

if (volumeStatus?.status === 'True') {
return true;
}

return false;
};
}
Loading