From 118eb0817a5e555d4c88f8e378eadcc84739864d Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 17:58:23 +0200 Subject: [PATCH 1/4] feat(kyverno): unregister the classic auto-vpa ClusterPolicy Step 1 of the shared catalog's two-reconcile move to the auto-vpa GeneratingPolicy form (devantler-tech/kyverno-policies#1). The classic policy's removal deletes the VPAs it generated; the GeneratingPolicy form is registered in a separate later revision so the two never run at once. The file stays, still tested, as the rollback form. Co-Authored-By: Claude Opus 5.5 --- .../infrastructure/cluster-policies/kustomization.yaml | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml index 3f7c7afe89..e143931c6c 100644 --- a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml +++ b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml @@ -8,7 +8,13 @@ resources: - best-practices/add-recommended-labels.yaml - best-practices/add-resource-defaults.yaml - best-practices/add-security-context.yaml - - best-practices/auto-vpa.yaml + # best-practices/auto-vpa.yaml is deliberately NOT registered: it is step 1 + # of the shared catalog's two-reconcile move to its GeneratingPolicy form + # (devantler-tech/kyverno-policies#1). Removing the classic policy deletes + # the VPAs it generated; the GeneratingPolicy form is registered in a later + # revision, once no `generate.kyverno.io/policy-name=auto-vpa` VPA remains, + # so the two forms never run at once. The file stays, still tested, as the + # rollback form until that move is complete. - best-practices/audit-privileged-rbac.yaml - best-practices/disable-default-sa-automount.yaml - best-practices/disallow-latest-tag.yaml From 1992b501fd9c3b581ec405d98be2584e9acf5dcd Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 18:21:31 +0200 Subject: [PATCH 2/4] feat(kyverno): register the catalog's auto-vpa GeneratingPolicy Step 2 of the shared catalog's two-reconcile move (#5130, devantler-tech/kyverno-policies#1): adopt the catalog's three auto-vpa GeneratingPolicy objects with one platform addition, a namespaceSelector that keeps them out of the system namespaces where prod ships static VPAs. The new exclusion test pins that addition with a selector-free control, and the fixture evaluator learns that a GeneratingPolicy's single rule is named after the policy. Merge only after step 1 has deployed and no classic auto-vpa VPA remains. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/ci.yaml | 6 + .../auto-vpa-generating-policy.yaml | 263 ++++++++++++++++++ .../cluster-policies/kustomization.yaml | 13 +- ...st-auto-vpa-generating-policy-exclusion.sh | 107 +++++++ ...est-validate-kyverno-fixture-evaluation.sh | 15 + .../validate-kyverno-fixture-evaluation.sh | 5 + .../generated-vpa.yaml | 23 ++ .../kyverno-test.yaml | 24 ++ .../resources.yaml | 18 ++ .../values.yaml | 7 + .../generated-vpa.yaml | 26 ++ .../kyverno-test.yaml | 24 ++ .../resources.yaml | 18 ++ .../values.yaml | 7 + .../generated-vpa.yaml | 26 ++ .../kyverno-test.yaml | 24 ++ .../resources.yaml | 18 ++ .../values.yaml | 7 + 18 files changed, 624 insertions(+), 7 deletions(-) create mode 100644 k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml create mode 100755 scripts/tests/test-auto-vpa-generating-policy-exclusion.sh create mode 100644 tests/auto-vpa-generating-policy-daemonset/generated-vpa.yaml create mode 100644 tests/auto-vpa-generating-policy-daemonset/kyverno-test.yaml create mode 100644 tests/auto-vpa-generating-policy-daemonset/resources.yaml create mode 100644 tests/auto-vpa-generating-policy-daemonset/values.yaml create mode 100644 tests/auto-vpa-generating-policy-deployment/generated-vpa.yaml create mode 100644 tests/auto-vpa-generating-policy-deployment/kyverno-test.yaml create mode 100644 tests/auto-vpa-generating-policy-deployment/resources.yaml create mode 100644 tests/auto-vpa-generating-policy-deployment/values.yaml create mode 100644 tests/auto-vpa-generating-policy-statefulset/generated-vpa.yaml create mode 100644 tests/auto-vpa-generating-policy-statefulset/kyverno-test.yaml create mode 100644 tests/auto-vpa-generating-policy-statefulset/resources.yaml create mode 100644 tests/auto-vpa-generating-policy-statefulset/values.yaml diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index ea55dd5bb6..2e5230245d 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -217,6 +217,10 @@ jobs: - 'tests/require-baseline-workers-for-hcloud-volumes/**' - 'tests/audit-privileged-rbac/**' - 'tests/auto-vpa/**' + # The platform's copy of the catalog auto-vpa GeneratingPolicy: its + # fixtures and its system-namespace exclusion test (#5130). + - 'tests/auto-vpa-generating-policy-*/**' + - 'scripts/tests/test-auto-vpa-generating-policy-exclusion.sh' - 'scripts/tests/test-audit-privileged-rbac.sh' # Renders the charts whose roles that policy excludes and applies the # policy to their RBAC, so a chart bump fails its own PR (#3792). @@ -1628,6 +1632,8 @@ jobs: bash scripts/tests/test-tetragon-agent-priority.sh shellcheck scripts/tests/test-audit-privileged-rbac.sh bash scripts/tests/test-audit-privileged-rbac.sh + shellcheck scripts/tests/test-auto-vpa-generating-policy-exclusion.sh + bash scripts/tests/test-auto-vpa-generating-policy-exclusion.sh shellcheck scripts/tests/test-arc-controller-availability.sh bash scripts/tests/test-arc-controller-availability.sh shellcheck scripts/tests/test-restrict-tenant-secret-stores.sh diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml new file mode 100644 index 0000000000..acae61ee1a --- /dev/null +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml @@ -0,0 +1,263 @@ +--- +# ADOPTED from devantler-tech/kyverno-policies +# policies/best-practices/auto-vpa-generating-policy.yaml at 44ed118 (#5130). The +# file is copied verbatim except for one marked PLATFORM ADDITION per policy: a +# namespaceSelector that keeps these policies out of the system namespaces, as the +# classic best-practices/auto-vpa.yaml did. The generate expressions must stay +# byte-for-byte the catalog's (whitespace aside): both static producer guards pin +# them (scripts/guard-consumer-discovery-conservation.sh and +# scripts/tests/crossplane-egress-policy-rules.yaml). In this repository the +# classic file is the rollback form and the "root kustomization.yaml" below means +# this folder's kustomization.yaml. The classic VPAs are named , these are +# -, so running both forms at once would put two VPAs on every +# workload: register this only after the classic policy's VPAs are gone. +# +# The catalog's auto-vpa: a port of the classic policies/best-practices/auto-vpa.yaml +# ClusterPolicy rules to the GeneratingPolicy API, which Kyverno keeps after classic +# ClusterPolicy generate rules are retired. The classic ClusterPolicy is a separate file. +# +# THIS IS THE REGISTERED FORM in the root kustomization.yaml. The classic file stays in the +# repository, unregistered, as the rollback form until every consumer has migrated: never +# apply both to one cluster, because both generate a VerticalPodAutoscaler for the same +# workload under the same name. The README's "Migrating to the GeneratingPolicy form" +# section is the consumer rollout and rollback procedure. +# +# Split into one policy per workload kind, mirroring the classic policy's three rules: +# GeneratingPolicy applies spec.matchConstraints to the whole policy, so three kinds with +# three different VPA bodies cannot share one policy without branching inside CEL. +# +# FLOOR: Kyverno 1.19.0. On 1.18.x spec.evaluation.generateExisting is schema-accepted but +# does not fire for PRE-EXISTING workloads (kyverno/kyverno#15722, a regression against the +# classic rule; fixed by kyverno/kyverno#16041 for 1.19.0 and never backported), so an +# existing workload would get its VPA only on its next CREATE/UPDATE admission. The +# minversion annotations below declare that floor. +apiVersion: policies.kyverno.io/v1 +kind: GeneratingPolicy +metadata: + name: auto-vpa-deployment + annotations: + policies.kyverno.io/title: Auto VPA for Deployments (GeneratingPolicy) + policies.kyverno.io/category: Autoscaling + policies.kyverno.io/severity: low + policies.kyverno.io/subject: Deployment + policies.kyverno.io/minversion: 1.19.0 + policies.kyverno.io/description: >- + Generates a recommendation-bounded VerticalPodAutoscaler for each Deployment. + GeneratingPolicy form of the classic auto-vpa rule of the same name. +spec: + # Parity with the classic rules' `synchronize: true` + `generateExisting: true`: + # on GeneratingPolicy both live under spec.evaluation and default to false, so + # omitting them would silently drop behavior the classic policy guarantees. + evaluation: + generateExisting: + enabled: true + synchronize: + enabled: true + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["deployments"] + # PLATFORM ADDITION (the only change from the catalog file): prod ships + # static VPAs for the controllers in these namespaces + # (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/), so a + # generated one would put a second VPA on each of those pods. + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - kube-public + - kube-node-lease + - kyverno + generate: + # Every value is dyn(): CEL infers a map's value type from its first entry, so a + # resource literal mixing strings with nested maps fails to compile without it. + # Fields read off `object` are dyn and need string() before concatenation. + - expression: >- + generator.Apply( + string(object.metadata.namespace), + [ + { + "apiVersion": dyn("autoscaling.k8s.io/v1"), + "kind": dyn("VerticalPodAutoscaler"), + "metadata": dyn({ + "name": string(object.metadata.name) + "-deployment", + "namespace": string(object.metadata.namespace) + }), + "spec": dyn({ + "targetRef": dyn({ + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": string(object.metadata.name) + }), + "updatePolicy": dyn({"updateMode": "InPlaceOrRecreate"}), + "resourcePolicy": dyn({ + "containerPolicies": [ + { + "containerName": dyn("*"), + "controlledResources": dyn(["cpu", "memory"]), + "controlledValues": dyn("RequestsAndLimits"), + "minAllowed": dyn({"cpu": "50m", "memory": "64Mi"}), + "maxAllowed": dyn({"cpu": "3", "memory": "6Gi"}) + } + ] + }) + }) + } + ] + ) +--- +apiVersion: policies.kyverno.io/v1 +kind: GeneratingPolicy +metadata: + name: auto-vpa-statefulset + annotations: + policies.kyverno.io/title: Auto VPA for StatefulSets (GeneratingPolicy) + policies.kyverno.io/category: Autoscaling + policies.kyverno.io/severity: low + policies.kyverno.io/subject: StatefulSet + policies.kyverno.io/minversion: 1.19.0 + policies.kyverno.io/description: >- + Generates a recommendation-bounded VerticalPodAutoscaler for each StatefulSet. + GeneratingPolicy form of the classic auto-vpa rule of the same name. +spec: + # Parity with the classic rules' `synchronize: true` + `generateExisting: true`: + # on GeneratingPolicy both live under spec.evaluation and default to false, so + # omitting them would silently drop behavior the classic policy guarantees. + evaluation: + generateExisting: + enabled: true + synchronize: + enabled: true + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["statefulsets"] + # PLATFORM ADDITION (the only change from the catalog file): prod ships + # static VPAs for the controllers in these namespaces + # (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/), so a + # generated one would put a second VPA on each of those pods. + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - kube-public + - kube-node-lease + - kyverno + generate: + - expression: >- + generator.Apply( + string(object.metadata.namespace), + [ + { + "apiVersion": dyn("autoscaling.k8s.io/v1"), + "kind": dyn("VerticalPodAutoscaler"), + "metadata": dyn({ + "name": string(object.metadata.name) + "-statefulset", + "namespace": string(object.metadata.namespace) + }), + "spec": dyn({ + "targetRef": dyn({ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "name": string(object.metadata.name) + }), + "updatePolicy": dyn({"updateMode": "Initial"}), + "resourcePolicy": dyn({ + "containerPolicies": [ + { + "containerName": dyn("*"), + "controlledResources": dyn(["cpu", "memory"]), + "controlledValues": dyn("RequestsAndLimits"), + "minAllowed": dyn({"cpu": "50m", "memory": "64Mi"}), + "maxAllowed": dyn({"cpu": "3", "memory": "6Gi"}) + } + ] + }) + }) + } + ] + ) +--- +apiVersion: policies.kyverno.io/v1 +kind: GeneratingPolicy +metadata: + name: auto-vpa-daemonset + annotations: + policies.kyverno.io/title: Auto VPA for DaemonSets (GeneratingPolicy) + policies.kyverno.io/category: Autoscaling + policies.kyverno.io/severity: low + policies.kyverno.io/subject: DaemonSet + policies.kyverno.io/minversion: 1.19.0 + policies.kyverno.io/description: >- + Generates a CPU-only recommendation-bounded VerticalPodAutoscaler for each + DaemonSet. GeneratingPolicy form of the classic auto-vpa rule of the same name. +spec: + # Parity with the classic rules' `synchronize: true` + `generateExisting: true`: + # on GeneratingPolicy both live under spec.evaluation and default to false, so + # omitting them would silently drop behavior the classic policy guarantees. + evaluation: + generateExisting: + enabled: true + synchronize: + enabled: true + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["daemonsets"] + # PLATFORM ADDITION (the only change from the catalog file): prod ships + # static VPAs for the controllers in these namespaces + # (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/), so a + # generated one would put a second VPA on each of those pods. + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - kube-public + - kube-node-lease + - kyverno + generate: + - expression: >- + generator.Apply( + string(object.metadata.namespace), + [ + { + "apiVersion": dyn("autoscaling.k8s.io/v1"), + "kind": dyn("VerticalPodAutoscaler"), + "metadata": dyn({ + "name": string(object.metadata.name) + "-daemonset", + "namespace": string(object.metadata.namespace) + }), + "spec": dyn({ + "targetRef": dyn({ + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "name": string(object.metadata.name) + }), + "updatePolicy": dyn({"updateMode": "InPlaceOrRecreate"}), + "resourcePolicy": dyn({ + "containerPolicies": [ + { + "containerName": dyn("*"), + "controlledResources": dyn(["cpu"]), + "controlledValues": dyn("RequestsAndLimits"), + "minAllowed": dyn({"cpu": "50m"}), + "maxAllowed": dyn({"cpu": "1"}) + } + ] + }) + }) + } + ] + ) diff --git a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml index e143931c6c..4dd2ed5331 100644 --- a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml +++ b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml @@ -8,13 +8,12 @@ resources: - best-practices/add-recommended-labels.yaml - best-practices/add-resource-defaults.yaml - best-practices/add-security-context.yaml - # best-practices/auto-vpa.yaml is deliberately NOT registered: it is step 1 - # of the shared catalog's two-reconcile move to its GeneratingPolicy form - # (devantler-tech/kyverno-policies#1). Removing the classic policy deletes - # the VPAs it generated; the GeneratingPolicy form is registered in a later - # revision, once no `generate.kyverno.io/policy-name=auto-vpa` VPA remains, - # so the two forms never run at once. The file stays, still tested, as the - # rollback form until that move is complete. + # The shared catalog's GeneratingPolicy form of auto-vpa (#5130, + # devantler-tech/kyverno-policies#1). best-practices/auto-vpa.yaml, the classic + # form, is deliberately NOT registered: it stays, still tested, as the + # rollback form. Never register both, because they name their VPAs + # differently and every workload would get two. + - best-practices/auto-vpa-generating-policy.yaml - best-practices/audit-privileged-rbac.yaml - best-practices/disable-default-sa-automount.yaml - best-practices/disallow-latest-tag.yaml diff --git a/scripts/tests/test-auto-vpa-generating-policy-exclusion.sh b/scripts/tests/test-auto-vpa-generating-policy-exclusion.sh new file mode 100755 index 0000000000..274f4a38bd --- /dev/null +++ b/scripts/tests/test-auto-vpa-generating-policy-exclusion.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# The platform's copy of the catalog's auto-vpa GeneratingPolicy adds one thing: +# a namespaceSelector that keeps it out of the system namespaces, where prod +# ships static VPAs (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/). +# A generated VPA there would put a second VPA on the same pods. A kyverno test +# cannot assert that a GeneratingPolicy generates nothing, so this script does +# it: for each policy and each excluded namespace, a test that expects a VPA +# there must fail with "Not found". The same test with the selector removed must +# pass, so a probe that could never see a generation cannot read as excluded +# (platform#5130). +set -euo pipefail + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd "${script_dir}/../.." && pwd)" +policy="${repo_root}/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml" +static_vpas="${repo_root}/k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers" + +work="$(mktemp -d)" +trap 'rm -rf "${work}"' EXIT + +# The classic policy's exclusions, plus every namespace prod gives static VPAs. +required="$( + { + printf '%s\n' kube-system kube-public kube-node-lease kyverno + kubectl kustomize "${static_vpas}" | + yq -N 'select(.kind == "VerticalPodAutoscaler") | .metadata.namespace' + } | sort -u +)" +if [[ -z "${required}" ]]; then + echo "FAIL: no namespaces to check" >&2 + exit 1 +fi + +checked=0 +for kind in deployment statefulset daemonset; do + name="auto-vpa-${kind}" + fixtures="${repo_root}/tests/auto-vpa-generating-policy-${kind}" + excluded="$(yq -N "select(.metadata.name == \"${name}\") + | .spec.matchConstraints.namespaceSelector.matchExpressions[] + | select(.key == \"kubernetes.io/metadata.name\" and .operator == \"NotIn\") + | .values[]" "${policy}" | sort -u)" + missing="$(comm -23 <(printf '%s\n' "${required}") <(printf '%s\n' "${excluded}"))" + if [[ -n "${missing}" ]]; then + echo "FAIL: ${name} does not exclude: ${missing//$'\n'/ }" >&2 + exit 1 + fi + yq "(select(.metadata.name == \"${name}\") | .spec.matchConstraints) + |= del(.namespaceSelector)" "${policy}" >"${work}/${kind}-unscoped.yaml" + + while IFS= read -r ns; do + case_dir="${work}/${kind}-${ns}" + mkdir -p "${case_dir}" + sed "s/^ namespace: apps-ns\$/ namespace: ${ns}/" "${fixtures}/resources.yaml" >"${case_dir}/resources.yaml" + sed "s/^ namespace: apps-ns\$/ namespace: ${ns}/" "${fixtures}/generated-vpa.yaml" >"${case_dir}/generated-vpa.yaml" + grep -q "namespace: ${ns}\$" "${case_dir}/resources.yaml" + grep -q "namespace: ${ns}\$" "${case_dir}/generated-vpa.yaml" + cat >"${case_dir}/values.yaml" <"${case_dir}/kyverno-test.yaml" <"${case_dir}/${variant}.log" 2>&1 || status=$? + if [[ "${variant}" == scoped ]]; then + if [[ "${status}" -eq 0 ]] || ! grep -q 'Not found' "${case_dir}/${variant}.log"; then + echo "FAIL: ${name} generated a VPA in ${ns}:" >&2 + cat "${case_dir}/${variant}.log" >&2 + exit 1 + fi + elif [[ "${status}" -ne 0 ]]; then + echo "FAIL: control: ${name} without its selector generated nothing in ${ns}, so the probe proves nothing:" >&2 + cat "${case_dir}/${variant}.log" >&2 + exit 1 + fi + done + checked=$((checked + 1)) + done <<<"${required}" +done + +echo "OK: ${checked} policy/namespace cases generate no VPA, and each generates one without the selector" diff --git a/scripts/tests/test-validate-kyverno-fixture-evaluation.sh b/scripts/tests/test-validate-kyverno-fixture-evaluation.sh index 20399975c9..b0cc4a7840 100755 --- a/scripts/tests/test-validate-kyverno-fixture-evaluation.sh +++ b/scripts/tests/test-validate-kyverno-fixture-evaluation.sh @@ -141,6 +141,21 @@ if kyverno_passes "generated object named apart from its trigger" "${root}"; the expect_pass "generated object named apart from its trigger" "${root}" fi +# #5130: a GeneratingPolicy has no spec.rules; kyverno names its one rule after the policy. +# A row naming any other rule must still fail check 1, and a row expecting an object the +# policy never generates must still fail. +root="$(copy generating-policy-rule auto-vpa-generating-policy-deployment)" +yq -i '.results[0].rule = "generate-vpa-for-deployment"' \ + "${root}/tests/auto-vpa-generating-policy-deployment/kyverno-test.yaml" +expect_fail "GeneratingPolicy row naming a rule it does not define" "${root}" 1 \ + "names rule generate-vpa-for-deployment of policy auto-vpa-deployment" +root="$(copy generating-policy-missing auto-vpa-generating-policy-deployment)" +yq -i '.metadata.namespace = "other-ns"' "${root}/tests/auto-vpa-generating-policy-deployment/resources.yaml" +yq -i '.namespaceSelector[0].name = "other-ns" | .namespaceSelector[0].labels."kubernetes.io/metadata.name" = "other-ns"' \ + "${root}/tests/auto-vpa-generating-policy-deployment/values.yaml" +yq -i '.metadata.name = "absent-deployment"' "${root}/tests/auto-vpa-generating-policy-deployment/generated-vpa.yaml" +expect_fail "GeneratingPolicy expecting an object it never generates" "${root}" 1 "kyverno test" + # A generate row names only the object it generates, so the triggers of one entry that all # generate the same fixed-name object share its name, and kyverno drops a misspelled one # without a row or a failure. diff --git a/scripts/validate-kyverno-fixture-evaluation.sh b/scripts/validate-kyverno-fixture-evaluation.sh index e54f6e226e..dbd75fa57c 100755 --- a/scripts/validate-kyverno-fixture-evaluation.sh +++ b/scripts/validate-kyverno-fixture-evaluation.sh @@ -101,6 +101,11 @@ for test_file in "${test_files[@]}"; do | .spec.rules[] | [$p, .name, ([.match.resources.kinds[]?, .match.any[]?.resources.kinds[]?, .match.all[]?.resources.kinds[]?] | join(",")), $c, $generates, $patches, $filters] | join("|")' "${dir}/${policy_path}" >>"${work}/rules" + # A GeneratingPolicy has no spec.rules: kyverno reports its one generate as a rule named + # after the policy, and generates no autogen rules for it (#5130). + yq -r 'select(.kind == "GeneratingPolicy" and .metadata.name != null) + | [.metadata.name, .metadata.name, "", "", "true", "false", "false"] | join("|")' \ + "${dir}/${policy_path}" >>"${work}/rules" done < <(yq -r '.policies[]' "${test_file}") while IFS=$'\t' read -r policy rule; do diff --git a/tests/auto-vpa-generating-policy-daemonset/generated-vpa.yaml b/tests/auto-vpa-generating-policy-daemonset/generated-vpa.yaml new file mode 100644 index 0000000000..71f16e0966 --- /dev/null +++ b/tests/auto-vpa-generating-policy-daemonset/generated-vpa.yaml @@ -0,0 +1,23 @@ +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: shared-daemonset + namespace: apps-ns +spec: + targetRef: + apiVersion: apps/v1 + kind: DaemonSet + name: shared + updatePolicy: + updateMode: InPlaceOrRecreate + resourcePolicy: + containerPolicies: + - containerName: "*" + controlledResources: + - cpu + controlledValues: RequestsAndLimits + minAllowed: + cpu: 50m + maxAllowed: + cpu: "1" diff --git a/tests/auto-vpa-generating-policy-daemonset/kyverno-test.yaml b/tests/auto-vpa-generating-policy-daemonset/kyverno-test.yaml new file mode 100644 index 0000000000..0f11c5dd9b --- /dev/null +++ b/tests/auto-vpa-generating-policy-daemonset/kyverno-test.yaml @@ -0,0 +1,24 @@ +--- +# The platform's copy of the catalog's auto-vpa-daemonset GeneratingPolicy (#5130). +# The expected VPA is the catalog's own fixture (tests/auto-vpa/generated-daemonset-vpa.yaml +# at 44ed118), moved to a non-system namespace: the platform copy must generate +# exactly what the catalog does there. The system-namespace exclusion, which a +# kyverno test cannot assert, is pinned by +# scripts/tests/test-auto-vpa-generating-policy-exclusion.sh. +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: auto-vpa-generating-policy-daemonset +policies: + - ../../k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml +resources: + - resources.yaml +variables: values.yaml +results: + - policy: auto-vpa-daemonset + rule: auto-vpa-daemonset + resources: + - apps-ns/shared + generatedResource: generated-vpa.yaml + kind: DaemonSet + result: pass diff --git a/tests/auto-vpa-generating-policy-daemonset/resources.yaml b/tests/auto-vpa-generating-policy-daemonset/resources.yaml new file mode 100644 index 0000000000..f3fccb372b --- /dev/null +++ b/tests/auto-vpa-generating-policy-daemonset/resources.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: apps/v1 +kind: DaemonSet +metadata: + name: shared + namespace: apps-ns +spec: + selector: + matchLabels: + app: web + template: + metadata: + labels: + app: web + spec: + containers: + - name: web + image: registry.k8s.io/pause:3.10 diff --git a/tests/auto-vpa-generating-policy-daemonset/values.yaml b/tests/auto-vpa-generating-policy-daemonset/values.yaml new file mode 100644 index 0000000000..f1b259038e --- /dev/null +++ b/tests/auto-vpa-generating-policy-daemonset/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +namespaceSelector: + - name: apps-ns + labels: + kubernetes.io/metadata.name: apps-ns diff --git a/tests/auto-vpa-generating-policy-deployment/generated-vpa.yaml b/tests/auto-vpa-generating-policy-deployment/generated-vpa.yaml new file mode 100644 index 0000000000..dbf25ed042 --- /dev/null +++ b/tests/auto-vpa-generating-policy-deployment/generated-vpa.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: shared-deployment + namespace: apps-ns +spec: + targetRef: + apiVersion: apps/v1 + kind: Deployment + name: shared + updatePolicy: + updateMode: InPlaceOrRecreate + resourcePolicy: + containerPolicies: + - containerName: "*" + controlledResources: + - cpu + - memory + controlledValues: RequestsAndLimits + minAllowed: + cpu: 50m + memory: 64Mi + maxAllowed: + cpu: "3" + memory: 6Gi diff --git a/tests/auto-vpa-generating-policy-deployment/kyverno-test.yaml b/tests/auto-vpa-generating-policy-deployment/kyverno-test.yaml new file mode 100644 index 0000000000..29720fa7b2 --- /dev/null +++ b/tests/auto-vpa-generating-policy-deployment/kyverno-test.yaml @@ -0,0 +1,24 @@ +--- +# The platform's copy of the catalog's auto-vpa-deployment GeneratingPolicy (#5130). +# The expected VPA is the catalog's own fixture (tests/auto-vpa/generated-deployment-vpa.yaml +# at 44ed118), moved to a non-system namespace: the platform copy must generate +# exactly what the catalog does there. The system-namespace exclusion, which a +# kyverno test cannot assert, is pinned by +# scripts/tests/test-auto-vpa-generating-policy-exclusion.sh. +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: auto-vpa-generating-policy-deployment +policies: + - ../../k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml +resources: + - resources.yaml +variables: values.yaml +results: + - policy: auto-vpa-deployment + rule: auto-vpa-deployment + resources: + - apps-ns/shared + generatedResource: generated-vpa.yaml + kind: Deployment + result: pass diff --git a/tests/auto-vpa-generating-policy-deployment/resources.yaml b/tests/auto-vpa-generating-policy-deployment/resources.yaml new file mode 100644 index 0000000000..3b157fae60 --- /dev/null +++ b/tests/auto-vpa-generating-policy-deployment/resources.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: shared + namespace: apps-ns +spec: + selector: + matchLabels: + app: web + template: + metadata: + labels: + app: web + spec: + containers: + - name: web + image: registry.k8s.io/pause:3.10 diff --git a/tests/auto-vpa-generating-policy-deployment/values.yaml b/tests/auto-vpa-generating-policy-deployment/values.yaml new file mode 100644 index 0000000000..f1b259038e --- /dev/null +++ b/tests/auto-vpa-generating-policy-deployment/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +namespaceSelector: + - name: apps-ns + labels: + kubernetes.io/metadata.name: apps-ns diff --git a/tests/auto-vpa-generating-policy-statefulset/generated-vpa.yaml b/tests/auto-vpa-generating-policy-statefulset/generated-vpa.yaml new file mode 100644 index 0000000000..9f7d68f809 --- /dev/null +++ b/tests/auto-vpa-generating-policy-statefulset/generated-vpa.yaml @@ -0,0 +1,26 @@ +--- +apiVersion: autoscaling.k8s.io/v1 +kind: VerticalPodAutoscaler +metadata: + name: shared-statefulset + namespace: apps-ns +spec: + targetRef: + apiVersion: apps/v1 + kind: StatefulSet + name: shared + updatePolicy: + updateMode: Initial + resourcePolicy: + containerPolicies: + - containerName: "*" + controlledResources: + - cpu + - memory + controlledValues: RequestsAndLimits + minAllowed: + cpu: 50m + memory: 64Mi + maxAllowed: + cpu: "3" + memory: 6Gi diff --git a/tests/auto-vpa-generating-policy-statefulset/kyverno-test.yaml b/tests/auto-vpa-generating-policy-statefulset/kyverno-test.yaml new file mode 100644 index 0000000000..80061bd998 --- /dev/null +++ b/tests/auto-vpa-generating-policy-statefulset/kyverno-test.yaml @@ -0,0 +1,24 @@ +--- +# The platform's copy of the catalog's auto-vpa-statefulset GeneratingPolicy (#5130). +# The expected VPA is the catalog's own fixture (tests/auto-vpa/generated-statefulset-vpa.yaml +# at 44ed118), moved to a non-system namespace: the platform copy must generate +# exactly what the catalog does there. The system-namespace exclusion, which a +# kyverno test cannot assert, is pinned by +# scripts/tests/test-auto-vpa-generating-policy-exclusion.sh. +apiVersion: cli.kyverno.io/v1alpha1 +kind: Test +metadata: + name: auto-vpa-generating-policy-statefulset +policies: + - ../../k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml +resources: + - resources.yaml +variables: values.yaml +results: + - policy: auto-vpa-statefulset + rule: auto-vpa-statefulset + resources: + - apps-ns/shared + generatedResource: generated-vpa.yaml + kind: StatefulSet + result: pass diff --git a/tests/auto-vpa-generating-policy-statefulset/resources.yaml b/tests/auto-vpa-generating-policy-statefulset/resources.yaml new file mode 100644 index 0000000000..62b3cb49dd --- /dev/null +++ b/tests/auto-vpa-generating-policy-statefulset/resources.yaml @@ -0,0 +1,18 @@ +--- +apiVersion: apps/v1 +kind: StatefulSet +metadata: + name: shared + namespace: apps-ns +spec: + selector: + matchLabels: + app: web + template: + metadata: + labels: + app: web + spec: + containers: + - name: web + image: registry.k8s.io/pause:3.10 diff --git a/tests/auto-vpa-generating-policy-statefulset/values.yaml b/tests/auto-vpa-generating-policy-statefulset/values.yaml new file mode 100644 index 0000000000..f1b259038e --- /dev/null +++ b/tests/auto-vpa-generating-policy-statefulset/values.yaml @@ -0,0 +1,7 @@ +--- +apiVersion: cli.kyverno.io/v1alpha1 +kind: Values +namespaceSelector: + - name: apps-ns + labels: + kubernetes.io/metadata.name: apps-ns From bd9418f2d78da14490ef8fd2b2091e7b8a95ccad Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 18:29:16 +0200 Subject: [PATCH 3/4] fix(kyverno): split the auto-vpa GeneratingPolicy copy and approve its fingerprints The naming gate requires one resource per file, so the catalog's three policies are now one file each. The production authorization ledger approves the three new GeneratingPolicy documents with the fingerprints the validator printed for them; splitting the file changes no rendered document. Co-Authored-By: Claude Opus 5.5 --- .../best-practices/auto-vpa-daemonset.yaml | 89 ++++++ .../best-practices/auto-vpa-deployment.yaml | 115 ++++++++ .../auto-vpa-generating-policy.yaml | 263 ------------------ .../best-practices/auto-vpa-statefulset.yaml | 89 ++++++ .../cluster-policies/kustomization.yaml | 4 +- ...st-auto-vpa-generating-policy-exclusion.sh | 3 +- .../approved-surface.txt | 6 + .../kyverno-test.yaml | 2 +- .../kyverno-test.yaml | 2 +- .../kyverno-test.yaml | 2 +- 10 files changed, 307 insertions(+), 268 deletions(-) create mode 100644 k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-daemonset.yaml create mode 100644 k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-deployment.yaml delete mode 100644 k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml create mode 100644 k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-statefulset.yaml diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-daemonset.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-daemonset.yaml new file mode 100644 index 0000000000..2be846915b --- /dev/null +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-daemonset.yaml @@ -0,0 +1,89 @@ +--- +# ADOPTED from devantler-tech/kyverno-policies +# policies/best-practices/auto-vpa-generating-policy.yaml at 44ed118 (#5130), which +# registers three GeneratingPolicy objects in one file. The platform keeps one +# resource per file, so it is split into auto-vpa-deployment.yaml (which also +# carries the catalog's own header), auto-vpa-statefulset.yaml and +# auto-vpa-daemonset.yaml. Each policy is copied verbatim except for one marked +# PLATFORM ADDITION: a namespaceSelector that keeps it out of the system +# namespaces, as the classic best-practices/auto-vpa.yaml did. The generate +# expressions must stay byte-for-byte the catalog's (whitespace aside): both static +# producer guards pin them (scripts/guard-consumer-discovery-conservation.sh and +# scripts/tests/crossplane-egress-policy-rules.yaml). The classic VPAs are named +# , these are -, so running both forms at once would put two VPAs +# on every workload: register these only after the classic policy's VPAs are gone. +apiVersion: policies.kyverno.io/v1 +kind: GeneratingPolicy +metadata: + name: auto-vpa-daemonset + annotations: + policies.kyverno.io/title: Auto VPA for DaemonSets (GeneratingPolicy) + policies.kyverno.io/category: Autoscaling + policies.kyverno.io/severity: low + policies.kyverno.io/subject: DaemonSet + policies.kyverno.io/minversion: 1.19.0 + policies.kyverno.io/description: >- + Generates a CPU-only recommendation-bounded VerticalPodAutoscaler for each + DaemonSet. GeneratingPolicy form of the classic auto-vpa rule of the same name. +spec: + # Parity with the classic rules' `synchronize: true` + `generateExisting: true`: + # on GeneratingPolicy both live under spec.evaluation and default to false, so + # omitting them would silently drop behavior the classic policy guarantees. + evaluation: + generateExisting: + enabled: true + synchronize: + enabled: true + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["daemonsets"] + # PLATFORM ADDITION (the only change from the catalog file): prod ships + # static VPAs for the controllers in these namespaces + # (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/), so a + # generated one would put a second VPA on each of those pods. + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - kube-public + - kube-node-lease + - kyverno + generate: + - expression: >- + generator.Apply( + string(object.metadata.namespace), + [ + { + "apiVersion": dyn("autoscaling.k8s.io/v1"), + "kind": dyn("VerticalPodAutoscaler"), + "metadata": dyn({ + "name": string(object.metadata.name) + "-daemonset", + "namespace": string(object.metadata.namespace) + }), + "spec": dyn({ + "targetRef": dyn({ + "apiVersion": "apps/v1", + "kind": "DaemonSet", + "name": string(object.metadata.name) + }), + "updatePolicy": dyn({"updateMode": "InPlaceOrRecreate"}), + "resourcePolicy": dyn({ + "containerPolicies": [ + { + "containerName": dyn("*"), + "controlledResources": dyn(["cpu"]), + "controlledValues": dyn("RequestsAndLimits"), + "minAllowed": dyn({"cpu": "50m"}), + "maxAllowed": dyn({"cpu": "1"}) + } + ] + }) + }) + } + ] + ) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-deployment.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-deployment.yaml new file mode 100644 index 0000000000..0f5be4ee78 --- /dev/null +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-deployment.yaml @@ -0,0 +1,115 @@ +--- +# ADOPTED from devantler-tech/kyverno-policies +# policies/best-practices/auto-vpa-generating-policy.yaml at 44ed118 (#5130), which +# registers three GeneratingPolicy objects in one file. The platform keeps one +# resource per file, so it is split into auto-vpa-deployment.yaml (which also +# carries the catalog's own header), auto-vpa-statefulset.yaml and +# auto-vpa-daemonset.yaml. Each policy is copied verbatim except for one marked +# PLATFORM ADDITION: a namespaceSelector that keeps it out of the system +# namespaces, as the classic best-practices/auto-vpa.yaml did. The generate +# expressions must stay byte-for-byte the catalog's (whitespace aside): both static +# producer guards pin them (scripts/guard-consumer-discovery-conservation.sh and +# scripts/tests/crossplane-egress-policy-rules.yaml). The classic VPAs are named +# , these are -, so running both forms at once would put two VPAs +# on every workload: register these only after the classic policy's VPAs are gone. +# +# The catalog's header follows. In this repository the classic file is the rollback +# form, and "the root kustomization.yaml" means this folder's kustomization.yaml. +# +# The catalog's auto-vpa: a port of the classic policies/best-practices/auto-vpa.yaml +# ClusterPolicy rules to the GeneratingPolicy API, which Kyverno keeps after classic +# ClusterPolicy generate rules are retired. The classic ClusterPolicy is a separate file. +# +# THIS IS THE REGISTERED FORM in the root kustomization.yaml. The classic file stays in the +# repository, unregistered, as the rollback form until every consumer has migrated: never +# apply both to one cluster, because both generate a VerticalPodAutoscaler for the same +# workload under the same name. The README's "Migrating to the GeneratingPolicy form" +# section is the consumer rollout and rollback procedure. +# +# Split into one policy per workload kind, mirroring the classic policy's three rules: +# GeneratingPolicy applies spec.matchConstraints to the whole policy, so three kinds with +# three different VPA bodies cannot share one policy without branching inside CEL. +# +# FLOOR: Kyverno 1.19.0. On 1.18.x spec.evaluation.generateExisting is schema-accepted but +# does not fire for PRE-EXISTING workloads (kyverno/kyverno#15722, a regression against the +# classic rule; fixed by kyverno/kyverno#16041 for 1.19.0 and never backported), so an +# existing workload would get its VPA only on its next CREATE/UPDATE admission. The +# minversion annotations below declare that floor. +apiVersion: policies.kyverno.io/v1 +kind: GeneratingPolicy +metadata: + name: auto-vpa-deployment + annotations: + policies.kyverno.io/title: Auto VPA for Deployments (GeneratingPolicy) + policies.kyverno.io/category: Autoscaling + policies.kyverno.io/severity: low + policies.kyverno.io/subject: Deployment + policies.kyverno.io/minversion: 1.19.0 + policies.kyverno.io/description: >- + Generates a recommendation-bounded VerticalPodAutoscaler for each Deployment. + GeneratingPolicy form of the classic auto-vpa rule of the same name. +spec: + # Parity with the classic rules' `synchronize: true` + `generateExisting: true`: + # on GeneratingPolicy both live under spec.evaluation and default to false, so + # omitting them would silently drop behavior the classic policy guarantees. + evaluation: + generateExisting: + enabled: true + synchronize: + enabled: true + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["deployments"] + # PLATFORM ADDITION (the only change from the catalog file): prod ships + # static VPAs for the controllers in these namespaces + # (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/), so a + # generated one would put a second VPA on each of those pods. + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - kube-public + - kube-node-lease + - kyverno + generate: + # Every value is dyn(): CEL infers a map's value type from its first entry, so a + # resource literal mixing strings with nested maps fails to compile without it. + # Fields read off `object` are dyn and need string() before concatenation. + - expression: >- + generator.Apply( + string(object.metadata.namespace), + [ + { + "apiVersion": dyn("autoscaling.k8s.io/v1"), + "kind": dyn("VerticalPodAutoscaler"), + "metadata": dyn({ + "name": string(object.metadata.name) + "-deployment", + "namespace": string(object.metadata.namespace) + }), + "spec": dyn({ + "targetRef": dyn({ + "apiVersion": "apps/v1", + "kind": "Deployment", + "name": string(object.metadata.name) + }), + "updatePolicy": dyn({"updateMode": "InPlaceOrRecreate"}), + "resourcePolicy": dyn({ + "containerPolicies": [ + { + "containerName": dyn("*"), + "controlledResources": dyn(["cpu", "memory"]), + "controlledValues": dyn("RequestsAndLimits"), + "minAllowed": dyn({"cpu": "50m", "memory": "64Mi"}), + "maxAllowed": dyn({"cpu": "3", "memory": "6Gi"}) + } + ] + }) + }) + } + ] + ) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml deleted file mode 100644 index acae61ee1a..0000000000 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml +++ /dev/null @@ -1,263 +0,0 @@ ---- -# ADOPTED from devantler-tech/kyverno-policies -# policies/best-practices/auto-vpa-generating-policy.yaml at 44ed118 (#5130). The -# file is copied verbatim except for one marked PLATFORM ADDITION per policy: a -# namespaceSelector that keeps these policies out of the system namespaces, as the -# classic best-practices/auto-vpa.yaml did. The generate expressions must stay -# byte-for-byte the catalog's (whitespace aside): both static producer guards pin -# them (scripts/guard-consumer-discovery-conservation.sh and -# scripts/tests/crossplane-egress-policy-rules.yaml). In this repository the -# classic file is the rollback form and the "root kustomization.yaml" below means -# this folder's kustomization.yaml. The classic VPAs are named , these are -# -, so running both forms at once would put two VPAs on every -# workload: register this only after the classic policy's VPAs are gone. -# -# The catalog's auto-vpa: a port of the classic policies/best-practices/auto-vpa.yaml -# ClusterPolicy rules to the GeneratingPolicy API, which Kyverno keeps after classic -# ClusterPolicy generate rules are retired. The classic ClusterPolicy is a separate file. -# -# THIS IS THE REGISTERED FORM in the root kustomization.yaml. The classic file stays in the -# repository, unregistered, as the rollback form until every consumer has migrated: never -# apply both to one cluster, because both generate a VerticalPodAutoscaler for the same -# workload under the same name. The README's "Migrating to the GeneratingPolicy form" -# section is the consumer rollout and rollback procedure. -# -# Split into one policy per workload kind, mirroring the classic policy's three rules: -# GeneratingPolicy applies spec.matchConstraints to the whole policy, so three kinds with -# three different VPA bodies cannot share one policy without branching inside CEL. -# -# FLOOR: Kyverno 1.19.0. On 1.18.x spec.evaluation.generateExisting is schema-accepted but -# does not fire for PRE-EXISTING workloads (kyverno/kyverno#15722, a regression against the -# classic rule; fixed by kyverno/kyverno#16041 for 1.19.0 and never backported), so an -# existing workload would get its VPA only on its next CREATE/UPDATE admission. The -# minversion annotations below declare that floor. -apiVersion: policies.kyverno.io/v1 -kind: GeneratingPolicy -metadata: - name: auto-vpa-deployment - annotations: - policies.kyverno.io/title: Auto VPA for Deployments (GeneratingPolicy) - policies.kyverno.io/category: Autoscaling - policies.kyverno.io/severity: low - policies.kyverno.io/subject: Deployment - policies.kyverno.io/minversion: 1.19.0 - policies.kyverno.io/description: >- - Generates a recommendation-bounded VerticalPodAutoscaler for each Deployment. - GeneratingPolicy form of the classic auto-vpa rule of the same name. -spec: - # Parity with the classic rules' `synchronize: true` + `generateExisting: true`: - # on GeneratingPolicy both live under spec.evaluation and default to false, so - # omitting them would silently drop behavior the classic policy guarantees. - evaluation: - generateExisting: - enabled: true - synchronize: - enabled: true - matchConstraints: - resourceRules: - - apiGroups: ["apps"] - apiVersions: ["v1"] - operations: ["CREATE", "UPDATE"] - resources: ["deployments"] - # PLATFORM ADDITION (the only change from the catalog file): prod ships - # static VPAs for the controllers in these namespaces - # (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/), so a - # generated one would put a second VPA on each of those pods. - namespaceSelector: - matchExpressions: - - key: kubernetes.io/metadata.name - operator: NotIn - values: - - kube-system - - kube-public - - kube-node-lease - - kyverno - generate: - # Every value is dyn(): CEL infers a map's value type from its first entry, so a - # resource literal mixing strings with nested maps fails to compile without it. - # Fields read off `object` are dyn and need string() before concatenation. - - expression: >- - generator.Apply( - string(object.metadata.namespace), - [ - { - "apiVersion": dyn("autoscaling.k8s.io/v1"), - "kind": dyn("VerticalPodAutoscaler"), - "metadata": dyn({ - "name": string(object.metadata.name) + "-deployment", - "namespace": string(object.metadata.namespace) - }), - "spec": dyn({ - "targetRef": dyn({ - "apiVersion": "apps/v1", - "kind": "Deployment", - "name": string(object.metadata.name) - }), - "updatePolicy": dyn({"updateMode": "InPlaceOrRecreate"}), - "resourcePolicy": dyn({ - "containerPolicies": [ - { - "containerName": dyn("*"), - "controlledResources": dyn(["cpu", "memory"]), - "controlledValues": dyn("RequestsAndLimits"), - "minAllowed": dyn({"cpu": "50m", "memory": "64Mi"}), - "maxAllowed": dyn({"cpu": "3", "memory": "6Gi"}) - } - ] - }) - }) - } - ] - ) ---- -apiVersion: policies.kyverno.io/v1 -kind: GeneratingPolicy -metadata: - name: auto-vpa-statefulset - annotations: - policies.kyverno.io/title: Auto VPA for StatefulSets (GeneratingPolicy) - policies.kyverno.io/category: Autoscaling - policies.kyverno.io/severity: low - policies.kyverno.io/subject: StatefulSet - policies.kyverno.io/minversion: 1.19.0 - policies.kyverno.io/description: >- - Generates a recommendation-bounded VerticalPodAutoscaler for each StatefulSet. - GeneratingPolicy form of the classic auto-vpa rule of the same name. -spec: - # Parity with the classic rules' `synchronize: true` + `generateExisting: true`: - # on GeneratingPolicy both live under spec.evaluation and default to false, so - # omitting them would silently drop behavior the classic policy guarantees. - evaluation: - generateExisting: - enabled: true - synchronize: - enabled: true - matchConstraints: - resourceRules: - - apiGroups: ["apps"] - apiVersions: ["v1"] - operations: ["CREATE", "UPDATE"] - resources: ["statefulsets"] - # PLATFORM ADDITION (the only change from the catalog file): prod ships - # static VPAs for the controllers in these namespaces - # (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/), so a - # generated one would put a second VPA on each of those pods. - namespaceSelector: - matchExpressions: - - key: kubernetes.io/metadata.name - operator: NotIn - values: - - kube-system - - kube-public - - kube-node-lease - - kyverno - generate: - - expression: >- - generator.Apply( - string(object.metadata.namespace), - [ - { - "apiVersion": dyn("autoscaling.k8s.io/v1"), - "kind": dyn("VerticalPodAutoscaler"), - "metadata": dyn({ - "name": string(object.metadata.name) + "-statefulset", - "namespace": string(object.metadata.namespace) - }), - "spec": dyn({ - "targetRef": dyn({ - "apiVersion": "apps/v1", - "kind": "StatefulSet", - "name": string(object.metadata.name) - }), - "updatePolicy": dyn({"updateMode": "Initial"}), - "resourcePolicy": dyn({ - "containerPolicies": [ - { - "containerName": dyn("*"), - "controlledResources": dyn(["cpu", "memory"]), - "controlledValues": dyn("RequestsAndLimits"), - "minAllowed": dyn({"cpu": "50m", "memory": "64Mi"}), - "maxAllowed": dyn({"cpu": "3", "memory": "6Gi"}) - } - ] - }) - }) - } - ] - ) ---- -apiVersion: policies.kyverno.io/v1 -kind: GeneratingPolicy -metadata: - name: auto-vpa-daemonset - annotations: - policies.kyverno.io/title: Auto VPA for DaemonSets (GeneratingPolicy) - policies.kyverno.io/category: Autoscaling - policies.kyverno.io/severity: low - policies.kyverno.io/subject: DaemonSet - policies.kyverno.io/minversion: 1.19.0 - policies.kyverno.io/description: >- - Generates a CPU-only recommendation-bounded VerticalPodAutoscaler for each - DaemonSet. GeneratingPolicy form of the classic auto-vpa rule of the same name. -spec: - # Parity with the classic rules' `synchronize: true` + `generateExisting: true`: - # on GeneratingPolicy both live under spec.evaluation and default to false, so - # omitting them would silently drop behavior the classic policy guarantees. - evaluation: - generateExisting: - enabled: true - synchronize: - enabled: true - matchConstraints: - resourceRules: - - apiGroups: ["apps"] - apiVersions: ["v1"] - operations: ["CREATE", "UPDATE"] - resources: ["daemonsets"] - # PLATFORM ADDITION (the only change from the catalog file): prod ships - # static VPAs for the controllers in these namespaces - # (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/), so a - # generated one would put a second VPA on each of those pods. - namespaceSelector: - matchExpressions: - - key: kubernetes.io/metadata.name - operator: NotIn - values: - - kube-system - - kube-public - - kube-node-lease - - kyverno - generate: - - expression: >- - generator.Apply( - string(object.metadata.namespace), - [ - { - "apiVersion": dyn("autoscaling.k8s.io/v1"), - "kind": dyn("VerticalPodAutoscaler"), - "metadata": dyn({ - "name": string(object.metadata.name) + "-daemonset", - "namespace": string(object.metadata.namespace) - }), - "spec": dyn({ - "targetRef": dyn({ - "apiVersion": "apps/v1", - "kind": "DaemonSet", - "name": string(object.metadata.name) - }), - "updatePolicy": dyn({"updateMode": "InPlaceOrRecreate"}), - "resourcePolicy": dyn({ - "containerPolicies": [ - { - "containerName": dyn("*"), - "controlledResources": dyn(["cpu"]), - "controlledValues": dyn("RequestsAndLimits"), - "minAllowed": dyn({"cpu": "50m"}), - "maxAllowed": dyn({"cpu": "1"}) - } - ] - }) - }) - } - ] - ) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-statefulset.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-statefulset.yaml new file mode 100644 index 0000000000..bf2ec26066 --- /dev/null +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-statefulset.yaml @@ -0,0 +1,89 @@ +--- +# ADOPTED from devantler-tech/kyverno-policies +# policies/best-practices/auto-vpa-generating-policy.yaml at 44ed118 (#5130), which +# registers three GeneratingPolicy objects in one file. The platform keeps one +# resource per file, so it is split into auto-vpa-deployment.yaml (which also +# carries the catalog's own header), auto-vpa-statefulset.yaml and +# auto-vpa-daemonset.yaml. Each policy is copied verbatim except for one marked +# PLATFORM ADDITION: a namespaceSelector that keeps it out of the system +# namespaces, as the classic best-practices/auto-vpa.yaml did. The generate +# expressions must stay byte-for-byte the catalog's (whitespace aside): both static +# producer guards pin them (scripts/guard-consumer-discovery-conservation.sh and +# scripts/tests/crossplane-egress-policy-rules.yaml). The classic VPAs are named +# , these are -, so running both forms at once would put two VPAs +# on every workload: register these only after the classic policy's VPAs are gone. +apiVersion: policies.kyverno.io/v1 +kind: GeneratingPolicy +metadata: + name: auto-vpa-statefulset + annotations: + policies.kyverno.io/title: Auto VPA for StatefulSets (GeneratingPolicy) + policies.kyverno.io/category: Autoscaling + policies.kyverno.io/severity: low + policies.kyverno.io/subject: StatefulSet + policies.kyverno.io/minversion: 1.19.0 + policies.kyverno.io/description: >- + Generates a recommendation-bounded VerticalPodAutoscaler for each StatefulSet. + GeneratingPolicy form of the classic auto-vpa rule of the same name. +spec: + # Parity with the classic rules' `synchronize: true` + `generateExisting: true`: + # on GeneratingPolicy both live under spec.evaluation and default to false, so + # omitting them would silently drop behavior the classic policy guarantees. + evaluation: + generateExisting: + enabled: true + synchronize: + enabled: true + matchConstraints: + resourceRules: + - apiGroups: ["apps"] + apiVersions: ["v1"] + operations: ["CREATE", "UPDATE"] + resources: ["statefulsets"] + # PLATFORM ADDITION (the only change from the catalog file): prod ships + # static VPAs for the controllers in these namespaces + # (k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers/), so a + # generated one would put a second VPA on each of those pods. + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: + - kube-system + - kube-public + - kube-node-lease + - kyverno + generate: + - expression: >- + generator.Apply( + string(object.metadata.namespace), + [ + { + "apiVersion": dyn("autoscaling.k8s.io/v1"), + "kind": dyn("VerticalPodAutoscaler"), + "metadata": dyn({ + "name": string(object.metadata.name) + "-statefulset", + "namespace": string(object.metadata.namespace) + }), + "spec": dyn({ + "targetRef": dyn({ + "apiVersion": "apps/v1", + "kind": "StatefulSet", + "name": string(object.metadata.name) + }), + "updatePolicy": dyn({"updateMode": "Initial"}), + "resourcePolicy": dyn({ + "containerPolicies": [ + { + "containerName": dyn("*"), + "controlledResources": dyn(["cpu", "memory"]), + "controlledValues": dyn("RequestsAndLimits"), + "minAllowed": dyn({"cpu": "50m", "memory": "64Mi"}), + "maxAllowed": dyn({"cpu": "3", "memory": "6Gi"}) + } + ] + }) + }) + } + ] + ) diff --git a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml index 4dd2ed5331..1d7190f067 100644 --- a/k8s/bases/infrastructure/cluster-policies/kustomization.yaml +++ b/k8s/bases/infrastructure/cluster-policies/kustomization.yaml @@ -13,7 +13,9 @@ resources: # form, is deliberately NOT registered: it stays, still tested, as the # rollback form. Never register both, because they name their VPAs # differently and every workload would get two. - - best-practices/auto-vpa-generating-policy.yaml + - best-practices/auto-vpa-daemonset.yaml + - best-practices/auto-vpa-deployment.yaml + - best-practices/auto-vpa-statefulset.yaml - best-practices/audit-privileged-rbac.yaml - best-practices/disable-default-sa-automount.yaml - best-practices/disallow-latest-tag.yaml diff --git a/scripts/tests/test-auto-vpa-generating-policy-exclusion.sh b/scripts/tests/test-auto-vpa-generating-policy-exclusion.sh index 274f4a38bd..4cc454137a 100755 --- a/scripts/tests/test-auto-vpa-generating-policy-exclusion.sh +++ b/scripts/tests/test-auto-vpa-generating-policy-exclusion.sh @@ -12,7 +12,7 @@ set -euo pipefail script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd "${script_dir}/../.." && pwd)" -policy="${repo_root}/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml" +policies="${repo_root}/k8s/bases/infrastructure/cluster-policies/best-practices" static_vpas="${repo_root}/k8s/providers/hetzner/infrastructure/vertical-pod-autoscalers" work="$(mktemp -d)" @@ -34,6 +34,7 @@ fi checked=0 for kind in deployment statefulset daemonset; do name="auto-vpa-${kind}" + policy="${policies}/${name}.yaml" fixtures="${repo_root}/tests/auto-vpa-generating-policy-${kind}" excluded="$(yq -N "select(.metadata.name == \"${name}\") | .spec.matchConstraints.namespaceSelector.matchExpressions[] diff --git a/scripts/validate-eks-ci-role-policy/approved-surface.txt b/scripts/validate-eks-ci-role-policy/approved-surface.txt index 1446828a5b..7ede6c0d9f 100644 --- a/scripts/validate-eks-ci-role-policy/approved-surface.txt +++ b/scripts/validate-eks-ci-role-policy/approved-surface.txt @@ -148,6 +148,12 @@ pkg.crossplane.io/v1|Provider||provider-upjet-unifi d85c9714e797838b3a1c88bb2543 pkg.crossplane.io/v1|Provider||upbound-provider-family-aws 64c7d602bf7460b7a38657a49539f5fa945ef8216be5e4296dddd04f69a944f8 +policies.kyverno.io/v1|GeneratingPolicy||auto-vpa-daemonset 28dd85d6dedce48c3d60055aad47da956107a320ec6d06e594452253a72d13dc + +policies.kyverno.io/v1|GeneratingPolicy||auto-vpa-deployment 804637a8c0e29af0ffb16e9fff9bd03fb25192676abf04ce500dbb5372bb082b + +policies.kyverno.io/v1|GeneratingPolicy||auto-vpa-statefulset 91cde3f6d780f91211e596a9dcbc8aad4c624406c3ceef0001416d3eeff00a0d + policies.kyverno.io/v1|MutatingPolicy||helm-release-install-crds e4eff687fb6ed05f18ab75747073569c4cede6309f560dd10d17adcfc71da2df rbac.authorization.k8s.io/v1|ClusterRoleBinding||ascoachingogvaner-external-dns 2aac6fc040557b155131fdc2ad9261f6ad2b337835915d7dcb50a3704e032478 diff --git a/tests/auto-vpa-generating-policy-daemonset/kyverno-test.yaml b/tests/auto-vpa-generating-policy-daemonset/kyverno-test.yaml index 0f11c5dd9b..76e0f62f74 100644 --- a/tests/auto-vpa-generating-policy-daemonset/kyverno-test.yaml +++ b/tests/auto-vpa-generating-policy-daemonset/kyverno-test.yaml @@ -10,7 +10,7 @@ kind: Test metadata: name: auto-vpa-generating-policy-daemonset policies: - - ../../k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml + - ../../k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-daemonset.yaml resources: - resources.yaml variables: values.yaml diff --git a/tests/auto-vpa-generating-policy-deployment/kyverno-test.yaml b/tests/auto-vpa-generating-policy-deployment/kyverno-test.yaml index 29720fa7b2..d7107b84ae 100644 --- a/tests/auto-vpa-generating-policy-deployment/kyverno-test.yaml +++ b/tests/auto-vpa-generating-policy-deployment/kyverno-test.yaml @@ -10,7 +10,7 @@ kind: Test metadata: name: auto-vpa-generating-policy-deployment policies: - - ../../k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml + - ../../k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-deployment.yaml resources: - resources.yaml variables: values.yaml diff --git a/tests/auto-vpa-generating-policy-statefulset/kyverno-test.yaml b/tests/auto-vpa-generating-policy-statefulset/kyverno-test.yaml index 80061bd998..0518ec7c6b 100644 --- a/tests/auto-vpa-generating-policy-statefulset/kyverno-test.yaml +++ b/tests/auto-vpa-generating-policy-statefulset/kyverno-test.yaml @@ -10,7 +10,7 @@ kind: Test metadata: name: auto-vpa-generating-policy-statefulset policies: - - ../../k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-generating-policy.yaml + - ../../k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-statefulset.yaml resources: - resources.yaml variables: values.yaml From af7e35f7720a028b172521fab7dfb1bf7ab8069d Mon Sep 17 00:00:00 2001 From: Nikolai Emil Damm Date: Tue, 20 Oct 2026 19:33:40 +0200 Subject: [PATCH 4/4] docs(kyverno): correct the catalog header's VPA-name claim for this repository Co-Authored-By: Claude Opus 5.5 --- .../cluster-policies/best-practices/auto-vpa-deployment.yaml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-deployment.yaml b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-deployment.yaml index 0f5be4ee78..6feb459e06 100644 --- a/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-deployment.yaml +++ b/k8s/bases/infrastructure/cluster-policies/best-practices/auto-vpa-deployment.yaml @@ -15,6 +15,8 @@ # # The catalog's header follows. In this repository the classic file is the rollback # form, and "the root kustomization.yaml" means this folder's kustomization.yaml. +# "Under the same name" below is not true here: the classic VPA is and this +# one is -deployment, so running both forms gives every workload two VPAs. # # The catalog's auto-vpa: a port of the classic policies/best-practices/auto-vpa.yaml # ClusterPolicy rules to the GeneratingPolicy API, which Kyverno keeps after classic