diff --git a/frontend/packages/integration-tests-cypress/mocks/volume-attributes-class.ts b/frontend/packages/integration-tests-cypress/mocks/volume-attributes-class.ts new file mode 100644 index 00000000000..07c4ff1dba4 --- /dev/null +++ b/frontend/packages/integration-tests-cypress/mocks/volume-attributes-class.ts @@ -0,0 +1,113 @@ +import type { DeploymentKind } from '@console/internal/module/k8s'; + +// VolumeAttributesClass names and constants +export const VAC_NAME_1 = 'aws-ebs-gp3-high-iops'; +export const VAC_NAME_2 = 'aws-ebs-gp3-low-iops'; +export const VAC_INVALID = 'invalid-vac-1'; +export const PVC_NAME = 'my-pvc-1'; +export const DEPLOYMENT_NAME = 'my-deployment-1'; + +// AWS EBS CSI driver VolumeAttributesClass for high IOPS +export const VAC_1 = { + apiVersion: 'storage.k8s.io/v1', + kind: 'VolumeAttributesClass', + metadata: { + name: VAC_NAME_1, + }, + driverName: 'ebs.csi.aws.com', + parameters: { + iops: '1000', + throughput: '125', + type: 'gp3', + }, +}; + +// AWS EBS CSI driver VolumeAttributesClass for low IOPS +export const VAC_2 = { + apiVersion: 'storage.k8s.io/v1', + kind: 'VolumeAttributesClass', + metadata: { + name: VAC_NAME_2, + }, + driverName: 'ebs.csi.aws.com', + parameters: { + iops: '3000', + throughput: '125', + type: 'gp3', + }, +}; + +// Invalid VAC with non-existent driver to trigger error state +export const VAC_INVALID_OBJ = { + apiVersion: 'storage.k8s.io/v1', + kind: 'VolumeAttributesClass', + metadata: { + name: VAC_INVALID, + }, + driverName: 'non.existent.driver', + parameters: { + iops: '1000', + throughput: '125', + type: 'gp3', + }, +}; + +export const getDeployment = (namespace: string, pvcName: string): DeploymentKind => ({ + apiVersion: 'apps/v1', + kind: 'Deployment', + metadata: { + name: DEPLOYMENT_NAME, + namespace, + labels: { + app: 'my-app', + }, + }, + spec: { + replicas: 1, + selector: { + matchLabels: { + app: 'my-app', + }, + }, + template: { + metadata: { + labels: { + app: 'my-app', + }, + }, + spec: { + containers: [ + { + name: 'app-container', + image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest', + ports: [{ containerPort: 80, protocol: 'TCP' }], + volumeMounts: [ + { + name: 'persistent-storage', + mountPath: '/data', + }, + ], + resources: { + requests: { + memory: '128Mi', + cpu: '100m', + }, + limits: { + memory: '256Mi', + cpu: '200m', + }, + }, + }, + ], + volumes: [ + { + name: 'persistent-storage', + persistentVolumeClaim: { + claimName: pvcName, + }, + }, + ], + }, + }, + }, +}); diff --git a/frontend/packages/integration-tests-cypress/tests/crud/resource-crud.cy.ts b/frontend/packages/integration-tests-cypress/tests/crud/resource-crud.cy.ts index d40d213fed3..0fbf23b03fb 100644 --- a/frontend/packages/integration-tests-cypress/tests/crud/resource-crud.cy.ts +++ b/frontend/packages/integration-tests-cypress/tests/crud/resource-crud.cy.ts @@ -78,6 +78,10 @@ describe('Kubernetes resource CRUD operations', () => { kind: 'snapshot.storage.k8s.io~v1~VolumeSnapshotContent', namespaced: false, }) + .set('storage.k8s.io~v1~VolumeAttributesClass', { + kind: 'storage.k8s.io~v1~VolumeAttributesClass', + namespaced: false, + }) : OrderedMap(); const k8sObjsWithSnapshots = k8sObjs.merge(snapshotObjs); @@ -146,6 +150,7 @@ describe('Kubernetes resource CRUD operations', () => { 'snapshot.storage.k8s.io~v1~VolumeSnapshotClass', 'snapshot.storage.k8s.io~v1~VolumeSnapshotContent', 'StatefulSet', + 'storage.k8s.io~v1~VolumeAttributesClass', 'StorageClass', 'user.openshift.io~v1~Group', ]); diff --git a/frontend/packages/integration-tests-cypress/tests/storage/volume-attributes-class.cy.ts b/frontend/packages/integration-tests-cypress/tests/storage/volume-attributes-class.cy.ts new file mode 100644 index 00000000000..dd03d40eaab --- /dev/null +++ b/frontend/packages/integration-tests-cypress/tests/storage/volume-attributes-class.cy.ts @@ -0,0 +1,107 @@ +import { + VAC_1, + VAC_2, + VAC_INVALID_OBJ, + VAC_NAME_1, + VAC_NAME_2, + VAC_INVALID, + PVC_NAME, + DEPLOYMENT_NAME, + getDeployment, +} from '../../mocks/volume-attributes-class'; +import { testName, checkErrors } from '../../support'; +import { resourceStatusShouldContain } from '../../views/common'; +import { detailsPage } from '../../views/details-page'; +import { volumeAttributesClass, pvcWithVAC } from '../../views/storage/volume-attributes-class'; + +// These tests are meant to be run on AWS as only AWS supports CSI drivers with modifyVolume (EBS CSI) +// Normalize env check: CI env vars are strings, so "false" would be truthy without explicit comparison. +const isAws = String(Cypress.env('BRIDGE_AWS')).toLowerCase() === 'true'; // Extract into reusable logic + +(isAws ? describe : describe.skip)('VolumeAttributesClass integration with PVC', () => { + before(() => { + cy.login(); + cy.createProjectWithCLI(testName); + + // Create VACs using apply + cy.exec(`echo '${JSON.stringify(VAC_1)}' | oc apply -f -`); + cy.exec(`echo '${JSON.stringify(VAC_2)}' | oc apply -f -`); + cy.exec(`echo '${JSON.stringify(VAC_INVALID_OBJ)}' | oc apply -f -`); + }); + + afterEach(() => { + checkErrors(); + }); + + after(() => { + // Cleanup test resources in proper order with waits to ensure complete deletion + + // Step 1: Delete Deployment (releases pod references to PVC) + cy.exec( + `oc delete deployment ${DEPLOYMENT_NAME} -n ${testName} --ignore-not-found=true --wait=true`, + { + failOnNonZeroExit: false, + timeout: 120000, + }, + ); + + // Step 2: Delete PVC (releases PVC reference to VAC) and wait for it + cy.exec(`oc delete pvc ${PVC_NAME} -n ${testName} --ignore-not-found=true --wait=true`, { + failOnNonZeroExit: false, + timeout: 120000, + }); + + // Step 3: Remove finalizers from all VACs + [VAC_NAME_1, VAC_NAME_2, VAC_INVALID].forEach((vacName) => { + cy.exec( + `oc patch volumeattributesclass ${vacName} -p '{"metadata":{"finalizers":[]}}' --type=merge`, + { failOnNonZeroExit: false, timeout: 30000 }, + ); + }); + + // Step 4: Initiate VAC deletion without waiting (cluster resources can be slow to delete) + cy.exec( + `oc delete volumeattributesclass ${VAC_NAME_1} ${VAC_NAME_2} ${VAC_INVALID} --ignore-not-found=true --wait=false`, + { + failOnNonZeroExit: false, + timeout: 30000, + }, + ); + + cy.deleteProjectWithCLI(testName); + }); + + it('creates a PVC with VolumeAttributesClass selected from the form dropdown', () => { + pvcWithVAC.createPVCWithVAC(testName, PVC_NAME, VAC_NAME_1); + detailsPage.titleShouldContain(PVC_NAME); + pvcWithVAC.verifyVACOnDetailsPage(VAC_NAME_1); + + // Create Deployment to bind the PVC + cy.exec(`echo '${JSON.stringify(getDeployment(testName, PVC_NAME))}' | oc create -f -`); + + // Wait for Deployment to be ready + cy.exec(`oc rollout status deployment/${DEPLOYMENT_NAME} -n ${testName} --timeout=120s`, { + failOnNonZeroExit: false, + timeout: 130000, + }); + + // Wait for PVC to be bound (required for VAC modification to be enabled) + pvcWithVAC.navigateToPVCDetails(testName, PVC_NAME); + resourceStatusShouldContain('Bound', { timeout: 45000 }); + + // pvcWithVAC.verifyVACOnDetailsPage(VAC_NAME_1); + }); + + it('modifies VolumeAttributesClass on PVC using the modal', () => { + pvcWithVAC.modifyVACOnPVC(testName, PVC_NAME, VAC_NAME_2); + pvcWithVAC.verifyVACOnDetailsPage(VAC_NAME_2); + }); + + it('handles invalid VAC gracefully by displaying requested VAC on details page', () => { + pvcWithVAC.modifyVACOnPVC(testName, PVC_NAME, VAC_INVALID); + pvcWithVAC.verifyVACOnDetailsPage(VAC_INVALID); + + // Navigate to VAC list page before cleanup to avoid 404 when PVC is deleted + volumeAttributesClass.navigateToVACList(); + }); +}); diff --git a/frontend/packages/integration-tests-cypress/views/storage/volume-attributes-class.ts b/frontend/packages/integration-tests-cypress/views/storage/volume-attributes-class.ts new file mode 100644 index 00000000000..cbe9c8fb446 --- /dev/null +++ b/frontend/packages/integration-tests-cypress/views/storage/volume-attributes-class.ts @@ -0,0 +1,72 @@ +import { detailsPage } from '../details-page'; +import { listPage } from '../list-page'; +import { modal } from '../modal'; + +export const volumeAttributesClass = { + navigateToVACList: () => { + cy.visit('/k8s/cluster/storage.k8s.io~v1~VolumeAttributesClass'); + listPage.dvRows.shouldBeLoaded(); + }, +}; + +export const pvcWithVAC = { + createPVCWithVAC: (namespace: string, pvcName: string, vacName: string) => { + cy.visit(`/k8s/ns/${namespace}/persistentvolumeclaims/~new/form`); + + // Wait for form to be fully loaded + cy.byTestID('pvc-name', { timeout: 30000 }).should('be.visible'); + + // Wait for VAC dropdown to load and select VAC + cy.byTestID('volumeattributesclass-dropdown', { timeout: 30000 }).should('be.visible').click(); + cy.byTestID('console-select-item').contains(vacName).click(); + + // Fill PVC name + cy.byTestID('pvc-name').clear().type(pvcName); + + // Set size + cy.byTestID('pvc-size').clear().type('1'); + + // Create PVC + cy.byTestID('create-pvc').click(); + + // Wait for navigation to details page + cy.location('pathname', { timeout: 30000 }).should( + 'include', + `persistentvolumeclaims/${pvcName}`, + ); + detailsPage.isLoaded(); + }, + + navigateToPVCDetails: (namespace: string, pvcName: string) => { + cy.visit(`/k8s/ns/${namespace}/persistentvolumeclaims/${pvcName}`); + detailsPage.isLoaded(); + }, + + modifyVACOnPVC: (namespace: string, pvcName: string, newVACName: string) => { + pvcWithVAC.navigateToPVCDetails(namespace, pvcName); + + // Open actions menu and wait for Modify VAC action to be enabled (not disabled) + cy.byLegacyTestID('actions-menu-button').click(); + cy.byTestActionID('Modify VolumeAttributesClass') + .should('be.visible') + .and('not.have.attr', 'aria-disabled', 'true') + .click(); + + modal.shouldBeOpened(); + modal.modalTitleShouldContain('Modify VolumeAttributesClass'); + + // Wait for and click the VAC dropdown in the modal + cy.byTestID('modify-vac-dropdown', { timeout: 15000 }).should('be.visible').click(); + cy.byTestID('console-select-item').contains(newVACName).click(); + + modal.submit(); + modal.shouldBeClosed(); + }, + + verifyVACOnDetailsPage: (vacName: string) => { + cy.get('[data-test-section-heading="PersistentVolumeClaim details"]') + .parent() + .contains(vacName) + .should('be.visible'); + }, +};