Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -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,
},
},
],
},
},
},
});
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, TestDefinition>();

const k8sObjsWithSnapshots = k8sObjs.merge(snapshotObjs);
Expand Down Expand Up @@ -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',
]);
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
});
});
Original file line number Diff line number Diff line change
@@ -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');
},
};