diff --git a/pkg/builder/webhook.go b/pkg/builder/webhook.go index cfb9f1a69d..825f8c8c7f 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 + mutationHandler admission.Handler 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.Handler, a MutatingWebhook will be wired for it. +func (blder *WebhookBuilder) WithMutationHandler(h admission.Handler) *WebhookBuilder { + blder.mutationHandler = h + 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 @@ -140,7 +147,9 @@ func (blder *WebhookBuilder) registerWebhooks() error { } // Register webhook(s) for type - blder.registerDefaultingWebhook() + if err := blder.registerDefaultingWebhook(); err != nil { + return err + } blder.registerValidatingWebhook() err = blder.registerConversionWebhook() @@ -151,8 +160,11 @@ func (blder *WebhookBuilder) registerWebhooks() error { } // registerDefaultingWebhook registers a defaulting webhook if necessary. -func (blder *WebhookBuilder) registerDefaultingWebhook() { - mwh := blder.getDefaultingWebhook() +func (blder *WebhookBuilder) registerDefaultingWebhook() error { + mwh, err := blder.getDefaultingWebhook() + if err != nil { + return err + } if mwh != nil { mwh.LogConstructor = blder.logConstructor path := generateMutatePath(blder.gvk) @@ -166,17 +178,27 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() { blder.mgr.GetWebhookServer().Register(path, mwh) } } + return nil } -func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook { +func (blder *WebhookBuilder) getDefaultingWebhook() (*admission.Webhook, error) { + var w *admission.Webhook + if handler := blder.mutationHandler; handler != nil { + w = &admission.Webhook{Handler: handler} + } if defaulter := blder.customDefaulter; defaulter != nil { - w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter) - if blder.recoverPanic != nil { - w = w.WithRecoverPanic(*blder.recoverPanic) + if w != nil { + return nil, errors.New("a WebhookBuilder can only define a MutationHandler or a Defaulter, but not both") } - return w + w = admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter) } - return nil + if w == nil { + return nil, nil + } + if blder.recoverPanic != nil { + w = w.WithRecoverPanic(*blder.recoverPanic) + } + return w, nil } // registerValidatingWebhook registers a validating webhook if necessary. diff --git a/pkg/builder/webhook_test.go b/pkg/builder/webhook_test.go index 3ed422d3e9..83ee6cde22 100644 --- a/pkg/builder/webhook_test.go +++ b/pkg/builder/webhook_test.go @@ -30,9 +30,12 @@ import ( . "github.com/onsi/ginkgo/v2" . "github.com/onsi/gomega" "github.com/onsi/gomega/gbytes" + "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/runtime/schema" + "k8s.io/utils/ptr" logf "sigs.k8s.io/controller-runtime/pkg/log" "sigs.k8s.io/controller-runtime/pkg/log/zap" @@ -217,6 +220,82 @@ func runTests(admissionReviewVersion string) { ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`)) }) + 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). + For(&TestDefaulter{}). + WithMutationHandler(&TestMutationHandler{}). + 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":"Mutating 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", func() { By("creating a controller manager") m, err := manager.New(cfg, manager.Options{}) @@ -592,6 +671,26 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err var _ admission.CustomDefaulter = &TestCustomDefaulter{} +// TestMutationHandler +type TestMutationHandler struct{} + +func (*TestMutationHandler) Handle(ctx context.Context, req admission.Request) admission.Response { + logf.FromContext(ctx).Info("Mutating object") + return admission.Response{ + AdmissionResponse: admissionv1.AdmissionResponse{ + Allowed: true, + PatchType: ptr.To(admissionv1.PatchTypeJSONPatch), + }, + Patches: []jsonpatch.Operation{{ + Operation: "replace", + Path: "/replica", + Value: "2", + }}, + } +} + +var _ admission.Handler = &TestMutationHandler{} + // TestCustomValidator. type TestCustomValidator struct{}