Skip to content

KubeVirt extension PoC #888

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
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
2 changes: 1 addition & 1 deletion frontend/__tests__/components/utils/firehose.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { Map as ImmutableMap } from 'immutable';
import Spy = jasmine.Spy;

import { Firehose } from '../../../public/components/utils/firehose';
import { FirehoseResource } from '../../../public/components/factory';
import { FirehoseResource } from '../../../public/components/utils';
import { K8sKind, K8sResourceKindReference } from '../../../public/module/k8s';
import { PodModel, ServiceModel } from '../../../public/models';

Expand Down
37 changes: 37 additions & 0 deletions frontend/__tests__/extend/kubevirt/module/k8s/vms.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import * as _ from 'lodash';

import { VirtualMachineModel } from '../../../../../public/models';
import { getVMStatus } from '../../../../../public/extend/kubevirt/module/k8s/vms';

// TODO: add this to __mocks__/k8sResourcesMocks module, should be in sync
// with VirtualMachine YAML template defined in public/models/yaml-templates
const testVirtualMachine = {
apiVersion: `${VirtualMachineModel.apiGroup}/${VirtualMachineModel.apiVersion}`,
kind: 'VirtualMachine',
metadata: {
name: 'example',
namespace: 'default',
},
spec: {
running: false,
template: {
// TODO: empty for now, see above comment
},
},
};

describe('getVMStatus', () => {
it('returns the status string based on spec.running', () => {
const vm1 = _.cloneDeep(testVirtualMachine);
vm1.spec.running = true;
expect(getVMStatus(vm1)).toBe('Running');

const vm2 = _.cloneDeep(testVirtualMachine);
vm2.spec.running = false;
expect(getVMStatus(vm2)).toBe('Stopped');

const vm3 = _.cloneDeep(testVirtualMachine);
vm3.spec.running = undefined;
expect(getVMStatus(vm3)).toBe('Stopped');
});
});
1 change: 1 addition & 0 deletions frontend/__tests__/features.spec.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ describe('featureReducer', () => {
[FLAGS.OPERATOR_HUB]: false,
[FLAGS.CLUSTER_API]: false,
[FLAGS.MACHINE_CONFIG]: false,
[FLAGS.KUBEVIRT]: false,
}));
});
});
Expand Down
24 changes: 24 additions & 0 deletions frontend/integration-tests/preload.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
/* eslint-disable no-undef */

import { addAlias } from 'module-alias';

// The `lodash-es` package ships code that uses ECMAScript modules. Since `ts-node`
// excludes everything under `node_modules` directory from compilation, attempting
// to import `lodash-es` code yields syntax errors.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a little confused why we're just seeing this problem now. Is there a reason we don't see errors for all of our integration tests?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both unit and integration tests use lodash instead of lodash-es package.

$ ack -r -l --ignore-dir=".cache-loader,node_modules" 'from '"'"'lodash(?!-es)' .
__tests__/components/operator-lifecycle-manager/catalog-source.spec.tsx
__tests__/components/operator-lifecycle-manager/install-plan.spec.tsx
__tests__/components/operator-lifecycle-manager/subscription.spec.tsx
__tests__/extend/kubevirt/module/k8s/vms.spec.js
integration-tests/tests/deploy-image.scenario.ts
integration-tests/tests/environment.scenario.ts
integration-tests/tests/filter.scenario.ts
integration-tests/tests/modal-annotations.scenario.ts
integration-tests/tests/olm/update-channel-approval.scenario.ts
integration-tests/tests/olm/etcd.scenario.ts
integration-tests/tests/source-to-image.scenario.ts
integration-tests/tests/crud.scenario.ts
integration-tests/views/crud.view.ts
integration-tests/protractor.conf.ts

Application code in public directory uses lodash-es in order to get better webpack optimization (tree shaking of ES modules).

Both unit and integration tests are executed within a Node.js (v8 LTS) env. ts-node avoids compiling files in node_modules directory (link). So unless the test runner explicitly supports transpilation of source files, importing code directly from node_modules may result in syntax errors.

Jest is configured with ts-jest transformer applied to .(ts|tsx|js|jsx) when loading any kind of module, i.e. including stuff in node_modules. Your Jest config also includes:

"transformIgnorePatterns": [
  "<rootDir>/node_modules/(?!lodash-es/.*)"
],

which means that Jest won't transform anything under node_modules/lodash-es.

On the other hand, Protractor doesn't seem to have any file transformation capability - we simply rely on ts-node to augment/hack the default Node.js module loading impl. by running tsc on files with .tsx? and .jsx? extensions. Again, ts-node does not process stuff in node_modules.

So, this problem occurs only in integration tests when trying to load (import) a module from node_modules directory which doesn't conform to supported Node.js (v8 LTS) syntax, such as lodash-es.

@spadgett Try adding following line into integration-tests/tests/crud.scenario.ts:

import { referenceForModel } from '../../public/module/k8s/k8s';

and running yarn test-suite --suite crud - Protractor will fail:

E/launcher - Error: /home/vszocs/go/src/github.com/openshift/console/frontend/node_modules/lodash-es/lodash.js:10
export { default as add } from './add.js';
^^^^^^

SyntaxError: Unexpected token export
    at new Script (vm.js:79:7)
    ....

This PR fixes the problem by adding lodash-es ➡️ lodash alias specifically for integration tests via integration-tests/preload.ts.

(It also fixes another related problem where expressions like SERVER_FLAGS.whatever throw because the global SERVER_FLAGS is not defined.)

Copy link
Contributor Author

@vojtechszocs vojtechszocs Jan 28, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a reason we don't see errors for all of our integration tests?

No integration test so far attempted to import code that eventually loaded lodash-es module.

So when I tried to import the referenceForModel function in crud.scenario.ts, I ran into this 😃

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, this PR adds an explicit dev-dependency on lodash. Up to now, lodash was pulled into node_modules through some other dependencies.

//
// Integration tests already use the `lodash` package, so we simply register alias
// to `lodash` in context of Node.js module resolution mechanism.

addAlias('lodash-es', 'lodash');

declare global {
interface window {
SERVER_FLAGS: object;
}
}

(global as any).window = {
SERVER_FLAGS: {
basePath: '/',
},
};
1 change: 1 addition & 0 deletions frontend/integration-tests/protractor.conf.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ export const config: Config = {
performance: ['tests/base.scenario.ts', 'tests/performance.scenario.ts'],
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'],
overview: ['tests/base.scenario.ts', 'tests/overview/overview.scenario.ts'],
kubevirt: ['tests/base.scenario.ts', 'tests/kubevirt/vm.actions.scenario.ts'],
e2e: [
'tests/base.scenario.ts',
'tests/crud.scenario.ts',
Expand Down
29 changes: 21 additions & 8 deletions frontend/integration-tests/tests/crud.scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ import * as crudView from '../views/crud.view';
import * as yamlView from '../views/yaml.view';
import * as namespaceView from '../views/namespace.view';
import * as createRoleBindingView from '../views/create-role-binding.view';
import { referenceForModel, kindForReference } from '../../public/module/k8s/k8s';
import { ClusterServiceBrokerModel, VirtualMachineModel } from '../../public/models';

const K8S_CREATION_TIMEOUT = 15000;

describe('Kubernetes resource CRUD operations', () => {
const testLabel = 'automatedTestName';
const leakedResources = new Set<string>();
const k8sObjs = OrderedMap<string, {kind: string, namespaced?: boolean}>()
const k8sObjs = OrderedMap<string, ObjMapValue>()
.set('pods', {kind: 'Pod'})
.set('services', {kind: 'Service'})
.set('serviceaccounts', {kind: 'ServiceAccount'})
Expand All @@ -38,15 +40,18 @@ describe('Kubernetes resource CRUD operations', () => {
.set('horizontalpodautoscalers', {kind: 'HorizontalPodAutoscaler'})
.set('networkpolicies', {kind: 'NetworkPolicy'})
.set('roles', {kind: 'Role'});
const openshiftObjs = OrderedMap<string, {kind: string, namespaced?: boolean}>()
const openshiftObjs = OrderedMap<string, ObjMapValue>()
.set('deploymentconfigs', {kind: 'DeploymentConfig'})
.set('buildconfigs', {kind: 'BuildConfig'})
.set('imagestreams', {kind: 'ImageStream'})
.set('routes', {kind: 'Route'});
const serviceCatalogObjs = OrderedMap<string, {kind: string, namespaced?: boolean}>()
.set('clusterservicebrokers', {kind: 'servicecatalog.k8s.io~v1beta1~ClusterServiceBroker', namespaced: false});
const serviceCatalogObjs = OrderedMap<string, ObjMapValue>()
.set('clusterservicebrokers', {kind: referenceForModel(ClusterServiceBrokerModel), namespaced: false});
const kubevirtObjs = OrderedMap<string, ObjMapValue>()
.set('virtualmachines', {kind: referenceForModel(VirtualMachineModel), crd: true});
let testObjs = browser.params.openshift === 'true' ? k8sObjs.merge(openshiftObjs) : k8sObjs;
testObjs = browser.params.servicecatalog === 'true' ? testObjs.merge(serviceCatalogObjs) : testObjs;
testObjs = browser.params.kubevirt === 'true' ? testObjs.merge(kubevirtObjs) : testObjs;

afterEach(() => {
checkLogs();
Expand All @@ -67,9 +72,11 @@ describe('Kubernetes resource CRUD operations', () => {
});
});

testObjs.forEach(({kind, namespaced = true}, resource) => {
testObjs.forEach(({kind, namespaced = true, crd = false}, resource) => {
const simpleKind = kindForReference(kind);
const suiteDesc = crd ? `${simpleKind} (CRD)` : kind;

describe(kind, () => {
describe(suiteDesc, () => {
const name = `${testName}-${kind.toLowerCase()}`;
it('displays a list view for the resource', async() => {
await browser.get(`${appHost}${namespaced ? `/k8s/ns/${testName}` : '/k8s/cluster'}/${resource}?name=${testName}`);
Expand Down Expand Up @@ -138,7 +145,7 @@ describe('Kubernetes resource CRUD operations', () => {
await browser.get(`${appHost}/search/${namespaced ? `ns/${testName}` : 'all-namespaces'}?kind=${kind}&q=${testLabel}%3d${testName}`);
await crudView.filterForName(name);
await crudView.resourceRowsPresent();
await crudView.editRow(kind)(name);
await crudView.editRow(simpleKind)(name);
}
});

Expand All @@ -148,7 +155,7 @@ describe('Kubernetes resource CRUD operations', () => {
// Filter by resource name to make sure the resource is on the first page of results.
// Otherwise the tests fail since we do virtual scrolling and the element isn't found.
await crudView.filterForName(name);
await crudView.deleteRow(kind)(name);
await crudView.deleteRow(simpleKind)(name);
leakedResources.delete(JSON.stringify({name, plural: resource, namespace: namespaced ? testName : undefined}));
});
});
Expand Down Expand Up @@ -365,3 +372,9 @@ describe('Kubernetes resource CRUD operations', () => {
});
});
});

type ObjMapValue = {
kind: string;
namespaced?: boolean;
crd?: boolean;
};
63 changes: 63 additions & 0 deletions frontend/integration-tests/tests/kubevirt/mocks.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
/* eslint-disable no-undef */
import { testName } from '../../protractor.conf';

const testLabel = 'automatedTestName';

export const testVM = {
apiVersion: 'kubevirt.io/v1alpha2',
kind: 'VirtualMachine',
metadata: {
name: `vm-${testName}`,
namespace: testName,
labels: {[testLabel]: testName},
},
spec: {
running: false,
template: {
spec: {
domain: {
cpu: {
cores: 1,
},
devices: {
disks: [
{
bootOrder: 1,
disk: {
bus: 'virtio',
},
name: 'rootdisk',
volumeName: 'rootdisk',
},
],
interfaces: [
{
bridge: {},
name: 'eth0',
},
],
},
resources: {
requests: {
memory: '1G',
},
},
},
networks: [
{
name: 'eth0',
pod: {},
},
],
volumes: [
{
containerDisk: {
image: 'kubevirt/cirros-registry-disk-demo:latest',
},
name: 'rootdisk',
},
],
},
},
},
};
17 changes: 17 additions & 0 deletions frontend/integration-tests/tests/kubevirt/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/* eslint-disable no-undef */
import { execSync } from 'child_process';

export function removeLeakedResources(leakedResources: Set<string>){
const leakedArray: Array<string> = [...leakedResources];
if (leakedArray.length > 0) {
console.error(`Leaked ${leakedArray.join()}`);
leakedArray.map(r => JSON.parse(r) as {name: string, namespace: string, kind: string})
.forEach(({name, namespace, kind}) => {
try {
execSync(`kubectl delete -n ${namespace} --cascade ${kind} ${name}`);
} catch (error) {
console.error(`Failed to delete ${kind} ${name}:\n${error}`);
}
});
}
}
96 changes: 96 additions & 0 deletions frontend/integration-tests/tests/kubevirt/vm.actions.scenario.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
/* eslint-disable no-undef */

import { execSync } from 'child_process';
import { browser, ExpectedConditions as until } from 'protractor';

import { appHost, testName } from '../../protractor.conf';
import { resourceRowsPresent, filterForName, isLoaded } from '../../views/crud.view';
import { testVM } from './mocks';
import { removeLeakedResources } from './utils';
import { detailViewAction, detailViewVMmStatus, listViewAction, listViewVMmStatus } from '../../views/kubevirt/vm.actions.view';

const VM_BOOTUP_TIMEOUT = 60000;
const VM_ACTIONS_TIMEOUT = 90000;

describe('Test VM actions', () => {
const leakedResources = new Set<string>();
afterAll(async() => {
removeLeakedResources(leakedResources);
});

describe('Test VM list view kebab actions', () => {
const vmName = `vm-list-view-actions-${testName}`;
beforeAll(async() => {
testVM.metadata.name = vmName;
execSync(`echo '${JSON.stringify(testVM)}' | kubectl create -f -`);
leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
});

// Workaround for https://github.com/kubevirt/web-ui/issues/177, remove when resolved
afterEach(async() => await browser.sleep(1000));

it('Navigates to VMs', async() => {
await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines`);
await isLoaded();
await filterForName(vmName);
await resourceRowsPresent();
});

it('Starts VM', async() => {
await listViewAction(vmName)('Start');
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Running'), VM_BOOTUP_TIMEOUT);
});

it('Restarts VM', async() => {
await listViewAction(vmName)('Restart');
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Starting'), VM_BOOTUP_TIMEOUT);
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Running'), VM_BOOTUP_TIMEOUT);
}, VM_ACTIONS_TIMEOUT);

it('Stops VM', async() => {
await listViewAction(vmName)('Stop');
await browser.wait(until.textToBePresentInElement(listViewVMmStatus(vmName), 'Off'), 10000);
});

it('Deletes VM', async() => {
await listViewAction(vmName)('Delete');
await browser.wait(until.not(until.presenceOf(listViewVMmStatus(vmName))));
leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
});
});

describe('Test VM detail view kebab actions', () => {
const vmName = `vm-detail-view-actions-${testName}`;
beforeAll(async() => {
testVM.metadata.name = vmName;
execSync(`echo '${JSON.stringify(testVM)}' | kubectl create -f -`);
leakedResources.add(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
});

it('Navigates to VMs detail page', async() => {
await browser.get(`${appHost}/k8s/all-namespaces/virtualmachines/${vmName}`);
await isLoaded();
});

it('Starts VM', async() => {
await detailViewAction('Start');
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Running'), VM_BOOTUP_TIMEOUT);
});

it('Restarts VM', async() => {
await detailViewAction('Restart');
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Starting'), VM_BOOTUP_TIMEOUT);
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Running'), VM_BOOTUP_TIMEOUT);
}, VM_ACTIONS_TIMEOUT);

it('Stops VM', async() => {
await detailViewAction('Stop');
await browser.wait(until.textToBePresentInElement(detailViewVMmStatus, 'Off'), 10000);
});

it('Deletes VM', async() => {
await detailViewAction('Delete');
leakedResources.delete(JSON.stringify({name: vmName, namespace: testName, kind: 'vm'}));
});
});
});
1 change: 1 addition & 0 deletions frontend/integration-tests/tests/performance.scenario.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ const chunkedRoutes = OrderedMap<string, {section: string, name: string}>()
.set('cron-job', {section: 'Workloads', name: 'Cron Jobs'})
.set('configmap', {section: 'Workloads', name: 'Config Maps'})
.set('hpa', {section: 'Workloads', name: 'HPAs'})
.set('virtual-machine', {section: 'Workloads', name: 'Virtual Machines'})
.set('service', {section: 'Networking', name: 'Services'})
.set('persistent-volume', {section: 'Storage', name: 'Persistent Volumes'})
.set('persistent-volume-claim', {section: 'Storage', name: 'Persistent Volume Claims'})
Expand Down
2 changes: 2 additions & 0 deletions frontend/integration-tests/views/crud.view.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ export const saveChangesBtn = $('#save-changes');
export const reloadBtn = $('#reload-object');
export const cancelBtn = $('#cancel');

export const confirmAction = () => browser.wait(until.presenceOf($('#confirm-action'))).then(() => $('#confirm-action').click());

/**
* Returns a promise that resolves after the loading spinner is not present.
*/
Expand Down
Loading