Skip to content

Commit 0be1d81

Browse files
committed
Add initial KubeVirt integration
1 parent c1d04c5 commit 0be1d81

38 files changed

+1082
-61
lines changed

frontend/__tests__/components/utils/firehose.spec.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Map as ImmutableMap } from 'immutable';
66
import Spy = jasmine.Spy;
77

88
import { Firehose } from '../../../public/components/utils/firehose';
9-
import { FirehoseResource } from '../../../public/components/factory';
9+
import { FirehoseResource } from '../../../public/components/utils';
1010
import { K8sKind, K8sResourceKindReference } from '../../../public/module/k8s';
1111
import { PodModel, ServiceModel } from '../../../public/models';
1212

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import * as _ from 'lodash';
2+
3+
import { VirtualMachineModel } from '../../../../../public/models';
4+
import { getVMStatus } from '../../../../../public/extend/kubevirt/module/k8s/vms';
5+
6+
// TODO: add this to __mocks__/k8sResourcesMocks module, should be in sync
7+
// with VirtualMachine YAML template defined in public/models/yaml-templates
8+
const testVirtualMachine = {
9+
apiVersion: `${VirtualMachineModel.apiGroup}/${VirtualMachineModel.apiVersion}`,
10+
kind: 'VirtualMachine',
11+
metadata: {
12+
name: 'example',
13+
namespace: 'default',
14+
},
15+
spec: {
16+
running: false,
17+
template: {
18+
// TODO: empty for now, see above comment
19+
},
20+
},
21+
};
22+
23+
describe('getVMStatus', () => {
24+
it('returns the status string based on spec.running', () => {
25+
const vm1 = _.cloneDeep(testVirtualMachine);
26+
vm1.spec.running = true;
27+
expect(getVMStatus(vm1)).toBe('Running');
28+
29+
const vm2 = _.cloneDeep(testVirtualMachine);
30+
vm2.spec.running = false;
31+
expect(getVMStatus(vm2)).toBe('Stopped');
32+
33+
const vm3 = _.cloneDeep(testVirtualMachine);
34+
vm3.spec.running = undefined;
35+
expect(getVMStatus(vm3)).toBe('Stopped');
36+
});
37+
});

frontend/__tests__/features.spec.tsx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ describe('featureReducer', () => {
4444
[FLAGS.OPERATOR_HUB]: false,
4545
[FLAGS.CLUSTER_API]: false,
4646
[FLAGS.MACHINE_CONFIG]: false,
47+
[FLAGS.KUBEVIRT]: false,
4748
}));
4849
});
4950
});

frontend/integration-tests/preload.ts

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
/* eslint-disable no-undef */
2+
3+
import { addAlias } from 'module-alias';
4+
5+
// The `lodash-es` package ships code that uses ECMAScript modules. Since `ts-node`
6+
// excludes everything under `node_modules` directory from compilation, attempting
7+
// to import `lodash-es` code yields syntax errors.
8+
//
9+
// Integration tests already use the `lodash` package, so we simply register alias
10+
// to `lodash` in context of Node.js module resolution mechanism.
11+
12+
addAlias('lodash-es', 'lodash');
13+
14+
declare global {
15+
interface window {
16+
SERVER_FLAGS: object;
17+
}
18+
}
19+
20+
(global as any).window = {
21+
SERVER_FLAGS: {
22+
basePath: '/',
23+
},
24+
};

frontend/integration-tests/protractor.conf.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -104,6 +104,7 @@ export const config: Config = {
104104
performance: ['tests/base.scenario.ts', 'tests/performance.scenario.ts'],
105105
serviceCatalog: ['tests/base.scenario.ts', 'tests/service-catalog/service-catalog.scenario.ts', 'tests/service-catalog/service-broker.scenario.ts', 'tests/service-catalog/service-class.scenario.ts', 'tests/service-catalog/service-binding.scenario.ts', 'tests/developer-catalog.scenario.ts'],
106106
overview: ['tests/base.scenario.ts', 'tests/overview/overview.scenario.ts'],
107+
kubevirt: ['tests/base.scenario.ts', 'tests/kubevirt/vm.actions.scenario.ts'],
107108
e2e: [
108109
'tests/base.scenario.ts',
109110
'tests/crud.scenario.ts',

frontend/integration-tests/tests/crud.scenario.ts

Lines changed: 21 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -11,13 +11,15 @@ import * as crudView from '../views/crud.view';
1111
import * as yamlView from '../views/yaml.view';
1212
import * as namespaceView from '../views/namespace.view';
1313
import * as createRoleBindingView from '../views/create-role-binding.view';
14+
import { referenceForModel, kindForReference } from '../../public/module/k8s/k8s';
15+
import { ClusterServiceBrokerModel, VirtualMachineModel } from '../../public/models';
1416

1517
const K8S_CREATION_TIMEOUT = 15000;
1618

1719
describe('Kubernetes resource CRUD operations', () => {
1820
const testLabel = 'automatedTestName';
1921
const leakedResources = new Set<string>();
20-
const k8sObjs = OrderedMap<string, {kind: string, namespaced?: boolean}>()
22+
const k8sObjs = OrderedMap<string, ObjMapValue>()
2123
.set('pods', {kind: 'Pod'})
2224
.set('services', {kind: 'Service'})
2325
.set('serviceaccounts', {kind: 'ServiceAccount'})
@@ -38,15 +40,18 @@ describe('Kubernetes resource CRUD operations', () => {
3840
.set('horizontalpodautoscalers', {kind: 'HorizontalPodAutoscaler'})
3941
.set('networkpolicies', {kind: 'NetworkPolicy'})
4042
.set('roles', {kind: 'Role'});
41-
const openshiftObjs = OrderedMap<string, {kind: string, namespaced?: boolean}>()
43+
const openshiftObjs = OrderedMap<string, ObjMapValue>()
4244
.set('deploymentconfigs', {kind: 'DeploymentConfig'})
4345
.set('buildconfigs', {kind: 'BuildConfig'})
4446
.set('imagestreams', {kind: 'ImageStream'})
4547
.set('routes', {kind: 'Route'});
46-
const serviceCatalogObjs = OrderedMap<string, {kind: string, namespaced?: boolean}>()
47-
.set('clusterservicebrokers', {kind: 'servicecatalog.k8s.io~v1beta1~ClusterServiceBroker', namespaced: false});
48+
const serviceCatalogObjs = OrderedMap<string, ObjMapValue>()
49+
.set('clusterservicebrokers', {kind: referenceForModel(ClusterServiceBrokerModel), namespaced: false});
50+
const kubevirtObjs = OrderedMap<string, ObjMapValue>()
51+
.set('virtualmachines', {kind: referenceForModel(VirtualMachineModel), crd: true});
4852
let testObjs = browser.params.openshift === 'true' ? k8sObjs.merge(openshiftObjs) : k8sObjs;
4953
testObjs = browser.params.servicecatalog === 'true' ? testObjs.merge(serviceCatalogObjs) : testObjs;
54+
testObjs = browser.params.kubevirt === 'true' ? testObjs.merge(kubevirtObjs) : testObjs;
5055

5156
afterEach(() => {
5257
checkLogs();
@@ -67,9 +72,11 @@ describe('Kubernetes resource CRUD operations', () => {
6772
});
6873
});
6974

70-
testObjs.forEach(({kind, namespaced = true}, resource) => {
75+
testObjs.forEach(({kind, namespaced = true, crd = false}, resource) => {
76+
const simpleKind = kindForReference(kind);
77+
const suiteDesc = crd ? `${simpleKind} (CRD)` : kind;
7178

72-
describe(kind, () => {
79+
describe(suiteDesc, () => {
7380
const name = `${testName}-${kind.toLowerCase()}`;
7481
it('displays a list view for the resource', async() => {
7582
await browser.get(`${appHost}${namespaced ? `/k8s/ns/${testName}` : '/k8s/cluster'}/${resource}?name=${testName}`);
@@ -138,7 +145,7 @@ describe('Kubernetes resource CRUD operations', () => {
138145
await browser.get(`${appHost}/search/${namespaced ? `ns/${testName}` : 'all-namespaces'}?kind=${kind}&q=${testLabel}%3d${testName}`);
139146
await crudView.filterForName(name);
140147
await crudView.resourceRowsPresent();
141-
await crudView.editRow(kind)(name);
148+
await crudView.editRow(simpleKind)(name);
142149
}
143150
});
144151

@@ -148,7 +155,7 @@ describe('Kubernetes resource CRUD operations', () => {
148155
// Filter by resource name to make sure the resource is on the first page of results.
149156
// Otherwise the tests fail since we do virtual scrolling and the element isn't found.
150157
await crudView.filterForName(name);
151-
await crudView.deleteRow(kind)(name);
158+
await crudView.deleteRow(simpleKind)(name);
152159
leakedResources.delete(JSON.stringify({name, plural: resource, namespace: namespaced ? testName : undefined}));
153160
});
154161
});
@@ -365,3 +372,9 @@ describe('Kubernetes resource CRUD operations', () => {
365372
});
366373
});
367374
});
375+
376+
type ObjMapValue = {
377+
kind: string;
378+
namespaced?: boolean;
379+
crd?: boolean;
380+
};
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
/* eslint-disable no-undef */
2+
import { testName } from '../../protractor.conf';
3+
4+
const testLabel = 'automatedTestName';
5+
6+
export const testVM = {
7+
apiVersion: 'kubevirt.io/v1alpha2',
8+
kind: 'VirtualMachine',
9+
metadata: {
10+
name: `vm-${testName}`,
11+
namespace: testName,
12+
labels: {[testLabel]: testName},
13+
},
14+
spec: {
15+
running: false,
16+
template: {
17+
spec: {
18+
domain: {
19+
cpu: {
20+
cores: 1,
21+
},
22+
devices: {
23+
disks: [
24+
{
25+
bootOrder: 1,
26+
disk: {
27+
bus: 'virtio',
28+
},
29+
name: 'rootdisk',
30+
volumeName: 'rootdisk',
31+
},
32+
],
33+
interfaces: [
34+
{
35+
bridge: {},
36+
name: 'eth0',
37+
},
38+
],
39+
},
40+
resources: {
41+
requests: {
42+
memory: '1G',
43+
},
44+
},
45+
},
46+
networks: [
47+
{
48+
name: 'eth0',
49+
pod: {},
50+
},
51+
],
52+
volumes: [
53+
{
54+
containerDisk: {
55+
image: 'kubevirt/cirros-registry-disk-demo:latest',
56+
},
57+
name: 'rootdisk',
58+
},
59+
],
60+
},
61+
},
62+
},
63+
};
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/* eslint-disable no-undef */
2+
import { execSync } from 'child_process';
3+
4+
export function removeLeakedResources(leakedResources: Set<string>){
5+
const leakedArray: Array<string> = [...leakedResources];
6+
if (leakedArray.length > 0) {
7+
console.error(`Leaked ${leakedArray.join()}`);
8+
leakedArray.map(r => JSON.parse(r) as {name: string, namespace: string, kind: string})
9+
.forEach(({name, namespace, kind}) => {
10+
try {
11+
execSync(`kubectl delete -n ${namespace} --cascade ${kind} ${name}`);
12+
} catch (error) {
13+
console.error(`Failed to delete ${kind} ${name}:\n${error}`);
14+
}
15+
});
16+
}
17+
}
Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
/* eslint-disable no-undef */
2+
3+
import { execSync } from 'child_process';
4+
import { browser, ExpectedConditions as until } from 'protractor';
5+
6+
import { appHost, testName } from '../../protractor.conf';
7+
import { resourceRowsPresent, filterForName, isLoaded } from '../../views/crud.view';
8+
import { testVM } from './mocks';
9+
import { removeLeakedResources } from './utils';
10+
import { detailViewAction, detailViewVMmStatus, listViewAction, listViewVMmStatus } from '../../views/kubevirt/vm.actions.view';
11+
12+
const VM_BOOTUP_TIMEOUT = 60000;
13+
const VM_ACTIONS_TIMEOUT = 90000;
14+
15+
describe('Test VM actions', () => {
16+
const leakedResources = new Set<string>();
17+
afterAll(async() => {
18+
removeLeakedResources(leakedResources);
19+
});
20+
21+
describe('Test VM list view kebab actions', () => {
22+
const vmName = `vm-list-view-actions-${testName}`;
23+
beforeAll(async() => {
24+
testVM.metadata.name = vmName;
25+
execSync(`echo '${JSON.stringify(testVM)}' | kubectl create -f -`);
26+
leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
27+
});
28+
29+
// Workaround for https://github.com/kubevirt/web-ui/issues/177, remove when resolved
30+
afterEach(async() => await browser.sleep(1000));
31+
32+
it('Navigates to VMs', async() => {
33+
await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines`);
34+
await isLoaded();
35+
await filterForName(vmName);
36+
await resourceRowsPresent();
37+
});
38+
39+
it('Starts VM', async() => {
40+
await listViewAction(vmName)('Start');
41+
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Running'), VM_BOOTUP_TIMEOUT);
42+
});
43+
44+
it('Restarts VM', async() => {
45+
await listViewAction(vmName)('Restart');
46+
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Starting'), VM_BOOTUP_TIMEOUT);
47+
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Running'), VM_BOOTUP_TIMEOUT);
48+
}, VM_ACTIONS_TIMEOUT);
49+
50+
it('Stops VM', async() => {
51+
await listViewAction(vmName)('Stop');
52+
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Off'), 10000);
53+
});
54+
55+
it('Deletes VM', async() => {
56+
await listViewAction(vmName)('Delete');
57+
await browser.wait(until.not(until.presenceOf(listViewVMmStatus(vmName))));
58+
leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
59+
});
60+
});
61+
62+
describe('Test VM detail view kebab actions', () => {
63+
const vmName = `vm-detail-view-actions-${testName}`;
64+
beforeAll(async() => {
65+
testVM.metadata.name = vmName;
66+
execSync(`echo '${JSON.stringify(testVM)}' | kubectl create -f -`);
67+
leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
68+
});
69+
70+
it('Navigates to VMs detail page', async() => {
71+
await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines/${vmName}`);
72+
await isLoaded();
73+
});
74+
75+
it('Starts VM', async() => {
76+
await detailViewAction('Start');
77+
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Running'), VM_BOOTUP_TIMEOUT);
78+
});
79+
80+
it('Restarts VM', async() => {
81+
await detailViewAction('Restart');
82+
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Starting'), VM_BOOTUP_TIMEOUT);
83+
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Running'), VM_BOOTUP_TIMEOUT);
84+
}, VM_ACTIONS_TIMEOUT);
85+
86+
it('Stops VM', async() => {
87+
await detailViewAction('Stop');
88+
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Off'), 10000);
89+
});
90+
91+
it('Deletes VM', async() => {
92+
await detailViewAction('Delete');
93+
leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
94+
});
95+
});
96+
});

frontend/integration-tests/tests/performance.scenario.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ const chunkedRoutes = OrderedMap<string, {section: string, name: string}>()
2020
.set('cron-job', {section: 'Workloads', name: 'Cron Jobs'})
2121
.set('configmap', {section: 'Workloads', name: 'Config Maps'})
2222
.set('hpa', {section: 'Workloads', name: 'HPAs'})
23+
.set('virtual-machine', {section: 'Workloads', name: 'Virtual Machines'})
2324
.set('service', {section: 'Networking', name: 'Services'})
2425
.set('persistent-volume', {section: 'Storage', name: 'Persistent Volumes'})
2526
.set('persistent-volume-claim', {section: 'Storage', name: 'Persistent Volume Claims'})

frontend/integration-tests/views/crud.view.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ export const saveChangesBtn = $('#save-changes');
1313
export const reloadBtn = $('#reload-object');
1414
export const cancelBtn = $('#cancel');
1515

16+
export const confirmAction = () => browser.wait(until.presenceOf($('#confirm-action'))).then(() => $('#confirm-action').click());
17+
1618
/**
1719
* Returns a promise that resolves after the loading spinner is not present.
1820
*/

0 commit comments

Comments
 (0)