Kustomize delete resource: Kyverno PDBs and ESO webhooks
Kustomize patch delete resource on a rendered Helm chart, and the same with Kapitan's mutate: Kyverno's forced PDBs and External Secrets' webhooks, on kind.
Some Helm charts render an object you need to own yourself and offer no
value to leave it out. This post covers two such cases from open issues:
the Kyverno chart creates a PodDisruptionBudget for every controller that
runs more than one replica, and the External Secrets chart renders its two
ValidatingWebhookConfigurations whenever it renders the webhook. In both,
the object is deleted from the rendered manifests, first with a Kustomize
patch over helm template, then with Kapitan’s
mutate generator.
Each patch was rendered from the chart version named in its section with Helm 4.3.1, Kustomize 5.8.3, and Kapitan 0.36.3 with mutate 0.1.0, and the result was installed on a kind cluster on Kubernetes 1.37.0. The guide Patch a Helm chart compares the places such a patch can live.
Kyverno Helm chart: PodDisruptionBudgets with more than one replica
kyverno/kyverno#11469,
opened in October 2024, reports that the chart’s PodDisruptionBudgets cannot
be turned off. The reporter wants to supply PDBs of their own instead. The
maintainer realshuting
asked
the authors of the change that introduced the behaviour whether there was a
reason to keep PDBs on for more than one replica, and the question is
unanswered. In chart 3.9.2, published to the chart repository and to
ghcr.io on 21 October 2026, the PDB template of each of the four
controllers has the same condition, here from
admission-controller/poddisruptionbudget.yaml:
{{- if or .Values.admissionController.podDisruptionBudget.enabled (gt (int .Values.admissionController.replicas) 1) -}}podDisruptionBudget.enabled: false is the default, and with more than one
replica the or ignores it. The chart’s values.yaml says so: “Will
always be enabled if replicas > 1. This non-declarative behavior should
ideally be avoided, but changing it now would be breaking.” With these
values the chart rendered four PDBs, and the same four with
podDisruptionBudget.enabled=false set for every controller:
admissionController:
replicas: 3
backgroundController:
replicas: 2
cleanupController:
replicas: 2
reportsController:
replicas: 2All four carry the label app.kubernetes.io/part-of: kyverno. Its value is
the chart’s fullname, so it follows the release name: release policy
gives policy-kyverno.
Kustomize over helm template
helm template kyverno oci://ghcr.io/kyverno/charts/kyverno --version 3.9.2 \
--namespace kyverno -f kyverno-values.yaml > helm.yaml
kustomize build .with this kustomization.yaml next to helm.yaml:
resources:
- helm.yaml
patches:
- target:
kind: PodDisruptionBudget
labelSelector: app.kubernetes.io/part-of=kyverno
patch: |-
$patch: delete
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: ignoredWith a target, Kustomize applies the patch to every object the selector
matches, here the four PDBs, and the metadata.name in the patch is
required but not used. The output was the render without the four PDBs and
otherwise unchanged. A selector that matches nothing is not an error: over
a render for release policy, the build passed and kept all four PDBs.
Kapitan’s mutate
The class pins the chart by version and by the digest of its OCI artifact, and reads the same values file:
parameters:
kapitan:
dependencies:
- type: helm
source: oci://ghcr.io/kyverno/charts/kyverno@sha256:83054f30148c3cfc001087c1f5772eda7c7224e9b74b7f0e70c39c9cafe009d1
chart_name: kyverno
version: 3.9.2
output_path: charts/kyverno
compile:
- input_type: helm
input_paths: [charts/kyverno]
output_path: kyverno
helm_values_files: [kyverno-values.yaml]
helm_params:
name: kyverno
namespace: kyvernoThe mutation deletes the PDBs by label and states how many it expects:
parameters:
mutations:
no-pdbs:
match: {apiVersion: policy/v1, kind: PodDisruptionBudget, labels: {app.kubernetes.io/part-of: kyverno}}
delete: true
expects: 4The target adds mutate as a second compile entry on
the chart’s output_path, as the guide shows. Each PDB file became a
single comment line, and the rest of the render was unchanged. With the
class changed to release policy, the compile stops:
mutate: mutation no-pdbs: expects 4, matched 0Helm hooks in the rendered output
helm template also renders the chart’s hooks: a post-upgrade Job
kyverno-migrate-resources with its ServiceAccount, ClusterRole and
ClusterRoleBinding, and the pre-delete Jobs kyverno-scale-to-zero and
kyverno-rm-webhooks. helm install runs each of them only at its hook.
kubectl apply of the whole output runs them at once, and on kind the
Job kyverno-scale-to-zero left every controller at zero replicas:
kyverno-admission-controller 0/0 0 0 5s
kyverno-background-controller 0/0 0 0 5sThe test therefore applied every object except those with a
helm.sh/hook annotation, as helm install would. A tool that maps Helm
hooks to its own, as Argo CD does, needs no such filter.
On kind
The four controllers, with three admission controller replicas and two of
each other, all reported Available, and the namespace held no
PodDisruptionBudget. The Kustomize output, without hooks, passed a
server-side dry run. To check that Kyverno still enforces policies, the
test added a ValidatingPolicy that requires the label team on ConfigMaps
in the namespace demo. A ConfigMap without it was refused:
error: failed to create configmap: admission webhook "vpol.validate.kyverno.svc-fail" denied the request: Policy require-team-label failed: label team is requiredExternal Secrets Helm chart: the ValidatingWebhookConfigurations
external-secrets/external-secrets#6961, opened in September 2026, asks for a value that leaves the two ValidatingWebhookConfigurations out of the chart while it still deploys the webhook. The reporter applies cluster-scoped objects from a separate chart and wants the configurations to come from there, with the external-secrets chart used as published. The maintainers asked for the use case. alekc called it “a very niche usecase which would be best suited by a custom helm chart for your org” and wrote that such a toggle “would mean allowing users to deploy a broken (unless they take separate steps outside of the helm chart) deployment of eso”. evrardj-roche closed the pull request with the toggle “temporarily until we have clarified the multi-tenant story” and pointed to the multi-namespace design in #7023.
In chart 2.13.0, the release of 16 October 2026,
validatingwebhook.yaml
renders secretstore-validate and externalsecret-validate under the same
condition as the webhook itself:
{{- if .Values.webhook.create }}
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: secretstore-validatewebhook.create=false removes the configurations and also the webhook’s
Deployment, Service, Secret and ServiceAccount. The configurations have
failurePolicy: Fail and no caBundle. cert-controller adds it, and its
ClusterRole
allows it to update only these two names:
- apiGroups:
- "admissionregistration.k8s.io"
resources:
- "validatingwebhookconfigurations"
resourceNames:
- "secretstore-validate"
- "externalsecret-validate"
verbs:
- "update"
- "patch"Kustomize over helm template
helm template external-secrets oci://ghcr.io/external-secrets/charts/external-secrets \
--version 2.13.0 --namespace external-secrets > helm.yaml
kustomize build .with this kustomization.yaml:
resources:
- helm.yaml
patches:
- target:
kind: ValidatingWebhookConfiguration
labelSelector: external-secrets.io/component=webhook
patch: |-
$patch: delete
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
metadata:
name: ignoredThe label external-secrets.io/component: webhook is also on the webhook’s
Service and Secret, so the target names the kind as well. The output
was the render without the two configurations and otherwise unchanged.
Kapitan’s mutate
The class pins chart 2.13.0 by OCI digest like the Kyverno one, and the mutation expects exactly the two configurations:
parameters:
mutations:
no-validating-webhooks:
match: {apiVersion: admissionregistration.k8s.io/v1, kind: ValidatingWebhookConfiguration, labels: {external-secrets.io/component: webhook}}
delete: true
expects: 2On kind
Without the configurations, the controller and the webhook Deployments became Available, and cert-controller did not become Ready. Its readiness probe kept failing with HTTP status 500:
external-secrets-cert-controller-56d7b45fc5-btm8q ready=false restarts=0With no webhook registered, a SecretStore whose fake provider has an empty key was accepted. The test then applied the two configurations as the plain render has them, names and labels included, standing in for the separate chart. cert-controller injected the CA and became Ready:
secretstore-validate caBundle bytes: 1196
externalsecret-validate caBundle bytes: 1196A valid SecretStore passed the webhook, which added its warning about the fake provider, and the one with the empty key was now refused:
Error from server (Forbidden): error when creating "eso-store-invalid.yaml": admission webhook "validate.secretstore.external-secrets.io" denied the request: key must be set in data 0The deletion is half of the change the issue asks for. The other chart has to apply both configurations with these names, or cert-controller cannot update them and stays unready.