From df4be176f0ca54336cb04ae8cc40698b66b48dfe Mon Sep 17 00:00:00 2001 From: Aldo Culquicondor Date: Fri, 23 Aug 2024 15:52:37 +0000 Subject: [PATCH] Allow to wire a mutation handler --- pkg/builder/webhook.go | 26 +++++++--- pkg/builder/webhook_test.go | 82 ++++++++++++++++++++++++++++++++ pkg/webhook/admission/webhook.go | 11 +++++ 3 files changed, 112 insertions(+), 7 deletions(-) diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go index cfb9f1a69d..0d947857eb 100644 --- a/pkg/builder/webhook.go +++ b/pkg/builder/webhook.go @@ -37,6 +37,7 @@ import ( // WebhookBuilder builds a Webhook. type WebhookBuilder struct { apiType runtime.Object + mutatorFactory admission.HandlerFactory customDefaulter admission.CustomDefaulter customValidator admission.CustomValidator gvk schema.GroupVersionKind @@ -65,6 +66,12 @@ func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder { return blder } +// WithMutationHandler takes an admission.ObjectHandler interface, a MutatingWebhook will be wired for this type. +func (blder *WebhookBuilder) WithMutatorFactory(factory admission.HandlerFactory) *WebhookBuilder { + blder.mutatorFactory = factory + return blder +} + // WithDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook will be wired for this type. func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter) *WebhookBuilder { blder.customDefaulter = defaulter @@ -169,14 +176,19 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() { } func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook { - if defaulter := blder.customDefaulter; defaulter != nil { - w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter) - if blder.recoverPanic != nil { - w = w.WithRecoverPanic(*blder.recoverPanic) - } - return w + var w *admission.Webhook + if factory := blder.mutatorFactory; factory != nil { + w = admission.WithHandlerFactory(blder.mgr.GetScheme(), blder.apiType, factory) + } else if defaulter := blder.customDefaulter; defaulter != nil { + w = admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter) } - return nil + if w == nil { + return nil + } + if blder.recoverPanic != nil { + w = w.WithRecoverPanic(*blder.recoverPanic) + } + return w } // registerValidatingWebhook registers a validating webhook if necessary. diff --git a/pkg/builder/webhook_test.go b/pkg/builder/webhook_test.go index 106825b2d1..f246382ab7 100644 --- a/pkg/builder/webhook_test.go +++ b/pkg/builder/webhook_test.go @@ -288,6 +288,82 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound)) }) + It("should scaffold a mutating webhook with a mutator", 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: testDefaulterGVK.GroupVersion()} + builder.Register(&TestDefaulter{}, &TestDefaulterList{}) + err = builder.AddToScheme(m.GetScheme()) + ExpectWithOffset(1, err).NotTo(HaveOccurred()) + + err = WebhookManagedBy(m). + WithMutatorFactory(mutatorFactoryForTestDefaulter(m.GetScheme())). + For(&TestDefaulter{}). + 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 + `", + "request":{ + "uid":"07e52e8d-4513-11e9-a716-42010a800270", + "kind":{ + "group":"foo.test.org", + "version":"v1", + "kind":"TestDefaulter" + }, + "resource":{ + "group":"foo.test.org", + "version":"v1", + "resource":"testdefaulter" + }, + "namespace":"default", + "name":"foo", + "operation":"CREATE", + "object":{ + "replica":1 + }, + "oldObject":null + } +}`) + + ctx, cancel := context.WithCancel(context.Background()) + 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":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)) + }) + It("should scaffold a custom validating webhook if the type implements the CustomValidator interface", func() { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) @@ -735,6 +811,12 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err var _ admission.CustomDefaulter = &TestCustomDefaulter{} +func mutatorFactoryForTestDefaulter(scheme *runtime.Scheme) admission.HandlerFactory { + return func(obj runtime.Object, _ admission.Decoder) admission.Handler { + return admission.WithCustomDefaulter(scheme, obj, &TestCustomDefaulter{}).Handler + } +} + // TestCustomValidator. type TestCustomValidator struct{} diff --git a/pkg/webhook/admission/webhook.go b/pkg/webhook/admission/webhook.go index cba6da2cb0..467fcf0d0e 100644 --- a/pkg/webhook/admission/webhook.go +++ b/pkg/webhook/admission/webhook.go @@ -27,6 +27,7 @@ import ( "gomodules.xyz/jsonpatch/v2" admissionv1 "k8s.io/api/admission/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/json" utilruntime "k8s.io/apimachinery/pkg/util/runtime" "k8s.io/klog/v2" @@ -95,6 +96,9 @@ func (r *Response) Complete(req Request) error { return nil } +// HandlerFactory can create a Handler for the given type. +type HandlerFactory func(obj runtime.Object, decoder Decoder) Handler + // Handler can handle an AdmissionRequest. type Handler interface { // Handle yields a response to an AdmissionRequest. @@ -114,6 +118,13 @@ func (f HandlerFunc) Handle(ctx context.Context, req Request) Response { return f(ctx, req) } +// WithHandlerFactory creates a new Webhook for a handler factory. +func WithHandlerFactory(scheme *runtime.Scheme, obj runtime.Object, factory HandlerFactory) *Webhook { + return &Webhook{ + Handler: factory(obj, NewDecoder(scheme)), + } +} + // Webhook represents each individual webhook. // // It must be registered with a webhook.Server or