Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 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
48 changes: 27 additions & 21 deletions ui/src/services/k8s/Metalk8sLocalVolumeProvider.test.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,27 @@
import { CoreV1Api, CustomObjectsApi } from '@kubernetes/client-node';
import { updateApiServerConfig } from './api';
import Metalk8sLocalVolumeProvider, {
VolumeType,
} from './Metalk8sLocalVolumeProvider';
import { updateApiServerConfig } from './api';
import { Metalk8sV1alpha1VolumeClient } from './Metalk8sVolumeClient.generated';

jest.mock('../k8s/api', () => ({
updateApiServerConfig: jest.fn(),
}));

const MOCK_GROUP = 'storage.metalk8s.scality.com';
const MOCK_VERSION = 'v1alpha1';
const MOCK_PLURAL = 'volumes';

describe('Metalk8sLocalVolumeProvider', () => {
let provider: Metalk8sLocalVolumeProvider;
const mockUrl = 'mock-url';
const mockToken = 'mock-token';
const mockToken = jest.fn(() => Promise.resolve('mock-token'));

const mockCustomObjectsApi = {
listClusterCustomObject: jest.fn(),
deleteClusterCustomObject: jest.fn(),
} as unknown as CustomObjectsApi;

const mockVolumeClient = {
deleteMetalk8sV1alpha1Volume: jest.fn().mockResolvedValue({ body: {} }),
getMetalk8sV1alpha1VolumeList: jest.fn(),
getMetalk8sV1alpha1Volume: jest.fn(),
createMetalk8sV1alpha1Volume: jest.fn(),
patchMetalk8sV1alpha1Volume: jest.fn(),
} as unknown as Metalk8sV1alpha1VolumeClient;

const mockCoreV1Api = {
listNode: jest.fn(),
listPersistentVolume: jest.fn(),
Expand All @@ -39,8 +35,6 @@ describe('Metalk8sLocalVolumeProvider', () => {
});

provider = new Metalk8sLocalVolumeProvider(mockUrl, mockToken);
provider.k8sClient = mockCoreV1Api;
provider.volumeClient = mockVolumeClient;
});

describe('listLocalPersistentVolumes', () => {
Expand Down Expand Up @@ -73,7 +67,7 @@ describe('Metalk8sLocalVolumeProvider', () => {
});

(
mockVolumeClient.getMetalk8sV1alpha1VolumeList as jest.Mock
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
).mockResolvedValue({
body: {
items: [
Expand Down Expand Up @@ -155,7 +149,7 @@ describe('Metalk8sLocalVolumeProvider', () => {
});

(
mockVolumeClient.getMetalk8sV1alpha1VolumeList as jest.Mock
mockCustomObjectsApi.listClusterCustomObject as jest.Mock
).mockRejectedValue(new Error('Failed to fetch volumes'));

await expect(
Expand All @@ -170,7 +164,7 @@ describe('Metalk8sLocalVolumeProvider', () => {
it('should detach hardware volumes and virtual volumes', async () => {
//S
(
mockVolumeClient.deleteMetalk8sV1alpha1Volume as jest.Mock
mockCustomObjectsApi.deleteClusterCustomObject as jest.Mock
).mockResolvedValue({
body: {},
});
Expand All @@ -193,17 +187,29 @@ describe('Metalk8sLocalVolumeProvider', () => {
]);
//V
expect(
mockVolumeClient.deleteMetalk8sV1alpha1Volume,
).toHaveBeenCalledWith('test-volume');
mockCustomObjectsApi.deleteClusterCustomObject,
).toHaveBeenCalledWith(
MOCK_GROUP,
MOCK_VERSION,
MOCK_PLURAL,
'test-volume',
{},
);
expect(
mockVolumeClient.deleteMetalk8sV1alpha1Volume,
).toHaveBeenCalledWith('test-lvm');
mockCustomObjectsApi.deleteClusterCustomObject,
).toHaveBeenCalledWith(
MOCK_GROUP,
MOCK_VERSION,
MOCK_PLURAL,
'test-lvm',
{},
);
});

it('should raise an error if metalk8s volume deletion fails', async () => {
//S
(
mockVolumeClient.deleteMetalk8sV1alpha1Volume as jest.Mock
mockCustomObjectsApi.deleteClusterCustomObject as jest.Mock
).mockRejectedValue(new Error('Failed to delete metalk8s volume'));
//E+V
await expect(
Expand Down
41 changes: 29 additions & 12 deletions ui/src/services/k8s/Metalk8sLocalVolumeProvider.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { CoreV1Api, V1PersistentVolume } from '@kubernetes/client-node';
import { V1PersistentVolume } from '@kubernetes/client-node';
import * as ApiK8s from './api';
import {
Metalk8sV1alpha1VolumeClient,
Expand All @@ -22,18 +22,24 @@ export type LocalPersistentVolume = V1PersistentVolume & {
};

export default class Metalk8sLocalVolumeProvider {
volumeClient: Metalk8sV1alpha1VolumeClient;
k8sClient: CoreV1Api;
constructor(url: string, token: string) {
const { coreV1, customObjects } = ApiK8s.updateApiServerConfig(url, token);
this.volumeClient = new Metalk8sV1alpha1VolumeClient(customObjects);
this.k8sClient = coreV1;
apiUrl: string;
constructor(apiUrl: string, private getToken: () => Promise<string>) {
this.apiUrl = apiUrl;
}

public listLocalPersistentVolumes = async (
serverName: string,
): Promise<LocalPersistentVolume[]> => {
try {
const nodes = await this.k8sClient.listNode();
const token = await this.getToken();
const { coreV1, customObjects } = ApiK8s.updateApiServerConfig(
this.apiUrl,
token,
);
const volumeClient = new Metalk8sV1alpha1VolumeClient(customObjects);
const k8sClient = coreV1;

const nodes = await k8sClient.listNode();
const nodeIP = nodes.body.items
.find((node) => node.metadata.name === serverName)
?.status.addresses.find((address) => address.type === 'InternalIP');
Expand All @@ -42,13 +48,13 @@ export default class Metalk8sLocalVolumeProvider {
throw new Error(`Failed to find IP for node ${serverName}`);
}

const volumes = await this.volumeClient.getMetalk8sV1alpha1VolumeList();
const volumes = await volumeClient.getMetalk8sV1alpha1VolumeList();

if (!isError(volumes)) {
const nodeVolumes = volumes.body.items.filter(
(volume) => volume.spec.nodeName === serverName,
);
const pv = await this.k8sClient.listPersistentVolume();
const pv = await k8sClient.listPersistentVolume();

const localPv = nodeVolumes.reduce((acc, item) => {
const isLocalPv = pv.body.items.find(
Expand All @@ -72,7 +78,7 @@ export default class Metalk8sLocalVolumeProvider {

return localPv;
} else {
throw new Error(`Failed to fetch metalk8s volumes: ${volumes.error}`);
throw new Error(`${volumes.error.message}`);
}
} catch (error) {
throw new Error(
Expand All @@ -87,10 +93,21 @@ export default class Metalk8sLocalVolumeProvider {
): Promise<void> => {
// The volume name is the same as the PV name
const volumeNames = localPVs.map((localPV) => 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) {
try {
await this.volumeClient.deleteMetalk8sV1alpha1Volume(volumeName);
const deleteVolume = await volumeClient.deleteMetalk8sV1alpha1Volume(
volumeName,
);

if (isError(deleteVolume)) {
throw new Error(
`Failed to delete MetalK8s volume ${volumeName}: ${deleteVolume.error.message}`,
);
}
} catch (error) {
throw new Error(
`Failed to delete MetalK8s volume ${volumeName}: ${
Expand Down
Loading