From fe2835a33bd37a55061b29f4fb88e5f655d0fbcf Mon Sep 17 00:00:00 2001 From: Amit Marcus Date: Tue, 8 Sep 2026 12:03:14 +0300 Subject: [PATCH] feat: expose reinvocationPolicy on kubernetesMutating bindings Wire the ReinvocationPolicy field from MutatingWebhook through hook config: add an optional `reinvocationPolicy: Never|IfNeeded` knob to kubernetesMutating entries, mirror it into the v1.MutatingWebhook spec, and add the enum to the schema. Unset stays nil so the API server owns the default. The schema restricts the field to kubernetesMutating; the equivalent knob does not exist on validating webhooks (they run once, after all mutations), and additionalProperties already rejects it on kubernetesValidating. Signed-off-by: Amit Marcus --- pkg/hook/config/config_test.go | 66 ++++++++++++++++++++++++++++++++++ pkg/hook/config/config_v1.go | 5 +++ pkg/hook/config/schemas.go | 5 +++ 3 files changed, 76 insertions(+) diff --git a/pkg/hook/config/config_test.go b/pkg/hook/config/config_test.go index 5d1b828a..eb333b82 100644 --- a/pkg/hook/config/config_test.go +++ b/pkg/hook/config/config_test.go @@ -614,6 +614,72 @@ kubernetesValidating: g.Expect(*wh.TimeoutSeconds).To(BeEquivalentTo(30)) }, }, + { + "v1 kubernetesMutating reinvocationPolicy", + ` +configVersion: v1 +kubernetesMutating: +- name: default.example.com + rules: + - apiVersions: ["v1"] + apiGroups: ["crd-domain.io"] + resources: ["MyCustomResource"] + operations: ["*"] +- name: reinvoke.example.com + reinvocationPolicy: IfNeeded + rules: + - apiVersions: ["v1"] + apiGroups: ["crd-domain.io"] + resources: ["MyCustomResource"] + operations: ["*"] +`, + func() { + g.Expect(err).ShouldNot(HaveOccurred()) + g.Expect(hookConfig.KubernetesMutating).Should(HaveLen(2)) + + // Unset: nil, so apiserver defaults to Never. + g.Expect(hookConfig.KubernetesMutating[0].Webhook.ReinvocationPolicy).Should(BeNil()) + + // Set: passed through. + rp := hookConfig.KubernetesMutating[1].Webhook.ReinvocationPolicy + g.Expect(rp).ShouldNot(BeNil()) + g.Expect(*rp).To(Equal(v1.IfNeededReinvocationPolicy)) + }, + }, + { + "v1 kubernetesMutating reinvocationPolicy invalid", + ` +configVersion: v1 +kubernetesMutating: +- name: bad.example.com + reinvocationPolicy: Sometimes + rules: + - apiVersions: ["v1"] + apiGroups: ["crd-domain.io"] + resources: ["MyCustomResource"] + operations: ["*"] +`, + func() { + g.Expect(err).Should(HaveOccurred()) + }, + }, + { + "v1 kubernetesValidating rejects reinvocationPolicy", + ` +configVersion: v1 +kubernetesValidating: +- name: nope.example.com + reinvocationPolicy: Never + rules: + - apiVersions: ["v1"] + apiGroups: ["crd-domain.io"] + resources: ["MyCustomResource"] + operations: ["*"] +`, + func() { + g.Expect(err).Should(HaveOccurred()) + }, + }, { "v1 kubernetesValidating name error", ` diff --git a/pkg/hook/config/config_v1.go b/pkg/hook/config/config_v1.go index 80d57de7..8dee752f 100644 --- a/pkg/hook/config/config_v1.go +++ b/pkg/hook/config/config_v1.go @@ -83,6 +83,9 @@ type KubernetesAdmissionConfigV1 struct { SideEffects *v1.SideEffectClass `json:"sideEffects"` TimeoutSeconds *int32 `json:"timeoutSeconds,omitempty"` MatchConditions []v1.MatchCondition `json:"matchConditions,omitempty"` + // ReinvocationPolicy is honored only for kubernetesMutating; the schema + // rejects it on kubernetesValidating (the k8s type has no such field). + ReinvocationPolicy *v1.ReinvocationPolicyType `json:"reinvocationPolicy,omitempty"` } // version 1 of kubernetes conversion configuration @@ -518,6 +521,8 @@ func convertMutating(cfgV1 KubernetesAdmissionConfigV1) htypes.MutatingConfig { webhook.MatchConditions = cfgV1.MatchConditions + webhook.ReinvocationPolicy = cfgV1.ReinvocationPolicy + cfg.Webhook = &admission.MutatingWebhookConfig{ MutatingWebhook: webhook, } diff --git a/pkg/hook/config/schemas.go b/pkg/hook/config/schemas.go index 6853a49a..480227fc 100644 --- a/pkg/hook/config/schemas.go +++ b/pkg/hook/config/schemas.go @@ -230,6 +230,11 @@ properties: timeoutSeconds: type: integer example: 10 + reinvocationPolicy: + type: string + enum: + - Never + - IfNeeded matchConditions: type: array items: