diff --git a/alias.go b/alias.go index 01ba012dcc..dde2c5b930 100644 --- a/alias.go +++ b/alias.go @@ -18,6 +18,7 @@ package controllerruntime import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "sigs.k8s.io/controller-runtime/pkg/builder" "sigs.k8s.io/controller-runtime/pkg/client/config" @@ -104,9 +105,6 @@ var ( // NewControllerManagedBy returns a new controller builder that will be started by the provided Manager. NewControllerManagedBy = builder.ControllerManagedBy - // NewWebhookManagedBy returns a new webhook builder that will be started by the provided Manager. - NewWebhookManagedBy = builder.WebhookManagedBy - // NewManager returns a new Manager for creating Controllers. // Note that if ContentType in the given config is not set, "application/vnd.kubernetes.protobuf" // will be used for all built-in resources of Kubernetes, and "application/json" is for other types @@ -155,3 +153,8 @@ var ( // SetLogger sets a concrete logging implementation for all deferred Loggers. SetLogger = log.SetLogger ) + +// NewWebhookManagedBy returns a new webhook builder for the provided type T. +func NewWebhookManagedBy[T runtime.Object](mgr manager.Manager, obj T) *builder.WebhookBuilder[T] { + return builder.WebhookManagedBy(mgr, obj) +} diff --git a/examples/builtins/main.go b/examples/builtins/main.go index 3a47814d8c..f30c652583 100644 --- a/examples/builtins/main.go +++ b/examples/builtins/main.go @@ -60,8 +60,7 @@ func main() { os.Exit(1) } - if err := ctrl.NewWebhookManagedBy(mgr). - For(&corev1.Pod{}). + if err := ctrl.NewWebhookManagedBy(mgr, &corev1.Pod{}). WithDefaulter(&podAnnotator{}). WithValidator(&podValidator{}). Complete(); err != nil { diff --git a/examples/builtins/mutatingwebhook.go b/examples/builtins/mutatingwebhook.go index a588eba8f9..0f150c9b6c 100644 --- a/examples/builtins/mutatingwebhook.go +++ b/examples/builtins/mutatingwebhook.go @@ -18,10 +18,8 @@ package main import ( "context" - "fmt" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" ) @@ -31,12 +29,8 @@ import ( // podAnnotator annotates Pods type podAnnotator struct{} -func (a *podAnnotator) Default(ctx context.Context, obj runtime.Object) error { +func (a *podAnnotator) Default(ctx context.Context, pod *corev1.Pod) error { log := logf.FromContext(ctx) - pod, ok := obj.(*corev1.Pod) - if !ok { - return fmt.Errorf("expected a Pod but got a %T", obj) - } if pod.Annotations == nil { pod.Annotations = map[string]string{} diff --git a/examples/builtins/validatingwebhook.go b/examples/builtins/validatingwebhook.go index 1bee7f7c84..eb08159688 100644 --- a/examples/builtins/validatingwebhook.go +++ b/examples/builtins/validatingwebhook.go @@ -21,7 +21,6 @@ import ( "fmt" corev1 "k8s.io/api/core/v1" - "k8s.io/apimachinery/pkg/runtime" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/webhook/admission" @@ -33,12 +32,8 @@ import ( type podValidator struct{} // validate admits a pod if a specific annotation exists. -func (v *podValidator) validate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *podValidator) validate(ctx context.Context, pod *corev1.Pod) (admission.Warnings, error) { log := logf.FromContext(ctx) - pod, ok := obj.(*corev1.Pod) - if !ok { - return nil, fmt.Errorf("expected a Pod but got a %T", obj) - } log.Info("Validating Pod") key := "example-mutating-admission-webhook" @@ -53,14 +48,14 @@ func (v *podValidator) validate(ctx context.Context, obj runtime.Object) (admiss return nil, nil } -func (v *podValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *podValidator) ValidateCreate(ctx context.Context, obj *corev1.Pod) (admission.Warnings, error) { return v.validate(ctx, obj) } -func (v *podValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { +func (v *podValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *corev1.Pod) (admission.Warnings, error) { return v.validate(ctx, newObj) } -func (v *podValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (v *podValidator) ValidateDelete(ctx context.Context, obj *corev1.Pod) (admission.Warnings, error) { return v.validate(ctx, obj) } diff --git a/examples/crd/main.go b/examples/crd/main.go index 0bf65c9890..eec58abc01 100644 --- a/examples/crd/main.go +++ b/examples/crd/main.go @@ -129,8 +129,7 @@ func main() { os.Exit(1) } - err = ctrl.NewWebhookManagedBy(mgr). - For(&api.ChaosPod{}). + err = ctrl.NewWebhookManagedBy(mgr, &api.ChaosPod{}). Complete() if err != nil { setupLog.Error(err, "unable to create webhook") diff --git a/pkg/builder/example_webhook_test.go b/pkg/builder/example_webhook_test.go index c26eba8a13..133da47272 100644 --- a/pkg/builder/example_webhook_test.go +++ b/pkg/builder/example_webhook_test.go @@ -40,8 +40,7 @@ func ExampleWebhookBuilder() { } err = builder. - WebhookManagedBy(mgr). // Create the WebhookManagedBy - For(&examplegroup.ChaosPod{}). // ChaosPod is a CRD. + WebhookManagedBy(mgr, &examplegroup.ChaosPod{}). // Create the WebhookManagedBy Complete() if err != nil { log.Error(err, "could not create webhook") diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go index bb5b6deb56..428100a66c 100644 --- a/pkg/builder/webhook.go +++ b/pkg/builder/webhook.go @@ -37,11 +37,13 @@ import ( ) // WebhookBuilder builds a Webhook. -type WebhookBuilder struct { +type WebhookBuilder[T runtime.Object] struct { apiType runtime.Object customDefaulter admission.CustomDefaulter + defaulter admission.Defaulter[T] customDefaulterOpts []admission.DefaulterOption customValidator admission.CustomValidator + validator admission.Validator[T] customPath string customValidatorCustomPath string customDefaulterCustomPath string @@ -56,59 +58,61 @@ type WebhookBuilder struct { } // WebhookManagedBy returns a new webhook builder. -func WebhookManagedBy(m manager.Manager) *WebhookBuilder { - return &WebhookBuilder{mgr: m} +func WebhookManagedBy[T runtime.Object](m manager.Manager, object T) *WebhookBuilder[T] { + return &WebhookBuilder[T]{mgr: m, apiType: object} } -// TODO(droot): update the GoDoc for conversion. - -// For takes a runtime.Object which should be a CR. -// If the given object implements the admission.Defaulter interface, a MutatingWebhook will be wired for this type. -// If the given object implements the admission.Validator interface, a ValidatingWebhook will be wired for this type. -func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder { - if blder.apiType != nil { - blder.err = errors.New("For(...) should only be called once, could not assign multiple objects for webhook registration") - } - blder.apiType = apiType +// WithCustomDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption) +// will be wired for this type. +// Deprecated: Use WithDefaulter instead. +func (blder *WebhookBuilder[T]) WithCustomDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder[T] { + blder.customDefaulter = defaulter + blder.customDefaulterOpts = opts return blder } -// WithDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption) -// will be wired for this type. -func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder { - blder.customDefaulter = defaulter +// WithDefaulter sets up the provided admission.Defaulter in a defaulting webhook. +func (blder *WebhookBuilder[T]) WithDefaulter(defaulter admission.Defaulter[T], opts ...admission.DefaulterOption) *WebhookBuilder[T] { + blder.defaulter = defaulter blder.customDefaulterOpts = opts return blder } -// WithValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type. -func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator) *WebhookBuilder { +// WithCustomValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type. +// Deprecated: Use WithValidator instead. +func (blder *WebhookBuilder[T]) WithCustomValidator(validator admission.CustomValidator) *WebhookBuilder[T] { blder.customValidator = validator return blder } +// WithValidator sets up the provided admission.Validator in a validating webhook. +func (blder *WebhookBuilder[T]) WithValidator(validator admission.Validator[T]) *WebhookBuilder[T] { + blder.validator = validator + return blder +} + // WithConverter takes a func that constructs a converter.Converter. -// The Converter will then be used by the conversion endpoint for the type passed into For(). -func (blder *WebhookBuilder) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder { +// The Converter will then be used by the conversion endpoint for the type passed into NewWebhookManagedBy() +func (blder *WebhookBuilder[T]) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder[T] { blder.converterConstructor = converterConstructor return blder } // WithLogConstructor overrides the webhook's LogConstructor. -func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder[T] { blder.logConstructor = logConstructor return blder } // WithContextFunc overrides the webhook's WithContextFunc. -func (blder *WebhookBuilder) WithContextFunc(contextFunc func(context.Context, *http.Request) context.Context) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithContextFunc(contextFunc func(context.Context, *http.Request) context.Context) *WebhookBuilder[T] { blder.contextFunc = contextFunc return blder } // RecoverPanic indicates whether panics caused by the webhook should be recovered. // Defaults to true. -func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder { +func (blder *WebhookBuilder[T]) RecoverPanic(recoverPanic bool) *WebhookBuilder[T] { blder.recoverPanic = &recoverPanic return blder } @@ -117,25 +121,25 @@ func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder { // // Deprecated: WithCustomPath should not be used anymore. // Please use WithValidatorCustomPath or WithDefaulterCustomPath instead. -func (blder *WebhookBuilder) WithCustomPath(customPath string) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithCustomPath(customPath string) *WebhookBuilder[T] { blder.customPath = customPath return blder } // WithValidatorCustomPath overrides the path of the Validator. -func (blder *WebhookBuilder) WithValidatorCustomPath(customPath string) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithValidatorCustomPath(customPath string) *WebhookBuilder[T] { blder.customValidatorCustomPath = customPath return blder } // WithDefaulterCustomPath overrides the path of the Defaulter. -func (blder *WebhookBuilder) WithDefaulterCustomPath(customPath string) *WebhookBuilder { +func (blder *WebhookBuilder[T]) WithDefaulterCustomPath(customPath string) *WebhookBuilder[T] { blder.customDefaulterCustomPath = customPath return blder } // Complete builds the webhook. -func (blder *WebhookBuilder) Complete() error { +func (blder *WebhookBuilder[T]) Complete() error { // Set the Config blder.loadRestConfig() @@ -146,13 +150,13 @@ func (blder *WebhookBuilder) Complete() error { return blder.registerWebhooks() } -func (blder *WebhookBuilder) loadRestConfig() { +func (blder *WebhookBuilder[T]) loadRestConfig() { if blder.config == nil { blder.config = blder.mgr.GetConfig() } } -func (blder *WebhookBuilder) setLogConstructor() { +func (blder *WebhookBuilder[T]) setLogConstructor() { if blder.logConstructor == nil { blder.logConstructor = func(base logr.Logger, req *admission.Request) logr.Logger { log := base.WithValues( @@ -172,11 +176,11 @@ func (blder *WebhookBuilder) setLogConstructor() { } } -func (blder *WebhookBuilder) isThereCustomPathConflict() bool { +func (blder *WebhookBuilder[T]) isThereCustomPathConflict() bool { return (blder.customPath != "" && blder.customDefaulter != nil && blder.customValidator != nil) || (blder.customPath != "" && blder.customDefaulterCustomPath != "") || (blder.customPath != "" && blder.customValidatorCustomPath != "") } -func (blder *WebhookBuilder) registerWebhooks() error { +func (blder *WebhookBuilder[T]) registerWebhooks() error { typ, err := blder.getType() if err != nil { return err @@ -217,8 +221,11 @@ func (blder *WebhookBuilder) registerWebhooks() error { } // registerDefaultingWebhook registers a defaulting webhook if necessary. -func (blder *WebhookBuilder) registerDefaultingWebhook() error { - mwh := blder.getDefaultingWebhook() +func (blder *WebhookBuilder[T]) registerDefaultingWebhook() error { + mwh, err := blder.getDefaultingWebhook() + if err != nil { + return err + } if mwh != nil { mwh.LogConstructor = blder.logConstructor mwh.WithContextFunc = blder.contextFunc @@ -244,20 +251,28 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() error { return nil } -func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook { - if defaulter := blder.customDefaulter; defaulter != nil { - w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter, blder.customDefaulterOpts...) - if blder.recoverPanic != nil { - w = w.WithRecoverPanic(*blder.recoverPanic) +func (blder *WebhookBuilder[T]) getDefaultingWebhook() (*admission.Webhook, error) { + var w *admission.Webhook + if blder.defaulter != nil { + if blder.customDefaulter != nil { + return nil, errors.New("only one of Defaulter or CustomDefaulter can be set") } - return w + w = admission.WithDefaulter(blder.mgr.GetScheme(), blder.defaulter, blder.customDefaulterOpts...) + } else if blder.customDefaulter != nil { + w = admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, blder.customDefaulter, blder.customDefaulterOpts...) } - return nil + if w != nil && blder.recoverPanic != nil { + w = w.WithRecoverPanic(*blder.recoverPanic) + } + return w, nil } // registerValidatingWebhook registers a validating webhook if necessary. -func (blder *WebhookBuilder) registerValidatingWebhook() error { - vwh := blder.getValidatingWebhook() +func (blder *WebhookBuilder[T]) registerValidatingWebhook() error { + vwh, err := blder.getValidatingWebhook() + if err != nil { + return err + } if vwh != nil { vwh.LogConstructor = blder.logConstructor vwh.WithContextFunc = blder.contextFunc @@ -283,18 +298,23 @@ func (blder *WebhookBuilder) registerValidatingWebhook() error { return nil } -func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook { - if validator := blder.customValidator; validator != nil { - w := admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, validator) - if blder.recoverPanic != nil { - w = w.WithRecoverPanic(*blder.recoverPanic) +func (blder *WebhookBuilder[T]) getValidatingWebhook() (*admission.Webhook, error) { + var w *admission.Webhook + if blder.validator != nil { + if blder.customValidator != nil { + return nil, errors.New("only one of Validator or CustomValidator can be set") } - return w + w = admission.WithValidator(blder.mgr.GetScheme(), blder.validator) + } else if blder.customValidator != nil { + w = admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, blder.customValidator) } - return nil + if w != nil && blder.recoverPanic != nil { + w = w.WithRecoverPanic(*blder.recoverPanic) + } + return w, nil } -func (blder *WebhookBuilder) registerConversionWebhook() error { +func (blder *WebhookBuilder[T]) registerConversionWebhook() error { if blder.converterConstructor != nil { converter, err := blder.converterConstructor(blder.mgr.GetScheme()) if err != nil { @@ -323,14 +343,14 @@ func (blder *WebhookBuilder) registerConversionWebhook() error { return nil } -func (blder *WebhookBuilder) getType() (runtime.Object, error) { +func (blder *WebhookBuilder[T]) getType() (runtime.Object, error) { if blder.apiType != nil { return blder.apiType, nil } - return nil, errors.New("For() must be called with a valid object") + return nil, errors.New("NewWebhookManagedBy() must be called with a valid object") } -func (blder *WebhookBuilder) isAlreadyHandled(path string) bool { +func (blder *WebhookBuilder[T]) isAlreadyHandled(path string) bool { if blder.mgr.GetWebhookServer().WebhookMux() == nil { return false } diff --git a/pkg/builder/webhook_test.go b/pkg/builder/webhook_test.go index 72538ef7bf..e10e693ab8 100644 --- a/pkg/builder/webhook_test.go +++ b/pkg/builder/webhook_test.go @@ -85,35 +85,36 @@ func runTests(admissionReviewVersion string) { close(stop) }) - It("should scaffold a custom defaulting webhook", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("scaffold a defaulting webhook", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaulterObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} - builder.Register(&TestDefaulter{}, &TestDefaulterList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - WithDefaulter(&TestCustomDefaulter{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestDefaulter" + "kind":"TestDefaulterObject" }, "resource":{ "group":"foo.test.org", @@ -130,68 +131,76 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a mutating webhook path") + path := generateMutatePath(testDefaulterGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"patch":`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":200`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook path that doesn't exist") + path = generateValidatePath(testDefaulterGVK) + _, err = reader.Seek(0, 0) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a mutating webhook path") - path := generateMutatePath(testDefaulterGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook path that doesn't exist") - path = generateValidatePath(testDefaulterGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - }) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + }, + Entry("Custom Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithCustomDefaulter(&TestCustomDefaulter{}) + }), + Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithDefaulter(&testDefaulter{}) + }), + ) - It("should scaffold a custom defaulting webhook with a custom path", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom defaulting webhook with a custom path", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaulterObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} - builder.Register(&TestDefaulter{}, &TestDefaulterList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - customPath := "/custom-defaulting-path" - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - WithDefaulter(&TestCustomDefaulter{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - WithDefaulterCustomPath(customPath). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + customPath := "/custom-defaulting-path" + webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + WithDefaulterCustomPath(customPath). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestDefaulter" + "kind":"TestDefaulterObject" }, "resource":{ "group":"foo.test.org", @@ -208,66 +217,73 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } - By("sending a request to a mutating webhook path that have been overriten by a custom path") - path, err := generateCustomPath(customPath) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a mutating webhook path") - path = generateMutatePath(testDefaulterGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - }) + By("sending a request to a mutating webhook path that have been overriten by a custom path") + path, err := generateCustomPath(customPath) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"patch":`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":200`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaulter"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a mutating webhook path") + path = generateMutatePath(testDefaulterGVK) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + }, + Entry("Custom Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithCustomDefaulter(&TestCustomDefaulter{}) + }), + Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithDefaulter(&testDefaulter{}) + }), + ) - It("should scaffold a custom defaulting webhook which recovers from panics", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom defaulting webhook which recovers from panics", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaulterObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} - builder.Register(&TestDefaulter{}, &TestDefaulterList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - WithDefaulter(&TestCustomDefaulter{}). - RecoverPanic(true). - // RecoverPanic defaults to true. - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{}) + build(webhookBuilder) + err = webhookBuilder. + RecoverPanic(true). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"", "version":"v1", - "kind":"TestDefaulter" + "kind":"TestDefaulterObject" }, "resource":{ "group":"", @@ -284,58 +300,65 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a mutating webhook path") - path := generateMutatePath(testDefaulterGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) - }) + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a mutating webhook path") + path := generateMutatePath(testDefaulterGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) + }, + Entry("CustomDefaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithCustomDefaulter(&TestCustomDefaulter{}) + }), + Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithDefaulter(&testDefaulter{}) + }), + ) - It("should scaffold a custom validating webhook", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom validating webhook", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestValidator{}, &TestValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + webhook := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhook) + err = webhook.WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { return admission.DefaultLogConstructor(testingLogger, req) }). - WithContextFunc(func(ctx context.Context, request *http.Request) context.Context { - return context.WithValue(ctx, userAgentCtxKey, request.Header.Get(userAgentHeader)) - }). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + WithContextFunc(func(ctx context.Context, request *http.Request) context.Context { + return context.WithValue(ctx, userAgentCtxKey, request.Header.Get(userAgentHeader)) + }). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"foo.test.org", @@ -353,13 +376,13 @@ func runTests(admissionReviewVersion string) { } } }`) - readerWithCxt := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + readerWithCxt := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"foo.test.org", @@ -378,81 +401,88 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a mutating webhook path that doesn't exist") + path := generateMutatePath(testValidatorGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + + By("sending a request to a validating webhook path") + path = generateValidatePath(testValidatorGVK) + _, err = reader.Seek(0, 0) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a mutating webhook path that doesn't exist") - path := generateMutatePath(testValidatorGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - - By("sending a request to a validating webhook path") - path = generateValidatePath(testValidatorGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook with context header validation") - path = generateValidatePath(testValidatorGVK) - _, err = readerWithCxt.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, readerWithCxt) - req.Header.Add("Content-Type", "application/json") - req.Header.Add(userAgentHeader, userAgentValue) - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - }) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":403`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook with context header validation") + path = generateValidatePath(testValidatorGVK) + _, err = readerWithCxt.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req = httptest.NewRequest("POST", svcBaseAddr+path, readerWithCxt) + req.Header.Add("Content-Type", "application/json") + req.Header.Add(userAgentHeader, userAgentValue) + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":200`)) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) - It("should scaffold a custom validating webhook with a custom path", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom validating webhook with a custom path", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestValidator{}, &TestValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - customPath := "/custom-validating-path" - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + customPath := "/custom-validating-path" + webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhookBuilder) + err = webhookBuilder.WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { return admission.DefaultLogConstructor(testingLogger, req) }). - WithValidatorCustomPath(customPath). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + WithValidatorCustomPath(customPath). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"foo.test.org", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"foo.test.org", @@ -471,64 +501,71 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } - By("sending a request to a valiting webhook path that have been overriten by a custom path") - path, err := generateCustomPath(customPath) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook path") - path = generateValidatePath(testValidatorGVK) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - }) + By("sending a request to a valiting webhook path that have been overriten by a custom path") + path, err := generateCustomPath(customPath) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body.String()).To(ContainSubstring(`"code":403`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook path") + path = generateValidatePath(testValidatorGVK) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) - It("should scaffold a custom validating webhook which recovers from panics", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom validating webhook which recovers from panics", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestValidator{}, &TestValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - RecoverPanic(true). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhookBuilder) + err = webhookBuilder.RecoverPanic(true). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"", @@ -544,56 +581,63 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } - By("sending a request to a validating webhook path") - path := generateValidatePath(testValidatorGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) - }) + By("sending a request to a validating webhook path") + path := generateValidatePath(testValidatorGVK) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":500`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) - It("should scaffold a custom validating webhook to validate deletes", func(specCtx SpecContext) { - By("creating a controller manager") - ctx, cancel := context.WithCancel(specCtx) + DescribeTable("should scaffold a custom validating webhook to validate deletes", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + ctx, cancel := context.WithCancel(specCtx) - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestValidator{}, &TestValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhookBuilder) + err = webhookBuilder.Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"", @@ -609,30 +653,30 @@ func runTests(admissionReviewVersion string) { } }`) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a validating webhook path to check for failed delete") - path := generateValidatePath(testValidatorGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - - reader = strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a validating webhook path to check for failed delete") + path := generateValidatePath(testValidatorGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) + + reader = strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ "group":"", "version":"v1", - "kind":"TestValidator" + "kind":"TestValidatorObject" }, "resource":{ "group":"", @@ -647,60 +691,49 @@ func runTests(admissionReviewVersion string) { } } }`) - By("sending a request to a validating webhook path with correct request") - path = generateValidatePath(testValidatorGVK) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - }) - - It("should send an error when trying to register a webhook with more than one For", func() { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testDefaultValidatorGVK.GroupVersion()} - builder.Register(&TestDefaulter{}, &TestDefaulterList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - For(&TestDefaulter{}). - Complete() - Expect(err).To(HaveOccurred()) - }) + By("sending a request to a validating webhook path with correct request") + path = generateValidatePath(testValidatorGVK) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) - It("should scaffold a custom defaulting and validating webhook", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom defaulting and validating webhook", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaultValidator])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaultValidator{}). - WithDefaulter(&TestCustomDefaultValidator{}). - WithValidator(&TestCustomDefaultValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + webhookBuilder := WebhookManagedBy(m, &TestDefaultValidator{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ @@ -725,69 +758,86 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } + + By("sending a request to a mutating webhook path") + path := generateMutatePath(testDefaultValidatorGVK) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook path") + path = generateValidatePath(testDefaultValidatorGVK) + _, err = reader.Seek(0, 0) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } - - By("sending a request to a mutating webhook path") - path := generateMutatePath(testDefaultValidatorGVK) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook path") - path = generateValidatePath(testDefaultValidatorGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - }) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + }, + Entry("CustomDefaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithCustomDefaulter(&TestCustomDefaultValidator{}) + b.WithCustomValidator(&TestCustomDefaultValidator{}) + }), + Entry("CustomDefaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithCustomDefaulter(&TestCustomDefaultValidator{}) + b.WithValidator(&testDefaultValidatorValidator{}) + }), + Entry("Defaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithDefaulter(&testValidatorDefaulter{}) + b.WithCustomValidator(&TestCustomDefaultValidator{}) + }), + Entry("Defaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithDefaulter(&testValidatorDefaulter{}) + b.WithValidator(&testDefaultValidatorValidator{}) + }), + ) - It("should scaffold a custom defaulting and validating webhook with a custom path for each of them", func(specCtx SpecContext) { - By("creating a controller manager") - m, err := manager.New(cfg, manager.Options{}) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + DescribeTable("should scaffold a custom defaulting and validating webhook with a custom path for each of them", + func(specCtx SpecContext, build func(*WebhookBuilder[*TestDefaultValidator])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) - err = builder.AddToScheme(m.GetScheme()) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) - validatingCustomPath := "/custom-validating-path" - defaultingCustomPath := "/custom-defaulting-path" - err = WebhookManagedBy(m). - For(&TestDefaultValidator{}). - WithDefaulter(&TestCustomDefaultValidator{}). - WithValidator(&TestCustomDefaultValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - WithValidatorCustomPath(validatingCustomPath). - WithDefaulterCustomPath(defaultingCustomPath). - Complete() - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - svr := m.GetWebhookServer() - ExpectWithOffset(1, svr).NotTo(BeNil()) + validatingCustomPath := "/custom-validating-path" + defaultingCustomPath := "/custom-defaulting-path" + webhookBuilder := WebhookManagedBy(m, &TestDefaultValidator{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + WithValidatorCustomPath(validatingCustomPath). + WithDefaulterCustomPath(defaultingCustomPath). + Complete() + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + svr := m.GetWebhookServer() + ExpectWithOffset(1, svr).NotTo(BeNil()) - reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", + reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `", "request":{ "uid":"07e52e8d-4513-11e9-a716-42010a800270", "kind":{ @@ -812,60 +862,77 @@ func runTests(admissionReviewVersion string) { } }`) - ctx, cancel := context.WithCancel(specCtx) - cancel() - err = svr.Start(ctx) - if err != nil && !os.IsNotExist(err) { - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - } + ctx, cancel := context.WithCancel(specCtx) + cancel() + err = svr.Start(ctx) + if err != nil && !os.IsNotExist(err) { + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + } - By("sending a request to a mutating webhook path that have been overriten by the custom path") - path, err := generateCustomPath(defaultingCustomPath) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req := httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w := httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable fields") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a mutating webhook path") - path = generateMutatePath(testDefaultValidatorGVK) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - - By("sending a request to a valiting webhook path that have been overriten by a custom path") - path, err = generateCustomPath(validatingCustomPath) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - _, err = reader.Seek(0, 0) - ExpectWithOffset(1, err).NotTo(HaveOccurred()) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) - By("sanity checking the response contains reasonable field") - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) - ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) - EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) - - By("sending a request to a validating webhook path") - path = generateValidatePath(testValidatorGVK) - req = httptest.NewRequest("POST", svcBaseAddr+path, reader) - req.Header.Add("Content-Type", "application/json") - w = httptest.NewRecorder() - svr.WebhookMux().ServeHTTP(w, req) - ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) - }) + By("sending a request to a mutating webhook path that have been overriten by the custom path") + path, err := generateCustomPath(defaultingCustomPath) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req := httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w := httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable fields") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Defaulting object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a mutating webhook path") + path = generateMutatePath(testDefaultValidatorGVK) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + + By("sending a request to a valiting webhook path that have been overriten by a custom path") + path, err = generateCustomPath(validatingCustomPath) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + _, err = reader.Seek(0, 0) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK)) + By("sanity checking the response contains reasonable field") + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`)) + ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`)) + EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testdefaultvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`)) + + By("sending a request to a validating webhook path") + path = generateValidatePath(testValidatorGVK) + req = httptest.NewRequest("POST", svcBaseAddr+path, reader) + req.Header.Add("Content-Type", "application/json") + w = httptest.NewRecorder() + svr.WebhookMux().ServeHTTP(w, req) + ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) + }, + Entry("CustomDefaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithCustomDefaulter(&TestCustomDefaultValidator{}) + b.WithCustomValidator(&TestCustomDefaultValidator{}) + }), + Entry("CustomDefaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithCustomDefaulter(&TestCustomDefaultValidator{}) + b.WithValidator(&testDefaultValidatorValidator{}) + }), + Entry("Defaulter + CustomValidator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithDefaulter(&testValidatorDefaulter{}) + b.WithCustomValidator(&TestCustomDefaultValidator{}) + }), + Entry("Defaulter + Validator", func(b *WebhookBuilder[*TestDefaultValidator]) { + b.WithDefaulter(&testValidatorDefaulter{}) + b.WithValidator(&testDefaultValidatorValidator{}) + }), + ) It("should not scaffold a custom defaulting and a custom validating webhook with the same custom path", func() { By("creating a controller manager") @@ -878,10 +945,9 @@ func runTests(admissionReviewVersion string) { err = builder.AddToScheme(m.GetScheme()) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaultValidator{}). - WithDefaulter(&TestCustomDefaultValidator{}). - WithValidator(&TestCustomDefaultValidator{}). + err = WebhookManagedBy(m, &TestDefaultValidator{}). + WithCustomDefaulter(&TestCustomDefaultValidator{}). + WithCustomValidator(&TestCustomDefaultValidator{}). WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { return admission.DefaultLogConstructor(testingLogger, req) }). @@ -890,77 +956,126 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, err).To(HaveOccurred()) }) - It("should not scaffold a custom defaulting when setting a custom path and a defaulting custom path", func() { - By("creating a controller manager") + DescribeTable("should not scaffold a custom defaulting when setting a custom path and a defaulting custom path", + func(build func(*WebhookBuilder[*TestDefaulterObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + webhookBuilder := WebhookManagedBy(m, &TestDefaulterObject{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + WithDefaulterCustomPath(customPath). + WithCustomPath(customPath). + Complete() + ExpectWithOffset(1, err).To(HaveOccurred()) + }, + Entry("CustomDefaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithCustomDefaulter(&TestCustomDefaulter{}) + }), + Entry("Defaulter", func(b *WebhookBuilder[*TestDefaulterObject]) { + b.WithDefaulter(&testDefaulter{}) + }), + ) + + DescribeTable("should not scaffold a custom validating when setting a custom path and a validating custom path", + func(build func(*WebhookBuilder[*TestValidatorObject])) { + By("creating a controller manager") + m, err := manager.New(cfg, manager.Options{}) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + By("registering the type in the Scheme") + builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + webhookBuilder := WebhookManagedBy(m, &TestValidatorObject{}) + build(webhookBuilder) + err = webhookBuilder. + WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { + return admission.DefaultLogConstructor(testingLogger, req) + }). + WithValidatorCustomPath(customPath). + WithCustomPath(customPath). + Complete() + ExpectWithOffset(1, err).To(HaveOccurred()) + }, + Entry("CustomValidator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithCustomValidator(&TestCustomValidator{}) + }), + Entry("Validator", func(b *WebhookBuilder[*TestValidatorObject]) { + b.WithValidator(&testValidator{}) + }), + ) + + It("should error if both a defaulter and a custom defaulter are set", func() { m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") - builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) + builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulterObject{}, &TestDefaulterList{}) err = builder.AddToScheme(m.GetScheme()) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestDefaulter{}). - WithDefaulter(&TestCustomDefaulter{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - WithDefaulterCustomPath(customPath). - WithCustomPath(customPath). + err = WebhookManagedBy(m, &TestDefaulterObject{}). + WithDefaulter(&testDefaulter{}). + WithCustomDefaulter(&TestCustomDefaulter{}). Complete() ExpectWithOffset(1, err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("only one of Defaulter or CustomDefaulter can be set")) }) - - It("should not scaffold a custom defaulting when setting a custom path and a validating custom path", func() { - By("creating a controller manager") + It("should error if both a validator and a custom validator are set", func() { m, err := manager.New(cfg, manager.Options{}) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - By("registering the type in the Scheme") builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()} - builder.Register(&TestDefaultValidator{}, &TestDefaultValidatorList{}) + builder.Register(&TestValidatorObject{}, &TestValidatorList{}) err = builder.AddToScheme(m.GetScheme()) ExpectWithOffset(1, err).NotTo(HaveOccurred()) - err = WebhookManagedBy(m). - For(&TestValidator{}). - WithValidator(&TestCustomValidator{}). - WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger { - return admission.DefaultLogConstructor(testingLogger, req) - }). - WithDefaulterCustomPath(customPath). - WithCustomPath(customPath). + err = WebhookManagedBy(m, &TestValidatorObject{}). + WithValidator(&testValidator{}). + WithCustomValidator(&TestCustomValidator{}). Complete() ExpectWithOffset(1, err).To(HaveOccurred()) + Expect(err.Error()).To(ContainSubstring("only one of Validator or CustomValidator can be set")) }) } // TestDefaulter. -var _ runtime.Object = &TestDefaulter{} +var _ runtime.Object = &TestDefaulterObject{} -const testDefaulterKind = "TestDefaulter" +const testDefaulterKind = "TestDefaulterObject" -type TestDefaulter struct { +type TestDefaulterObject struct { Replica int `json:"replica,omitempty"` Panic bool `json:"panic,omitempty"` } var testDefaulterGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testDefaulterKind} -func (d *TestDefaulter) GetObjectKind() schema.ObjectKind { return d } -func (d *TestDefaulter) DeepCopyObject() runtime.Object { - return &TestDefaulter{ +func (d *TestDefaulterObject) GetObjectKind() schema.ObjectKind { return d } +func (d *TestDefaulterObject) DeepCopyObject() runtime.Object { + return &TestDefaulterObject{ Replica: d.Replica, } } -func (d *TestDefaulter) GroupVersionKind() schema.GroupVersionKind { +func (d *TestDefaulterObject) GroupVersionKind() schema.GroupVersionKind { return testDefaulterGVK } -func (d *TestDefaulter) SetGroupVersionKind(gvk schema.GroupVersionKind) {} +func (d *TestDefaulterObject) SetGroupVersionKind(gvk schema.GroupVersionKind) {} var _ runtime.Object = &TestDefaulterList{} @@ -970,29 +1085,29 @@ func (*TestDefaulterList) GetObjectKind() schema.ObjectKind { return nil } func (*TestDefaulterList) DeepCopyObject() runtime.Object { return nil } // TestValidator. -var _ runtime.Object = &TestValidator{} +var _ runtime.Object = &TestValidatorObject{} -const testValidatorKind = "TestValidator" +const testValidatorKind = "TestValidatorObject" -type TestValidator struct { +type TestValidatorObject struct { Replica int `json:"replica,omitempty"` Panic bool `json:"panic,omitempty"` } var testValidatorGVK = schema.GroupVersionKind{Group: "foo.test.org", Version: "v1", Kind: testValidatorKind} -func (v *TestValidator) GetObjectKind() schema.ObjectKind { return v } -func (v *TestValidator) DeepCopyObject() runtime.Object { - return &TestValidator{ +func (v *TestValidatorObject) GetObjectKind() schema.ObjectKind { return v } +func (v *TestValidatorObject) DeepCopyObject() runtime.Object { + return &TestValidatorObject{ Replica: v.Replica, } } -func (v *TestValidator) GroupVersionKind() schema.GroupVersionKind { +func (v *TestValidatorObject) GroupVersionKind() schema.GroupVersionKind { return testValidatorGVK } -func (v *TestValidator) SetGroupVersionKind(gvk schema.GroupVersionKind) {} +func (v *TestValidatorObject) SetGroupVersionKind(gvk schema.GroupVersionKind) {} var _ runtime.Object = &TestValidatorList{} @@ -1035,10 +1150,16 @@ type TestDefaultValidatorList struct{} func (*TestDefaultValidatorList) GetObjectKind() schema.ObjectKind { return nil } func (*TestDefaultValidatorList) DeepCopyObject() runtime.Object { return nil } -// TestCustomDefaulter. type TestCustomDefaulter struct{} func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error { + d := obj.(*TestDefaulterObject) //nolint:ifshort + return (&testDefaulter{}).Default(ctx, d) +} + +type testDefaulter struct{} + +func (*testDefaulter) Default(ctx context.Context, obj *TestDefaulterObject) error { logf.FromContext(ctx).Info("Defaulting object") req, err := admission.RequestFromContext(ctx) if err != nil { @@ -1048,13 +1169,12 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err return fmt.Errorf("expected Kind TestDefaulter got %q", req.Kind.Kind) } - d := obj.(*TestDefaulter) //nolint:ifshort - if d.Panic { + if obj.Panic { panic("fake panic test") } - if d.Replica < 2 { - d.Replica = 2 + if obj.Replica < 2 { + obj.Replica = 2 } return nil @@ -1062,11 +1182,27 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err var _ admission.CustomDefaulter = &TestCustomDefaulter{} -// TestCustomValidator. - type TestCustomValidator struct{} func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + v := obj.(*TestValidatorObject) //nolint:ifshort + return (&testValidator{}).ValidateCreate(ctx, v) +} + +func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { + v := newObj.(*TestValidatorObject) + old := oldObj.(*TestValidatorObject) + return (&testValidator{}).ValidateUpdate(ctx, old, v) +} + +func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { + v := obj.(*TestValidatorObject) //nolint:ifshort + return (&testValidator{}).ValidateDelete(ctx, v) +} + +type testValidator struct{} + +func (*testValidator) ValidateCreate(ctx context.Context, obj *TestValidatorObject) (admission.Warnings, error) { logf.FromContext(ctx).Info("Validating object") req, err := admission.RequestFromContext(ctx) if err != nil { @@ -1076,18 +1212,17 @@ func (*TestCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Obje return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind) } - v := obj.(*TestValidator) //nolint:ifshort - if v.Panic { + if obj.Panic { panic("fake panic test") } - if v.Replica < 0 { + if obj.Replica < 0 { return nil, errors.New("number of replica should be greater than or equal to 0") } return nil, nil } -func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) { +func (*testValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *TestValidatorObject) (admission.Warnings, error) { logf.FromContext(ctx).Info("Validating object") req, err := admission.RequestFromContext(ctx) if err != nil { @@ -1097,13 +1232,11 @@ func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj r return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind) } - v := newObj.(*TestValidator) - old := oldObj.(*TestValidator) - if v.Replica < 0 { + if newObj.Replica < 0 { return nil, errors.New("number of replica should be greater than or equal to 0") } - if v.Replica < old.Replica { - return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", v.Replica, old.Replica) + if newObj.Replica < oldObj.Replica { + return nil, fmt.Errorf("new replica %v should not be fewer than old replica %v", newObj.Replica, oldObj.Replica) } userAgent, ok := ctx.Value(userAgentCtxKey).(string) @@ -1114,7 +1247,7 @@ func (*TestCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj r return nil, nil } -func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) { +func (*testValidator) ValidateDelete(ctx context.Context, obj *TestValidatorObject) (admission.Warnings, error) { logf.FromContext(ctx).Info("Validating object") req, err := admission.RequestFromContext(ctx) if err != nil { @@ -1124,8 +1257,7 @@ func (*TestCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Obje return nil, fmt.Errorf("expected Kind TestValidator got %q", req.Kind.Kind) } - v := obj.(*TestValidator) //nolint:ifshort - if v.Replica > 0 { + if obj.Replica > 0 { return nil, errors.New("number of replica should be less than or equal to 0 to delete") } return nil, nil @@ -1214,3 +1346,23 @@ func (*TestCustomDefaultValidator) ValidateDelete(ctx context.Context, obj runti } var _ admission.CustomValidator = &TestCustomValidator{} + +type testValidatorDefaulter struct{} + +func (*testValidatorDefaulter) Default(ctx context.Context, obj *TestDefaultValidator) error { + return (&TestCustomDefaultValidator{}).Default(ctx, obj) +} + +type testDefaultValidatorValidator struct{} + +func (*testDefaultValidatorValidator) ValidateCreate(ctx context.Context, obj *TestDefaultValidator) (admission.Warnings, error) { + return (&TestCustomDefaultValidator{}).ValidateCreate(ctx, obj) +} + +func (*testDefaultValidatorValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *TestDefaultValidator) (admission.Warnings, error) { + return (&TestCustomDefaultValidator{}).ValidateUpdate(ctx, oldObj, newObj) +} + +func (*testDefaultValidatorValidator) ValidateDelete(ctx context.Context, obj *TestDefaultValidator) (admission.Warnings, error) { + return (&TestCustomDefaultValidator{}).ValidateDelete(ctx, obj) +} diff --git a/pkg/webhook/admission/defaulter_custom.go b/pkg/webhook/admission/defaulter_custom.go index a703cbd2c5..1dc8af10ee 100644 --- a/pkg/webhook/admission/defaulter_custom.go +++ b/pkg/webhook/admission/defaulter_custom.go @@ -21,6 +21,7 @@ import ( "encoding/json" "errors" "net/http" + "reflect" "slices" "gomodules.xyz/jsonpatch/v2" @@ -31,11 +32,15 @@ import ( "k8s.io/apimachinery/pkg/util/sets" ) -// CustomDefaulter defines functions for setting defaults on resources. -type CustomDefaulter interface { - Default(ctx context.Context, obj runtime.Object) error +// Defaulter defines functions for setting defaults on resources. +type Defaulter[T runtime.Object] interface { + Default(ctx context.Context, obj T) error } +// CustomDefaulter defines functions for setting defaults on resources. +// Deprecated: CustomDefaulter is deprecated, use Defaulter instead +type CustomDefaulter = Defaulter[runtime.Object] + type defaulterOptions struct { removeUnknownOrOmitableFields bool } @@ -50,6 +55,29 @@ func DefaulterRemoveUnknownOrOmitableFields(o *defaulterOptions) { o.removeUnknownOrOmitableFields = true } +// WithDefaulter creates a new Webhook for a Defaulter interface. +func WithDefaulter[T runtime.Object](scheme *runtime.Scheme, defaulter Defaulter[T], opts ...DefaulterOption) *Webhook { + options := &defaulterOptions{} + for _, o := range opts { + o(options) + } + return &Webhook{ + Handler: &defaulterForType[T]{ + defaulter: defaulter, + decoder: NewDecoder(scheme), + removeUnknownOrOmitableFields: options.removeUnknownOrOmitableFields, + new: func() T { + var zero T + typ := reflect.TypeOf(zero) + if typ.Kind() == reflect.Ptr { + return reflect.New(typ.Elem()).Interface().(T) + } + return zero + }, + }, + } +} + // WithCustomDefaulter creates a new Webhook for a CustomDefaulter interface. func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter CustomDefaulter, opts ...DefaulterOption) *Webhook { options := &defaulterOptions{} @@ -57,33 +85,30 @@ func WithCustomDefaulter(scheme *runtime.Scheme, obj runtime.Object, defaulter C o(options) } return &Webhook{ - Handler: &defaulterForType{ - object: obj, + Handler: &defaulterForType[runtime.Object]{ defaulter: defaulter, decoder: NewDecoder(scheme), removeUnknownOrOmitableFields: options.removeUnknownOrOmitableFields, + new: func() runtime.Object { return obj.DeepCopyObject() }, }, } } -type defaulterForType struct { - defaulter CustomDefaulter - object runtime.Object +type defaulterForType[T runtime.Object] struct { + defaulter Defaulter[T] decoder Decoder removeUnknownOrOmitableFields bool + new func() T } // Handle handles admission requests. -func (h *defaulterForType) Handle(ctx context.Context, req Request) Response { +func (h *defaulterForType[T]) Handle(ctx context.Context, req Request) Response { if h.decoder == nil { panic("decoder should never be nil") } if h.defaulter == nil { panic("defaulter should never be nil") } - if h.object == nil { - panic("object should never be nil") - } // Always skip when a DELETE operation received in custom mutation handler. if req.Operation == admissionv1.Delete { @@ -98,15 +123,15 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response { ctx = NewContextWithRequest(ctx, req) // Get the object in the request - obj := h.object.DeepCopyObject() + obj := h.new() if err := h.decoder.Decode(req, obj); err != nil { return Errored(http.StatusBadRequest, err) } // Keep a copy of the object if needed - var originalObj runtime.Object + var originalObj T if !h.removeUnknownOrOmitableFields { - originalObj = obj.DeepCopyObject() + originalObj = obj.DeepCopyObject().(T) } // Default the object @@ -131,7 +156,7 @@ func (h *defaulterForType) Handle(ctx context.Context, req Request) Response { return handlerResponse } -func (h *defaulterForType) dropSchemeRemovals(r Response, original runtime.Object, raw []byte) Response { +func (h *defaulterForType[T]) dropSchemeRemovals(r Response, original T, raw []byte) Response { const opRemove = "remove" if !r.Allowed || r.PatchType == nil { return r diff --git a/pkg/webhook/admission/validator_custom.go b/pkg/webhook/admission/validator_custom.go index ef1be52a8f..abd68e88bf 100644 --- a/pkg/webhook/admission/validator_custom.go +++ b/pkg/webhook/admission/validator_custom.go @@ -21,6 +21,7 @@ import ( "errors" "fmt" "net/http" + "reflect" v1 "k8s.io/api/admission/v1" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -30,54 +31,77 @@ import ( // Warnings represents warning messages. type Warnings []string -// CustomValidator defines functions for validating an operation. +// Validator defines functions for validating an operation. // The object to be validated is passed into methods as a parameter. -type CustomValidator interface { +type Validator[T runtime.Object] interface { // ValidateCreate validates the object on creation. // The optional warnings will be added to the response as warning messages. // Return an error if the object is invalid. - ValidateCreate(ctx context.Context, obj runtime.Object) (warnings Warnings, err error) + ValidateCreate(ctx context.Context, obj T) (warnings Warnings, err error) // ValidateUpdate validates the object on update. // The optional warnings will be added to the response as warning messages. // Return an error if the object is invalid. - ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (warnings Warnings, err error) + ValidateUpdate(ctx context.Context, oldObj, newObj T) (warnings Warnings, err error) // ValidateDelete validates the object on deletion. // The optional warnings will be added to the response as warning messages. // Return an error if the object is invalid. - ValidateDelete(ctx context.Context, obj runtime.Object) (warnings Warnings, err error) + ValidateDelete(ctx context.Context, obj T) (warnings Warnings, err error) +} + +// CustomValidator defines functions for validating an operation. +// Deprecated: CustomValidator is deprecated, use Validator instead +type CustomValidator = Validator[runtime.Object] + +// WithValidator creates a new Webhook for validating the provided type. +func WithValidator[T runtime.Object](scheme *runtime.Scheme, validator Validator[T]) *Webhook { + return &Webhook{ + Handler: &validatorForType[T]{ + validator: validator, + decoder: NewDecoder(scheme), + new: func() T { + var zero T + typ := reflect.TypeOf(zero) + if typ.Kind() == reflect.Ptr { + return reflect.New(typ.Elem()).Interface().(T) + } + return zero + }, + }, + } } -// WithCustomValidator creates a new Webhook for validating the provided type. +// WithCustomValidator creates a new Webhook for a CustomValidator. +// Deprecated: WithCustomValidator is deprecated, use WithValidator instead func WithCustomValidator(scheme *runtime.Scheme, obj runtime.Object, validator CustomValidator) *Webhook { return &Webhook{ - Handler: &validatorForType{object: obj, validator: validator, decoder: NewDecoder(scheme)}, + Handler: &validatorForType[runtime.Object]{ + validator: validator, + decoder: NewDecoder(scheme), + new: func() runtime.Object { return obj.DeepCopyObject() }, + }, } } -type validatorForType struct { - validator CustomValidator - object runtime.Object +type validatorForType[T runtime.Object] struct { + validator Validator[T] decoder Decoder + new func() T } // Handle handles admission requests. -func (h *validatorForType) Handle(ctx context.Context, req Request) Response { +func (h *validatorForType[T]) Handle(ctx context.Context, req Request) Response { if h.decoder == nil { panic("decoder should never be nil") } if h.validator == nil { panic("validator should never be nil") } - if h.object == nil { - panic("object should never be nil") - } ctx = NewContextWithRequest(ctx, req) - // Get the object in the request - obj := h.object.DeepCopyObject() + obj := h.new() var err error var warnings []string @@ -93,7 +117,7 @@ func (h *validatorForType) Handle(ctx context.Context, req Request) Response { warnings, err = h.validator.ValidateCreate(ctx, obj) case v1.Update: - oldObj := obj.DeepCopyObject() + oldObj := h.new() if err := h.decoder.DecodeRaw(req.Object, obj); err != nil { return Errored(http.StatusBadRequest, err) } diff --git a/pkg/webhook/alias.go b/pkg/webhook/alias.go index 2882e7bab3..b4f16a3f5f 100644 --- a/pkg/webhook/alias.go +++ b/pkg/webhook/alias.go @@ -24,9 +24,11 @@ import ( // define some aliases for common bits of the webhook functionality // CustomDefaulter defines functions for setting defaults on resources. +// Deprecated: Use admission.Defaulter instead. type CustomDefaulter = admission.CustomDefaulter // CustomValidator defines functions for validating an operation. +// Deprecated: Use admission.Validator instead. type CustomValidator = admission.CustomValidator // AdmissionRequest defines the input for an admission handler.