From a9b5023d87a841cbf94fc08dfcdb1dc7d726f8a1 Mon Sep 17 00:00:00 2001 From: Zack Layne Date: Thu, 10 Mar 2022 16:28:59 -0500 Subject: [PATCH] Update policy set to new resource spec (#1313) * Update policy set to new resource spec Signed-off-by: Zack Layne * code smells Signed-off-by: Zack Layne --- frontend/public/locales/en/translation.json | 2 + frontend/src/resources/policy-set.ts | 19 +- .../useClusterPolicyViolationsColumn.tsx | 12 +- .../src/routes/Governance/common/util.tsx | 69 +++++ .../Governance/policies/Policies.test.tsx | 7 - .../PolicyDetailsOverview.test.tsx | 7 - .../policy-sets/PolicySets.test.tsx | 123 +------- .../Governance/policy-sets/PolicySets.tsx | 94 +++--- .../components/CardViewToolbarFilter.tsx | 118 ++------ .../components/PolicySetCard.test.tsx | 30 +- .../policy-sets/components/PolicySetCard.tsx | 104 +++---- .../PolicySetDetailSidebar.test.tsx | 281 ++++++++++++------ .../components/PolicySetDetailSidebar.tsx | 220 ++++++++------ .../policy-sets/usePolicySetSummary.tsx | 73 ----- resources/policy-set-with-1-placement.yaml | 2 +- resources/policy-set-with-2-placements.yaml | 4 +- .../policy-set-with-placement-aws-gcp.yaml | 2 +- resources/policy-with-1-placement.yaml | 2 +- resources/policy-with-2-placements.yaml | 4 +- 19 files changed, 520 insertions(+), 653 deletions(-) delete mode 100644 frontend/src/routes/Governance/policy-sets/usePolicySetSummary.tsx diff --git a/frontend/public/locales/en/translation.json b/frontend/public/locales/en/translation.json index 78a279f877b..798b4a442ae 100644 --- a/frontend/public/locales/en/translation.json +++ b/frontend/public/locales/en/translation.json @@ -265,6 +265,8 @@ "Cluster violation": "Cluster violation", "Cluster violations": "Cluster violations", "Cluster where the selected Argo application resources are deployed.": "Cluster where the selected Argo application resources are deployed.", + "Cluster with policy violations": "Cluster with policy violations", + "Cluster without policy violations": "Cluster without policy violations", "cluster.count.local": "Local", "cluster.count.none": "None", "cluster.create.ai.subtitle": "Discover new hosts", diff --git a/frontend/src/resources/policy-set.ts b/frontend/src/resources/policy-set.ts index a2e78f64ab9..6e011791b89 100644 --- a/frontend/src/resources/policy-set.ts +++ b/frontend/src/resources/policy-set.ts @@ -35,24 +35,11 @@ export interface PolicySetSpec { export interface PolicySetStatus { compliant?: 'NonCompliant' | 'Compliant' placement?: PolicySetStatusPlacement[] - results: PolicySetStatusResult[] } export interface PolicySetStatusPlacement { - placement: string - placementBinding: string + placement?: string + placementRule?: string + placementBinding?: string placementDecisions?: string[] } - -export interface PolicySetStatusResult { - policy: string - compliant?: 'NonCompliant' | 'Compliant' - message?: string - clusters?: PolicySetResultCluster[] -} - -export interface PolicySetResultCluster { - clusterName: string - clusterNamespace: string - compliant: 'NonCompliant' | 'Compliant' -} diff --git a/frontend/src/routes/Governance/clusters/useClusterPolicyViolationsColumn.tsx b/frontend/src/routes/Governance/clusters/useClusterPolicyViolationsColumn.tsx index 1d26453c216..6d701728510 100644 --- a/frontend/src/routes/Governance/clusters/useClusterPolicyViolationsColumn.tsx +++ b/frontend/src/routes/Governance/clusters/useClusterPolicyViolationsColumn.tsx @@ -2,7 +2,7 @@ import { IAcmTableColumn } from '@stolostron/ui-components' import { Fragment } from 'react' import { useTranslation } from '../../../lib/acm-i18next' -import { Cluster, PolicySetResultCluster } from '../../../resources' +import { Cluster } from '../../../resources' import { PolicyViolationIcons2 } from '../components/PolicyViolations' import { ClusterViolationSummaryMap } from '../overview/ClusterViolationSummary' @@ -39,12 +39,12 @@ export function useClusterPolicyViolationsColumn( export function usePolicySetClusterPolicyViolationsColumn( clusterViolationSummaryMap: ClusterViolationSummaryMap -): IAcmTableColumn { +): IAcmTableColumn { const { t } = useTranslation() return { header: t('Policy violations'), - cell: (cluster: PolicySetResultCluster) => { - const clusterViolationSummary = clusterViolationSummaryMap[cluster.clusterName ?? ''] + cell: (cluster: string) => { + const clusterViolationSummary = clusterViolationSummaryMap[cluster ?? ''] if (!clusterViolationSummary) return return ( { - const lhsViolations = clusterViolationSummaryMap[lhs.clusterName ?? ''] - const rhsViolations = clusterViolationSummaryMap[rhs.clusterName ?? ''] + const lhsViolations = clusterViolationSummaryMap[lhs ?? ''] + const rhsViolations = clusterViolationSummaryMap[rhs ?? ''] if (lhsViolations === rhsViolations) return 0 if (!lhsViolations) return -1 if (!rhsViolations) return 1 diff --git a/frontend/src/routes/Governance/common/util.tsx b/frontend/src/routes/Governance/common/util.tsx index ca1f32c6dbe..84ee880ed1f 100644 --- a/frontend/src/routes/Governance/common/util.tsx +++ b/frontend/src/routes/Governance/common/util.tsx @@ -18,6 +18,11 @@ import { import { PlacementDecision } from '../../../resources/placement-decision' import ResourceLabels from '../../Applications/components/ResourceLabels' +export interface PolicyCompliance { + policyName: string + clusterCompliance: { clusterName: string; compliance: 'Compliant' | 'NonCompliant' }[] +} + export function getPlacementBindingsForResource(resource: Policy | PolicySet, placementBindings: PlacementBinding[]) { return placementBindings.filter( (placementBinding) => @@ -74,6 +79,70 @@ export function getPoliciesForPolicySet(policySet: PolicySet, policies: Policy[] ) } +export function getPolicyComplianceForPolicySet( + policySet: PolicySet, + policies: Policy[], + placementDecisions: PlacementDecision[], + resourceBindings: PlacementBinding[], + placements: (Placement | PlacementRule)[] +) { + const policySetPlacementDecisions = getPlacementDecisionsForResource( + policySet, + placementDecisions, + resourceBindings, + placements + ) + const policySetPolicies = getPoliciesForPolicySet(policySet, policies) + + const policyCompliance: PolicyCompliance[] = [] + for (const placementDecision of policySetPlacementDecisions) { + for (const decision of placementDecision.status.decisions) { + for (const policy of policySetPolicies) { + const policyIdx = policyCompliance.findIndex((p) => p.policyName === policy.metadata.name!) + const policyClusterStatus = policy.status?.status?.find( + (clusterStatus) => clusterStatus.clustername === decision.clusterName + ) + if (policyClusterStatus?.compliant === 'NonCompliant') { + if (policyIdx < 0) { + policyCompliance.push({ + policyName: policy.metadata.name!, + clusterCompliance: [ + { + clusterName: decision.clusterName, + compliance: 'NonCompliant', + }, + ], + }) + } else { + policyCompliance[policyIdx].clusterCompliance.push({ + clusterName: decision.clusterName, + compliance: 'NonCompliant', + }) + } + } else if (policyClusterStatus?.compliant === 'Compliant') { + if (policyIdx < 0) { + policyCompliance.push({ + policyName: policy.metadata.name!, + clusterCompliance: [ + { + clusterName: decision.clusterName, + compliance: 'Compliant', + }, + ], + }) + } else { + policyCompliance[policyIdx].clusterCompliance.push({ + clusterName: decision.clusterName, + compliance: 'Compliant', + }) + } + } + } + } + } + return policyCompliance +} + export function getClustersComplianceForPolicySet( policySet: PolicySet, policies: Policy[], diff --git a/frontend/src/routes/Governance/policies/Policies.test.tsx b/frontend/src/routes/Governance/policies/Policies.test.tsx index 9e45c47d807..4972590de5a 100644 --- a/frontend/src/routes/Governance/policies/Policies.test.tsx +++ b/frontend/src/routes/Governance/policies/Policies.test.tsx @@ -108,13 +108,6 @@ const policySet0: PolicySet = { status: { compliant: 'Compliant', placement: [{ placement: 'policy-set-with-1-placement', placementBinding: 'policy-set-with-1-placement' }], - results: [ - { - clusters: [{ clusterName: 'local-cluster', clusterNamespace: 'local-cluster', compliant: 'Compliant' }], - compliant: 'Compliant', - policy: 'policy-set-with-1-placement-policy-1', - }, - ], }, } diff --git a/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsOverview.test.tsx b/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsOverview.test.tsx index c454c8da1d4..0022ae12b4f 100644 --- a/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsOverview.test.tsx +++ b/frontend/src/routes/Governance/policies/policy-details/PolicyDetailsOverview.test.tsx @@ -65,13 +65,6 @@ const policySet: PolicySet = { status: { compliant: 'Compliant', placement: [{ placement: 'policy-set-with-1-placement', placementBinding: 'policy-set-with-1-placement' }], - results: [ - { - clusters: [{ clusterName: 'local-cluster', clusterNamespace: 'local-cluster', compliant: 'Compliant' }], - compliant: 'Compliant', - policy: 'policy-set-with-1-placement-policy', - }, - ], }, } diff --git a/frontend/src/routes/Governance/policy-sets/PolicySets.test.tsx b/frontend/src/routes/Governance/policy-sets/PolicySets.test.tsx index 60902a56b8a..56324f42d95 100644 --- a/frontend/src/routes/Governance/policy-sets/PolicySets.test.tsx +++ b/frontend/src/routes/Governance/policy-sets/PolicySets.test.tsx @@ -27,6 +27,7 @@ const policySet0: PolicySet = { ], }, status: { + compliant: 'NonCompliant', placement: [ { placement: 'placement1', @@ -34,104 +35,6 @@ const policySet0: PolicySet = { placementDecisions: ['placementdecision1'], }, ], - results: [ - { - policy: 'policy-testing', - compliant: 'NonCompliant', - clusters: [ - { - clusterName: 'local-cluster', - clusterNamespace: 'local-cluster', - compliant: 'Compliant', - }, - { - clusterName: 'managed1', - clusterNamespace: 'managed1', - compliant: 'NonCompliant', - }, - { - clusterName: 'managed2', - clusterNamespace: 'managed2', - compliant: 'NonCompliant', - }, - ], - }, - { - policy: 'policy-role', - compliant: 'NonCompliant', - clusters: [ - { - clusterName: 'local-cluster', - clusterNamespace: 'local-cluster', - compliant: 'Compliant', - }, - { - clusterName: 'managed2', - clusterNamespace: 'managed2', - compliant: 'NonCompliant', - }, - ], - }, - { - policy: 'policy-securitycontextconstraints', - compliant: 'Compliant', - clusters: [ - { - clusterName: 'local-cluster', - clusterNamespace: 'local-cluster', - compliant: 'Compliant', - }, - ], - }, - { - policy: 'policy-testing-1', - compliant: 'Compliant', - clusters: [ - { - clusterName: 'local-cluster', - clusterNamespace: 'local-cluster', - compliant: 'Compliant', - }, - { - clusterName: 'managed1', - clusterNamespace: 'managed1', - compliant: 'NonCompliant', - }, - { - clusterName: 'managed2', - clusterNamespace: 'managed2', - compliant: 'NonCompliant', - }, - ], - }, - { - policy: 'policy-role-1', - compliant: 'Compliant', - clusters: [ - { - clusterName: 'local-cluster', - clusterNamespace: 'local-cluster', - compliant: 'Compliant', - }, - { - clusterName: 'managed2', - clusterNamespace: 'managed2', - compliant: 'NonCompliant', - }, - ], - }, - { - policy: 'policy-securitycontextconstraints-1', - compliant: 'Compliant', - clusters: [ - { - clusterName: 'local-cluster', - clusterNamespace: 'local-cluster', - compliant: 'Compliant', - }, - ], - }, - ], }, } const policySet1: PolicySet = { @@ -146,6 +49,7 @@ const policySet1: PolicySet = { policies: ['policy-1'], }, status: { + compliant: 'Compliant', placement: [ { placement: 'placement1', @@ -153,29 +57,6 @@ const policySet1: PolicySet = { placementDecisions: ['placementdecision1'], }, ], - results: [ - { - policy: 'policy-1', - compliant: 'Compliant', - clusters: [ - { - clusterName: 'local-cluster', - clusterNamespace: 'local-cluster', - compliant: 'Compliant', - }, - { - clusterName: 'managed1', - clusterNamespace: 'managed1', - compliant: 'Compliant', - }, - { - clusterName: 'managed2', - clusterNamespace: 'managed2', - compliant: 'NonCompliant', - }, - ], - }, - ], }, } export const mockEmptyPolicySets: PolicySet[] = [] diff --git a/frontend/src/routes/Governance/policy-sets/PolicySets.tsx b/frontend/src/routes/Governance/policy-sets/PolicySets.tsx index 0c6ed6aa995..a7a9c6b0518 100644 --- a/frontend/src/routes/Governance/policy-sets/PolicySets.tsx +++ b/frontend/src/routes/Governance/policy-sets/PolicySets.tsx @@ -22,33 +22,23 @@ import CardViewToolbarFilter from './components/CardViewToolbarFilter' import CardViewToolbarSearch from './components/CardViewToolbarSearch' import PolicySetCard from './components/PolicySetCard' -function clusterViolationFilterFn(policySet: PolicySet) { - if (!policySet.status) return false - return ( - policySet.status.results.filter( - (result) => result.clusters && result.clusters?.some((cluster) => cluster.compliant === 'NonCompliant') - ).length > 0 - ) -} -function clusterNonViolationFilterFn(policySet: PolicySet) { - if (!policySet.status) return false - return policySet.status.results.every((result) => { - return (result.clusters && result.clusters.every((cluster) => cluster.compliant !== 'NonCompliant')) ?? false - }) -} -function policyViolationFilterFn(policySet: PolicySet) { - if (!policySet.status) return false - return policySet.status.results.filter((result) => result.compliant === 'NonCompliant').length > 0 +function violationFilterFn(policySet: PolicySet) { + if (!policySet.status) { + return false + } + return policySet.status.compliant === 'NonCompliant' } -function policyNonViolationFilterFn(policySet: PolicySet) { - if (!policySet.status) return false - return policySet.status.results.every((result) => { - return (result && result.compliant && result.compliant !== 'NonCompliant') ?? false - }) +function nonViolationFilterFn(policySet: PolicySet) { + if (!policySet.status) { + return false + } + return policySet.status.compliant === 'Compliant' } -function policyUnknownFilterFn(policySet: PolicySet) { - if (!policySet.status) return false - return policySet.status.results.filter((result) => !result.compliant).length > 0 +function unknownStatusFilterFn(policySet: PolicySet) { + if (!policySet.status) { + return true + } + return !policySet.status.compliant } function getPresetURIFilters() { @@ -82,6 +72,7 @@ export default function PolicySetsPage() { const [page, setPage] = useState(1) const [perPage, setPerPage] = useState(10) const [filteredPolicySets, setFilteredPolicySets] = useState(policySets) + const [selectedCardID, setSelectedCardID] = useState('') const updatePerPage = useCallback( (newPerPage: number) => { @@ -100,49 +91,34 @@ export default function PolicySetsPage() { if (violationFilters.length === 0) { return true } - let clusterFilterMatch = - violationFilters.includes('cluster-violation') || violationFilters.includes('cluster-no-violation') - ? false - : true - let policyFilterMatch = - violationFilters.includes('policy-violation') || - violationFilters.includes('policy-no-violation') || - violationFilters.includes('policy-unknown') + let filterMatch = + violationFilters.includes('violation') || + violationFilters.includes('no-violation') || + violationFilters.includes('no-status') ? false : true for (const filter of violationFilters) { switch (filter) { - case 'cluster-violation': - if (clusterViolationFilterFn(policySet)) { - clusterFilterMatch = true - } - break - case 'cluster-no-violation': - if (clusterNonViolationFilterFn(policySet)) { - clusterFilterMatch = true - } - break - case 'policy-violation': - if (policyViolationFilterFn(policySet)) { - policyFilterMatch = true + case 'violation': + if (violationFilterFn(policySet)) { + filterMatch = true } break - case 'policy-no-violation': - if (policyNonViolationFilterFn(policySet)) { - policyFilterMatch = true + case 'no-violation': + if (nonViolationFilterFn(policySet)) { + filterMatch = true } break - case 'policy-unknown': - if (policyUnknownFilterFn(policySet)) { - policyFilterMatch = true + case 'no-status': + if (unknownStatusFilterFn(policySet)) { + filterMatch = true } break } } - // AND different group filter selections - return clusterFilterMatch && policyFilterMatch + return filterMatch }) // multi values are OR, multi attributes are AND @@ -250,11 +226,17 @@ export default function PolicySetsPage() { ) : ( - {/* Need to compute all cards here then slice. The PolicySet card render uses usePolicySetSummary which includes a react hook. + {/* Need to compute all cards here then slice. The PolicySet card render uses react hooks. So paging to a page with less cards than the previous causes a react hook error if rendered in time. */} {filteredPolicySets .map((policyset: PolicySet) => { - return + return ( + + ) }) .slice((actualPage - 1) * perPage, (actualPage - 1) * perPage + perPage)} diff --git a/frontend/src/routes/Governance/policy-sets/components/CardViewToolbarFilter.tsx b/frontend/src/routes/Governance/policy-sets/components/CardViewToolbarFilter.tsx index 5df727af235..91cf4945fb0 100644 --- a/frontend/src/routes/Governance/policy-sets/components/CardViewToolbarFilter.tsx +++ b/frontend/src/routes/Governance/policy-sets/components/CardViewToolbarFilter.tsx @@ -1,15 +1,7 @@ /* Copyright Contributors to the Open Cluster Management project */ import { makeStyles } from '@material-ui/styles' -import { - Badge, - Divider, - Select, - SelectGroup, - SelectOption, - SelectOptionObject, - SelectVariant, -} from '@patternfly/react-core' +import { Badge, Select, SelectGroup, SelectOption, SelectOptionObject, SelectVariant } from '@patternfly/react-core' import { FilterIcon } from '@patternfly/react-icons' import { useState } from 'react' import { useRecoilState } from 'recoil' @@ -30,6 +22,8 @@ const useStyles = makeStyles({ }, }) +const noViolation = 'no-violation' + export default function CardViewToolbarFilter(props: { setViolationFilters: React.Dispatch> }) { @@ -53,102 +47,40 @@ export default function CardViewToolbarFilter(props: { } const selectOptions = [ - - -1} - > -
- {'With violation'} - - { - policySets.filter((policySet: PolicySet) => { - if (policySet && policySet.status && policySet.status.results) { - return ( - policySet.status.results.filter((result) => - result.clusters?.some((cluster) => cluster.compliant === 'NonCompliant') - ).length > 0 - ) - } - return [] - }).length - } - -
-
- -1} - > -
- {'Without violation'} - - { - policySets.filter((policySet: PolicySet) => { - if (policySet && policySet.status && policySet.status.results) { - return policySet.status.results.every((result) => { - return ( - (result.clusters && - result.clusters.every( - (cluster) => cluster.compliant !== 'NonCompliant' - )) ?? - false - ) - }) - } - return [] - }).length - } - -
-
-
, - , - + -1} + key={'violation'} + inputId={'violation'} + value={'violation'} + isChecked={selectedFilters.indexOf('violation') > -1} >
{'With violation'} { policySets.filter((policySet: PolicySet) => { - if (policySet && policySet.status && policySet.status.results) { - return ( - policySet.status.results.filter((result) => result.compliant === 'NonCompliant') - .length > 0 - ) + if (policySet.status && policySet.status.compliant) { + return policySet.status.compliant === 'NonCompliant' } - return [] + return false }).length }
-1} + key={noViolation} + inputId={noViolation} + value={noViolation} + isChecked={selectedFilters.indexOf(noViolation) > -1} >
{'Without violation'} { policySets.filter((policySet: PolicySet) => { - if (policySet && policySet.status && policySet.status.results) { - return policySet.status.results.every((result) => { - return ( - (result && result.compliant && result.compliant !== 'NonCompliant') ?? false - ) - }) + if (policySet.status && policySet.status.compliant) { + return policySet.status.compliant === 'Compliant' } return false }).length @@ -157,20 +89,20 @@ export default function CardViewToolbarFilter(props: {
-1} + key={'no-status'} + inputId={'no-status'} + value={'no-status'} + isChecked={selectedFilters.indexOf('no-status') > -1} >
- {'Unknown'} + {'No status'} { policySets.filter((policySet: PolicySet) => { - if (policySet && policySet.status && policySet.status.results) { - return policySet.status.results.filter((result) => !result.compliant).length > 0 + if (!policySet.status) { + return true } - return false + return policySet.status && policySet.status.compliant === undefined }).length } diff --git a/frontend/src/routes/Governance/policy-sets/components/PolicySetCard.test.tsx b/frontend/src/routes/Governance/policy-sets/components/PolicySetCard.test.tsx index 369ce9dfcb7..65acfac36be 100644 --- a/frontend/src/routes/Governance/policy-sets/components/PolicySetCard.test.tsx +++ b/frontend/src/routes/Governance/policy-sets/components/PolicySetCard.test.tsx @@ -2,7 +2,7 @@ import { render } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { RecoilRoot } from 'recoil' -import { waitForSelector, waitForText } from '../../../../lib/test-util' +import { waitForText } from '../../../../lib/test-util' import { Placement, PlacementBinding, PlacementRule, PolicySet } from '../../../../resources' import PolicySetCard from './PolicySetCard' @@ -26,18 +26,6 @@ const policySet: PolicySet = { status: { compliant: 'Compliant', placement: [{ placement: 'policy-set-with-1-placement', placementBinding: 'policy-set-with-1-placement' }], - results: [ - { - clusters: [{ clusterName: 'local-cluster', clusterNamespace: 'local-cluster', compliant: 'Compliant' }], - compliant: 'Compliant', - policy: 'policy-set-with-1-placement-policy-1', - }, - { - clusters: [{ clusterName: 'local-cluster', clusterNamespace: 'local-cluster', compliant: 'Compliant' }], - compliant: 'Compliant', - policy: 'policy-set-with-1-placement-policy-2', - }, - ], }, } @@ -48,10 +36,10 @@ export const mockPlacementBindings: PlacementBinding[] = [] describe('Policy Set Card', () => { test('Should render Policy Set Card content correctly', async () => { - const { container } = render( + render( - + {}} /> ) @@ -60,15 +48,7 @@ describe('Policy Set Card', () => { await waitForText('policy-set-with-1-placement') // wait card desc - PolicySet desc await waitForText('Policy set with a single Placement and PlacementBinding.') - // wait card cluster count - PolicySet desc - await waitForSelector( - container, - '#policyset-test-policy-set-with-1-placement-rule > div.pf-c-card__body > div > div:nth-child(2) > span' - ) - // wait card policy count - PolicySet desc - await waitForSelector( - container, - '#policyset-test-policy-set-with-1-placement-rule > div.pf-c-card__body > div > div:nth-child(3) > span' - ) + // wait card compliance status + await waitForText('No violations') }) }) diff --git a/frontend/src/routes/Governance/policy-sets/components/PolicySetCard.tsx b/frontend/src/routes/Governance/policy-sets/components/PolicySetCard.tsx index 5e8de4cb079..24ed4d36c20 100644 --- a/frontend/src/routes/Governance/policy-sets/components/PolicySetCard.tsx +++ b/frontend/src/routes/Governance/policy-sets/components/PolicySetCard.tsx @@ -9,6 +9,9 @@ import { CardHeader, CardTitle, Checkbox, + DescriptionListDescription, + DescriptionListGroup, + DescriptionListTerm, Dropdown, DropdownItem, DropdownSeparator, @@ -18,8 +21,9 @@ import { Stack, StackItem, } from '@patternfly/react-core' +import { CheckCircleIcon, ExclamationCircleIcon } from '@patternfly/react-icons' import { AcmDrawerContext } from '@stolostron/ui-components' -import { ReactNode, useCallback, useContext, useMemo, useState } from 'react' +import { ReactNode, useCallback, useContext, useState } from 'react' import { useHistory } from 'react-router-dom' import { useRecoilState } from 'recoil' import { placementBindingsState, placementRulesState, placementsState } from '../../../../atoms' @@ -27,61 +31,33 @@ import { useTranslation } from '../../../../lib/acm-i18next' import { deletePolicySet } from '../../../../lib/delete-policyset' import { NavigationPath } from '../../../../NavigationPath' import { PolicySet } from '../../../../resources' -import { ClusterPolicyViolationIcons } from '../../components/ClusterPolicyViolations' -import { PolicyViolationIcons } from '../../components/PolicyViolations' -import { IPolicyRisks } from '../../useGovernanceData' import { PolicySetDetailSidebar } from '../components/PolicySetDetailSidebar' -import { IPolicySetSummary, usePolicySetSummary } from '../usePolicySetSummary' -function getClusterRisks(policySetSummary: IPolicySetSummary) { - const clusterViolationCount = policySetSummary.clusterViolations - const clusterNonViolationCount = policySetSummary.clusterCount - clusterViolationCount - const clusterRisks: IPolicyRisks = { - synced: clusterNonViolationCount, - high: clusterViolationCount, - medium: 0, - low: 0, - unknown: 0, - } - return clusterRisks -} - -function getPolicyRisks(policySetSummary: IPolicySetSummary) { - const policyViolationCount = policySetSummary.policyViolations - const policyUnknownCount = policySetSummary.policyUnknownStatusCount - const policyNonViolationCount = policySetSummary.policyCount - policyViolationCount - policyUnknownCount - const policyRisks: IPolicyRisks = { - synced: policyNonViolationCount, - high: policyViolationCount, - medium: 0, - low: 0, - unknown: policyUnknownCount, - } - return policyRisks -} - -export default function PolicySetCard(props: { policySet: PolicySet }) { - const { policySet } = props +export default function PolicySetCard(props: { + policySet: PolicySet + selectedCardID: string + setSelectedCardID: React.Dispatch> +}) { + const { policySet, selectedCardID, setSelectedCardID } = props const { t } = useTranslation() const { setDrawerContext } = useContext(AcmDrawerContext) const [isKebabOpen, setIsKebabOpen] = useState(false) const [modal, setModal] = useState() const history = useHistory() - const policySetSummary = usePolicySetSummary(policySet) - - const { clusterRisks, policyRisks } = useMemo(() => { - const clusterRisks = getClusterRisks(policySetSummary) - const policyRisks = getPolicyRisks(policySetSummary) - return { policySetSummary, clusterRisks, policyRisks } - }, [policySetSummary]) + const cardID = `policyset-${policySet.metadata.namespace}-${policySet.metadata.name}` function onClick(event: React.MouseEvent) { + const newSelectedCard = event.currentTarget.id === selectedCardID ? '' : event.currentTarget.id + setSelectedCardID(newSelectedCard) if (!event.currentTarget.contains(event.target as Node)) { return } setDrawerContext({ isExpanded: true, - onCloseClick: () => setDrawerContext(undefined), + onCloseClick: () => { + setDrawerContext(undefined) + setSelectedCardID('') + }, panelContent: , panelContentProps: { defaultSize: '40%' }, isInline: true, @@ -109,8 +85,10 @@ export default function PolicySetCard(props: { policySet: PolicySet }) { isRounded isHoverable isFullHeight - id={`policyset-${policySet.metadata.namespace}-${policySet.metadata.name}`} - key={`policyset-${policySet.metadata.namespace}-${policySet.metadata.name}`} + isSelectable + isSelected={selectedCardID === cardID} + id={cardID} + key={cardID} style={{ transition: 'box-shadow 0.25s', cursor: 'pointer' }} onClick={onClick} > @@ -180,25 +158,25 @@ export default function PolicySetCard(props: { policySet: PolicySet }) { {policySet.spec.description &&
{policySet.spec.description ?? ''}
} - {policySetSummary.clusterCount > 0 && ( -
- - {policySetSummary.clusterCount} clusters - -
- -
-
- )} - {policySetSummary.policyCount > 0 && ( -
- - {policySetSummary.policyCount} policies - -
- -
-
+ {policySet.status?.compliant && ( + + + {t('Status')} + + + {policySet.status?.compliant === 'Compliant' ? ( +
+ {' '} + {t('No violations')} +
+ ) : ( +
+ {' '} + {t('Violations')} +
+ )} +
+
)}
diff --git a/frontend/src/routes/Governance/policy-sets/components/PolicySetDetailSidebar.test.tsx b/frontend/src/routes/Governance/policy-sets/components/PolicySetDetailSidebar.test.tsx index 68053301e8c..74c79d6e612 100644 --- a/frontend/src/routes/Governance/policy-sets/components/PolicySetDetailSidebar.test.tsx +++ b/frontend/src/routes/Governance/policy-sets/components/PolicySetDetailSidebar.test.tsx @@ -3,11 +3,16 @@ import { render } from '@testing-library/react' import { MemoryRouter } from 'react-router-dom' import { RecoilRoot } from 'recoil' -import { managedClustersState, policiesState } from '../../../../atoms' +import { + managedClustersState, + placementBindingsState, + placementDecisionsState, + placementRulesState, + policiesState, +} from '../../../../atoms' import { clickByText, waitForText } from '../../../../lib/test-util' +import { PlacementBinding, PlacementDecision, PlacementRule, Policy, PolicySet } from '../../../../resources' import { ManagedCluster } from '../../../../resources/managed-cluster' -import { Policy } from '../../../../resources/policy' -import { PolicySet } from '../../../../resources/policy-set' import { PolicySetDetailSidebar } from './PolicySetDetailSidebar' const mockLocalCluster: ManagedCluster = { @@ -24,138 +29,224 @@ const mockLocalCluster: ManagedCluster = { }, } -const mockManaged: ManagedCluster = { - apiVersion: 'cluster.open-cluster-management.io/v1', - kind: 'ManagedCluster', - metadata: { - labels: { - cloud: 'Amazon', - name: 'managed', - openshiftVersion: '4.9.7', - vendor: 'OpenShift', - }, - name: 'managed', - }, -} -export const mockManagedClusters: ManagedCluster[] = [mockLocalCluster, mockManaged] +const mockManagedClusters: ManagedCluster[] = [mockLocalCluster] -const mockPolicyRole: Policy = { +const mockPolicy: Policy = { apiVersion: 'policy.open-cluster-management.io/v1', kind: 'Policy', metadata: { - name: 'policy-role', - namespace: 'policies', - ownerReferences: [ - { - apiVersion: 'apps.open-cluster-management.io/v1', - kind: 'Subscription', - name: 'demo-stable-policies-sub', - uid: 'cb14759d-b895-4ced-a481-e76fab556b3b', - }, - ], + annotations: { + 'policy.open-cluster-management.io/categories': 'PR.IP Information Protection Processes and Procedures', + 'policy.open-cluster-management.io/controls': 'PR.IP-1 Baseline Configuration', + 'policy.open-cluster-management.io/standards': 'NIST-CSF', + }, + name: 'policy-set-with-1-placement-rule-policy-1', + namespace: 'test', + resourceVersion: '830175', + uid: 'cd18cf67-fe2d-4141-8649-4a2f002898d9', }, spec: { disabled: false, + 'policy-templates': [ + { + objectDefinition: { + apiVersion: 'policy.open-cluster-management.io/v1', + kind: 'ConfigurationPolicy', + metadata: { + name: 'policy-set-with-1-placement-rule-policy-1', + }, + spec: { + namespaceSelector: { + exclude: ['kube-*'], + include: ['default'], + }, + remediationAction: 'inform', + severity: 'low', + }, + }, + }, + ], remediationAction: 'inform', }, - status: { placement: [{ placementBinding: 'binding-policy-role', placementRule: 'placement-policy-role' }] }, + status: { + compliant: 'Compliant', + placement: [ + { + placementBinding: 'policy-set-with-1-placement-rule', + placementRule: 'policy-set-with-1-placement-rule', + policySet: 'policy-set-with-1-placement-rule', + }, + ], + status: [ + { + clustername: 'local-cluster', + clusternamespace: 'local-cluster', + compliant: 'Compliant', + }, + ], + }, } -const mockPolicyTesting: Policy = { +const mockPolicy0: Policy = { apiVersion: 'policy.open-cluster-management.io/v1', kind: 'Policy', metadata: { - name: 'policy-testing', - namespace: 'policies', - ownerReferences: [ - { - apiVersion: 'apps.open-cluster-management.io/v1', - kind: 'Subscription', - name: 'demo-stable-policies-sub', - uid: 'cb14759d-b895-4ced-a481-e76fab556b3b', - }, - ], + annotations: { + 'policy.open-cluster-management.io/categories': 'PR.IP Information Protection Processes and Procedures', + 'policy.open-cluster-management.io/controls': 'PR.IP-1 Baseline Configuration', + 'policy.open-cluster-management.io/standards': 'NIST-CSF', + }, + labels: { + 'policy.open-cluster-management.io/cluster-name': 'local-cluster', + 'policy.open-cluster-management.io/cluster-namespace': 'local-cluster', + 'policy.open-cluster-management.io/root-policy': 'test.policy-set-with-1-placement-rule-policy-1', + }, + name: 'test.policy-set-with-1-placement-rule-policy-1', + namespace: 'local-cluster', }, spec: { disabled: false, + 'policy-templates': [ + { + objectDefinition: { + apiVersion: 'policy.open-cluster-management.io/v1', + kind: 'ConfigurationPolicy', + metadata: { + name: 'policy-set-with-1-placement-rule-policy-1', + }, + spec: { + namespaceSelector: { + exclude: ['kube-*'], + include: ['default'], + }, + remediationAction: 'inform', + severity: 'low', + }, + }, + }, + ], remediationAction: 'inform', }, status: { - placement: [ + compliant: 'Compliant', + details: [ { - placementBinding: 'binding-policy-limitclusteradmin', - placementRule: 'placement-policy-limitclusteradmin', + compliant: 'Compliant', + history: [ + { + eventName: 'test.policy-set-with-1-placement-rule-policy-1.16db07645ba22e3d', + lastTimestamp: '2022-03-10T13:16:56Z', + message: + 'Compliant; notification - namespaces [test] found as specified, therefore this Object template is compliant', + }, + ], + templateMeta: { + creationTimestamp: null, + name: 'policy-set-with-1-placement-rule-policy-1', + }, }, ], }, } -const mockPolicySecurityContextConstraints: Policy = { - apiVersion: 'policy.open-cluster-management.io/v1', - kind: 'Policy', +const mockPolicies: Policy[] = [mockPolicy, mockPolicy0] + +const mockPlacementRule: PlacementRule = { + apiVersion: 'apps.open-cluster-management.io/v1', + kind: 'PlacementRule', metadata: { - name: 'policy-securitycontextconstraints', - namespace: 'policies', - ownerReferences: [ + name: 'policy-set-with-1-placement-rule', + namespace: 'test', + uid: 'ff9f446b-c3e2-49ff-9305-b8385344070b', + }, + spec: { + clusterConditions: [ { - apiVersion: 'apps.open-cluster-management.io/v1', - kind: 'Subscription', - name: 'demo-stable-policies-sub', - uid: 'cb14759d-b895-4ced-a481-e76fab556b3b', + status: 'True', + type: 'ManagedClusterConditionAvailable', }, ], - }, - spec: { - disabled: false, - remediationAction: 'enforce', + clusterSelector: { + matchExpressions: [ + { + key: 'local-cluster', + operator: 'In', + values: ['true'], + }, + ], + }, }, status: { - placement: [ + decisions: [ { - placementBinding: 'binding-policy-limitclusteradmin', - placementRule: 'placement-policy-limitclusteradmin', + clusterName: 'local-cluster', + clusterNamespace: 'local-cluster', }, ], }, } -export const mockPolicies: Policy[] = [mockPolicyRole, mockPolicyTesting, mockPolicySecurityContextConstraints] +const mockPlacementRules: PlacementRule[] = [mockPlacementRule] + +const mockPlacementBinding: PlacementBinding = { + apiVersion: 'policy.open-cluster-management.io/v1', + kind: 'PlacementBinding', + metadata: { + name: 'policy-set-with-1-placement-rule', + namespace: 'test', + resourceVersion: '152043', + uid: '2c3359ea-b9b6-451e-981d-8107a1060281', + }, + placementRef: { + apiGroup: 'apps.open-cluster-management.io', + kind: 'PlacementRule', + name: 'policy-set-with-1-placement-rule', + }, + subjects: [ + { apiGroup: 'policy.open-cluster-management.io', kind: 'PolicySet', name: 'policy-set-with-1-placement-rule' }, + ], +} +const mockPlacementBindings: PlacementBinding[] = [mockPlacementBinding] + +const mockPlacementDecision: PlacementDecision = { + apiVersion: 'cluster.open-cluster-management.io/v1beta1', + kind: 'PlacementDecision', + metadata: { + resourceVersion: '152066', + name: 'policy-set-with-1-placement-rule-decision-1', + namespace: 'test', + ownerReferences: [ + { + apiVersion: 'apps.open-cluster-management.io/v1', + blockOwnerDeletion: true, + controller: true, + kind: 'PlacementRule', + name: 'policy-set-with-1-placement-rule', + uid: 'ff9f446b-c3e2-49ff-9305-b8385344070b', + }, + ], + labels: { 'cluster.open-cluster-management.io/placementrule': 'policy-set-with-1-placement-rule' }, + }, + status: { decisions: [{ clusterName: 'local-cluster', reason: '' }] }, +} +const mockPlacementDecisions: PlacementDecision[] = [mockPlacementDecision] describe('PolicySets Page', () => { test('Should render PolicySet page correctly', async () => { const policySet: PolicySet = { apiVersion: 'policy.open-cluster-management.io/v1beta1', kind: 'PolicySet', - metadata: { name: 'test-policyset', namespace: 'test-ns' }, + metadata: { + name: 'policy-set-with-1-placement-rule', + namespace: 'test', + }, spec: { - description: 'Policies for PCI-2 compliance', - policies: ['policy-testing', 'policy-role', 'policy-securitycontextconstraints'], + description: 'Policy set with a single PlacementRule and PlacementBinding.', + policies: ['policy-set-with-1-placement-rule-policy-1'], }, status: { + compliant: 'Compliant', placement: [ { - placement: 'placement', - placementBinding: 'placementBinding', - placementDecisions: ['placementDecision'], - }, - ], - results: [ - { - policy: 'policy-testing', - compliant: 'Compliant', - clusters: [ - { clusterName: 'local-cluster', clusterNamespace: 'local-cluster', compliant: 'Compliant' }, - { clusterName: 'managed', clusterNamespace: 'managed', compliant: 'Compliant' }, - ], - }, - { - policy: 'policy-role', - compliant: 'NonCompliant', - clusters: [{ clusterName: 'managed', clusterNamespace: 'managed', compliant: 'NonCompliant' }], - }, - { - policy: 'policy-securitycontextconstraints', - compliant: 'Compliant', - clusters: [ - { clusterName: 'local-cluster', clusterNamespace: 'local-cluster', compliant: 'Compliant' }, - ], + placementBinding: 'policy-set-with-1-placement-rule', + placementRule: 'policy-set-with-1-placement-rule', }, ], }, @@ -166,6 +257,9 @@ describe('PolicySets Page', () => { initializeState={(snapshot) => { snapshot.set(policiesState, mockPolicies) snapshot.set(managedClustersState, mockManagedClusters) + snapshot.set(placementRulesState, mockPlacementRules) + snapshot.set(placementBindingsState, mockPlacementBindings) + snapshot.set(placementDecisionsState, mockPlacementDecisions) }} > @@ -178,20 +272,17 @@ describe('PolicySets Page', () => { await waitForText(policySet.metadata.name!) // Check clusters with violation count - await waitForText('1 Clusters with policy violations') + await waitForText('0 Clusters with policy violations') // Check policies with violation count - await waitForText('1 Clusters without policy violations') + await waitForText('1 Cluster without policy violations') // Find the cluster names iin table await waitForText(mockLocalCluster.metadata.name!) - await waitForText(mockManaged.metadata.name!) // switch to the policies table await clickByText('Policies') // find the policy names in table - await waitForText(mockPolicyRole.metadata.name!) - await waitForText(mockPolicyTesting.metadata.name!) - await waitForText(mockPolicySecurityContextConstraints.metadata.name!) + await waitForText(mockPolicy.metadata.name!) }) }) diff --git a/frontend/src/routes/Governance/policy-sets/components/PolicySetDetailSidebar.tsx b/frontend/src/routes/Governance/policy-sets/components/PolicySetDetailSidebar.tsx index a09f088b202..c561cf9c8f6 100644 --- a/frontend/src/routes/Governance/policy-sets/components/PolicySetDetailSidebar.tsx +++ b/frontend/src/routes/Governance/policy-sets/components/PolicySetDetailSidebar.tsx @@ -8,10 +8,18 @@ import { AcmLabels, AcmTable, compareNumbers, compareStrings } from '@stolostron import { TFunction } from 'i18next' import { useMemo, useState } from 'react' import { useRecoilState } from 'recoil' -import { managedClustersState, usePolicies } from '../../../../atoms' +import { + managedClustersState, + placementBindingsState, + placementDecisionsState, + placementRulesState, + placementsState, + usePolicies, +} from '../../../../atoms' import { useTranslation } from '../../../../lib/acm-i18next' -import { PolicySet, PolicySetResultCluster, PolicySetStatusResult } from '../../../../resources' +import { Policy, PolicySet } from '../../../../resources' import { usePolicySetClusterPolicyViolationsColumn } from '../../clusters/useClusterPolicyViolationsColumn' +import { getClustersSummaryForPolicySet, getPolicyComplianceForPolicySet, PolicyCompliance } from '../../common/util' import { ClusterPolicyViolationIcons2 } from '../../components/ClusterPolicyViolations' import { useClusterViolationSummaryMap } from '../../overview/ClusterViolationSummary' @@ -68,17 +76,23 @@ const useStyles = makeStyles({ }, }) -function renderDonutChart(clusters: PolicySetResultCluster[], t: TFunction) { - const clusterCompliantCount = clusters.filter((cluster) => cluster.compliant === 'Compliant').length - const clusterNonCompliantCount = clusters.filter((cluster) => cluster.compliant === 'NonCompliant').length +function renderDonutChart(clusterComplianceSummary: { compliant: string[]; nonCompliant: string[] }, t: TFunction) { + const clusterCompliantCount = clusterComplianceSummary.compliant.length + const clusterNonCompliantCount = clusterComplianceSummary.nonCompliant.length const formattedData = [ { - key: t('Clusters without policy violations'), + key: + clusterCompliantCount === 1 + ? t('Cluster without policy violations') + : t('Clusters without policy violations'), value: clusterCompliantCount, isPrimary: true, }, { - key: t('Clusters with policy violations'), + key: + clusterNonCompliantCount === 1 + ? t('Cluster with policy violations') + : t('Clusters with policy violations'), value: clusterNonCompliantCount, }, ] @@ -109,7 +123,9 @@ function renderDonutChart(clusters: PolicySetResultCluster[], t: TFunction) { right: 275, top: 20, }} - title={`${((clusterCompliantCount / clusters.length) * 100).toFixed(0)}%`} + title={`${((clusterCompliantCount / (clusterCompliantCount + clusterNonCompliantCount)) * 100).toFixed( + 0 + )}%`} width={450} height={200} colorScale={['#0066CC', '#C9190B']} @@ -123,44 +139,68 @@ export function PolicySetDetailSidebar(props: { policySet: PolicySet }) { const { t } = useTranslation() const [managedClusters] = useRecoilState(managedClustersState) const policies = usePolicies() + const [placements] = useRecoilState(placementsState) + const [placementRules] = useRecoilState(placementRulesState) + const [placementBindings] = useRecoilState(placementBindingsState) + const [placementDecisions] = useRecoilState(placementDecisionsState) const [type, setType] = useState<'Clusters' | 'Policies'>('Clusters') const selectType = (type: 'Clusters' | 'Policies') => { setType(type) } - const policySetClusters: PolicySetResultCluster[] = useMemo(() => { - const clusters: PolicySetResultCluster[] = [] - policySet.status?.results.forEach((statusResult: PolicySetStatusResult) => { - const currentClusters = statusResult.clusters ?? [] - currentClusters.forEach((cluster: PolicySetResultCluster) => { - const matchIdx = clusters.findIndex( - (c: PolicySetResultCluster) => c.clusterName === cluster.clusterName - ) - if (matchIdx === -1) { - clusters.push(cluster) - } - if (matchIdx > -1 && clusters[matchIdx].compliant === 'Compliant') { - clusters.splice(matchIdx, 1) - clusters.push(cluster) - } - }) - }) - return clusters - }, [policySet]) + const { policySetClusters, policySetClusterCompliance, policySetPolicies } = useMemo(() => { + const placementRuleClusterCompliance = getClustersSummaryForPolicySet( + policySet, + policies, + placementDecisions, + placementBindings, + placementRules + ) + const placementClusterCompliance = getClustersSummaryForPolicySet( + policySet, + policies, + placementDecisions, + placementBindings, + placements + ) + + const placementRulePolicyCompliance = getPolicyComplianceForPolicySet( + policySet, + policies, + placementDecisions, + placementBindings, + placementRules + ) + const placementPolicyCompliance = getPolicyComplianceForPolicySet( + policySet, + policies, + placementDecisions, + placementBindings, + placements + ) + + const psClusterCompliance: { + compliant: string[] + nonCompliant: string[] + } = { + compliant: [...placementRuleClusterCompliance.compliant, ...placementClusterCompliance.compliant], + nonCompliant: [...placementRuleClusterCompliance.nonCompliant, ...placementClusterCompliance.nonCompliant], + } + const psClusters: string[] = [...psClusterCompliance.compliant, ...psClusterCompliance.nonCompliant] + const psPolicies: PolicyCompliance[] = [...placementRulePolicyCompliance, ...placementPolicyCompliance] - const policySetPolicies: PolicySetStatusResult[] = useMemo(() => { - const policies: PolicySetStatusResult[] = [] - policySet.status?.results.map((result: PolicySetStatusResult) => { - policies.push(result) - }) - return policies - }, [policySet]) + return { + policySetClusters: psClusters, + policySetClusterCompliance: psClusterCompliance, + policySetPolicies: psPolicies, + } + }, [policySet, policies, placementDecisions, placementBindings, placementRules, placements]) const clusterViolationSummaryMap = useClusterViolationSummaryMap( policies.filter( - (policy) => + (policy: Policy) => policy.metadata.namespace === policySet.metadata.namespace && - policySetPolicies.find((p) => p.policy === policy.metadata.name) + policySetPolicies.find((p) => p.policyName === policy.metadata.name) ) ) const clusterPolicyViolationsColumn = usePolicySetClusterPolicyViolationsColumn(clusterViolationSummaryMap) @@ -169,23 +209,21 @@ export function PolicySetDetailSidebar(props: { policySet: PolicySet }) { () => [ { header: t('Cluster name'), - search: (cluster: PolicySetResultCluster) => cluster.clusterName, - sort: (a: PolicySetResultCluster, b: PolicySetResultCluster) => + search: (cluster: string) => cluster, + sort: (a: string, b: string) => /* istanbul ignore next */ - compareStrings(a.clusterName, b.clusterName), - cell: (cluster: PolicySetResultCluster) => ( - - {cluster.clusterName} - + compareStrings(a, b), + cell: (cluster: string) => ( + {cluster} ), }, clusterPolicyViolationsColumn, { header: t('Labels'), - cell: (clusterInfo: PolicySetResultCluster) => { - const cluster = managedClusters.find((cluster) => cluster.metadata.name === clusterInfo.clusterName) - if (cluster && cluster.metadata.labels) { - const labelKeys = Object.keys(cluster.metadata.labels) + cell: (cluster: string) => { + const clusterMatch = managedClusters.find((c) => c.metadata.name === cluster) + if (clusterMatch && clusterMatch.metadata.labels) { + const labelKeys = Object.keys(clusterMatch.metadata.labels) const collapse = [ 'clusterID', @@ -207,7 +245,7 @@ export function PolicySetDetailSidebar(props: { policySet: PolicySet }) { }) return ( [ { header: t('Policy name'), - search: (policy: PolicySetStatusResult) => policy.policy, - sort: (a: PolicySetStatusResult, b: PolicySetStatusResult) => + search: (policy: PolicyCompliance) => policy.policyName, + sort: (a: PolicyCompliance, b: PolicyCompliance) => /* istanbul ignore next */ - compareStrings(a.policy, b.policy), - cell: (policy: PolicySetStatusResult) => { - // TODO after policydetails page is added - // {policy.policy} - return policy.policy + compareStrings(a.policyName, b.policyName), + cell: (policy: PolicyCompliance) => { + const currentPolicy = policies.find((p: Policy) => p.metadata.name === policy.policyName) + return ( + + {policy.policyName} + + ) }, }, { header: t('Cluster violation'), - sort: (a: PolicySetStatusResult, b: PolicySetStatusResult) => { + sort: (a: PolicyCompliance, b: PolicyCompliance) => { let violationCountA = 0 let violationCountB = 0 - a?.clusters?.forEach((c: PolicySetResultCluster) => { - if (c.compliant === 'NonCompliant') { - violationCountA++ + a?.clusterCompliance?.forEach( + (c: { clusterName: string; compliance: 'Compliant' | 'NonCompliant' }) => { + if (c.compliance === 'NonCompliant') { + violationCountA++ + } } - }) - b?.clusters?.forEach((c: PolicySetResultCluster) => { - if (c.compliant === 'NonCompliant') { - violationCountB++ + ) + b?.clusterCompliance?.forEach( + (c: { clusterName: string; compliance: 'Compliant' | 'NonCompliant' }) => { + if (c.compliance === 'NonCompliant') { + violationCountB++ + } } - }) + ) return compareNumbers(violationCountA, violationCountB) }, - cell: (policy: PolicySetStatusResult) => { + cell: (policy: PolicyCompliance) => { let violationCount = 0 // Get total count of cluster violations for a specific policy - const hasCompliance = policy?.clusters?.filter((cluster) => cluster.compliant) ?? [] + const hasCompliance = policy?.clusterCompliance?.filter((cluster) => cluster.compliance) ?? [] if (hasCompliance.length > 0) { - hasCompliance.forEach((c: PolicySetResultCluster) => { - if (c.compliant === 'NonCompliant') { - violationCount++ + const currentPolicy = policies.find((p: Policy) => p.metadata.name === policy.policyName) + hasCompliance.forEach( + (c: { clusterName: string; compliance: 'Compliant' | 'NonCompliant' }) => { + if (c.compliance === 'NonCompliant') { + violationCount++ + } } - }) + ) return ( ) } @@ -277,14 +329,14 @@ export function PolicySetDetailSidebar(props: { policySet: PolicySet }) { }, { header: t('Remediation'), - sort: (a: PolicySetStatusResult, b: PolicySetStatusResult) => { - const policyA = policies.find((p) => p.metadata.name === a.policy) - const policyB = policies.find((p) => p.metadata.name === b.policy) + sort: (a: PolicyCompliance, b: PolicyCompliance) => { + const policyA = policies.find((p: Policy) => p.metadata.name === a.policyName) + const policyB = policies.find((p: Policy) => p.metadata.name === b.policyName) /* istanbul ignore next */ return compareStrings(policyA?.spec.remediationAction, policyB?.spec.remediationAction) }, - cell: (policyStatus: PolicySetStatusResult) => { - const policy = policies.find((p) => p.metadata.name === policyStatus.policy) + cell: (policyStatus: PolicyCompliance) => { + const policy = policies.find((p: Policy) => p.metadata.name === policyStatus.policyName) return policy?.spec.remediationAction ?? '-' }, }, @@ -307,7 +359,7 @@ export function PolicySetDetailSidebar(props: { policySet: PolicySet }) {
- {policySetClusters.length > 0 && renderDonutChart(policySetClusters, t)} + {policySetClusters.length > 0 && renderDonutChart(policySetClusterCompliance, t)}
@@ -326,15 +378,15 @@ export function PolicySetDetailSidebar(props: { policySet: PolicySet }) {
{type === 'Clusters' ? ( - + plural="Clusters" items={policySetClusters} - initialSort={{ + sort={{ index: 1, // default to sorting by violation count direction: 'desc', }} columns={clusterColumnDefs} - keyFn={(item: any) => item.policy} + keyFn={(item: string) => item} tableActions={[]} rowActions={[]} gridBreakPoint={TableGridBreakpoint.none} @@ -342,15 +394,15 @@ export function PolicySetDetailSidebar(props: { policySet: PolicySet }) { searchPlaceholder={t('Find by name')} /> ) : ( - - plural="Clusters" + + plural="Policies" items={policySetPolicies} - initialSort={{ - index: 1, // default to sorting by violation count + sort={{ + index: 0, // default to sorting by violation count direction: 'desc', }} columns={policyColumnDefs} - keyFn={(item: any) => item.policy} + keyFn={(item: PolicyCompliance) => item.policyName} tableActions={[]} rowActions={[]} gridBreakPoint={TableGridBreakpoint.none} diff --git a/frontend/src/routes/Governance/policy-sets/usePolicySetSummary.tsx b/frontend/src/routes/Governance/policy-sets/usePolicySetSummary.tsx deleted file mode 100644 index 34d50d41a79..00000000000 --- a/frontend/src/routes/Governance/policy-sets/usePolicySetSummary.tsx +++ /dev/null @@ -1,73 +0,0 @@ -/* Copyright Contributors to the Open Cluster Management project */ -import { useEffect, useState } from 'react' -import { PolicySet } from '../../../resources' - -export interface IPolicySetSummary { - policyCount: number - policyViolations: number - policyUnknownStatusCount: number - clusterCount: number - clusterViolations: number -} - -export function usePolicySetSummary(policySet: PolicySet) { - const [summary, setSummary] = useState({ - policyCount: 0, - policyViolations: 0, - policyUnknownStatusCount: 0, - clusterCount: 0, - clusterViolations: 0, - }) - - useEffect(() => { - const newPolicySetSummary: IPolicySetSummary = { - policyCount: 0, - policyViolations: 0, - policyUnknownStatusCount: 0, - clusterCount: 0, - clusterViolations: 0, - } - - if (policySet && policySet.status && policySet.status.results) { - calculatePolicySetPolicyStats(newPolicySetSummary, policySet) - caculatePolicySetClusterStats(newPolicySetSummary, policySet) - } - setSummary(newPolicySetSummary) - }, [policySet]) - - return summary -} - -function calculatePolicySetPolicyStats(summary: IPolicySetSummary, policySet: PolicySet) { - summary.policyCount = 0 - summary.policyViolations = 0 - summary.policyUnknownStatusCount = 0 - for (const result of policySet.status?.results ?? []) { - summary.policyCount++ - if (!result.compliant) { - summary.policyUnknownStatusCount++ - } else if (result.compliant === 'NonCompliant') { - summary.policyViolations++ - } - } -} - -function caculatePolicySetClusterStats(summary: IPolicySetSummary, policySet: PolicySet) { - const clusterStats: { [clusterName: string]: boolean } = {} - for (const result of policySet.status?.results ?? []) { - if (!result.clusters) continue - for (const clusterResult of result.clusters) { - // If the cluster results dont have compliance status we want to skip the clustere - if (clusterResult.compliant) { - if (clusterResult.compliant === 'Compliant' && clusterStats[clusterResult.clusterName] !== false) { - clusterStats[clusterResult.clusterName] = true - } else { - clusterStats[clusterResult.clusterName] = false - } - } - } - const clusterNames = Object.keys(clusterStats) - summary.clusterCount = clusterNames.length - summary.clusterViolations = clusterNames.filter((clusterName: string) => !clusterStats[clusterName]).length - } -} diff --git a/resources/policy-set-with-1-placement.yaml b/resources/policy-set-with-1-placement.yaml index 993b72ce3fa..651ba9e42bc 100644 --- a/resources/policy-set-with-1-placement.yaml +++ b/resources/policy-set-with-1-placement.yaml @@ -9,7 +9,7 @@ spec: - policy-set-with-1-placement-policy-1 - policy-set-with-1-placement-policy-2 --- -apiVersion: cluster.open-cluster-management.io/v1alpha1 +apiVersion: cluster.open-cluster-management.io/v1beta1 kind: Placement metadata: name: policy-set-with-1-placement diff --git a/resources/policy-set-with-2-placements.yaml b/resources/policy-set-with-2-placements.yaml index b1484ba5030..4a693607120 100644 --- a/resources/policy-set-with-2-placements.yaml +++ b/resources/policy-set-with-2-placements.yaml @@ -9,7 +9,7 @@ spec: - policy-set-with-2-placements-policy-1 - policy-set-with-2-placements-policy-2 --- -apiVersion: cluster.open-cluster-management.io/v1alpha1 +apiVersion: cluster.open-cluster-management.io/v1beta1 kind: Placement metadata: name: policy-set-with-2-placements-1 @@ -38,7 +38,7 @@ subjects: kind: PolicySet apiGroup: policy.open-cluster-management.io --- -apiVersion: cluster.open-cluster-management.io/v1alpha1 +apiVersion: cluster.open-cluster-management.io/v1beta1 kind: Placement metadata: name: policy-set-with-2-placements-2 diff --git a/resources/policy-set-with-placement-aws-gcp.yaml b/resources/policy-set-with-placement-aws-gcp.yaml index 1edd26b4f94..2062a826f81 100644 --- a/resources/policy-set-with-placement-aws-gcp.yaml +++ b/resources/policy-set-with-placement-aws-gcp.yaml @@ -8,7 +8,7 @@ spec: As a customer I want to place policies on my Amazon and Google clusters in my-cluster-set in regions east and west but not on my production clusters. policies: [] --- -apiVersion: cluster.open-cluster-management.io/v1alpha1 +apiVersion: cluster.open-cluster-management.io/v1beta1 kind: Placement metadata: name: policy-set-with-placement-aws-gcp-1 diff --git a/resources/policy-with-1-placement.yaml b/resources/policy-with-1-placement.yaml index e910b4e5b85..047e633c138 100644 --- a/resources/policy-with-1-placement.yaml +++ b/resources/policy-with-1-placement.yaml @@ -46,7 +46,7 @@ subjects: kind: Policy apiGroup: policy.open-cluster-management.io --- -apiVersion: cluster.open-cluster-management.io/v1alpha1 +apiVersion: cluster.open-cluster-management.io/v1beta1 kind: Placement metadata: name: policy-with-1-placement diff --git a/resources/policy-with-2-placements.yaml b/resources/policy-with-2-placements.yaml index 4d03f7c0146..9313029b97f 100644 --- a/resources/policy-with-2-placements.yaml +++ b/resources/policy-with-2-placements.yaml @@ -32,7 +32,7 @@ spec: metadata: name: test --- -apiVersion: cluster.open-cluster-management.io/v1alpha1 +apiVersion: cluster.open-cluster-management.io/v1beta1 kind: Placement metadata: name: policy-with-2-placements-1 @@ -61,7 +61,7 @@ subjects: kind: PolicySet apiGroup: policy.open-cluster-management.io --- -apiVersion: cluster.open-cluster-management.io/v1alpha1 +apiVersion: cluster.open-cluster-management.io/v1beta1 kind: Placement metadata: name: policy-with-2-placements-2