Skip to content

Commit

Permalink
Allow to wire a mutation handler
Browse files Browse the repository at this point in the history
  • Loading branch information
alculquicondor committed Sep 3, 2024
1 parent 3854680 commit 56c3467
Show file tree
Hide file tree
Showing 3 changed files with 112 additions and 7 deletions.
26 changes: 19 additions & 7 deletions pkg/builder/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -65,6 +66,12 @@ func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder {
return blder
}

// WithMutatorFactory takes an admission.HandlerFactory, a MutatingWebhook will be wired for the handler that this factory creates.
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
Expand Down Expand Up @@ -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.
Expand Down
82 changes: 82 additions & 0 deletions pkg/builder/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{})
Expand Down Expand Up @@ -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{}
Expand Down
11 changes: 11 additions & 0 deletions pkg/webhook/admission/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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.
Expand All @@ -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
Expand Down

0 comments on commit 56c3467

Please sign in to comment.