diff --git a/scripts/guard-consumer-discovery-conservation.sh b/scripts/guard-consumer-discovery-conservation.sh index 41e48cd5c..b2a02656d 100755 --- a/scripts/guard-consumer-discovery-conservation.sh +++ b/scripts/guard-consumer-discovery-conservation.sh @@ -749,7 +749,10 @@ while IFS=$'\t' read -r file label; do # The second reviewed shape (#5102) is the shared catalog's auto-vpa: a # Kyverno GeneratingPolicy whose spec holds only evaluation settings, one # apps resource rule (apiGroups, apiVersions, operations, resources) naming - # one deployments, daemonsets or statefulsets resource, and one generate + # one deployments, daemonsets or statefulsets resource, optionally a + # namespaceSelector that only excludes named namespaces (#5125: matchExpressions + # alone, each kubernetes.io/metadata.name NotIn a list of strings; a label + # selector is not CEL and can only narrow what triggers), and one generate # expression equal, whitespace aside, to the reviewed one for that resource # (REVIEWED_AUTO_VPA_*). That expression builds only a # VerticalPodAutoscaler for the triggering workload, in its own namespace, @@ -791,7 +794,19 @@ while IFS=$'\t' read -r file label; do and ([.spec.evaluation | keys | .[] | select((. == "admission" or . == "generateExisting" or . == "synchronize" or . == "orphanDownstreamOnPolicyDelete") | not)] | length) == 0)) and (.spec.matchConstraints | type) == "!!map" - and ([.spec.matchConstraints | keys | .[] | select(. != "resourceRules")] | length) == 0 + and ([.spec.matchConstraints | keys | .[] | select((. == "resourceRules" + or . == "namespaceSelector") | not)] | length) == 0 + and ((.spec.matchConstraints | has("namespaceSelector") | not) + or ((.spec.matchConstraints.namespaceSelector | type) == "!!map" + and ([.spec.matchConstraints.namespaceSelector | keys | .[] | select(. != "matchExpressions")] | length) == 0 + and (.spec.matchConstraints.namespaceSelector.matchExpressions | type) == "!!seq" + and (.spec.matchConstraints.namespaceSelector.matchExpressions | length) > 0 + and ([.spec.matchConstraints.namespaceSelector.matchExpressions[] | select(type != "!!map" + or ([keys | .[] | select((. == "key" or . == "operator" or . == "values") | not)] | length) != 0 + or (.key | type) != "!!str" or .key != "kubernetes.io/metadata.name" + or (.operator | type) != "!!str" or .operator != "NotIn" + or (.values | type) != "!!seq" or (.values | length) == 0 + or ([.values[] | select(type != "!!str")] | length) != 0)] | length) == 0)) and (.spec.matchConstraints.resourceRules | type) == "!!seq" and (.spec.matchConstraints.resourceRules | length) == 1 and (.spec.matchConstraints.resourceRules[0] | type) == "!!map" diff --git a/scripts/tests/crossplane-egress-policy-rules.yaml b/scripts/tests/crossplane-egress-policy-rules.yaml index f64430b77..d2f6b7c02 100644 --- a/scripts/tests/crossplane-egress-policy-rules.yaml +++ b/scripts/tests/crossplane-egress-policy-rules.yaml @@ -644,7 +644,10 @@ rules: # policies/best-practices/auto-vpa-generating-policy.yaml): a # GeneratingPolicy whose spec holds only evaluation settings, one apps # resource rule (apiGroups, apiVersions, operations, resources) naming one - # deployments, daemonsets or statefulsets resource, and one generate + # deployments, daemonsets or statefulsets resource, optionally (#5125) a + # namespaceSelector whose only field is matchExpressions, each + # kubernetes.io/metadata.name NotIn a list of names (a label selector is not + # CEL and can only narrow which workloads trigger it), and one generate # expression that equals the catalog's expression for that resource once # spaces, tabs and line breaks are removed from both. Every other spec field # is refused, because variables and matchConditions are CEL that can read or @@ -707,7 +710,20 @@ rules: 'admission', 'generateExisting', 'synchronize', 'orphanDownstreamOnPolicyDelete'])) && has(object.spec.matchConstraints) && - object.spec.matchConstraints.all(key, key == 'resourceRules') && + object.spec.matchConstraints.all(key, + key in ['resourceRules', 'namespaceSelector']) && + (!has(object.spec.matchConstraints.namespaceSelector) || + (object.spec.matchConstraints.namespaceSelector.all(key, + key == 'matchExpressions') && + has(object.spec.matchConstraints.namespaceSelector.matchExpressions) && + object.spec.matchConstraints.namespaceSelector.matchExpressions.size() > 0 && + object.spec.matchConstraints.namespaceSelector.matchExpressions.all(expression, + expression.all(key, key in ['key', 'operator', 'values']) && + has(expression.key) && + expression.key == 'kubernetes.io/metadata.name' && + has(expression.operator) && expression.operator == 'NotIn' && + has(expression.values) && expression.values.size() > 0 && + expression.values.all(value, type(value) == string)))) && has(object.spec.matchConstraints.resourceRules) && object.spec.matchConstraints.resourceRules.size() == 1 && object.spec.matchConstraints.resourceRules[0].all(key, key in [ diff --git a/scripts/tests/test-crossplane-egress-policy.sh b/scripts/tests/test-crossplane-egress-policy.sh index 336df9f1e..e2d57e0fd 100755 --- a/scripts/tests/test-crossplane-egress-policy.sh +++ b/scripts/tests/test-crossplane-egress-policy.sh @@ -1175,6 +1175,76 @@ assert_helm_rules_accept \ "${auto_vpa_one_line_fixture}" \ 'a reviewed GeneratingPolicy that differs only in whitespace is the reviewed exception' +# #5125: a namespaceSelector that only excludes named namespaces keeps the +# reviewed shape; every other selector form voids it. +auto_vpa_namespace_exclusion_fixture="${auto_vpa_generating_policy_fixture/ matchConstraints: +/ matchConstraints: + namespaceSelector: + matchExpressions: + - key: kubernetes.io/metadata.name + operator: NotIn + values: [\"kube-system\", \"kube-public\", \"kube-node-lease\", \"kyverno\"] +}" +[ "${auto_vpa_namespace_exclusion_fixture}" != "${auto_vpa_generating_policy_fixture}" ] || + fail 'the namespace-excluded GeneratingPolicy must differ from the reviewed one' +assert_helm_rules_accept \ + 'auto-vpa-generating-policy-namespace-exclusion' \ + "${auto_vpa_namespace_exclusion_fixture}" \ + 'a reviewed GeneratingPolicy that excludes named namespaces is the reviewed exception' + +assert_namespace_exclusion_variant_rejected() { + local fixture_name="$1" + local fixture_body="$2" + local failure_message="$3" + + [ "${fixture_body}" != "${auto_vpa_namespace_exclusion_fixture}" ] || + fail "${fixture_name}: the variant must differ from the namespace-excluded GeneratingPolicy" + assert_helm_rules_reject \ + "${fixture_name}" \ + "${fixture_body}" \ + "${failure_message}" \ + 'reject-cel-policy-producers' +} + +assert_namespace_exclusion_variant_rejected \ + 'generating-policy-namespace-empty-expressions' \ + "${auto_vpa_namespace_exclusion_fixture/ matchExpressions: + - key: kubernetes.io\/metadata.name + operator: NotIn + values: \[\"kube-system\", \"kube-public\", \"kube-node-lease\", \"kyverno\"\]/ matchExpressions: []}" \ + 'a namespaceSelector with no expressions must void the reviewed GeneratingPolicy shape' +assert_namespace_exclusion_variant_rejected \ + 'generating-policy-namespace-match-labels' \ + "${auto_vpa_namespace_exclusion_fixture/ matchExpressions:/ matchLabels: + team: platform + matchExpressions:}" \ + 'a namespaceSelector with matchLabels must void the reviewed GeneratingPolicy shape' +assert_namespace_exclusion_variant_rejected \ + 'generating-policy-namespace-in-operator' \ + "${auto_vpa_namespace_exclusion_fixture/operator: NotIn/operator: In}" \ + 'a namespaceSelector that selects namespaces must void the reviewed GeneratingPolicy shape' +assert_namespace_exclusion_variant_rejected \ + 'generating-policy-namespace-exists-operator' \ + "${auto_vpa_namespace_exclusion_fixture/operator: NotIn/operator: Exists}" \ + 'an Exists namespaceSelector must void the reviewed GeneratingPolicy shape' +assert_namespace_exclusion_variant_rejected \ + 'generating-policy-namespace-other-key' \ + "${auto_vpa_namespace_exclusion_fixture/key: kubernetes.io\/metadata.name/key: team}" \ + 'a namespaceSelector on another label must void the reviewed GeneratingPolicy shape' +assert_namespace_exclusion_variant_rejected \ + 'generating-policy-namespace-empty-values' \ + "${auto_vpa_namespace_exclusion_fixture/values: \[\"kube-system\", \"kube-public\", \"kube-node-lease\", \"kyverno\"\]/values: []}" \ + 'a namespaceSelector with no values must void the reviewed GeneratingPolicy shape' +assert_reviewed_generation_variant_rejected \ + 'generating-policy-object-selector' \ + "${auto_vpa_generating_policy_fixture/ matchConstraints: +/ matchConstraints: + objectSelector: + matchLabels: + app: x +}" \ + 'an objectSelector must void the reviewed GeneratingPolicy shape' + helm_admin_network_policy_fixture=$'apiVersion: policy.networking.k8s.io/v1alpha1\nkind: AdminNetworkPolicy\nmetadata:\n name: crossplane-world-egress\nspec:\n priority: 10\n subject:\n namespaces:\n matchLabels:\n kubernetes.io/metadata.name: crossplane-system\n egress:\n - name: allow-world\n action: Allow\n to:\n - networks: [0.0.0.0/0]' assert_helm_rules_reject \ 'helm-crossplane-admin-network-policy' \ diff --git a/scripts/tests/test-guard-consumer-discovery-conservation.sh b/scripts/tests/test-guard-consumer-discovery-conservation.sh index df1a7d28a..2aa0a7437 100755 --- a/scripts/tests/test-guard-consumer-discovery-conservation.sh +++ b/scripts/tests/test-guard-consumer-discovery-conservation.sh @@ -1221,6 +1221,32 @@ YAML { printf 'FAIL: the one-line variant did not change the reviewed GeneratingPolicy\n' >&2; failures=$((failures + 1)); } printf ' - cel-generation.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" expect_pass 'the reviewed auto-vpa GeneratingPolicy on one line does not alter consumers' "$root" '2 consumer(s)' + # #5125: a NotIn exclusion of named namespaces passes too; a label selector + # only narrows which workloads the policy reacts to. + root="$(fixture reviewed-cel-generation-namespace-exclusion)" + yq '.spec.matchConstraints.namespaceSelector = {"matchExpressions": [{"key": "kubernetes.io/metadata.name", "operator": "NotIn", "values": ["kube-system", "kube-public", "kube-node-lease", "kyverno"]}]}' \ + "$WORK/reviewed-cel-generation/k8s/providers/prod/apps/cel-generation.yaml" >"$root/k8s/providers/prod/apps/cel-generation.yaml" + printf ' - cel-generation.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" + expect_pass 'the reviewed auto-vpa GeneratingPolicy with a namespace exclusion does not alter consumers' "$root" '2 consumer(s)' + for target in match-labels in-operator exists-operator other-key object-selector empty-expressions empty-values non-string-value extra-expression-key; do + root="$(fixture "reviewed-cel-generation-namespace-exclusion-$target")" + cp "$WORK/reviewed-cel-generation-namespace-exclusion/k8s/providers/prod/apps/cel-generation.yaml" "$root/k8s/providers/prod/apps/cel-generation.yaml" + case "$target" in + match-labels) yq -i '.spec.matchConstraints.namespaceSelector.matchLabels = {"team": "platform"}' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; + in-operator) yq -i '.spec.matchConstraints.namespaceSelector.matchExpressions[0].operator = "In"' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; + exists-operator) yq -i '.spec.matchConstraints.namespaceSelector.matchExpressions[0] |= (.operator = "Exists" | del(.values))' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; + other-key) yq -i '.spec.matchConstraints.namespaceSelector.matchExpressions[0].key = "team"' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; + object-selector) yq -i '.spec.matchConstraints.objectSelector = {"matchLabels": {"app": "x"}}' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; + empty-expressions) yq -i '.spec.matchConstraints.namespaceSelector.matchExpressions = []' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; + empty-values) yq -i '.spec.matchConstraints.namespaceSelector.matchExpressions[0].values = []' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; + non-string-value) yq -i '.spec.matchConstraints.namespaceSelector.matchExpressions[0].values += [{"name": "x"}]' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; + extra-expression-key) yq -i '.spec.matchConstraints.namespaceSelector.matchExpressions[0].extra = "x"' "$root/k8s/providers/prod/apps/cel-generation.yaml" ;; + esac + cmp -s "$WORK/reviewed-cel-generation-namespace-exclusion/k8s/providers/prod/apps/cel-generation.yaml" "$root/k8s/providers/prod/apps/cel-generation.yaml" && + { printf 'FAIL: the %s variant did not change the namespace-excluded GeneratingPolicy\n' "$target" >&2; failures=$((failures + 1)); } + printf ' - cel-generation.yaml\n' >>"$root/k8s/providers/prod/apps/kustomization.yaml" + expect_refusal "a $target namespace selector is outside the reviewed shape" "$root" 'unevaluated CEL admission mutation' 'consumers are UNKNOWN' + done for target in network-policy-kind source-api-version namespace-trigger extra-group variables resource-clone workload-clone computed-key escaped-key label-read generated-deployment unbound-apps-kind unbound-target-ref other-target-name literal-apply-namespace literal-object-namespace mismatched-trigger second-generation match-conditions foreign-group split-literal delete-operation any-version; do root="$(fixture "reviewed-cel-generation-$target")" cp "$WORK/reviewed-cel-generation/k8s/providers/prod/apps/cel-generation.yaml" "$root/k8s/providers/prod/apps/cel-generation.yaml"