mirror of
https://github.com/harvester/harvester-ui-extension.git
synced 2026-10-14 10:06:04 +08:00
- Introduce test targets. - Adding additional tests. Related to: https://github.com/harvester/harvester/issues/11892 Signed-off-by: Volker Theile <vtheile@suse.com>
324 lines
10 KiB
JavaScript
324 lines
10 KiB
JavaScript
import { reactive } from 'vue';
|
|
import VirtVm from '../kubevirt.io.virtualmachine';
|
|
import { POD, PVC } from '@shell/config/types';
|
|
import { HCI } from '../../types';
|
|
|
|
// The Steve base model and the shell resource class pull in most of the dashboard, none of that is needed here
|
|
jest.mock('../harvester', () => ({ __esModule: true, default: class {} }));
|
|
jest.mock('@shell/plugins/dashboard-store/resource-class', () => ({ colorForState: (state) => (state === 'Running' ? 'text-success' : 'text-info') }));
|
|
|
|
const VM_ID = 'ns1/vm1';
|
|
|
|
/**
|
|
* Create a VM model instance without the full Steve model plumbing. Only the store getters that the
|
|
* VM model reads are mocked, backed by plain lists (like the real store, `all` returns the same array
|
|
* that is mutated in place on updates).
|
|
*
|
|
* The model reads its store either through `$rootGetters` (`harvester/...`) or through `$getters`, the getters of the
|
|
* store it lives in. Both are backed by the same data, so the tests do not depend on which of them the model uses.
|
|
*/
|
|
function createVm({
|
|
vm = {}, vmi, pods = [], pvcs = [], restores = [], backups = [], claims = ['disk-pvc'], printableStatus
|
|
} = {}) {
|
|
const lists = reactive({
|
|
[POD]: pods,
|
|
[PVC]: pvcs,
|
|
[HCI.RESTORE]: restores,
|
|
[HCI.BACKUP]: backups,
|
|
});
|
|
const byId = { [HCI.VMI]: vmi ? [vmi] : [] };
|
|
|
|
const getters = {
|
|
all: (type) => lists[type] || [],
|
|
byId: (type, id) => (byId[type] || lists[type] || []).find((x) => x.id === id),
|
|
schemaFor: () => undefined,
|
|
};
|
|
const rootGetters = {
|
|
'harvester-common/getFeatureEnabled': () => false,
|
|
currentProduct: { inStore: 'harvester' },
|
|
'harvester/all': getters.all,
|
|
'harvester/byId': getters.byId,
|
|
};
|
|
|
|
const obj = Object.create(VirtVm.prototype);
|
|
|
|
Object.assign(obj, {
|
|
id: VM_ID,
|
|
type: HCI.VM,
|
|
metadata: {
|
|
name: 'vm1', namespace: 'ns1', annotations: {}
|
|
},
|
|
spec: {
|
|
runStrategy: 'Always',
|
|
template: {
|
|
spec: {
|
|
domain: { cpu: { cores: 1 }, resources: { limits: { memory: '1Gi' } } },
|
|
volumes: claims.map((c) => ({ name: c, persistentVolumeClaim: { claimName: c } })),
|
|
}
|
|
}
|
|
},
|
|
status: {
|
|
created: true, ready: true, printableStatus: printableStatus || 'Running', conditions: []
|
|
},
|
|
...vm,
|
|
});
|
|
Object.defineProperty(obj, '$rootGetters', { value: rootGetters });
|
|
Object.defineProperty(obj, '$getters', { value: getters });
|
|
Object.defineProperty(obj, 't', { value: (k) => k });
|
|
|
|
return { vm: obj, lists };
|
|
}
|
|
|
|
const readyVmi = (extra = {}) => ({
|
|
id: VM_ID,
|
|
metadata: { name: 'vm1', namespace: 'ns1' },
|
|
status: {
|
|
phase: 'Running',
|
|
conditions: [{ type: 'Ready', status: 'True' }],
|
|
...extra
|
|
}
|
|
});
|
|
|
|
const pvc = (name, encrypted, ns = 'ns1') => ({
|
|
id: `${ ns }/${ name }`,
|
|
metadata: { name, namespace: ns },
|
|
isEncrypted: encrypted,
|
|
});
|
|
|
|
describe('class VirtVm', () => {
|
|
describe('restoreResource', () => {
|
|
it('should distinguish snapshots from backups and follow backup updates', () => {
|
|
const { vm, lists } = createVm({
|
|
vm: {
|
|
metadata: {
|
|
namespace: 'ns1', name: 'vm1', annotations: { 'restore.harvesterhci.io/name': 'r1' }
|
|
}
|
|
},
|
|
restores: [{
|
|
id: 'ns1/r1', isComplete: false, spec: { virtualMachineBackupNamespace: 'ns1', virtualMachineBackupName: 'b1' }
|
|
}],
|
|
backups: [
|
|
{ id: 'ns2/b1', spec: { type: 'snapshot' } },
|
|
{ id: 'ns1/b1', spec: { type: 'backup' } },
|
|
],
|
|
});
|
|
|
|
expect(vm.restoreResource.fromSnapshot).toBe(false);
|
|
expect(vm.restoreState).toBe(false);
|
|
|
|
lists[HCI.BACKUP][1].spec.type = 'snapshot';
|
|
expect(vm.restoreResource.fromSnapshot).toBe(true);
|
|
|
|
lists[HCI.BACKUP].splice(1, 1);
|
|
expect(vm.restoreResource.fromSnapshot).toBe(false);
|
|
|
|
lists[HCI.RESTORE][0].isComplete = true;
|
|
expect(vm.restoreState).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('encryptedVolumeType', () => {
|
|
it('should be none without volumes', () => {
|
|
const { vm } = createVm({ claims: [] });
|
|
|
|
expect(vm.encryptedVolumeType).toBe('none');
|
|
});
|
|
|
|
it('should be none if no PVC is encrypted', () => {
|
|
const { vm } = createVm({ pvcs: [pvc('disk-pvc', false)] });
|
|
|
|
expect(vm.encryptedVolumeType).toBe('none');
|
|
});
|
|
|
|
it('should be all if every PVC is encrypted', () => {
|
|
const { vm } = createVm({
|
|
claims: ['a', 'b'],
|
|
pvcs: [pvc('a', true), pvc('b', true), pvc('other', false)]
|
|
});
|
|
|
|
expect(vm.encryptedVolumeType).toBe('all');
|
|
});
|
|
|
|
it('should be partial if some PVCs are encrypted', () => {
|
|
const { vm } = createVm({
|
|
claims: ['a', 'b'],
|
|
pvcs: [pvc('a', true), pvc('b', false)]
|
|
});
|
|
|
|
expect(vm.encryptedVolumeType).toBe('partial');
|
|
});
|
|
|
|
it('should follow PVCs that are added to the store later', () => {
|
|
const { vm, lists } = createVm({ claims: ['a'], pvcs: [] });
|
|
|
|
expect(vm.encryptedVolumeType).toBe('none');
|
|
|
|
lists[PVC].push(pvc('a', true));
|
|
|
|
expect(vm.encryptedVolumeType).toBe('all');
|
|
});
|
|
});
|
|
|
|
describe('actualState', () => {
|
|
it('should be Off if the VM is halted', () => {
|
|
const { vm } = createVm({
|
|
vm: {
|
|
spec: { runStrategy: 'Halted', template: { spec: { volumes: [], domain: {} } } },
|
|
status: { created: false, conditions: [] }
|
|
}
|
|
});
|
|
|
|
expect(vm.actualState).toBe('Off');
|
|
});
|
|
|
|
it('should be Running if the VMI is running and ready', () => {
|
|
const { vm } = createVm({ vmi: readyVmi() });
|
|
|
|
expect(vm.actualState).toBe('Running');
|
|
});
|
|
|
|
it('should be Not Ready if the VMI is running but not ready', () => {
|
|
const { vm } = createVm({ vmi: readyVmi({ conditions: [{ type: 'Ready', status: 'False' }] }) });
|
|
|
|
expect(vm.actualState).toBe('Not Ready');
|
|
});
|
|
|
|
it('should be Paused if the VMI has the Paused condition', () => {
|
|
const { vm } = createVm({ vmi: readyVmi({ conditions: [{ type: 'Ready', status: 'True' }, { type: 'Paused', status: 'True' }] }) });
|
|
|
|
expect(vm.actualState).toBe('Paused');
|
|
});
|
|
|
|
it('should be Stopping if the VM is not expected to run anymore but the VMI exists', () => {
|
|
const { vm } = createVm({
|
|
vm: { spec: { runStrategy: 'Halted', template: { spec: { volumes: [], domain: {} } } } },
|
|
vmi: readyVmi()
|
|
});
|
|
|
|
expect(vm.actualState).toBe('Stopping');
|
|
});
|
|
|
|
it('should be Starting while the launcher pod is not ready', () => {
|
|
const { vm } = createVm({
|
|
vmi: readyVmi({ phase: 'Scheduling', conditions: [] }),
|
|
pods: [{
|
|
metadata: { namespace: 'ns1', ownerReferences: [{ name: 'vm1' }] },
|
|
getPodStatus: { status: 'POD_NOT_SCHEDULABLE' }
|
|
}]
|
|
});
|
|
|
|
expect(vm.actualState).toBe('Starting');
|
|
});
|
|
|
|
it('should be Unschedulable if the VM is starting and has an unschedulable condition', () => {
|
|
const { vm } = createVm({
|
|
vmi: readyVmi({ phase: 'Pending', conditions: [] }),
|
|
vm: { status: { created: true, conditions: [{ reason: 'Unschedulable', message: 'no node' }] } }
|
|
});
|
|
|
|
expect(vm.actualState).toBe('Unschedulable');
|
|
expect(vm.isUnschedulable.message).toBe('no node');
|
|
});
|
|
|
|
it('should not be Unschedulable without an unschedulable condition', () => {
|
|
const { vm } = createVm({ vmi: readyVmi() });
|
|
|
|
expect(vm.isUnschedulable).toBeNull();
|
|
expect(vm.actualState).toBe('Running');
|
|
});
|
|
|
|
it('should not be Unschedulable if the VM is neither starting nor stopping', () => {
|
|
const { vm } = createVm({
|
|
vm: {
|
|
spec: { runStrategy: 'Halted', template: { spec: { volumes: [], domain: {} } } },
|
|
status: { created: false, conditions: [{ reason: 'Unschedulable' }] }
|
|
}
|
|
});
|
|
|
|
expect(vm.isUnschedulable).toBeNull();
|
|
expect(vm.actualState).toBe('Off');
|
|
});
|
|
|
|
it('should be Terminating if the VM is being deleted', () => {
|
|
const { vm } = createVm({
|
|
vmi: readyVmi(),
|
|
vm: {
|
|
metadata: {
|
|
name: 'vm1', namespace: 'ns1', annotations: {}, deletionTimestamp: 'now'
|
|
}
|
|
}
|
|
});
|
|
|
|
expect(vm.actualState).toBe('Terminating');
|
|
});
|
|
|
|
it('should be VM error if the VM has a failure condition', () => {
|
|
const { vm } = createVm({ vm: { status: { created: true, conditions: [{ type: 'Failure', message: 'boom' }] } } });
|
|
|
|
expect(vm.actualState).toBe('VM error');
|
|
});
|
|
|
|
it('should be Restoring while a restore is in progress', () => {
|
|
const { vm } = createVm({
|
|
vmi: readyVmi(),
|
|
vm: {
|
|
metadata: {
|
|
name: 'vm1', namespace: 'ns1', annotations: { 'restore.harvesterhci.io/name': 'r1' }
|
|
}
|
|
},
|
|
restores: [{ id: 'ns1/r1', isComplete: false }]
|
|
});
|
|
|
|
expect(vm.actualState).toBe('Restoring');
|
|
});
|
|
|
|
it('should not be Restoring if the restore of the annotation does not exist', () => {
|
|
const { vm } = createVm({
|
|
vmi: readyVmi(),
|
|
vm: {
|
|
metadata: {
|
|
name: 'vm1', namespace: 'ns1', annotations: { 'restore.harvesterhci.io/name': 'other' }
|
|
}
|
|
},
|
|
restores: [{ id: 'ns1/r1', isComplete: false }]
|
|
});
|
|
|
|
expect(vm.actualState).toBe('Running');
|
|
});
|
|
});
|
|
|
|
describe('podResource', () => {
|
|
it('should find the launcher pod by owner name', () => {
|
|
const pod = { metadata: { namespace: 'ns1', ownerReferences: [{ name: 'vm1' }] } };
|
|
const { vm } = createVm({ vmi: readyVmi(), pods: [pod] });
|
|
|
|
expect(vm.podResource).toStrictEqual(pod);
|
|
});
|
|
|
|
it('should return the last pod if several pods have the same owner', () => {
|
|
// e.g. the source and the target pod while the VM migrates
|
|
const source = {
|
|
metadata: {
|
|
name: 'launcher-source', namespace: 'ns1', ownerReferences: [{ name: 'vm1' }]
|
|
}
|
|
};
|
|
const target = {
|
|
metadata: {
|
|
name: 'launcher-target', namespace: 'ns1', ownerReferences: [{ name: 'vm1' }]
|
|
}
|
|
};
|
|
const { vm } = createVm({ vmi: readyVmi(), pods: [source, target] });
|
|
|
|
expect(vm.podResource.metadata.name).toBe('launcher-target');
|
|
});
|
|
});
|
|
|
|
describe('isRestartRequired', () => {
|
|
it('should reflect the RestartRequired condition', () => {
|
|
const { vm } = createVm({ vm: { status: { conditions: [{ type: 'RestartRequired', status: 'True' }] } } });
|
|
|
|
expect(vm.isRestartRequired).toBe(true);
|
|
});
|
|
});
|
|
});
|