diff --git a/pkg/webhook/parser.go b/pkg/webhook/parser.go index 0ce3fded0..5a25a3332 100644 --- a/pkg/webhook/parser.go +++ b/pkg/webhook/parser.go @@ -23,6 +23,7 @@ limitations under the License. package webhook import ( + "encoding/json" "fmt" "slices" "strings" @@ -159,6 +160,26 @@ type Config struct { // The URL configuration should be between quotes. // `url` cannot be specified when `path` is specified. URL string `marker:"url,optional"` + + // NamespaceSelector limits which namespaces trigger this webhook. The webhook runs only for + // requests in namespaces that match the selector. Value is a JSON object with the same shape + // as the Kubernetes LabelSelector (matchLabels and/or matchExpressions). + // + // Example: + // + // // +kubebuilder:webhook:...,namespaceSelector=`{"matchLabels":{"webhook-enabled":"true"}}` + // // +kubebuilder:webhook:...,namespaceSelector=`{"matchExpressions":[{"key":"environment","operator":"In","values":["dev","staging","prod"]}]}` + NamespaceSelector string `marker:"namespaceSelector,optional"` + + // ObjectSelector limits which objects trigger this webhook. The webhook runs only for requests + // whose object matches the selector. Value is a JSON object with the same shape as the + // Kubernetes LabelSelector (matchLabels and/or matchExpressions). + // + // Example: + // + // // +kubebuilder:webhook:...,objectSelector=`{"matchLabels":{"managed-by":"my-operator"}}` + // // +kubebuilder:webhook:...,objectSelector=`{"matchExpressions":[{"key":"app-type","operator":"In","values":["web","api","worker"]}]}` + ObjectSelector string `marker:"objectSelector,optional"` } // verbToAPIVariant converts a marker's verb to the proper value for the API. @@ -180,6 +201,26 @@ func verbToAPIVariant(verbRaw string) admissionregv1.OperationType { } } +// parseLabelSelector parses a JSON string into a LabelSelector. The JSON must match the +// Kubernetes LabelSelector type (matchLabels and/or matchExpressions). It returns nil for empty input. +func parseLabelSelector(selectorStr string) (*metav1.LabelSelector, error) { + selectorStr = strings.TrimSpace(selectorStr) + if selectorStr == "" { + return nil, nil + } + + var selector metav1.LabelSelector + if err := json.Unmarshal([]byte(selectorStr), &selector); err != nil { + return nil, fmt.Errorf("label selector must be valid JSON (e.g. {\"matchLabels\":{\"key\":\"value\"}}): %w", err) + } + + if selector.MatchLabels == nil && len(selector.MatchExpressions) == 0 { + return nil, fmt.Errorf("label selector must specify at least one of matchLabels or matchExpressions") + } + + return &selector, nil +} + // ToMutatingWebhookConfiguration converts this WebhookConfig to its Kubernetes API form. func (c WebhookConfig) ToMutatingWebhookConfiguration() (admissionregv1.MutatingWebhookConfiguration, error) { if !c.Mutating { @@ -222,6 +263,16 @@ func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) { return admissionregv1.MutatingWebhook{}, err } + namespaceSelector, err := c.namespaceSelector() + if err != nil { + return admissionregv1.MutatingWebhook{}, fmt.Errorf("invalid namespaceSelector: %w", err) + } + + objectSelector, err := c.objectSelector() + if err != nil { + return admissionregv1.MutatingWebhook{}, fmt.Errorf("invalid objectSelector: %w", err) + } + return admissionregv1.MutatingWebhook{ Name: c.Name, Rules: c.rules(), @@ -232,6 +283,8 @@ func (c Config) ToMutatingWebhook() (admissionregv1.MutatingWebhook, error) { TimeoutSeconds: c.timeoutSeconds(), AdmissionReviewVersions: c.AdmissionReviewVersions, ReinvocationPolicy: c.reinvocationPolicy(), + NamespaceSelector: namespaceSelector, + ObjectSelector: objectSelector, }, nil } @@ -251,6 +304,16 @@ func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) return admissionregv1.ValidatingWebhook{}, err } + namespaceSelector, err := c.namespaceSelector() + if err != nil { + return admissionregv1.ValidatingWebhook{}, fmt.Errorf("invalid namespaceSelector: %w", err) + } + + objectSelector, err := c.objectSelector() + if err != nil { + return admissionregv1.ValidatingWebhook{}, fmt.Errorf("invalid objectSelector: %w", err) + } + return admissionregv1.ValidatingWebhook{ Name: c.Name, Rules: c.rules(), @@ -260,6 +323,8 @@ func (c Config) ToValidatingWebhook() (admissionregv1.ValidatingWebhook, error) SideEffects: c.sideEffects(), TimeoutSeconds: c.timeoutSeconds(), AdmissionReviewVersions: c.AdmissionReviewVersions, + NamespaceSelector: namespaceSelector, + ObjectSelector: objectSelector, }, nil } @@ -402,6 +467,16 @@ func (c Config) reinvocationPolicy() *admissionregv1.ReinvocationPolicyType { return &reinvocationPolicy } +// namespaceSelector returns the LabelSelector for the webhook's namespace filter, or nil if unset. +func (c Config) namespaceSelector() (*metav1.LabelSelector, error) { + return parseLabelSelector(c.NamespaceSelector) +} + +// objectSelector returns the LabelSelector for the webhook's object filter, or nil if unset. +func (c Config) objectSelector() (*metav1.LabelSelector, error) { + return parseLabelSelector(c.ObjectSelector) +} + // webhookVersions returns the target API versions of the {Mutating,Validating}WebhookConfiguration objects for a webhook. func (c Config) webhookVersions() ([]string, error) { // If WebhookVersions is not specified, we default it to `v1`. diff --git a/pkg/webhook/parser_integration_test.go b/pkg/webhook/parser_integration_test.go index b7d773016..3b9bd25c4 100644 --- a/pkg/webhook/parser_integration_test.go +++ b/pkg/webhook/parser_integration_test.go @@ -526,6 +526,146 @@ var _ = Describe("Webhook Generation From Parsing to CustomResourceDefinition", Expect(err).To(HaveOccurred()) }) + It("should properly generate webhook definition with namespaceSelector", func() { + By("switching into testdata to appease go modules") + cwd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + Expect(os.Chdir("./testdata/valid-namespaceselector")).To(Succeed()) + defer func() { Expect(os.Chdir(cwd)).To(Succeed()) }() + + By("loading the roots") + pkgs, err := loader.LoadRoots(".") + Expect(err).NotTo(HaveOccurred()) + Expect(pkgs).To(HaveLen(1)) + + By("setting up the parser") + reg := &markers.Registry{} + Expect(reg.Register(webhook.ConfigDefinition)).To(Succeed()) + Expect(reg.Register(webhook.WebhookConfigDefinition)).To(Succeed()) + + By("requesting that the manifest be generated") + outputDir, err := os.MkdirTemp("", "webhook-integration-test") + Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(outputDir) + genCtx := &genall.GenerationContext{ + Collector: &markers.Collector{Registry: reg}, + Roots: pkgs, + OutputRule: genall.OutputToDirectory(outputDir), + } + Expect(webhook.Generator{}.Generate(genCtx)).To(Succeed()) + for _, r := range genCtx.Roots { + Expect(r.Errors).To(HaveLen(0)) + } + + By("loading the generated v1 YAML") + actualFile, err := os.ReadFile(path.Join(outputDir, "manifests.yaml")) + Expect(err).NotTo(HaveOccurred()) + actualManifest := &admissionregv1.ValidatingWebhookConfiguration{} + Expect(yaml.UnmarshalStrict(actualFile, actualManifest)).To(Succeed()) + + By("loading the desired v1 YAML") + expectedFile, err := os.ReadFile("manifests.yaml") + Expect(err).NotTo(HaveOccurred()) + expectedManifest := &admissionregv1.ValidatingWebhookConfiguration{} + Expect(yaml.UnmarshalStrict(expectedFile, expectedManifest)).To(Succeed()) + + By("comparing the two") + assertSame(actualManifest, expectedManifest) + }) + + It("should properly generate webhook definition with objectSelector", func() { + By("switching into testdata to appease go modules") + cwd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + Expect(os.Chdir("./testdata/valid-objectselector")).To(Succeed()) + defer func() { Expect(os.Chdir(cwd)).To(Succeed()) }() + + By("loading the roots") + pkgs, err := loader.LoadRoots(".") + Expect(err).NotTo(HaveOccurred()) + Expect(pkgs).To(HaveLen(1)) + + By("setting up the parser") + reg := &markers.Registry{} + Expect(reg.Register(webhook.ConfigDefinition)).To(Succeed()) + Expect(reg.Register(webhook.WebhookConfigDefinition)).To(Succeed()) + + By("requesting that the manifest be generated") + outputDir, err := os.MkdirTemp("", "webhook-integration-test") + Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(outputDir) + genCtx := &genall.GenerationContext{ + Collector: &markers.Collector{Registry: reg}, + Roots: pkgs, + OutputRule: genall.OutputToDirectory(outputDir), + } + Expect(webhook.Generator{}.Generate(genCtx)).To(Succeed()) + for _, r := range genCtx.Roots { + Expect(r.Errors).To(HaveLen(0)) + } + + By("loading the generated v1 YAML") + actualFile, err := os.ReadFile(path.Join(outputDir, "manifests.yaml")) + Expect(err).NotTo(HaveOccurred()) + actualManifest := &admissionregv1.MutatingWebhookConfiguration{} + Expect(yaml.UnmarshalStrict(actualFile, actualManifest)).To(Succeed()) + + By("loading the desired v1 YAML") + expectedFile, err := os.ReadFile("manifests.yaml") + Expect(err).NotTo(HaveOccurred()) + expectedManifest := &admissionregv1.MutatingWebhookConfiguration{} + Expect(yaml.UnmarshalStrict(expectedFile, expectedManifest)).To(Succeed()) + + By("comparing the two") + assertSame(actualManifest, expectedManifest) + }) + + It("should properly generate webhook definition with matchExpressions in selectors", func() { + By("switching into testdata to appease go modules") + cwd, err := os.Getwd() + Expect(err).NotTo(HaveOccurred()) + Expect(os.Chdir("./testdata/valid-selectors-matchexpressions")).To(Succeed()) + defer func() { Expect(os.Chdir(cwd)).To(Succeed()) }() + + By("loading the roots") + pkgs, err := loader.LoadRoots(".") + Expect(err).NotTo(HaveOccurred()) + Expect(pkgs).To(HaveLen(1)) + + By("setting up the parser") + reg := &markers.Registry{} + Expect(reg.Register(webhook.ConfigDefinition)).To(Succeed()) + Expect(reg.Register(webhook.WebhookConfigDefinition)).To(Succeed()) + + By("requesting that the manifest be generated") + outputDir, err := os.MkdirTemp("", "webhook-integration-test") + Expect(err).NotTo(HaveOccurred()) + defer os.RemoveAll(outputDir) + genCtx := &genall.GenerationContext{ + Collector: &markers.Collector{Registry: reg}, + Roots: pkgs, + OutputRule: genall.OutputToDirectory(outputDir), + } + Expect(webhook.Generator{}.Generate(genCtx)).To(Succeed()) + for _, r := range genCtx.Roots { + Expect(r.Errors).To(HaveLen(0)) + } + + By("loading the generated v1 YAML") + actualFile, err := os.ReadFile(path.Join(outputDir, "manifests.yaml")) + Expect(err).NotTo(HaveOccurred()) + actualMutating, actualValidating := unmarshalBothV1(actualFile) + + By("loading the desired v1 YAML") + expectedFile, err := os.ReadFile("manifests.yaml") + Expect(err).NotTo(HaveOccurred()) + expectedMutating, expectedValidating := unmarshalBothV1(expectedFile) + + By("comparing the two") + assertSame(actualMutating, expectedMutating) + assertSame(actualValidating, expectedValidating) + }) + }) func unmarshalBothV1(in []byte) (mutating admissionregv1.MutatingWebhookConfiguration, validating admissionregv1.ValidatingWebhookConfiguration) { diff --git a/pkg/webhook/parser_test.go b/pkg/webhook/parser_test.go new file mode 100644 index 000000000..fe4d63f2b --- /dev/null +++ b/pkg/webhook/parser_test.go @@ -0,0 +1,130 @@ +/* +Copyright 2019 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package webhook + +import ( + "testing" + + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" +) + +// TestParseLabelSelector verifies that JSON label selector strings are parsed correctly into +// LabelSelector values. Most inputs use the form used with backticks in markers (no escaped quotes). +// One case uses escaped quotes to ensure both forms produce the same result. +func TestParseLabelSelector(t *testing.T) { + tests := []struct { + name string + input string + expected *metav1.LabelSelector + expectError bool + }{ + {name: "empty", input: "", expected: nil}, + { + name: "matchLabels without escapes (backtick form)", + input: `{"matchLabels":{"key":"value"}}`, + expected: &metav1.LabelSelector{ + MatchLabels: map[string]string{"key": "value"}, + }, + }, + { + name: "matchLabels with escaped quotes (same result as backtick form)", + input: "{\"matchLabels\":{\"key\":\"value\"}}", + expected: &metav1.LabelSelector{ + MatchLabels: map[string]string{"key": "value"}, + }, + }, + { + name: "matchExpressions without escapes", + input: `{"matchExpressions":[{"key":"env","operator":"In","values":["dev","prod"]}]}`, + expected: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "env", Operator: metav1.LabelSelectorOpIn, Values: []string{"dev", "prod"}}, + }, + }, + }, + { + name: "matchExpressions hyphenated key, doc example without escapes", + input: `{"matchExpressions":[{"key":"app-type","operator":"In","values":["web","api","worker"]}]}`, + expected: &metav1.LabelSelector{ + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "app-type", Operator: metav1.LabelSelectorOpIn, Values: []string{"web", "api", "worker"}}, + }, + }, + }, + { + name: "matchLabels and matchExpressions without escapes", + input: `{"matchLabels":{"managed-by":"controller"},"matchExpressions":[{"key":"tier","operator":"In","values":["frontend","backend"]}]}`, + expected: &metav1.LabelSelector{ + MatchLabels: map[string]string{"managed-by": "controller"}, + MatchExpressions: []metav1.LabelSelectorRequirement{ + {Key: "tier", Operator: metav1.LabelSelectorOpIn, Values: []string{"frontend", "backend"}}, + }, + }, + }, + {name: "invalid JSON", input: "{invalid", expectError: true}, + {name: "empty object", input: "{}", expectError: true}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result, err := parseLabelSelector(tt.input) + if tt.expectError { + if err == nil { + t.Error("expected error, got nil") + } + return + } + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if tt.expected == nil { + if result != nil { + t.Errorf("expected nil, got %+v", result) + } + return + } + if len(tt.expected.MatchLabels) != len(result.MatchLabels) { + t.Errorf("MatchLabels: expected %d, got %d", len(tt.expected.MatchLabels), len(result.MatchLabels)) + } + for k, v := range tt.expected.MatchLabels { + if result.MatchLabels[k] != v { + t.Errorf("MatchLabels[%s]: expected %q, got %q", k, v, result.MatchLabels[k]) + } + } + if len(tt.expected.MatchExpressions) != len(result.MatchExpressions) { + t.Errorf("MatchExpressions: expected %d, got %d", len(tt.expected.MatchExpressions), len(result.MatchExpressions)) + } + for i, exp := range tt.expected.MatchExpressions { + if i >= len(result.MatchExpressions) { + break + } + res := result.MatchExpressions[i] + if exp.Key != res.Key || exp.Operator != res.Operator { + t.Errorf("MatchExpressions[%d]: expected %+v, got %+v", i, exp, res) + } + if len(exp.Values) != len(res.Values) { + t.Errorf("MatchExpressions[%d].Values: expected %v, got %v", i, exp.Values, res.Values) + } + for j, v := range exp.Values { + if j < len(res.Values) && res.Values[j] != v { + t.Errorf("MatchExpressions[%d].Values[%d]: expected %q, got %q", i, j, v, res.Values[j]) + } + } + } + }) + } +} diff --git a/pkg/webhook/testdata/valid-namespaceselector/manifests.yaml b/pkg/webhook/testdata/valid-namespaceselector/manifests.yaml new file mode 100644 index 000000000..3a4f8e477 --- /dev/null +++ b/pkg/webhook/testdata/valid-namespaceselector/manifests.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-testdata-kubebuilder-io-v1-cronjob + failurePolicy: Fail + name: validation.cronjob.testdata.kubebuilder.io + namespaceSelector: + matchLabels: + webhook-enabled: "true" + rules: + - apiGroups: + - testdata.kubebuilder.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - cronjobs + sideEffects: None diff --git a/pkg/webhook/testdata/valid-namespaceselector/webhook.go b/pkg/webhook/testdata/valid-namespaceselector/webhook.go new file mode 100644 index 000000000..1bb3ab9d2 --- /dev/null +++ b/pkg/webhook/testdata/valid-namespaceselector/webhook.go @@ -0,0 +1,19 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cronjob + +// Validating webhook with namespaceSelector (matchLabels). Uses backticks so no escapes are needed. +// +kubebuilder:webhook:verbs=create;update,path=/validate-testdata-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,groups=testdata.kubebuilder.io,resources=cronjobs,versions=v1,name=validation.cronjob.testdata.kubebuilder.io,sideEffects=None,admissionReviewVersions=v1,namespaceSelector=`{"matchLabels":{"webhook-enabled":"true"}}` diff --git a/pkg/webhook/testdata/valid-objectselector/manifests.yaml b/pkg/webhook/testdata/valid-objectselector/manifests.yaml new file mode 100644 index 000000000..9110ea9ee --- /dev/null +++ b/pkg/webhook/testdata/valid-objectselector/manifests.yaml @@ -0,0 +1,29 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-testdata-kubebuilder-io-v1-cronjob + failurePolicy: Fail + name: default.cronjob.testdata.kubebuilder.io + objectSelector: + matchLabels: + managed-by: myoperator + rules: + - apiGroups: + - testdata.kubebuilder.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - cronjobs + sideEffects: None diff --git a/pkg/webhook/testdata/valid-objectselector/webhook.go b/pkg/webhook/testdata/valid-objectselector/webhook.go new file mode 100644 index 000000000..942a909d8 --- /dev/null +++ b/pkg/webhook/testdata/valid-objectselector/webhook.go @@ -0,0 +1,19 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cronjob + +// Mutating webhook with objectSelector (matchLabels). Uses backticks so no escapes are needed. +// +kubebuilder:webhook:verbs=create;update,path=/mutate-testdata-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,groups=testdata.kubebuilder.io,resources=cronjobs,versions=v1,name=default.cronjob.testdata.kubebuilder.io,sideEffects=None,admissionReviewVersions=v1,objectSelector=`{"matchLabels":{"managed-by":"myoperator"}}` diff --git a/pkg/webhook/testdata/valid-selectors-matchexpressions/manifests.yaml b/pkg/webhook/testdata/valid-selectors-matchexpressions/manifests.yaml new file mode 100644 index 000000000..470ad84a0 --- /dev/null +++ b/pkg/webhook/testdata/valid-selectors-matchexpressions/manifests.yaml @@ -0,0 +1,69 @@ +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: MutatingWebhookConfiguration +metadata: + name: mutating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /mutate-testdata-kubebuilder-io-v1-cronjob + failurePolicy: Fail + name: default.cronjob.testdata.kubebuilder.io + objectSelector: + matchExpressions: + - key: tier + operator: In + values: + - frontend + - backend + matchLabels: + managed-by: controller + rules: + - apiGroups: + - testdata.kubebuilder.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - cronjobs + sideEffects: None +--- +apiVersion: admissionregistration.k8s.io/v1 +kind: ValidatingWebhookConfiguration +metadata: + name: validating-webhook-configuration +webhooks: +- admissionReviewVersions: + - v1 + clientConfig: + service: + name: webhook-service + namespace: system + path: /validate-testdata-kubebuilder-io-v1-cronjob + failurePolicy: Fail + name: validation.cronjob.testdata.kubebuilder.io + namespaceSelector: + matchExpressions: + - key: environment + operator: In + values: + - dev + - staging + - prod + rules: + - apiGroups: + - testdata.kubebuilder.io + apiVersions: + - v1 + operations: + - CREATE + - UPDATE + resources: + - cronjobs + sideEffects: None diff --git a/pkg/webhook/testdata/valid-selectors-matchexpressions/webhook.go b/pkg/webhook/testdata/valid-selectors-matchexpressions/webhook.go new file mode 100644 index 000000000..95002f6ee --- /dev/null +++ b/pkg/webhook/testdata/valid-selectors-matchexpressions/webhook.go @@ -0,0 +1,21 @@ +/* + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package cronjob + +// Validating webhook with namespaceSelector (matchExpressions). Uses backticks so no escapes are needed. +// +kubebuilder:webhook:verbs=create;update,path=/validate-testdata-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,groups=testdata.kubebuilder.io,resources=cronjobs,versions=v1,name=validation.cronjob.testdata.kubebuilder.io,sideEffects=None,admissionReviewVersions=v1,namespaceSelector=`{"matchExpressions":[{"key":"environment","operator":"In","values":["dev","staging","prod"]}]}` +// Mutating webhook with objectSelector (matchLabels and matchExpressions). Uses backticks so no escapes are needed. +// +kubebuilder:webhook:verbs=create;update,path=/mutate-testdata-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,groups=testdata.kubebuilder.io,resources=cronjobs,versions=v1,name=default.cronjob.testdata.kubebuilder.io,sideEffects=None,admissionReviewVersions=v1,objectSelector=`{"matchLabels":{"managed-by":"controller"},"matchExpressions":[{"key":"tier","operator":"In","values":["frontend","backend"]}]}` diff --git a/pkg/webhook/zz_generated.markerhelp.go b/pkg/webhook/zz_generated.markerhelp.go index 53ce42f59..df6241774 100644 --- a/pkg/webhook/zz_generated.markerhelp.go +++ b/pkg/webhook/zz_generated.markerhelp.go @@ -104,6 +104,14 @@ func (Config) Help() *markers.DefinitionHelp { Summary: "allows mutating webhooks configuration to specify an external URL when generating", Details: "the manifests, instead of using the internal service communication. Should be in format of\nhttps://address:port/path\nWhen this option is specified, the serviceConfig.Service is removed from webhook the manifest.\nThe URL configuration should be between quotes.\n`url` cannot be specified when `path` is specified.", }, + "NamespaceSelector": { + Summary: "limits which namespaces trigger this webhook.", + Details: "The webhook runs only for requests in namespaces that match the selector.\nThe value is a JSON object with the same shape as the Kubernetes LabelSelector\n(matchLabels and/or matchExpressions). Use backticks around the JSON so you do not\nneed to escape quotes, e.g. namespaceSelector=`{\"matchLabels\":{\"key\":\"value\"}}`.", + }, + "ObjectSelector": { + Summary: "limits which objects trigger this webhook.", + Details: "The webhook runs only for requests whose object matches the selector.\nThe value is a JSON object with the same shape as the Kubernetes LabelSelector\n(matchLabels and/or matchExpressions). Use backticks around the JSON so you do not\nneed to escape quotes, e.g. objectSelector=`{\"matchLabels\":{\"key\":\"value\"}}`.", + }, }, } }