Skip to content
Draft
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
@@ -1,5 +1,6 @@
import { selectors } from './Policies.selectors';
import DndSimulatorDataTransfer from '../../helpers/dndSimulatorDataTransfer';
import { hasFeatureFlag } from '../../helpers/features';
import { selectors } from './Policies.selectors';

// --- Form Interaction ---

Expand Down Expand Up @@ -61,7 +62,41 @@ export function addInclusionWithNamespace(namespace: string) {

export function addExclusionWithDeployment(deploymentName: string) {
cy.contains('button', 'Add exclusion').click();
cy.get('[aria-label="Deployment name"]').type(deploymentName);
cy.get('[aria-label="Workload name"]').type(deploymentName);
}

function workloadKindCheckboxId(label: 'CronJobs' | 'Jobs') {
return label === 'CronJobs' ? 'exclude-workload-kind-CRON_JOB' : 'exclude-workload-kind-JOB';
}

export function toggleWorkloadKindExclusion(label: 'CronJobs' | 'Jobs') {
cy.contains('h4', 'By kind');
// "Jobs" is a substring of "CronJobs", so a label text match clicks the wrong checkbox.
cy.get(`label[for="${workloadKindCheckboxId(label)}"]`).click();
}

export function assertWorkloadKindExclusion(label: 'CronJobs' | 'Jobs', isChecked: boolean) {
cy.get(`#${workloadKindCheckboxId(label)}`).should(isChecked ? 'be.checked' : 'not.be.checked');
}

export function goToWizardStep(stepName: string) {
// Nested steps such as Resources stay hidden until their expandable parent is open.
cy.contains('button.pf-v6-c-wizard__nav-link', stepName).then(($step) => {
const collapsedParent = $step
.parents('.pf-v6-c-wizard__nav-item.pf-m-expandable:not(.pf-m-expanded)')
.filter((_, el) => !Cypress.$(el).children('.pf-v6-c-wizard__nav-link').is($step));
if (collapsedParent.length > 0) {
cy.wrap(collapsedParent.last()).children('button.pf-v6-c-wizard__nav-link').click();
}
});
cy.contains('button.pf-v6-c-wizard__nav-link', stepName).click();
}

export function skipFiltersStepIfPresent() {
if (hasFeatureFlag('ROX_EVALUATION_FILTER') && hasFeatureFlag('ROX_INIT_CONTAINER_SUPPORT')) {
assertStepHeading('Filters');
clickNext();
}
}

// --- Step 5: Actions ---
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import withAuth from '../../helpers/basicAuth';
import { hasFeatureFlag } from '../../helpers/features';
import { getInputByLabel } from '../../helpers/formHelpers';
import { deletePolicyIfExists, doPolicyPageAction, visitPolicies } from './Policies.helpers';
import { selectors } from './Policies.selectors';
import {
addExclusionWithDeployment,
assertStepHeading,
assertWorkloadKindExclusion,
clickNext,
clickSave,
dragFieldIntoSection,
expandCriteriaCategory,
goToWizardStep,
selectCategory,
skipFiltersStepIfPresent,
startPolicyWizard,
toggleWorkloadKindExclusion,
verifyPolicyDetails,
verifyPolicyInTable,
} from './policyWizard.helpers';

const POLICY_NAME = 'CYPRESS_TEST_WORKLOAD_KIND_EXCLUSION';
const EXCLUDED_WORKLOAD_NAME = 'system-admin';

describe('Policy workload kind exclusion', () => {
withAuth();

before(function () {
if (!hasFeatureFlag('ROX_POLICY_WORKLOAD_KIND_EXCLUSION')) {
this.skip();
}
});

beforeEach(() => {
deletePolicyIfExists(POLICY_NAME);
});

afterEach(() => {
deletePolicyIfExists(POLICY_NAME);
});

it('should exclude CronJobs, persist on reopen, and combine with a workload name exclusion', () => {
visitPolicies();
startPolicyWizard();

getInputByLabel('Name').type(POLICY_NAME);
cy.contains('label', 'High').click();
selectCategory('Privileges');
getInputByLabel('Description').type(
'Exclude CronJobs and a named workload from this policy'
);
clickNext();

assertStepHeading('Lifecycle');
cy.contains('label', 'Deploy').click();
clickNext();

assertStepHeading('Rules');
expandCriteriaCategory('Container configuration');
dragFieldIntoSection(`${selectors.step3.policyCriteria.key}:contains("Privileged")`);
cy.get(selectors.step3.policyCriteria.groupCards).should('have.length.gte', 1);
clickNext();

assertStepHeading('Resources');
cy.contains(
'Define one or more clusters, namespaces or workloads (if applicable) to be excluded from this policy.'
);
toggleWorkloadKindExclusion('CronJobs');
assertWorkloadKindExclusion('CronJobs', true);
assertWorkloadKindExclusion('Jobs', false);
cy.contains('Policy will exclude CronJobs.');
addExclusionWithDeployment(EXCLUDED_WORKLOAD_NAME);
clickNext();

skipFiltersStepIfPresent();

assertStepHeading('Actions');
clickNext();

assertStepHeading('Review policy');
cy.contains('dt', 'Workload kind').next('dd').should('contain', 'CronJobs');
cy.contains(EXCLUDED_WORKLOAD_NAME);
clickSave();

verifyPolicyInTable(POLICY_NAME);
verifyPolicyDetails(POLICY_NAME, {
severity: 'High',
lifecycle: 'Deploy',
categories: 'Privileges',
description: 'Exclude CronJobs and a named workload from this policy',
response: 'Inform',
scope: ['Workload kind', 'CronJobs', EXCLUDED_WORKLOAD_NAME],
});

doPolicyPageAction('Edit policy');
goToWizardStep('Resources');
assertWorkloadKindExclusion('CronJobs', true);
assertWorkloadKindExclusion('Jobs', false);
cy.get('[aria-label="Workload name"]').should('have.value', EXCLUDED_WORKLOAD_NAME);

toggleWorkloadKindExclusion('Jobs');
assertWorkloadKindExclusion('Jobs', true);
cy.contains('Policy will exclude CronJobs and Jobs.');

goToWizardStep('Review');
cy.contains('dt', 'Workload kind').next('dd').should('have.text', 'CronJobs, Jobs');
clickSave();

visitPolicies();
verifyPolicyDetails(POLICY_NAME, {
severity: 'High',
lifecycle: 'Deploy',
categories: 'Privileges',
description: 'Exclude CronJobs and a named workload from this policy',
response: 'Inform',
scope: ['Workload kind', 'CronJobs, Jobs', EXCLUDED_WORKLOAD_NAME],
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,9 +43,9 @@ function ExclusionDeploymentDetails({
desc={`${namespaceLabel.key}=${namespaceLabel.value}`}
/>
)}
{deploymentName && <DescriptionListItem term="Deployment" desc={deploymentName} />}
{deploymentName && <DescriptionListItem term="Workload" desc={deploymentName} />}
{label && (
<DescriptionListItem term="Deployment label" desc={`${label.key}=${label.value}`} />
<DescriptionListItem term="Workload label" desc={`${label.key}=${label.value}`} />
)}
</DescriptionList>
);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { ReactElement } from 'react';
import { DescriptionList } from '@patternfly/react-core';

import DescriptionListItem from 'Components/DescriptionListItem';
import type { PolicyWorkloadKind } from 'types/policy.proto';

import { formatWorkloadKindList } from '../policies.utils';

type ExclusionWorkloadKindDetailsProps = {
kinds: PolicyWorkloadKind[];
};

function ExclusionWorkloadKindDetails({ kinds }: ExclusionWorkloadKindDetailsProps): ReactElement {
return (
<DescriptionList isCompact isHorizontal horizontalTermWidthModifier={{ default: '16ch' }}>
<DescriptionListItem term="Workload kind" desc={formatWorkloadKindList(kinds)} />
</DescriptionList>
);
}

export default ExclusionWorkloadKindDetails;
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ function InclusionScopeDetails({ clusters, scope }: InclusionScopeDetailsProps):
/>
)}
{label && (
<DescriptionListItem term="Deployment label" desc={`${label.key}=${label.value}`} />
<DescriptionListItem term="Workload label" desc={`${label.key}=${label.value}`} />
)}
</DescriptionList>
);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,12 @@ import useFetchClustersForPermissions from 'hooks/useFetchClustersForPermissions
import type { PolicyExclusion, PolicyScope } from 'types/policy.proto';
import InclusionScopeDetails from './InclusionScopeDetails';
import ExclusionDeploymentDetails from './ExclusionDeploymentDetails';
import { getExcludedDeployments, getExcludedImageNames } from '../policies.utils';
import ExclusionWorkloadKindDetails from './ExclusionWorkloadKindDetails';
import {
getExcludedDeployments,
getExcludedImageNames,
getExcludedWorkloadKinds,
} from '../policies.utils';

type PolicyScopeSectionProps = {
scope: PolicyScope[];
Expand All @@ -24,12 +29,15 @@ type PolicyScopeSectionProps = {
function PolicyScopeSection({ scope, exclusions }: PolicyScopeSectionProps): ReactElement {
const { clusters } = useFetchClustersForPermissions(['Deployment']);
const excludedDeployments = getExcludedDeployments(exclusions);
const excludedWorkloadKinds = getExcludedWorkloadKinds(exclusions);
const imageExclusionNames = getExcludedImageNames(exclusions);

const hasIncludedScope = scope?.length > 0;
const hasExcludedDeployments = excludedDeployments.length > 0;
const hasExcludedWorkloadKinds = excludedWorkloadKinds.length > 0;
const hasImageExclusions = imageExclusionNames.length > 0;
const hasAnyResources = hasIncludedScope || hasExcludedDeployments || hasImageExclusions;
const hasExcludedResources = hasExcludedDeployments || hasExcludedWorkloadKinds;
const hasAnyResources = hasIncludedScope || hasExcludedResources || hasImageExclusions;

return (
<>
Expand All @@ -54,10 +62,21 @@ function PolicyScopeSection({ scope, exclusions }: PolicyScopeSectionProps): Rea
</Grid>
</>
)}
{hasExcludedDeployments && (
{hasExcludedResources && (
<>
<Title headingLevel="h3">Excluded resources</Title>
<Grid hasGutter md={12} xl={6}>
{hasExcludedWorkloadKinds && (
<GridItem>
<Card>
<CardBody>
<ExclusionWorkloadKindDetails
kinds={excludedWorkloadKinds}
/>
</CardBody>
</Card>
</GridItem>
)}
{excludedDeployments.map((excludedDeployment, index) => (
// eslint-disable-next-line react/no-array-index-key
<GridItem key={index}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,6 +97,7 @@ function PolicyBehaviorForm({ hasActiveViolations }: PolicyBehaviorFormProps) {

// Do not validate the following, because changed values are on other steps.
setFieldValue('excludedImageNames', [], false);
setFieldValue('excludedWorkloadKinds', [], false);
values.scope.forEach(({ label, ...rest }, idx) => {
if (label) {
setFieldValue(`scope[${idx}]`, { ...rest }, false);
Expand Down Expand Up @@ -130,6 +131,7 @@ function PolicyBehaviorForm({ hasActiveViolations }: PolicyBehaviorFormProps) {
scope: [],
excludedImageNames: [],
excludedDeploymentScopes: [],
excludedWorkloadKinds: [],
enforcementActions: [],
});
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,28 +59,28 @@ function ExclusionScopeCard({
value={excludedDeploymentScope.scope?.namespace ?? ''}
/>
</FormGroup>
<FormGroup label="Deployment">
<FormGroup label="Workload">
<TextInput
aria-label="Deployment name"
aria-label="Workload name"
name={`${scopePath}.name`}
onChange={handleChange}
placeholder="Deployment name"
placeholder="Workload name"
type="text"
value={excludedDeploymentScope.name ?? ''}
/>
</FormGroup>
<FormGroup label="Deployment label">
<FormGroup label="Workload label">
<Flex direction={{ default: 'row' }} flexWrap={{ default: 'nowrap' }}>
<TextInput
aria-label="Deployment label key"
aria-label="Workload label key"
name={`${scopePath}.scope.label.key`}
onChange={handleChange}
placeholder="Label key"
type="text"
value={excludedDeploymentScope.scope?.label?.key ?? ''}
/>
<TextInput
aria-label="Deployment label value"
aria-label="Workload label value"
name={`${scopePath}.scope.label.value`}
onChange={handleChange}
placeholder="Label value"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,18 +162,18 @@ function InclusionScopeCard({
)}
</Flex>
</FormGroup>
<FormGroup label="Deployment label">
<FormGroup label="Workload label">
<Flex direction={{ default: 'row' }} flexWrap={{ default: 'nowrap' }}>
<TextInput
aria-label="Deployment label key"
aria-label="Workload label key"
name={`${scopePath}.label.key`}
onChange={handleChange}
placeholder="Label key"
type="text"
value={scope.label?.key ?? ''}
/>
<TextInput
aria-label="Deployment label value"
aria-label="Workload label value"
name={`${scopePath}.label.value`}
onChange={handleChange}
placeholder="Label value"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function PolicyScopeCardLegacy({
}));

// Note! Currently this filtering is only relevant to the exclusion scope, therefore accesses `value.scope` instead of `value`.
// If at some point the inclusion scope gains a deployment filter, this will need to be updated.
// If at some point the inclusion scope gains a workload filter, this will need to be updated.
const selectedNamespaceValue = scope?.namespace;
const deploymentSearchFilter: SearchFilter = {
'Cluster ID': scope?.cluster ? [scope.cluster] : undefined,
Expand Down Expand Up @@ -158,22 +158,22 @@ function PolicyScopeCardLegacy({
</FlexItem>
{type === 'exclusion' && (
<FlexItem>
<FormGroup label="Deployment" fieldId={`${name}-deployment`}>
<FormGroup label="Workload" fieldId={`${name}-deployment`}>
<AutocompleteSelect
searchCategory="DEPLOYMENTS"
searchTerm="Deployment"
value={value.name || ''}
onChange={handleChangeDeployment}
onSearch={handleChangeDeployment}
textLabel="Select a deployment"
textLabel="Select a workload"
searchFilter={deploymentSearchFilter}
isDisabled={hasAuditLogEventSource}
/>
</FormGroup>
</FlexItem>
)}
<FlexItem>
<FormGroup label="Deployment label" fieldId={`${name}-label`}>
<FormGroup label="Workload label" fieldId={`${name}-label`}>
<Flex
direction={{ default: 'row' }}
flexWrap={{ default: 'nowrap' }}
Expand Down
Loading
Loading