From 7ee607c42e56eefc62ef44f5acb0f26ce3f0cf5d Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 13:23:24 +0200 Subject: [PATCH 1/4] feat(kyverno): migrate validate-replica-floor to ValidatingPolicy Fixes #5120 Co-Authored-By: Claude Opus 5.5 --- .../validate-replica-floor.yaml | 193 ++++++++---------- tests/policy-failure-actions.json | 2 +- .../validate-replica-floor/kyverno-test.yaml | 15 +- tests/validate-replica-floor/resources.yaml | 77 +++++++ 4 files changed, 179 insertions(+), 108 deletions(-) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/validate-replica-floor.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/validate-replica-floor.yaml index ad310c78c7..2acb3e4596 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/validate-replica-floor.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/validate-replica-floor.yaml @@ -14,11 +14,11 @@ # deliberately. # # Exemptions (a replica floor is *wrong*, not missing, for these) are expressed -# as preconditions rather than rule excludes. A false precondition emits a -# current `skip` PolicyReport result; an exclude emits no replacement result and -# can leave an older `fail` result behind after the exemption is added (#2573). -# This keeps Policy Reporter current without deleting or patching shared -# reports. See docs/policy-reports.md for the regression test and rollout check. +# as matchConditions, never as a narrower match scope: a false matchCondition +# still evaluates the resource and reports `skip`, while a resource outside the +# match scope gets no replacement result and can keep an older `fail` after the +# exemption is added (#2573). See docs/policy-reports.md for the regression +# test and rollout check. # # Exemption categories: # * the opt-out label platform.devantler.tech/replica-floor: exempt on the @@ -44,8 +44,8 @@ # The other leader-election controllers that USED to be exempt (cloudnative-pg, # reloader, trust-manager, hcloud CCM/CSI, snapshot-controller, kyverno # background, flagger) opt in to 2+ replicas for HA instead of opting out. -apiVersion: kyverno.io/v1 -kind: ClusterPolicy +apiVersion: policies.kyverno.io/v1 +kind: ValidatingPolicy metadata: name: validate-replica-floor annotations: @@ -53,106 +53,91 @@ metadata: policies.kyverno.io/category: Best Practices, High Availability policies.kyverno.io/severity: medium policies.kyverno.io/subject: Deployment, StatefulSet - policies.kyverno.io/minversion: 1.13.0 + policies.kyverno.io/minversion: 1.17.0 policies.kyverno.io/description: >- Flags Deployments and StatefulSets running fewer than 2 replicas so their drain-safe PDBs actually preserve availability. Workloads that are intentionally singleton, scale-to-zero or controller-managed opt out with the platform.devantler.tech/replica-floor: exempt label. spec: - background: true - rules: - - name: require-replica-floor - match: - any: - - resources: - kinds: - - Deployment - - StatefulSet - preconditions: - all: - # Durable opt-out on the workload itself. Keep exemptions here, not in - # rule.exclude, so a background scan records a fresh `skip` result. - - key: >- - {{ request.object.metadata.labels."platform.devantler.tech/replica-floor" - || '' }} - operator: NotEquals - value: exempt - # Durable opt-out on the pod template (charts expose podLabels more - # widely than top-level workload labels). - - key: >- - {{ request.object.spec.template.metadata.labels."platform.devantler.tech/replica-floor" - || '' }} - operator: NotEquals - value: exempt - # Whole namespaces whose controllers are intentionally singleton or - # controller-managed: Velero has no leader election; Flux, Coroot and - # Kubescape controllers sit off serving paths; Crossplane reconcilers - # are external-state single writers. Crossplane is exempt by namespace - # because provider Deployment names include a changing revision hash. - - key: "{{ request.object.metadata.namespace || '' }}" - operator: AnyNotIn - value: - - velero - - flux-system - - observability - - kubescape - - crossplane-system - # Flagger owns replica counts for generated *-primary Deployments. - - key: "{{ request.object.metadata.name || '' }}" - operator: NotEquals - value: "?*-primary" - # Stable-name exemptions documented above. A false precondition emits - # `skip`, which supersedes any older failure for this policy and rule. - - key: "{{ request.object.metadata.name || '' }}" - operator: AnyNotIn - value: - # Single-replica apps/UIs and paused Flagger canary sources. - - actual-budget-actualbudget - - fleet - - headlamp - - whoami - - hubble-ui - - longhorn-ui - - homepage - - umami-umami - # Single-writer or ephemeral controllers with no useful second - # replica: DNS, disabled issuer and the canary load generator. - - external-dns - - origin-ca-issuer - - flagger-loadtester - # Lean leader-election controllers whose outage delays only an - # idempotent control loop. Production may still opt them into HA. - - vertical-pod-autoscaler-vpa-recommender - - vertical-pod-autoscaler-vpa-updater - - kyverno-reports-controller - - kyverno-cleanup-controller - # Controller-owned or single-by-design components triaged in the - # June 2026 HA sweep. SPIRE needs an external datastore for real - # HA. - - cluster-autoscaler-hetzner-cluster-autoscaler - - keda-operator-metrics-apiserver - - longhorn-driver-deployer - - spire-server - # CNPG-I exposes its gRPC readiness endpoint only on the elected - # leader, so a second replica can never become Ready. - - plugin-barman-cloud - # Crossview and its ephemeral bundled Postgres intentionally form - # one non-HA unit until the datastore is replaced. - - crossview - - crossview-postgres - validate: - failureAction: Audit - message: >- - {{ request.object.kind }} '{{ request.object.metadata.name }}' runs - {{ request.object.spec.replicas || `1` }} replica(s); the platform HA - floor is 2 so its maxUnavailable: 1 PDB keeps it available through a - node drain (see #1882). If it is intentionally singleton, scale-to-zero - or controller-managed, add the label - 'platform.devantler.tech/replica-floor: exempt'. - deny: - conditions: - all: - - key: "{{ request.object.spec.replicas || `1` }}" - operator: LessThan - value: 2 + validationActions: + - Audit + evaluation: + background: + enabled: true + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["deployments", "statefulsets"] + matchConditions: + # Durable opt-out on the workload itself. + - name: workload-not-exempt + expression: >- + object.metadata.?labels[?'platform.devantler.tech/replica-floor'].orValue('') != 'exempt' + # Durable opt-out on the pod template (charts expose podLabels more + # widely than top-level workload labels). + - name: pod-template-not-exempt + expression: >- + object.spec.?template.?metadata.?labels[?'platform.devantler.tech/replica-floor'].orValue('') != 'exempt' + # Whole namespaces whose controllers are intentionally singleton or + # controller-managed: Velero has no leader election; Flux, Coroot and + # Kubescape controllers sit off serving paths; Crossplane reconcilers + # are external-state single writers. Crossplane is exempt by namespace + # because provider Deployment names include a changing revision hash. + - name: not-exempt-namespace + expression: >- + !(object.metadata.?namespace.orValue('') in [ + 'velero', 'flux-system', 'observability', 'kubescape', + 'crossplane-system']) + # Flagger owns replica counts for generated *-primary Deployments. + - name: not-flagger-primary + expression: >- + !object.metadata.name.matches('^.+-primary$') + # Single-replica apps/UIs and paused Flagger canary sources. + - name: not-singleton-app + expression: >- + !(object.metadata.name in [ + 'actual-budget-actualbudget', 'fleet', 'headlamp', 'whoami', + 'hubble-ui', 'longhorn-ui', 'homepage', 'umami-umami']) + # Single-writer or ephemeral controllers with no useful second replica: + # DNS, disabled issuer and the canary load generator. + - name: not-single-writer + expression: >- + !(object.metadata.name in [ + 'external-dns', 'origin-ca-issuer', 'flagger-loadtester']) + # Lean leader-election controllers whose outage delays only an idempotent + # control loop. Production may still opt them into HA. + - name: not-lean-controller + expression: >- + !(object.metadata.name in [ + 'vertical-pod-autoscaler-vpa-recommender', + 'vertical-pod-autoscaler-vpa-updater', + 'kyverno-reports-controller', 'kyverno-cleanup-controller']) + # Controller-owned or single-by-design components triaged in the June 2026 + # HA sweep. SPIRE needs an external datastore for real HA. CNPG-I exposes + # its gRPC readiness endpoint only on the elected leader, so a second + # replica can never become Ready. Crossview and its ephemeral bundled + # Postgres intentionally form one non-HA unit until the datastore is + # replaced. + - name: not-single-by-design + expression: >- + !(object.metadata.name in [ + 'cluster-autoscaler-hetzner-cluster-autoscaler', + 'keda-operator-metrics-apiserver', 'longhorn-driver-deployer', + 'spire-server', 'plugin-barman-cloud', 'crossview', + 'crossview-postgres']) + variables: + # An unset replica count defaults to 1; an explicit 0 stays 0. + - name: replicas + expression: object.spec.?replicas.orValue(1) + validations: + - expression: "variables.replicas >= 2" + messageExpression: >- + object.kind + " '" + object.metadata.name + "' runs " + + string(variables.replicas) + " replica(s); the platform HA floor is 2 + so its maxUnavailable: 1 PDB keeps it available through a node drain + (see #1882). If it is intentionally singleton, scale-to-zero or + controller-managed, add the label + 'platform.devantler.tech/replica-floor: exempt'." diff --git a/tests/policy-failure-actions.json b/tests/policy-failure-actions.json index c3715885fe..d3ea8126e6 100644 --- a/tests/policy-failure-actions.json +++ b/tests/policy-failure-actions.json @@ -40,7 +40,7 @@ "validate-pod-security/require-seccomp-profile": "Enforce", "validate-pod-security/deny-container-seccomp-unconfined": "Enforce", "validate-replica-ceiling": "Audit", - "validate-replica-floor/require-replica-floor": "Audit", + "validate-replica-floor": "Audit", "enforce-flux-best-practices/kustomization-recommended-settings": "Enforce", "enforce-flux-best-practices/helmrelease-recommended-settings": "Enforce", "restrict-arc-retirement/fence-pool-retirement": "Enforce", diff --git a/tests/validate-replica-floor/kyverno-test.yaml b/tests/validate-replica-floor/kyverno-test.yaml index 2689590b3f..71ee8c17b7 100644 --- a/tests/validate-replica-floor/kyverno-test.yaml +++ b/tests/validate-replica-floor/kyverno-test.yaml @@ -10,19 +10,28 @@ resources: - resources.yaml results: - policy: validate-replica-floor - rule: require-replica-floor resources: - normal-singleton + - replicas-unset + - scaled-to-zero kind: Deployment result: fail - policy: validate-replica-floor - rule: require-replica-floor + resources: + - statefulset-singleton + kind: StatefulSet + result: fail + - policy: validate-replica-floor resources: - normal-ha kind: Deployment result: pass - policy: validate-replica-floor - rule: require-replica-floor + resources: + - statefulset-ha + kind: StatefulSet + result: pass + - policy: validate-replica-floor resources: - workload-label-exempt - pod-label-exempt diff --git a/tests/validate-replica-floor/resources.yaml b/tests/validate-replica-floor/resources.yaml index 3869a2f226..c97f248aea 100644 --- a/tests/validate-replica-floor/resources.yaml +++ b/tests/validate-replica-floor/resources.yaml @@ -134,3 +134,80 @@ spec: containers: - name: app image: nginx:1.29.0 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: replicas-unset + namespace: test +spec: + selector: + matchLabels: + app: replicas-unset + template: + metadata: + labels: + app: replicas-unset + spec: + containers: + - name: app + image: nginx:1.29.0 +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: scaled-to-zero + namespace: test +spec: + replicas: 0 + selector: + matchLabels: + app: scaled-to-zero + template: + metadata: + labels: + app: scaled-to-zero + spec: + containers: + - name: app + image: nginx:1.29.0 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: statefulset-singleton + namespace: test +spec: + replicas: 1 + serviceName: statefulset-singleton + selector: + matchLabels: + app: statefulset-singleton + template: + metadata: + labels: + app: statefulset-singleton + spec: + containers: + - name: app + image: nginx:1.29.0 +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: statefulset-ha + namespace: test +spec: + replicas: 3 + serviceName: statefulset-ha + selector: + matchLabels: + app: statefulset-ha + template: + metadata: + labels: + app: statefulset-ha + spec: + containers: + - name: app + image: nginx:1.29.0 From cddb320e7f4f5b987c7b723f3eb481de5d31869b Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 13:34:45 +0200 Subject: [PATCH 2/4] fix(kyverno): keep replica-floor exemptions in the validation so reports stay current A false matchCondition writes no PolicyReport result, so an exemption added after a failure could leave that failure in place. Exempt workloads now get a current pass, as validate-replica-ceiling already does. Co-Authored-By: Claude Opus 5.5 --- docs/policy-reports.md | 39 +++++---- .../validate-replica-floor.yaml | 82 +++++++++---------- .../validate-replica-floor/kyverno-test.yaml | 2 +- 3 files changed, 61 insertions(+), 62 deletions(-) diff --git a/docs/policy-reports.md b/docs/policy-reports.md index 52e0ad2fee..17bcbacddb 100644 --- a/docs/policy-reports.md +++ b/docs/policy-reports.md @@ -6,15 +6,18 @@ that property for the Policy Reporter compliance dashboard. ## Exemptions must produce a current result -For report-backed validation policies, prefer a false `preconditions` entry for -an exemption instead of `rule.exclude` when the exempt resource may already have -a result: - -- a false precondition emits `result: skip` for the same policy, rule and - resource; -- `exclude` or a changed `match` emits no replacement result. - -The second shape can leave an older result visible even after hourly background +For report-backed validation policies, express an exemption so that the exempt +resource still gets a current result when it may already have one: + +- in a `ClusterPolicy`, a false `preconditions` entry emits `result: skip` for + the same policy, rule and resource; +- in a `ValidatingPolicy`, fold the exemption into the validation expression + (`variables.exempt || `), which emits `result: pass`. A false + `matchConditions` entry writes no result to the report; +- `exclude`, a changed `match` or a narrower `matchConstraints` emits no + replacement result. + +A shape that writes no result can leave an older result visible even after hourly background scans. Issue #2573 reproduced this with `validate-replica-floor`: eleven June failures remained while unrelated results in the same reports kept refreshing. The policy's committed Kyverno test pins the supported exemption shape: @@ -23,9 +26,10 @@ The policy's committed Kyverno test pins the supported exemption shape: kyverno test tests/validate-replica-floor --require-tests ``` -The test requires a normal singleton to fail, two replicas to pass, and every -supported exemption form (workload label, pod-template label, namespace, exact -name and wildcard primary name) to emit `skip`. +The test requires a normal singleton, an unset replica count, a scaled-to-zero +workload and a single-replica StatefulSet to fail, two or more replicas to pass, +and every supported exemption form (workload label, pod-template label, +namespace, exact name and wildcard primary name) to emit `pass`. `kyverno test` counts a fixture row as passing when its rule never evaluated the resource (`REASON=Excluded`), whatever the row declared, and it drops a @@ -43,8 +47,9 @@ which a static rule-evaluation test cannot establish. ## Safe rollout and verification -1. Express a new replica-floor exemption as a precondition in - `validate-replica-floor.yaml`. Do not add a new `exclude` entry. +1. Express a new replica-floor exemption in the `exempt` variables of + `validate-replica-floor.yaml`. Do not add a `matchConditions` entry or + narrow its match scope. 2. Run the Kyverno test above and the normal local/prod static validation. 3. Let Flux deploy the policy change. Do not patch or delete PolicyReports by hand and do not restart the reports controller. @@ -58,7 +63,7 @@ which a static rule-evaluation test cannot establish. | map({result: .[0].result, count: length})' ``` -The precondition changes only the target policy/rule result. Other results in +The exemption changes only the target policy result. Other results in the same per-resource report remain controller-owned and untouched. ## Automatic pruning of stale failures @@ -86,8 +91,8 @@ What operators should expect: two hours old; in those first two hours a report can still be deleted, and it stays missing until the controller recovers and rescans. - A result left behind by an `exclude` on a resource that no other rule still - evaluates is not pruned either. Use a precondition for the exemption (see - above) so the scan rewrites it as a current `skip`. + evaluates is not pruned either. Express the exemption as described above so the + scan rewrites it as a current result. - Cluster-scoped `ClusterPolicyReport` objects are not covered. ## Authorization review dispatch diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/validate-replica-floor.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/validate-replica-floor.yaml index 2acb3e4596..0781761618 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/validate-replica-floor.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/validate-replica-floor.yaml @@ -13,12 +13,11 @@ # workloads in Kyverno's PolicyReports for a human to raise (or exempt) # deliberately. # -# Exemptions (a replica floor is *wrong*, not missing, for these) are expressed -# as matchConditions, never as a narrower match scope: a false matchCondition -# still evaluates the resource and reports `skip`, while a resource outside the -# match scope gets no replacement result and can keep an older `fail` after the -# exemption is added (#2573). See docs/policy-reports.md for the regression -# test and rollout check. +# Exemptions (a replica floor is *wrong*, not missing, for these) are part of the +# validation expression, so an exempt workload gets a current `pass` result. +# Neither a narrower match scope nor a false matchCondition writes a result, +# so either could leave an older `fail` in place after the exemption is added +# (#2573). See docs/policy-reports.md for the regression test and rollout check. # # Exemption categories: # * the opt-out label platform.devantler.tech/replica-floor: exempt on the @@ -71,69 +70,64 @@ spec: apiVersions: ["v1"] operations: ["CREATE", "UPDATE"] resources: ["deployments", "statefulsets"] - matchConditions: + variables: # Durable opt-out on the workload itself. - - name: workload-not-exempt + - name: workloadExempt expression: >- - object.metadata.?labels[?'platform.devantler.tech/replica-floor'].orValue('') != 'exempt' + object.metadata.?labels[?'platform.devantler.tech/replica-floor'].orValue('') == 'exempt' # Durable opt-out on the pod template (charts expose podLabels more # widely than top-level workload labels). - - name: pod-template-not-exempt + - name: templateExempt expression: >- - object.spec.?template.?metadata.?labels[?'platform.devantler.tech/replica-floor'].orValue('') != 'exempt' + object.spec.?template.?metadata.?labels[?'platform.devantler.tech/replica-floor'].orValue('') == 'exempt' # Whole namespaces whose controllers are intentionally singleton or # controller-managed: Velero has no leader election; Flux, Coroot and # Kubescape controllers sit off serving paths; Crossplane reconcilers # are external-state single writers. Crossplane is exempt by namespace # because provider Deployment names include a changing revision hash. - - name: not-exempt-namespace + - name: namespaceExempt expression: >- - !(object.metadata.?namespace.orValue('') in [ + object.metadata.?namespace.orValue('') in [ 'velero', 'flux-system', 'observability', 'kubescape', - 'crossplane-system']) + 'crossplane-system'] # Flagger owns replica counts for generated *-primary Deployments. - - name: not-flagger-primary + - name: flaggerPrimary expression: >- - !object.metadata.name.matches('^.+-primary$') - # Single-replica apps/UIs and paused Flagger canary sources. - - name: not-singleton-app + object.metadata.name.matches('^.+-primary$') + # Named exemptions, one list per reason: single-replica apps/UIs and paused + # Flagger canary sources; single-writer or ephemeral controllers (DNS, + # disabled issuer, canary load generator); lean leader-election controllers + # whose outage delays only an idempotent loop (production may still opt + # them into HA); and components single by design from the June 2026 HA + # sweep -- SPIRE needs an external datastore for real HA, CNPG-I exposes + # its gRPC readiness endpoint only on the elected leader, and Crossview with + # its bundled Postgres is one non-HA unit until the datastore is replaced. + - name: namedExempt expression: >- - !(object.metadata.name in [ + object.metadata.name in [ 'actual-budget-actualbudget', 'fleet', 'headlamp', 'whoami', - 'hubble-ui', 'longhorn-ui', 'homepage', 'umami-umami']) - # Single-writer or ephemeral controllers with no useful second replica: - # DNS, disabled issuer and the canary load generator. - - name: not-single-writer - expression: >- - !(object.metadata.name in [ - 'external-dns', 'origin-ca-issuer', 'flagger-loadtester']) - # Lean leader-election controllers whose outage delays only an idempotent - # control loop. Production may still opt them into HA. - - name: not-lean-controller - expression: >- - !(object.metadata.name in [ + 'hubble-ui', 'longhorn-ui', 'homepage', 'umami-umami'] || + object.metadata.name in [ + 'external-dns', 'origin-ca-issuer', 'flagger-loadtester'] || + object.metadata.name in [ 'vertical-pod-autoscaler-vpa-recommender', 'vertical-pod-autoscaler-vpa-updater', - 'kyverno-reports-controller', 'kyverno-cleanup-controller']) - # Controller-owned or single-by-design components triaged in the June 2026 - # HA sweep. SPIRE needs an external datastore for real HA. CNPG-I exposes - # its gRPC readiness endpoint only on the elected leader, so a second - # replica can never become Ready. Crossview and its ephemeral bundled - # Postgres intentionally form one non-HA unit until the datastore is - # replaced. - - name: not-single-by-design - expression: >- - !(object.metadata.name in [ + 'kyverno-reports-controller', 'kyverno-cleanup-controller'] || + object.metadata.name in [ 'cluster-autoscaler-hetzner-cluster-autoscaler', 'keda-operator-metrics-apiserver', 'longhorn-driver-deployer', 'spire-server', 'plugin-barman-cloud', 'crossview', - 'crossview-postgres']) - variables: + 'crossview-postgres'] + - name: exempt + expression: >- + variables.workloadExempt || variables.templateExempt || + variables.namespaceExempt || variables.flaggerPrimary || + variables.namedExempt # An unset replica count defaults to 1; an explicit 0 stays 0. - name: replicas expression: object.spec.?replicas.orValue(1) validations: - - expression: "variables.replicas >= 2" + - expression: "variables.exempt || variables.replicas >= 2" messageExpression: >- object.kind + " '" + object.metadata.name + "' runs " + string(variables.replicas) + " replica(s); the platform HA floor is 2 diff --git a/tests/validate-replica-floor/kyverno-test.yaml b/tests/validate-replica-floor/kyverno-test.yaml index 71ee8c17b7..099458bc34 100644 --- a/tests/validate-replica-floor/kyverno-test.yaml +++ b/tests/validate-replica-floor/kyverno-test.yaml @@ -39,4 +39,4 @@ results: - keda-operator-metrics-apiserver - demo-primary kind: Deployment - result: skip + result: pass From 92098a607614726b0094815f5d55162de9405276 Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 13:35:02 +0200 Subject: [PATCH 3/4] docs(kyverno): rewrap the policy-report exemption paragraph Co-Authored-By: Claude Opus 5.5 --- docs/policy-reports.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/policy-reports.md b/docs/policy-reports.md index 17bcbacddb..4162e0befb 100644 --- a/docs/policy-reports.md +++ b/docs/policy-reports.md @@ -17,9 +17,10 @@ resource still gets a current result when it may already have one: - `exclude`, a changed `match` or a narrower `matchConstraints` emits no replacement result. -A shape that writes no result can leave an older result visible even after hourly background -scans. Issue #2573 reproduced this with `validate-replica-floor`: eleven June -failures remained while unrelated results in the same reports kept refreshing. +A shape that writes no result can leave an older result visible even after +hourly background scans. Issue #2573 reproduced this with +`validate-replica-floor`: eleven June failures remained while unrelated +results in the same reports kept refreshing. The policy's committed Kyverno test pins the supported exemption shape: ```bash From de3e92e7b7ba3a8b3f42f5d0dfd987879e20a26b Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 14:16:42 +0200 Subject: [PATCH 4/4] test(policies): read replica-floor exemptions structurally in the snapshotter guard The guard grepped for the old YAML list item, which can no longer match now that the exemptions are CEL string literals. Read the namedExempt expression with yq and fail when it is missing or no longer a name list. Co-Authored-By: Claude Opus 5.5 --- scripts/tests/test-coroot-availability-remediation.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/tests/test-coroot-availability-remediation.sh b/scripts/tests/test-coroot-availability-remediation.sh index 568b741bf6..b726089c3e 100755 --- a/scripts/tests/test-coroot-availability-remediation.sh +++ b/scripts/tests/test-coroot-availability-remediation.sh @@ -140,7 +140,13 @@ SNAPSHOTTER_SUBSTITUTION="${snapshotter_substitution}" yq e -e \ "${longhorn_release}" >/dev/null || fail 'Longhorn must continue sourcing the snapshotter replica count from the production contract' -if grep -Fxq ' - csi-snapshotter' "${replica_floor}"; then +# The exemptions are CEL expressions; read them structurally so a format change +# fails here instead of leaving a text match that can never fire. +replica_floor_exemptions="$(yq e -e '.spec.variables[] | select(.name == "namedExempt") | .expression' "${replica_floor}")" || + fail 'the replica floor must keep its named exemptions in the namedExempt variable' +[[ "${replica_floor_exemptions}" == *"object.metadata.name in ["* ]] || + fail 'the replica floor named exemptions must stay a name list this guard can read' +if [[ "${replica_floor_exemptions}" == *csi-snapshotter* ]]; then fail 'the HA CSI snapshotter must not remain exempt from the replica floor' fi