Skip to content

Commit cd7afc5

Browse files
Allow to wire a mutation handler
1 parent 3854680 commit cd7afc5

File tree

3 files changed

+111
-7
lines changed

3 files changed

+111
-7
lines changed

pkg/builder/webhook.go

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ import (
3737
// WebhookBuilder builds a Webhook.
3838
type WebhookBuilder struct {
3939
apiType runtime.Object
40+
mutatorFactory admission.HandlerFactory
4041
customDefaulter admission.CustomDefaulter
4142
customValidator admission.CustomValidator
4243
gvk schema.GroupVersionKind
@@ -65,6 +66,12 @@ func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder {
6566
return blder
6667
}
6768

69+
// WithMutationHandler takes an admission.ObjectHandler interface, a MutatingWebhook will be wired for this type.
70+
func (blder *WebhookBuilder) WithMutatorFactory(factory admission.HandlerFactory) *WebhookBuilder {
71+
blder.mutatorFactory = factory
72+
return blder
73+
}
74+
6875
// WithDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook will be wired for this type.
6976
func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter) *WebhookBuilder {
7077
blder.customDefaulter = defaulter
@@ -169,14 +176,19 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() {
169176
}
170177

171178
func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook {
172-
if defaulter := blder.customDefaulter; defaulter != nil {
173-
w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter)
174-
if blder.recoverPanic != nil {
175-
w = w.WithRecoverPanic(*blder.recoverPanic)
176-
}
177-
return w
179+
var w *admission.Webhook
180+
if factory := blder.mutatorFactory; factory != nil {
181+
w = admission.WithHandlerFactory(blder.mgr.GetScheme(), blder.apiType, factory)
182+
} else if defaulter := blder.customDefaulter; defaulter != nil {
183+
w = admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter)
178184
}
179-
return nil
185+
if w == nil {
186+
return nil
187+
}
188+
if blder.recoverPanic != nil {
189+
w = w.WithRecoverPanic(*blder.recoverPanic)
190+
}
191+
return w
180192
}
181193

182194
// registerValidatingWebhook registers a validating webhook if necessary.

pkg/builder/webhook_test.go

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -288,6 +288,82 @@ func runTests(admissionReviewVersion string) {
288288
ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
289289
})
290290

291+
It("should scaffold a mutating webhook with a mutator", func() {
292+
By("creating a controller manager")
293+
m, err := manager.New(cfg, manager.Options{})
294+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
295+
296+
By("registering the type in the Scheme")
297+
builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
298+
builder.Register(&TestDefaulter{}, &TestDefaulterList{})
299+
err = builder.AddToScheme(m.GetScheme())
300+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
301+
302+
err = WebhookManagedBy(m).
303+
WithMutatorFactory(mutatorFactoryForTestDefaulter(m.GetScheme())).
304+
For(&TestDefaulter{}).
305+
WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
306+
return admission.DefaultLogConstructor(testingLogger, req)
307+
}).
308+
Complete()
309+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
310+
svr := m.GetWebhookServer()
311+
ExpectWithOffset(1, svr).NotTo(BeNil())
312+
313+
reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
314+
"request":{
315+
"uid":"07e52e8d-4513-11e9-a716-42010a800270",
316+
"kind":{
317+
"group":"foo.test.org",
318+
"version":"v1",
319+
"kind":"TestDefaulter"
320+
},
321+
"resource":{
322+
"group":"foo.test.org",
323+
"version":"v1",
324+
"resource":"testdefaulter"
325+
},
326+
"namespace":"default",
327+
"name":"foo",
328+
"operation":"CREATE",
329+
"object":{
330+
"replica":1
331+
},
332+
"oldObject":null
333+
}
334+
}`)
335+
336+
ctx, cancel := context.WithCancel(context.Background())
337+
cancel()
338+
err = svr.Start(ctx)
339+
if err != nil && !os.IsNotExist(err) {
340+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
341+
}
342+
343+
By("sending a request to a mutating webhook path")
344+
path := generateMutatePath(testDefaulterGVK)
345+
req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
346+
req.Header.Add("Content-Type", "application/json")
347+
w := httptest.NewRecorder()
348+
svr.WebhookMux().ServeHTTP(w, req)
349+
ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
350+
By("sanity checking the response contains reasonable fields")
351+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
352+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
353+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
354+
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"`))
355+
356+
By("sending a request to a validating webhook path that doesn't exist")
357+
path = generateValidatePath(testDefaulterGVK)
358+
_, err = reader.Seek(0, 0)
359+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
360+
req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
361+
req.Header.Add("Content-Type", "application/json")
362+
w = httptest.NewRecorder()
363+
svr.WebhookMux().ServeHTTP(w, req)
364+
ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
365+
})
366+
291367
It("should scaffold a custom validating webhook if the type implements the CustomValidator interface", func() {
292368
By("creating a controller manager")
293369
m, err := manager.New(cfg, manager.Options{})
@@ -735,6 +811,12 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err
735811

736812
var _ admission.CustomDefaulter = &TestCustomDefaulter{}
737813

814+
func mutatorFactoryForTestDefaulter(scheme *runtime.Scheme) admission.HandlerFactory {
815+
return func(obj runtime.Object, _ admission.Decoder) admission.Handler {
816+
return admission.WithCustomDefaulter(scheme, obj, &TestCustomDefaulter{}).Handler
817+
}
818+
}
819+
738820
// TestCustomValidator.
739821

740822
type TestCustomValidator struct{}

pkg/webhook/admission/webhook.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ import (
2727
"gomodules.xyz/jsonpatch/v2"
2828
admissionv1 "k8s.io/api/admission/v1"
2929
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
30+
"k8s.io/apimachinery/pkg/runtime"
3031
"k8s.io/apimachinery/pkg/util/json"
3132
utilruntime "k8s.io/apimachinery/pkg/util/runtime"
3233
"k8s.io/klog/v2"
@@ -95,6 +96,8 @@ func (r *Response) Complete(req Request) error {
9596
return nil
9697
}
9798

99+
type HandlerFactory func(obj runtime.Object, decoder Decoder) Handler
100+
98101
// Handler can handle an AdmissionRequest.
99102
type Handler interface {
100103
// Handle yields a response to an AdmissionRequest.
@@ -114,6 +117,13 @@ func (f HandlerFunc) Handle(ctx context.Context, req Request) Response {
114117
return f(ctx, req)
115118
}
116119

120+
// WithMutator creates a new Webhook for a handler factory.
121+
func WithHandlerFactory(scheme *runtime.Scheme, obj runtime.Object, factory HandlerFactory) *Webhook {
122+
return &Webhook{
123+
Handler: factory(obj, NewDecoder(scheme)),
124+
}
125+
}
126+
117127
// Webhook represents each individual webhook.
118128
//
119129
// It must be registered with a webhook.Server or

0 commit comments

Comments
 (0)