diff --git a/frontend/integration-tests/protractor.conf.ts b/frontend/integration-tests/protractor.conf.ts index 8231fab708..311f98c833 100644 --- a/frontend/integration-tests/protractor.conf.ts +++ b/frontend/integration-tests/protractor.conf.ts @@ -172,6 +172,7 @@ export const config: Config = { '../packages/kubevirt-plugin/integration-tests/tests/vm.actions.scenario.ts', '../packages/kubevirt-plugin/integration-tests/tests/vm.migration.scenario.ts', '../packages/kubevirt-plugin/integration-tests/tests/vm.resources.scenario.ts', + '../packages/kubevirt-plugin/integration-tests/tests/vm.clone.scenario.ts', ]), all: suite([ 'tests/crud.scenario.ts', diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/models/cloneDialog.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/models/cloneDialog.ts new file mode 100644 index 0000000000..b221d3b7a7 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/models/cloneDialog.ts @@ -0,0 +1,34 @@ +/* eslint-disable no-unused-vars, no-undef */ +import { browser, ExpectedConditions as until } from 'protractor'; +import { click } from '../../../../console-shared/src/test-utils/utils'; +import { fillInput, selectSelectorOption } from '../utils/utils'; +import { PAGE_LOAD_TIMEOUT_SECS } from '../utils/consts'; +import * as cloneDialogView from '../../views/cloneDialog.view'; + +export class CloneDialog { + async close() { + await click(cloneDialogView.cancelButton); + await browser.wait(until.invisibilityOf(cloneDialogView.modalDialog), PAGE_LOAD_TIMEOUT_SECS); + } + + async fillName(name: string) { + await fillInput(cloneDialogView.nameInput, name); + } + + async fillDescription(description: string) { + await fillInput(cloneDialogView.descriptionInput, description); + } + + async selectNamespace(namespace: string) { + await selectSelectorOption(cloneDialogView.namespaceSelectorId, namespace); + } + + async startOnCreation() { + await click(cloneDialogView.startOnCreationCheckBox); + } + + async clone() { + await click(cloneDialogView.confirmButton); + await browser.wait(until.invisibilityOf(cloneDialogView.modalDialog), PAGE_LOAD_TIMEOUT_SECS); + } +} diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/models/virtualMachine.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/models/virtualMachine.ts index 0bfcde7957..21fa6eb8c5 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/models/virtualMachine.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/models/virtualMachine.ts @@ -12,12 +12,13 @@ import { VMConfig } from '../utils/types'; import { PAGE_LOAD_TIMEOUT_SECS, VM_BOOTUP_TIMEOUT_SECS, + VM_MIGRATION_TIMEOUT_SECS, WIZARD_CREATE_VM_ERROR, WIZARD_TABLE_FIRST_ROW, TABS, - DASH, + VM_IMPORT_TIMEOUT_SECS, } from '../utils/consts'; -import { detailViewAction } from '../../views/vm.actions.view'; +import { detailViewAction, listViewAction } from '../../views/vm.actions.view'; import { tableRowForName } from '../../views/kubevirtDetailView.view'; import { Wizard } from './wizard'; import { KubevirtDetailView } from './kubevirtDetailView'; @@ -47,18 +48,27 @@ export class VirtualMachine extends KubevirtDetailView { confirmDialog = false; } - await detailViewAction(action, confirmDialog); + await detailViewAction(`${action} Virtual Machine`, confirmDialog); if (waitForAction !== false) { await vmView.waitForActionFinished(action, timeout); } } + async listViewAction(action: string) { + await this.navigateToListView(); + + let confirmDialog = true; + if (['Clone'].includes(action)) { + confirmDialog = false; + } + + await listViewAction(this.name)(`${action} Virtual Machine`, confirmDialog); + } + async waitForMigrationComplete(fromNode: string, timeout: number) { + await vmView.waitForStatusIcon(vmView.statusIcons.running, VM_MIGRATION_TIMEOUT_SECS); await browser.wait( - until.and( - waitForStringNotInElement(vmView.vmDetailNode(this.namespace, this.name), fromNode), - waitForStringNotInElement(vmView.vmDetailNode(this.namespace, this.name), DASH), - ), + waitForStringNotInElement(vmView.vmDetailNode(this.namespace, this.name), fromNode), timeout, ); } @@ -160,9 +170,13 @@ export class VirtualMachine extends KubevirtDetailView { } await wizard.next(); + await this.navigateToTab(TABS.OVERVIEW); if (startOnCreation === true) { // If startOnCreation is true, wait for VM to boot up await vmView.waitForStatusIcon(vmView.statusIcons.running, VM_BOOTUP_TIMEOUT_SECS); + } else { + // Else wait for possible import to finish + await vmView.waitForStatusIcon(vmView.statusIcons.off, VM_IMPORT_TIMEOUT_SECS); } } } diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/consts.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/consts.ts index 93ceca800b..91960cd4e7 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/consts.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/consts.ts @@ -1,17 +1,18 @@ export const DASH = '-'; -export const { STORAGE_CLASS = 'nfs-sc' } = process.env; +export const { STORAGE_CLASS = 'rook-ceph-block' } = process.env; // TIMEOUTS const SEC = 1000; -export const CLONE_VM_TIMEOUT_SECS = 300 * SEC; +export const CLONE_VM_TIMEOUT_SECS = 360 * SEC; export const CLONED_VM_BOOTUP_TIMEOUT_SECS = 150 * SEC; export const PAGE_LOAD_TIMEOUT_SECS = 15 * SEC; export const TEMPLATE_ACTIONS_TIMEOUT_SECS = 90 * SEC; export const VM_ACTIONS_TIMEOUT_SECS = 250 * SEC; -export const VM_BOOTUP_TIMEOUT_SECS = 200 * SEC; -export const VM_MIGRATION_TIMEOUT_SECS = 190 * SEC; +export const VM_BOOTUP_TIMEOUT_SECS = 230 * SEC; +export const VM_MIGRATION_TIMEOUT_SECS = 260 * SEC; export const VM_STOP_TIMEOUT_SECS = 10 * SEC; export const VM_IP_ASSIGNMENT_TIMEOUT_SECS = 180 * SEC; +export const VM_IMPORT_TIMEOUT_SECS = 80 * SEC; export const WINDOWS_IMPORT_TIMEOUT_SECS = 150 * SEC; export const VM_CREATE_AND_EDIT_TIMEOUT_SECS = 200 * SEC; diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/mocks.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/mocks.ts index 7a0b156358..470d29c43e 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/mocks.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/mocks.ts @@ -3,7 +3,7 @@ import { CloudInitConfig } from './types'; import { STORAGE_CLASS } from './consts'; import { getRandomMacAddress } from './utils'; -export const multusNad = { +export const multusNAD = { apiVersion: 'k8s.cni.cncf.io/v1', kind: 'NetworkAttachmentDefinition', metadata: { @@ -45,12 +45,12 @@ export const dataVolumeManifest = ({ name, namespace, sourceURL, accessMode, vol }; }; -export const basicVmConfig = { +export const basicVMConfig = { operatingSystem: 'Red Hat Enterprise Linux 7.6', flavor: 'tiny', workloadProfile: 'desktop', sourceURL: 'https://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img', - sourceContainer: 'kubevirt/cirros-registry-disk-demo:latest', + sourceContainer: 'kubevirt/cirros-registry-disk-demo', cloudInitScript: `#cloud-config\nuser: cloud-user\npassword: atomic\nchpasswd: {expire: False}\nhostname: vm-${testName}.example.com`, }; @@ -58,7 +58,7 @@ export const networkInterface = { name: `nic1-${testName.slice(-5)}`, mac: getRandomMacAddress(), binding: 'bridge', - networkDefinition: multusNad.metadata.name, + networkDefinition: multusNAD.metadata.name, }; export const networkBindingMethods = { @@ -82,7 +82,7 @@ export const hddDisk = { export const cloudInitCustomScriptConfig: CloudInitConfig = { useCloudInit: true, useCustomScript: true, - customScript: basicVmConfig.cloudInitScript, + customScript: basicVMConfig.cloudInitScript, }; export function getVmManifest( @@ -91,25 +91,28 @@ export function getVmManifest( name?: string, cloudinit?: string, ) { + const vmName = name || `${provisionSource.toLowerCase()}-${namespace.slice(-5)}`; const metadata = { - name: name || `${provisionSource.toLowerCase()}-${namespace.slice(-5)}`, + name: vmName, annotations: { 'name.os.template.kubevirt.io/rhel7.6': 'Red Hat Enterprise Linux 7.6', description: namespace, }, namespace, labels: { - app: `vm-${provisionSource.toLowerCase()}-${namespace}`, + app: vmName, 'flavor.template.kubevirt.io/tiny': 'true', 'os.template.kubevirt.io/rhel7.6': 'true', - 'vm.kubevirt.io/template': 'rhel7-desktop-tiny', + 'vm.kubevirt.io/template': 'rhel7-desktop-tiny-v0.6.2', 'vm.kubevirt.io/template-namespace': 'openshift', + 'vm.kubevirt.io/template.revision': '1', + 'vm.kubevirt.io/template.version': 'v0.6.2', 'workload.template.kubevirt.io/desktop': 'true', }, }; const urlSource = { http: { - url: 'https://download.cirros-cloud.net/0.4.0/cirros-0.4.0-x86_64-disk.img', + url: basicVMConfig.sourceURL, }, }; const dataVolumeTemplate = { @@ -119,6 +122,7 @@ export function getVmManifest( spec: { pvc: { accessModes: ['ReadWriteMany'], + volumeMode: 'Block', resources: { requests: { storage: '1Gi', @@ -137,7 +141,7 @@ export function getVmManifest( }; const containerDisk = { containerDisk: { - image: 'kubevirt/cirros-registry-disk-demo:latest', + image: basicVMConfig.sourceContainer, }, name: 'rootdisk', }; @@ -201,6 +205,8 @@ export function getVmManifest( template: { metadata: { labels: { + 'kubevirt.io/domain': metadata.name, + 'kubevirt.io/size': 'tiny', 'vm.kubevirt.io/name': metadata.name, }, }, @@ -213,6 +219,13 @@ export function getVmManifest( }, devices: { disks, + inputs: [ + { + bus: 'virtio', + name: 'tablet', + type: 'tablet', + }, + ], interfaces: [ { bootOrder: 2, @@ -228,6 +241,7 @@ export function getVmManifest( }, }, }, + evictionStrategy: 'LiveMigrate', terminationGracePeriodSeconds: 0, networks: [ { diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/utils.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/utils.ts index 533f93de38..d49864c9f7 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/utils.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/utils/utils.ts @@ -1,8 +1,14 @@ /* eslint-disable no-await-in-loop */ import { execSync } from 'child_process'; import * as _ from 'lodash'; -import { ElementFinder, browser, ExpectedConditions as until } from 'protractor'; -import { STORAGE_CLASS } from './consts'; +import { $, $$, ElementFinder, browser, by, ExpectedConditions as until } from 'protractor'; +import { appHost } from '../../../../../integration-tests/protractor.conf'; +import { + isLoaded, + createYAMLButton, + rowForName, +} from '../../../../../integration-tests/views/crud.view'; +import { STORAGE_CLASS, PAGE_LOAD_TIMEOUT_SECS } from './consts'; import { NodePortService } from './types'; export async function fillInput(elem: ElementFinder, value: string) { @@ -13,7 +19,7 @@ export async function fillInput(elem: ElementFinder, value: string) { if (attempts < 0) { throw Error(`Failed to fill input with value: '${value}'.`); } - await browser.wait(until.elementToBeClickable(elem)); + await browser.wait(until.and(until.presenceOf(elem), until.elementToBeClickable(elem))); // TODO: line below can be removed when pf4 tables in use. await elem.click(); await elem.clear(); @@ -21,10 +27,40 @@ export async function fillInput(elem: ElementFinder, value: string) { } while ((await elem.getAttribute('value')) !== value && attempts > 0); } +export async function createProject(name: string) { + // Use projects if OpenShift so non-admin users can run tests. + const resource = browser.params.openshift === 'true' ? 'projects' : 'namespaces'; + await browser.get(`${appHost}/k8s/cluster/${resource}`); + await isLoaded(); + const exists = await rowForName(name).isPresent(); + if (!exists) { + await createYAMLButton.click(); + await browser.wait(until.presenceOf($('.modal-body__field'))); + await $$('.modal-body__field') + .get(0) + .$('input') + .sendKeys(name); + await $$('.modal-body__field') + .get(1) + .$('input') + .sendKeys(`test-name=${name}`); + await $('.modal-content') + .$('#confirm-action') + .click(); + await browser.wait(until.urlContains(`/${name}`), PAGE_LOAD_TIMEOUT_SECS); + } +} + export async function getInputValue(elem: ElementFinder) { return elem.getAttribute('value'); } +export async function selectSelectorOption(selectorId: string, option: string) { + await $(selectorId) + .all(by.css(`option[value="${option}"]`)) + .click(); +} + export function getRandStr(length: number) { return Math.random() .toString(36) diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.actions.scenario.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.actions.scenario.ts index 3fc775d64d..087ddef01b 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.actions.scenario.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.actions.scenario.ts @@ -6,8 +6,11 @@ import { isLoaded, textFilter, } from '../../../../integration-tests/views/crud.view'; -import { listViewAction } from '../views/vm.actions.view'; -import { waitForActionFinished } from '../views/virtualMachine.view'; +import { + waitForActionFinished, + waitForStatusIcon, + statusIcons, +} from '../views/virtualMachine.view'; import { addLeakableResource, createResource, @@ -23,12 +26,13 @@ import { PAGE_LOAD_TIMEOUT_SECS, VMACTIONS, TABS, + VM_IMPORT_TIMEOUT_SECS, } from './utils/consts'; import { VirtualMachine } from './models/virtualMachine'; describe('Test VM actions', () => { const leakedResources = new Set(); - const testVm = getVmManifest('Container', testName); + const testVM = getVmManifest('URL', testName); afterAll(async () => { removeLeakedResources(leakedResources); @@ -36,29 +40,36 @@ describe('Test VM actions', () => { describe('Test VM list view kebab actions', () => { const vmName = `vm-list-actions-${testName}`; + let vm: VirtualMachine; beforeAll(async () => { - testVm.metadata.name = vmName; - createResource(testVm); - addLeakableResource(leakedResources, testVm); + testVM.metadata.name = vmName; + createResource(testVM); + addLeakableResource(leakedResources, testVM); + vm = new VirtualMachine(testVM.metadata); // Navigate to Virtual Machines page await browser.get(`${appHost}/k8s/ns/${testName}/virtualmachines`); await isLoaded(); await fillInput(textFilter, vmName); await resourceRowsPresent(); - }); + await waitForStatusIcon(statusIcons.off, VM_IMPORT_TIMEOUT_SECS); + }, VM_IMPORT_TIMEOUT_SECS); - it('Starts VM', async () => { - await listViewAction(vmName)(VMACTIONS.START, true); - await fillInput(textFilter, vmName); - await waitForActionFinished(VMACTIONS.START); - }); + it( + 'Starts VM', + async () => { + await vm.listViewAction(VMACTIONS.START); + await fillInput(textFilter, vmName); + await waitForActionFinished(VMACTIONS.START); + }, + VM_BOOTUP_TIMEOUT_SECS, + ); it( 'Restarts VM', async () => { - await listViewAction(vmName)(VMACTIONS.RESTART, true); + await vm.listViewAction(VMACTIONS.RESTART); await fillInput(textFilter, vmName); await waitForActionFinished(VMACTIONS.RESTART); }, @@ -66,17 +77,17 @@ describe('Test VM actions', () => { ); it('Stops VM', async () => { - await listViewAction(vmName)(VMACTIONS.STOP, true); + await vm.listViewAction(VMACTIONS.STOP); await fillInput(textFilter, vmName); await waitForActionFinished(VMACTIONS.STOP); }); it('Deletes VM', async () => { - await listViewAction(vmName)(VMACTIONS.DELETE, true); + await vm.listViewAction(VMACTIONS.DELETE); await isLoaded(); await fillInput(textFilter, vmName); await browser.wait(until.and(waitForCount(resourceRows, 0)), PAGE_LOAD_TIMEOUT_SECS); - removeLeakableResource(leakedResources, testVm); + removeLeakableResource(leakedResources, testVM); }); }); @@ -85,9 +96,9 @@ describe('Test VM actions', () => { const vm = new VirtualMachine({ name: vmName, namespace: testName }); beforeAll(async () => { - testVm.metadata.name = vmName; - createResource(testVm); - addLeakableResource(leakedResources, testVm); + testVM.metadata.name = vmName; + createResource(testVM); + addLeakableResource(leakedResources, testVM); await vm.navigateToTab(TABS.OVERVIEW); }); @@ -116,7 +127,7 @@ describe('Test VM actions', () => { await isLoaded(); await fillInput(textFilter, vmName); await browser.wait(until.and(waitForCount(resourceRows, 0)), PAGE_LOAD_TIMEOUT_SECS); - removeLeakableResource(leakedResources, testVm); + removeLeakableResource(leakedResources, testVM); }); }); }); diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.clone.scenario.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.clone.scenario.ts new file mode 100644 index 0000000000..cebeb8abca --- /dev/null +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.clone.scenario.ts @@ -0,0 +1,314 @@ +/* eslint-disable no-undef, max-nested-callbacks */ +import { execSync } from 'child_process'; +import { $, browser, ExpectedConditions as until } from 'protractor'; +import * as _ from 'lodash'; +import { appHost, testName } from '../../../../integration-tests/protractor.conf'; +import { + filterForName, + isLoaded, + resourceRowsPresent, + resourceRows, +} from '../../../../integration-tests/views/crud.view'; +import * as cloneDialogView from '../views/cloneDialog.view'; +import { statusIcons, waitForStatusIcon } from '../views/virtualMachine.view'; +import { + removeLeakedResources, + waitForCount, + searchYAML, + withResource, + createResources, + deleteResources, + createResource, + addLeakableResource, + removeLeakableResource, +} from '../../../console-shared/src/test-utils/utils'; +import { getVolumes, getDataVolumeTemplates } from '../../src/selectors/vm/selectors'; +import { getResourceObject, getRandStr, createProject } from './utils/utils'; +import { + CLONE_VM_TIMEOUT_SECS, + VM_BOOTUP_TIMEOUT_SECS, + VM_IMPORT_TIMEOUT_SECS, + PAGE_LOAD_TIMEOUT_SECS, + CLONED_VM_BOOTUP_TIMEOUT_SECS, + TABS, + VMACTIONS, +} from './utils/consts'; +import { + basicVMConfig, + networkInterface, + multusNAD, + getVmManifest, + cloudInitCustomScriptConfig, + rootDisk, +} from './utils/mocks'; +import { VirtualMachine } from './models/virtualMachine'; +import { CloneDialog } from './models/cloneDialog'; + +describe('Test clone VM.', () => { + const leakedResources = new Set(); + const cloneDialog = new CloneDialog(); + const testCloningNamespace = `${testName}-cloning`; + + beforeAll(async () => { + await createProject(testCloningNamespace); + }); + + afterAll(() => { + execSync(`kubectl delete namespace ${testCloningNamespace}`); + }); + + describe('Test Clone VM dialog validation', () => { + const testContainerVM = getVmManifest('Container', testName); + const vm = new VirtualMachine(testContainerVM.metadata); + const testNameValidationVM = getVmManifest( + 'Container', + testCloningNamespace, + testContainerVM.metadata.name, + ); + + beforeAll(async () => { + createResources([testContainerVM, testNameValidationVM]); + }); + + afterAll(() => { + deleteResources([testContainerVM, testNameValidationVM]); + }); + + it( + 'Displays warning in clone wizard when cloned vm is running.', + async () => { + await vm.action(VMACTIONS.START, false); + await waitForStatusIcon(statusIcons.starting, PAGE_LOAD_TIMEOUT_SECS); + await vm.action(VMACTIONS.CLONE); + await browser.wait( + until.and( + until.presenceOf(cloneDialogView.warningMessage), + until.textToBePresentInElement( + cloneDialogView.warningMessage, + `${vm.name} is still running.`, + ), + ), + PAGE_LOAD_TIMEOUT_SECS, + ); + expect(cloneDialogView.confirmButton.isEnabled()).toBeTruthy(); + await cloneDialog.close(); + await waitForStatusIcon(statusIcons.running, VM_BOOTUP_TIMEOUT_SECS); + await vm.action(VMACTIONS.STOP); + }, + VM_BOOTUP_TIMEOUT_SECS, + ); + + it('Prefills correct data in the clone VM dialog.', async () => { + await vm.action('Clone'); + expect(cloneDialogView.nameInput.getAttribute('value')).toEqual(`${vm.name}-clone`); + expect(cloneDialogView.descriptionInput.getText()).toEqual( + testContainerVM.metadata.annotations.description, + ); + // Check preselected value of NS dropdown + expect($(cloneDialogView.namespaceSelectorId).getAttribute('value')).toEqual(vm.namespace); + await cloneDialog.close(); + }); + + it('Validates VM name.', async () => { + await vm.action('Clone'); + + expect(cloneDialogView.warningMessage.isPresent()).toBe(false); + + // Check warning is displayed when VM has same name as existing VM + await cloneDialog.fillName(vm.name); + await browser.wait(until.presenceOf(cloneDialogView.nameHelperMessage)); + expect(cloneDialogView.nameHelperMessage.getText()).toMatch(/already used/); + + // Check warning is displayed when VM has same name as existing VM in another namespace + await cloneDialog.fillName(testNameValidationVM.metadata.name); + await cloneDialog.selectNamespace(testNameValidationVM.metadata.namespace); + await browser.wait(until.presenceOf(cloneDialogView.nameHelperMessage)); + expect(cloneDialogView.nameHelperMessage.getText()).toMatch(/already used/); + + await cloneDialog.close(); + }); + }); + + describe('Test cloning settings.', () => { + const testVM = getVmManifest('URL', testName, `cloningvm-${getRandStr(5)}`); + const vm = new VirtualMachine(testVM.metadata); + const clonedVM = new VirtualMachine({ + name: `${vm.name}-clone`, + namespace: vm.namespace, + }); + + beforeAll(async () => { + createResources([multusNAD, testVM]); + await vm.navigateToTab(TABS.OVERVIEW); + await waitForStatusIcon(statusIcons.off, VM_IMPORT_TIMEOUT_SECS); + await vm.addNIC(networkInterface); + await vm.action(VMACTIONS.START); + }, VM_IMPORT_TIMEOUT_SECS + VM_BOOTUP_TIMEOUT_SECS); + + afterAll(() => { + deleteResources([multusNAD, testVM, clonedVM.asResource()]); + removeLeakableResource(leakedResources, clonedVM.asResource()); + removeLeakedResources(leakedResources); + }); + + it( + 'Clones VM to a different namespace', + async () => { + const vmClonedToOtherNS = new VirtualMachine({ + name: `${vm.name}-${getRandStr(4)}`, + namespace: testCloningNamespace, + }); + await vm.action(VMACTIONS.CLONE); + await cloneDialog.fillName(vmClonedToOtherNS.name); + await cloneDialog.selectNamespace(vmClonedToOtherNS.namespace); + await cloneDialog.clone(); + await withResource(leakedResources, vmClonedToOtherNS.asResource(), async () => { + await vmClonedToOtherNS.navigateToTab(TABS.OVERVIEW); + await waitForStatusIcon(statusIcons.off, VM_IMPORT_TIMEOUT_SECS); + }); + }, + VM_IMPORT_TIMEOUT_SECS, + ); + + it( + 'Start cloned VM on creation', + async () => { + await vm.action(VMACTIONS.CLONE); + await cloneDialog.startOnCreation(); + await cloneDialog.clone(); + addLeakableResource(leakedResources, clonedVM.asResource()); + + await clonedVM.navigateToTab(TABS.OVERVIEW); + await waitForStatusIcon(statusIcons.running, CLONE_VM_TIMEOUT_SECS); + }, + VM_BOOTUP_TIMEOUT_SECS + CLONE_VM_TIMEOUT_SECS, + ); + + it('Running VM is stopped when cloned', async () => { + await vm.navigateToTab(TABS.OVERVIEW); + await waitForStatusIcon(statusIcons.off, PAGE_LOAD_TIMEOUT_SECS); + }); + + it('Cloned VM has cleared MAC addresses.', async () => { + await clonedVM.navigateToTab(TABS.NICS); + await browser.wait(until.and(waitForCount(resourceRows, 2)), PAGE_LOAD_TIMEOUT_SECS); + const addedNIC = (await clonedVM.getAttachedNICs()).find( + (nic) => nic.name === networkInterface.name, + ); + expect(addedNIC.mac === networkInterface.mac).toBe(false); + }); + + it('Cloned VM has vm.kubevirt.io/name label.', async () => { + expect( + searchYAML(`vm.kubevirt.io/name: ${vm.name}`, clonedVM.name, clonedVM.namespace, 'vm'), + ).toBeTruthy(); + }); + }); + + describe('Test DataVolumes of cloned VMs', () => { + const urlVMManifest = getVmManifest('URL', testName); + const urlVM = new VirtualMachine(urlVMManifest.metadata); + const cloudInitVmProvisionConfig = { + method: 'URL', + source: basicVMConfig.sourceURL, + }; + + it( + 'Test clone VM with URL source.', + async () => { + createResource(urlVMManifest); + await withResource(leakedResources, urlVM.asResource(), async () => { + await urlVM.navigateToTab(TABS.OVERVIEW); + await waitForStatusIcon(statusIcons.off, VM_IMPORT_TIMEOUT_SECS); + await urlVM.action(VMACTIONS.START); + await urlVM.action(VMACTIONS.CLONE); + await cloneDialog.clone(); + + const clonedVM = new VirtualMachine({ + name: `${urlVM.name}-clone`, + namespace: urlVM.namespace, + }); + await withResource(leakedResources, clonedVM.asResource(), async () => { + await clonedVM.action(VMACTIONS.START, true, CLONED_VM_BOOTUP_TIMEOUT_SECS); + + // Check cloned PVC exists + const clonedVMDiskName = `${clonedVM.name}-${urlVM.name}-rootdisk-clone`; + await browser.get(`${appHost}/k8s/ns/${testName}/persistentvolumeclaims`); + await isLoaded(); + await filterForName(clonedVMDiskName); + await resourceRowsPresent(); + + // Verify cloned disk dataVolumeTemplate is present in cloned VM manifest + const clonedVMJSON = getResourceObject( + clonedVM.name, + clonedVM.namespace, + clonedVM.kind, + ); + const clonedDataVolumeTemplate = getDataVolumeTemplates(clonedVMJSON); + const result = _.find(clonedDataVolumeTemplate, function(o) { + return o.metadata.name === clonedVMDiskName; + }); + expect(_.get(result, 'spec.source.pvc.name')).toEqual(`${urlVM.name}-rootdisk`); + }); + }); + }, + CLONE_VM_TIMEOUT_SECS, + ); + + it( + 'Test clone VM with URL source and Cloud Init.', + async () => { + const ciVMConfig = { + name: `ci-${testName}`, + namespace: testName, + description: `Default description ${testName}`, + provisionSource: cloudInitVmProvisionConfig, + storageResources: [rootDisk], + networkResources: [], + flavor: basicVMConfig.flavor, + operatingSystem: basicVMConfig.operatingSystem, + workloadProfile: basicVMConfig.workloadProfile, + startOnCreation: false, + cloudInit: cloudInitCustomScriptConfig, + }; + const expectedDisks = [rootDisk, { name: 'cloudinitdisk', size: '', storageClass: '-' }]; + const ciVM = new VirtualMachine(ciVMConfig); + await ciVM.create(ciVMConfig); + await withResource(leakedResources, ciVM.asResource(), async () => { + await ciVM.action(VMACTIONS.CLONE); + await cloneDialog.clone(); + const clonedVM = new VirtualMachine({ + name: `${ciVMConfig.name}-clone`, + namespace: ciVM.namespace, + }); + await withResource(leakedResources, clonedVM.asResource(), async () => { + // Check disks on cloned VM + const disks = await clonedVM.getAttachedDisks(); + expectedDisks.forEach((disk) => { + expect(_.find(disks, disk)).toBeDefined(); + }); + + // Verify configuration of cloudinitdisk is the same + const clonedVMJSON = getResourceObject( + clonedVM.name, + clonedVM.namespace, + clonedVM.kind, + ); + const clonedVMVolumes = getVolumes(clonedVMJSON); + const result = _.find(clonedVMVolumes, function(o) { + return o.name === 'cloudinitdisk'; + }); + expect(result).toBeDefined(); + expect(_.get(result, 'cloudInitNoCloud.userData', '')).toEqual( + cloudInitCustomScriptConfig.customScript, + ); + + // Verify the cloned VM can boot + await clonedVM.action(VMACTIONS.START, true, CLONED_VM_BOOTUP_TIMEOUT_SECS); + }); + }); + }, + CLONE_VM_TIMEOUT_SECS, + ); + }); +}); diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.migration.scenario.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.migration.scenario.ts index 3e07968c15..6796f58cd8 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.migration.scenario.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.migration.scenario.ts @@ -18,24 +18,27 @@ import { VM_BOOTUP_TIMEOUT_SECS, VM_ACTIONS_TIMEOUT_SECS, VM_MIGRATION_TIMEOUT_SECS, + VM_IMPORT_TIMEOUT_SECS, PAGE_LOAD_TIMEOUT_SECS, TABS, } from './utils/consts'; import { VirtualMachine } from './models/virtualMachine'; describe('Test VM Migration', () => { - const testVm = getVmManifest('Container', testName); + const testVm = getVmManifest('URL', testName); let vm: VirtualMachine; const MIGRATE_VM = 'Migrate Virtual Machine'; const CANCEL_MIGRATION = 'Cancel Virtual Machine Migration'; const VM_BOOT_AND_MIGRATE_TIMEOUT = VM_BOOTUP_TIMEOUT_SECS + VM_MIGRATION_TIMEOUT_SECS; - beforeEach(() => { + beforeEach(async () => { testVm.metadata.name = `migrationvm-${getRandStr(4)}`; vm = new VirtualMachine(testVm.metadata); createResource(testVm); - }); + await vm.navigateToTab(TABS.OVERVIEW); + await waitForStatusIcon(statusIcons.off, VM_IMPORT_TIMEOUT_SECS); + }, VM_IMPORT_TIMEOUT_SECS); afterEach(() => { deleteResource(testVm); @@ -44,7 +47,6 @@ describe('Test VM Migration', () => { it( 'Migrate VM action button is displayed appropriately', async () => { - await vm.navigateToTab(TABS.OVERVIEW); expect(await getDetailActionDropdownOptions()).not.toContain(MIGRATE_VM); expect(await getDetailActionDropdownOptions()).not.toContain(CANCEL_MIGRATION); @@ -60,19 +62,6 @@ describe('Test VM Migration', () => { VM_BOOTUP_TIMEOUT_SECS, ); - it( - 'Migrate VM', - async () => { - await vm.action('Start'); - const sourceNode = await vmDetailNode(vm.namespace, vm.name).getText(); - - await vm.action('Migrate'); - await vm.waitForMigrationComplete(sourceNode, VM_MIGRATION_TIMEOUT_SECS); - expect(statusIcon(statusIcons.running).isPresent()).toBeTruthy(); - }, - VM_BOOT_AND_MIGRATE_TIMEOUT, - ); - it( 'Migrate already migrated VM', async () => { @@ -93,6 +82,7 @@ describe('Test VM Migration', () => { it( 'Cancel ongoing VM migration', async () => { + await waitForStatusIcon(statusIcons.off, VM_IMPORT_TIMEOUT_SECS); await vm.action('Start'); const sourceNode = await vmDetailNode(vm.namespace, vm.name).getText(); diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.overview.scenario.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.overview.scenario.ts index 910a0b7cb0..6281946aa4 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.overview.scenario.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.overview.scenario.ts @@ -7,7 +7,7 @@ import { createResource, deleteResource, } from '../../../console-shared/src/test-utils/utils'; -import { getVmManifest, basicVmConfig } from './utils/mocks'; +import { getVmManifest, basicVMConfig } from './utils/mocks'; import { exposeServices } from './utils/utils'; import { VirtualMachine } from './models/virtualMachine'; import { TABS, DASH, VM_BOOTUP_TIMEOUT_SECS } from './utils/consts'; @@ -57,11 +57,11 @@ describe('Test VM overview', () => { const expectation = { name: vmName, description: testName, - os: basicVmConfig.operatingSystem, - profile: basicVmConfig.workloadProfile, + os: basicVMConfig.operatingSystem, + profile: basicVMConfig.workloadProfile, template: 'rhel7-desktop-tiny', bootOrder: ['rootdisk', 'nic0', 'cloudinitdisk'], - flavor: basicVmConfig.flavor, + flavor: basicVMConfig.flavor, ip: DASH, pod: DASH, node: DASH, diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.resources.scenario.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.resources.scenario.ts index 1c4a9e0433..bffa045840 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.resources.scenario.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.resources.scenario.ts @@ -10,7 +10,7 @@ import { getDropdownOptions, } from '../../../console-shared/src/test-utils/utils'; import { getInterfaces } from '../../src/selectors/vm/selectors'; -import { multusNad, hddDisk, networkInterface, getVmManifest } from './utils/mocks'; +import { multusNAD, hddDisk, networkInterface, getVmManifest } from './utils/mocks'; import { getResourceObject } from './utils/utils'; import { VM_BOOTUP_TIMEOUT_SECS, VM_ACTIONS_TIMEOUT_SECS, TABS } from './utils/consts'; import { VirtualMachine } from './models/virtualMachine'; @@ -20,12 +20,12 @@ describe('Add/remove disks and NICs on respective VM pages', () => { const vm = new VirtualMachine(testVm.metadata); beforeAll(async () => { - createResources([multusNad, testVm]); + createResources([multusNAD, testVm]); await vm.action('Start'); }, VM_BOOTUP_TIMEOUT_SECS); afterAll(() => { - deleteResources([multusNad, testVm]); + deleteResources([multusNAD, testVm]); }); it( diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.wizard.configs.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.wizard.configs.ts index 7102cdfc53..368bcf54ee 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.wizard.configs.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.wizard.configs.ts @@ -1,6 +1,6 @@ import { OrderedMap } from 'immutable'; import { - basicVmConfig, + basicVMConfig, networkInterface, rootDisk, hddDisk, @@ -18,9 +18,9 @@ export const vmConfig = (name: string, provisionConfig, testName: string) => { }, namespace: testName, description: `Default description ${testName}`, - flavor: basicVmConfig.flavor, - operatingSystem: basicVmConfig.operatingSystem, - workloadProfile: basicVmConfig.workloadProfile, + flavor: basicVMConfig.flavor, + operatingSystem: basicVMConfig.operatingSystem, + workloadProfile: basicVMConfig.workloadProfile, }; return { @@ -42,7 +42,7 @@ export const getTestDataVolume = (testName: string) => dataVolumeManifest({ name: `toclone-${testName}`, namespace: testName, - sourceURL: basicVmConfig.sourceURL, + sourceURL: basicVMConfig.sourceURL, accessMode: resolveStorageDataAttribute(kubevirtStorage, 'accessMode'), volumeMode: resolveStorageDataAttribute(kubevirtStorage, 'volumeMode'), }); @@ -67,7 +67,7 @@ export const getProvisionConfigs = (testName: string) => .set(CONFIG_NAME_URL, { provision: { method: CONFIG_NAME_URL, - source: basicVmConfig.sourceURL, + source: basicVMConfig.sourceURL, }, networkResources: [networkInterface], storageResources: [rootDisk], @@ -75,7 +75,7 @@ export const getProvisionConfigs = (testName: string) => .set(CONFIG_NAME_CONTAINER, { provision: { method: CONFIG_NAME_CONTAINER, - source: basicVmConfig.sourceContainer, + source: basicVMConfig.sourceContainer, }, networkResources: [networkInterface], storageResources: [hddDisk], diff --git a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.wizard.scenario.ts b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.wizard.scenario.ts index 62fb7ba92a..2d7ca04392 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.wizard.scenario.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/tests/vm.wizard.scenario.ts @@ -10,7 +10,7 @@ import { statusIcons, waitForStatusIcon } from '../views/virtualMachine.view'; import { VirtualMachine } from './models/virtualMachine'; import { getResourceObject, resolveStorageDataAttribute } from './utils/utils'; import { VM_BOOTUP_TIMEOUT_SECS, CLONE_VM_TIMEOUT_SECS, TABS } from './utils/consts'; -import { multusNad } from './utils/mocks'; +import { multusNAD } from './utils/mocks'; import { vmConfig, getProvisionConfigs, @@ -24,11 +24,11 @@ describe('Kubevirt create VM using wizard', () => { const testDataVolume = getTestDataVolume(testName); beforeAll(async () => { - createResources([multusNad, testDataVolume]); + createResources([multusNAD, testDataVolume]); }); afterAll(async () => { - deleteResources([multusNad, testDataVolume]); + deleteResources([multusNAD, testDataVolume]); }); afterEach(() => { diff --git a/frontend/packages/kubevirt-plugin/integration-tests/views/cloneDialog.view.ts b/frontend/packages/kubevirt-plugin/integration-tests/views/cloneDialog.view.ts new file mode 100644 index 0000000000..c6e0a52352 --- /dev/null +++ b/frontend/packages/kubevirt-plugin/integration-tests/views/cloneDialog.view.ts @@ -0,0 +1,15 @@ +import { $, element, by } from 'protractor'; + +export const modalDialog = $('.ReactModal__Content'); + +export const warningMessage = $('.kubevirt-clone-vm-modal__error-group--end'); + +export const nameHelperMessage = $('#clone-dialog-vm-name-helper'); + +export const nameInput = $('#clone-dialog-vm-name'); +export const descriptionInput = $('#clone-dialog-vm-description'); +export const namespaceSelectorId = '#clone-dialog-vm-namespace'; +export const startOnCreationCheckBox = $('#clone-dialog-vm-start'); + +export const confirmButton = $('#confirm-action'); +export const cancelButton = element(by.buttonText('Cancel')); diff --git a/frontend/packages/kubevirt-plugin/integration-tests/views/virtualMachine.view.ts b/frontend/packages/kubevirt-plugin/integration-tests/views/virtualMachine.view.ts index 919503e72b..d041b58c96 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/views/virtualMachine.view.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/views/virtualMachine.view.ts @@ -8,7 +8,7 @@ import { UNEXPECTED_ACTION_ERROR, } from '../tests/utils/consts'; import { resourceTitle } from '../../../../integration-tests/views/crud.view'; -import { nameInput } from './wizard.view'; +import { nameInput as cloneDialogNameInput } from './cloneDialog.view'; export const statusIcon = (status) => $(`.kubevirt-status__icon.${status}`); export const statusLink = $('a.kubevirt-status__link'); @@ -48,7 +48,8 @@ export const vmDetailTemplate = (namespace, vmName) => export const vmDetailNamespace = (namespace, vmName) => $(vmDetailItemId(namespace, vmName, 'namespace')); export const vmDetailPod = (namespace, vmName) => $(vmDetailItemId(namespace, vmName, 'pod')); -export const vmDetailNode = (namespace, vmName) => $(vmDetailItemId(namespace, vmName, 'node')); +export const vmDetailNode = (namespace, vmName) => + $(vmDetailItemId(namespace, vmName, 'node')).$('a'); export const vmDetailFlavor = (namespace, vmName) => $(vmDetailItemId(namespace, vmName, 'flavor')); export const vmDetailFlavorEditButton = (namespace, vmName) => $(vmDetailItemId(namespace, vmName, 'flavor-edit')); @@ -106,10 +107,9 @@ export async function waitForActionFinished(action: string, timeout?: number) { break; case 'Clone': await browser.wait( - until.presenceOf(nameInput), + until.presenceOf(cloneDialogNameInput), resolveTimeout(timeout, PAGE_LOAD_TIMEOUT_SECS), ); - await browser.sleep(500); // Wait until the fade in effect is finished, otherwise we may misclick break; case 'Migrate': await waitForStatusIcon( diff --git a/frontend/packages/kubevirt-plugin/integration-tests/views/vm.actions.view.ts b/frontend/packages/kubevirt-plugin/integration-tests/views/vm.actions.view.ts index bb8efac545..72894c6a74 100644 --- a/frontend/packages/kubevirt-plugin/integration-tests/views/vm.actions.view.ts +++ b/frontend/packages/kubevirt-plugin/integration-tests/views/vm.actions.view.ts @@ -1,13 +1,12 @@ import { $, $$, browser, ExpectedConditions as until } from 'protractor'; import { rowForName, resourceRowsPresent } from '../../../../integration-tests/views/crud.view'; -import { waitForCount } from '../../../console-shared/src/test-utils/utils'; +import { waitForCount, click } from '../../../console-shared/src/test-utils/utils'; const disabledDropdownButtons = $$('.pf-m-disabled'); -const listViewKebabDropdown = '.pf-c-dropdown__toggle'; -const listViewKebabDropdownMenu = '.pf-c-dropdown__menu'; -export const detailViewDropdown = '[data-test-id=actions-menu-button]'; -export const detailViewDropdownMenu = '[data-test-id=action-items]'; +const listViewKebabDropdown = '[data-test-id="kebab-button"]'; +export const detailViewDropdown = '[data-test-id="actions-menu-button"]'; +export const detailViewDropdownMenu = '[data-test-id="action-items"]'; export async function confirmAction() { const dialogOverlay = $('.co-overlay'); @@ -25,18 +24,12 @@ export async function confirmAction() { /** * Selects option button from given dropdown element. */ -const selectDropdownItem = (getActionsDropdown, getActionsDropdownMenu) => async ( - action: string, -) => { +const selectDropdownItem = (getActionsDropdown) => async (action: string) => { await browser .wait(until.elementToBeClickable(getActionsDropdown())) .then(() => getActionsDropdown().click()); await browser.wait(waitForCount(disabledDropdownButtons, 0)); - const option = getActionsDropdownMenu() - .$$('button') - .filter((button) => button.getText().then((text) => text.startsWith(action))) - .first(); - await browser.wait(until.elementToBeClickable(option)).then(() => option.click()); + await click($(`[data-test-action="${action}"]`)); }; /** @@ -48,8 +41,7 @@ export const listViewAction = (name) => async (action: string, confirm?: boolean rowForName(name) .$$(listViewKebabDropdown) .first(); - const getActionsDropdownMenu = () => rowForName(name).$(listViewKebabDropdownMenu); - await selectDropdownItem(getActionsDropdown, getActionsDropdownMenu)(action); + await selectDropdownItem(getActionsDropdown)(action); if (confirm === true) { await confirmAction(); } @@ -60,8 +52,7 @@ export const listViewAction = (name) => async (action: string, confirm?: boolean */ export const detailViewAction = async (action, confirm?) => { const getActionsDropdown = () => $(detailViewDropdown); - const getActionsDropdownMenu = () => $(detailViewDropdownMenu); - await selectDropdownItem(getActionsDropdown, getActionsDropdownMenu)(action); + await selectDropdownItem(getActionsDropdown)(action); await browser.wait(until.not(until.presenceOf($(detailViewDropdownMenu)))); if (confirm === true) { await confirmAction();