Skip to content
Merged
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
Expand Up @@ -82,7 +82,7 @@ const ModifyVACModalComponent: FC<ModifyVACModalComponentProps> = ({ resource, c
>
{t('console-app~Save')}
</Button>
<Button variant="link" onClick={cancel}>
<Button variant="link" onClick={cancel} data-test-id="modal-cancel-action">
{t('console-app~Cancel')}
</Button>
</ModalFooterWithAlerts>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
import type { DeploymentKind } from '@console/internal/module/k8s';

// Factory function to generate unique fixture names and objects per test run
// This prevents collisions when tests run concurrently on shared clusters
export const getVACFixtures = (suffix: string) => {
const names = {
TEST_VAC_LOW_IOPS: `test-vac-low-iops-${suffix}`,
TEST_VAC_HIGH_IOPS: `test-vac-high-iops-${suffix}`,
TEST_VAC_INVALID: `test-vac-invalid-${suffix}`,
TEST_STORAGECLASS: `test-storageclass-${suffix}`,
// Namespace-scoped, no suffix needed
TEST_PVC: 'test-pvc',
TEST_DEPLOYMENT: 'test-deployment',
};

return {
...names,
VAC_LOW_IOPS: {
apiVersion: 'storage.k8s.io/v1',
kind: 'VolumeAttributesClass',
metadata: { name: names.TEST_VAC_LOW_IOPS },
driverName: 'ebs.csi.aws.com',
parameters: { iops: '3000', throughput: '125', type: 'gp3' },
},
VAC_HIGH_IOPS: {
apiVersion: 'storage.k8s.io/v1',
kind: 'VolumeAttributesClass',
metadata: { name: names.TEST_VAC_HIGH_IOPS },
driverName: 'ebs.csi.aws.com',
// Uses identical parameters to VAC_LOW_IOPS to minimize CSI driver modification time and reduce test flakiness.
// This allows verification of VAC name fields on the PVC details page without long waits for actual volume operations.
parameters: { iops: '3000', throughput: '125', type: 'gp3' },
},
VAC_INVALID: {
apiVersion: 'storage.k8s.io/v1',
kind: 'VolumeAttributesClass',
metadata: { name: names.TEST_VAC_INVALID },
driverName: 'ebs.csi.aws.com',
parameters: { iops: '999999', throughput: '999999', type: 'gp3' },
},
STORAGE_CLASS: {
apiVersion: 'storage.k8s.io/v1',
kind: 'StorageClass',
metadata: { name: names.TEST_STORAGECLASS },
provisioner: 'ebs.csi.aws.com',
allowVolumeExpansion: true,
},
getDeployment: (namespace: string, pvcName: string): DeploymentKind => ({
apiVersion: 'apps/v1',
kind: 'Deployment',
metadata: { name: names.TEST_DEPLOYMENT, namespace },
spec: {
replicas: 1,
selector: { matchLabels: { app: 'test-app' } },
template: {
metadata: { labels: { app: 'test-app' } },
spec: {
containers: [
{
name: 'container',
image: 'image-registry.openshift-image-registry.svc:5000/openshift/httpd:latest',
volumeMounts: [{ name: 'storage', mountPath: '/data' }],
},
],
volumes: [{ name: 'storage', persistentVolumeClaim: { claimName: pvcName } }],
},
},
},
}),
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,6 @@ module.exports = (on, config) => {
config.env.BRIDGE_KUBEADMIN_PASSWORD = process.env.BRIDGE_KUBEADMIN_PASSWORD;
config.env.OAUTH_BASE_ADDRESS = process.env.OAUTH_BASE_ADDRESS;
config.env.OPENSHIFT_CI = process.env.OPENSHIFT_CI;
config.env.BRIDGE_AWS = process.env.BRIDGE_AWS;
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Duplicate of the change in PR This ensures that AWS-specific tests run in CI. I will remove this if the other PR merges first.

return config;
};
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,184 @@
import { getVACFixtures } from '../../mocks/volume-attributes-class';
import { testName, checkErrors } from '../../support';
import { resourceStatusShouldContain } from '../../views/common';
import { detailsPage } from '../../views/details-page';
import { listPage } from '../../views/list-page';
import { modal } from '../../views/modal';

// These tests require AWS platform with EBS CSI driver for modifyVolume support
const isAws = String(Cypress.env('BRIDGE_AWS')).toLowerCase() === 'true';

if (isAws) {
describe('VolumeAttributesClass E2E tests', () => {
// Generate unique fixtures per test run to avoid collisions on shared clusters
const fixtures = getVACFixtures(testName);
const {
VAC_LOW_IOPS,
VAC_HIGH_IOPS,
VAC_INVALID,
STORAGE_CLASS,
TEST_VAC_LOW_IOPS,
TEST_VAC_HIGH_IOPS,
TEST_VAC_INVALID,
TEST_PVC,
TEST_DEPLOYMENT,
TEST_STORAGECLASS,
getDeployment,
} = fixtures;

before(() => {
cy.login();
cy.createProjectWithCLI(testName);
// Create StorageClass for PVC provisioning
cy.exec(`echo '${JSON.stringify(STORAGE_CLASS)}' | oc apply -f -`);
// Create VolumeAttributesClasses for testing
cy.exec(`echo '${JSON.stringify(VAC_LOW_IOPS)}' | oc apply -f -`);
cy.exec(`echo '${JSON.stringify(VAC_HIGH_IOPS)}' | oc apply -f -`);
cy.exec(`echo '${JSON.stringify(VAC_INVALID)}' | oc apply -f -`);
// Create Deployment that will consume the PVC
cy.exec(`echo '${JSON.stringify(getDeployment(testName, TEST_PVC))}' | oc apply -f -`);
});

afterEach(() => {
checkErrors();
});

after(() => {
// Navigate to VAC list page to avoid 404 during cleanup
cy.visit('/k8s/cluster/storage.k8s.io~v1~VolumeAttributesClass');
listPage.dvRows.shouldBeLoaded();

// Delete Deployment first to release PVC
cy.exec(
`oc delete deployment ${TEST_DEPLOYMENT} -n ${testName} --ignore-not-found=true --wait=true`,
{
failOnNonZeroExit: false,
timeout: 120000,
},
);

// Delete PVC to release VAC reference
cy.exec(`oc delete pvc ${TEST_PVC} -n ${testName} --ignore-not-found=true --wait=true`, {
failOnNonZeroExit: false,
timeout: 120000,
});

// Remove finalizers from VACs to allow deletion
[TEST_VAC_LOW_IOPS, TEST_VAC_HIGH_IOPS, TEST_VAC_INVALID].forEach((vacName) => {
cy.exec(
`oc patch volumeattributesclass ${vacName} -p '{"metadata":{"finalizers":[]}}' --type=merge`,
{ failOnNonZeroExit: false, timeout: 30000 },
);
});

// Delete VACs without waiting (cluster-scoped resources can be slow)
cy.exec(
`oc delete volumeattributesclass ${TEST_VAC_LOW_IOPS} ${TEST_VAC_HIGH_IOPS} ${TEST_VAC_INVALID} --ignore-not-found=true --wait=false`,
{
failOnNonZeroExit: false,
timeout: 30000,
},
);

// Delete StorageClass
cy.exec(`oc delete storageclass ${TEST_STORAGECLASS} --ignore-not-found=true --wait=false`, {
failOnNonZeroExit: false,
timeout: 30000,
});

cy.deleteProjectWithCLI(testName);
});

it('creates PVC with VolumeAttributesClass and verifies it appears on details page', () => {
cy.visit(`/k8s/ns/${testName}/persistentvolumeclaims/~new/form`);
cy.byTestID('pvc-name').should('exist').clear().type(TEST_PVC);
cy.byTestID('pvc-size').clear().type('1');

// Select StorageClass from dropdown
cy.byTestID('storageclass-dropdown').click();
cy.byTestID('console-select-item').contains(TEST_STORAGECLASS).click();

// Select VolumeAttributesClass from dropdown
cy.byTestID('volumeattributesclass-dropdown').click();
cy.byTestID('console-select-item').contains(TEST_VAC_LOW_IOPS).click();

// Create PVC and navigate to details page
cy.byTestID('create-pvc').click();
detailsPage.titleShouldContain(TEST_PVC);

// Verify requested VAC is displayed
cy.byLegacyTestID('pvc-requested-vac', { timeout: 30000 }).should(
'contain.text',
TEST_VAC_LOW_IOPS,
);

// Wait for PVC to reach Bound status
resourceStatusShouldContain('Bound', { timeout: 120000 });

// Verify current VAC matches requested VAC
cy.byLegacyTestID('pvc-current-vac', { timeout: 30000 }).should('exist');
cy.byLegacyTestID('pvc-current-vac').should('contain.text', TEST_VAC_LOW_IOPS);
});

it('modifies VolumeAttributesClass via modal and verifies update', () => {
cy.visit(`/k8s/ns/${testName}/persistentvolumeclaims/${TEST_PVC}`);
detailsPage.isLoaded();

// Open Modify VolumeAttributesClass modal
detailsPage.clickPageActionFromDropdown('Modify VolumeAttributesClass');
modal.shouldBeOpened();

// Select new VolumeAttributesClass
cy.byTestID('modify-vac-dropdown').click();
cy.byTestID('console-select-item').contains(TEST_VAC_HIGH_IOPS).click();
modal.submit();
modal.shouldBeClosed();

// Verify requested VAC updated to new value
cy.byLegacyTestID('pvc-requested-vac', { timeout: 30000 }).should(
'contain.text',
TEST_VAC_HIGH_IOPS,
);

// Verify current VAC updated to new value
cy.byLegacyTestID('pvc-current-vac', { timeout: 30000 }).should(
'contain.text',
TEST_VAC_HIGH_IOPS,
);
});

it('attempts invalid VAC modification and verifies error alert', () => {
cy.visit(`/k8s/ns/${testName}/persistentvolumeclaims/${TEST_PVC}`);
detailsPage.isLoaded();

// Open Modify VolumeAttributesClass modal and select invalid VAC
detailsPage.clickPageActionFromDropdown('Modify VolumeAttributesClass');
modal.shouldBeOpened();
cy.byTestID('modify-vac-dropdown').click();
cy.byTestID('console-select-item').contains(TEST_VAC_INVALID).click();
modal.submit();
modal.shouldBeClosed();

// Verify requested VAC updated to invalid value
cy.byLegacyTestID('pvc-requested-vac', { timeout: 30000 }).should(
'contain.text',
TEST_VAC_INVALID,
);

// Verify current VAC remains at previous valid value
cy.byLegacyTestID('pvc-current-vac').should('exist');
cy.byLegacyTestID('pvc-current-vac').should('contain.text', TEST_VAC_HIGH_IOPS);

// Verify error alert appears after CSI driver rejects modification
cy.byLegacyTestID('vac-error-alert', { timeout: 60000 }).should('be.visible');
cy.byLegacyTestID('vac-error-alert').should(
'contain.text',
'VolumeAttributesClass modification failed',
);
});
});
} else {
describe('Skipping VolumeAttributesClass Tests', () => {
it('requires AWS platform with EBS CSI driver', () => {});
});
}
1 change: 1 addition & 0 deletions frontend/public/components/persistent-volume-claim.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,7 @@ const PVCDetails: FC<PVCDetailsProps> = ({ obj: pvc }) => {
title={t('public~VolumeAttributesClass modification failed')}
className="co-alert co-alert--margin-bottom-sm"
actionClose={<AlertActionCloseButton onClose={() => setIsErrorAlertDismissed(true)} />}
data-test-id="vac-error-alert"
>
{t(
'public~VolumeAttributesClass modification failed. Your volume settings could not be updated. Please try again.',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { FC } from 'react';
import * as fuzzy from 'fuzzysearch';
import { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next';
import { Alert } from '@patternfly/react-core';
import { ConsoleSelect } from '@console/internal/components/utils/console-select';
import { LoadingInline, ResourceName, ResourceIcon } from '.';
import { css } from '@patternfly/react-styles';
Expand Down Expand Up @@ -75,12 +76,6 @@ export const VolumeAttributesClassDropdownInner: FC<VolumeAttributesClassDropdow

useEffect(() => {
if (loadError) {
setState((prevState) => ({
...prevState,
title: (
<div className="cos-error-title">{t('public~Error loading {{desc}}', { desc })}</div>
),
}));
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Don't update state for errors. Removed and handled in the render.

return;
}
if (!loaded) {
Expand Down Expand Up @@ -169,7 +164,11 @@ export const VolumeAttributesClassDropdownInner: FC<VolumeAttributesClassDropdow

return (
<>
{loaded && (
{loadError ? (
<Alert isInline variant="danger" title={t('public~Error loading VolumeAttributesClass')}>
{loadError.message || t('public~Unable to load VolumeAttributesClass resources')}
</Alert>
) : loaded ? (
<div>
<label
className={css(hideClassName, {
Expand Down Expand Up @@ -200,7 +199,7 @@ export const VolumeAttributesClassDropdownInner: FC<VolumeAttributesClassDropdow
</p>
)}
</div>
)}
) : null}
</>
);
};
Expand Down
2 changes: 2 additions & 0 deletions frontend/public/locales/en/public.json
Original file line number Diff line number Diff line change
Expand Up @@ -1557,6 +1557,8 @@
"ConfigMap or Secret": "ConfigMap or Secret",
"Select a resource": "Select a resource",
"Select VolumeAttributesClass": "Select VolumeAttributesClass",
"Error loading VolumeAttributesClass": "Error loading VolumeAttributesClass",
"Unable to load VolumeAttributesClass resources": "Unable to load VolumeAttributesClass resources",
"Defines mutable volume parameters like IOPS and throughput.": "Defines mutable volume parameters like IOPS and throughput.",
"Secret referenced in the {{triggerProperty}} webhook trigger does not contain \"WebHookSecretKey\" key. Webhook trigger won’t work due to the invalid secret reference": "Secret referenced in the {{triggerProperty}} webhook trigger does not contain \"WebHookSecretKey\" key. Webhook trigger won’t work due to the invalid secret reference",
"Copy URL with Secret": "Copy URL with Secret",
Expand Down