Skip to content

Commit ea977b9

Browse files
Allow to wire a mutation handler
1 parent 236e448 commit ea977b9

File tree

3 files changed

+111
-7
lines changed

3 files changed

+111
-7
lines changed

pkg/builder/webhook.go

Lines changed: 18 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+
// WithMutatorFactory takes an admission.HandlerFactory, a MutatingWebhook will be wired for the handler that this factory creates.
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,18 @@ 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)
184+
} else {
185+
return nil
178186
}
179-
return nil
187+
if blder.recoverPanic != nil {
188+
w = w.WithRecoverPanic(*blder.recoverPanic)
189+
}
190+
return w
180191
}
181192

182193
// 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
@@ -217,6 +217,82 @@ func runTests(admissionReviewVersion string) {
217217
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"message":"panic: fake panic test [recovered]`))
218218
})
219219

220+
It("should scaffold a mutating webhook with a mutator", func() {
221+
By("creating a controller manager")
222+
m, err := manager.New(cfg, manager.Options{})
223+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
224+
225+
By("registering the type in the Scheme")
226+
builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
227+
builder.Register(&TestDefaulter{}, &TestDefaulterList{})
228+
err = builder.AddToScheme(m.GetScheme())
229+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
230+
231+
err = WebhookManagedBy(m).
232+
WithMutatorFactory(mutatorFactoryForTestDefaulter(m.GetScheme())).
233+
For(&TestDefaulter{}).
234+
WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
235+
return admission.DefaultLogConstructor(testingLogger, req)
236+
}).
237+
Complete()
238+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
239+
svr := m.GetWebhookServer()
240+
ExpectWithOffset(1, svr).NotTo(BeNil())
241+
242+
reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
243+
"request":{
244+
"uid":"07e52e8d-4513-11e9-a716-42010a800270",
245+
"kind":{
246+
"group":"foo.test.org",
247+
"version":"v1",
248+
"kind":"TestDefaulter"
249+
},
250+
"resource":{
251+
"group":"foo.test.org",
252+
"version":"v1",
253+
"resource":"testdefaulter"
254+
},
255+
"namespace":"default",
256+
"name":"foo",
257+
"operation":"CREATE",
258+
"object":{
259+
"replica":1
260+
},
261+
"oldObject":null
262+
}
263+
}`)
264+
265+
ctx, cancel := context.WithCancel(context.Background())
266+
cancel()
267+
err = svr.Start(ctx)
268+
if err != nil && !os.IsNotExist(err) {
269+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
270+
}
271+
272+
By("sending a request to a mutating webhook path")
273+
path := generateMutatePath(testDefaulterGVK)
274+
req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
275+
req.Header.Add("Content-Type", "application/json")
276+
w := httptest.NewRecorder()
277+
svr.WebhookMux().ServeHTTP(w, req)
278+
ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
279+
By("sanity checking the response contains reasonable fields")
280+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
281+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
282+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
283+
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"`))
284+
285+
By("sending a request to a validating webhook path that doesn't exist")
286+
path = generateValidatePath(testDefaulterGVK)
287+
_, err = reader.Seek(0, 0)
288+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
289+
req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
290+
req.Header.Add("Content-Type", "application/json")
291+
w = httptest.NewRecorder()
292+
svr.WebhookMux().ServeHTTP(w, req)
293+
ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
294+
})
295+
220296
It("should scaffold a custom validating webhook", func() {
221297
By("creating a controller manager")
222298
m, err := manager.New(cfg, manager.Options{})
@@ -592,6 +668,12 @@ func (*TestCustomDefaulter) Default(ctx context.Context, obj runtime.Object) err
592668

593669
var _ admission.CustomDefaulter = &TestCustomDefaulter{}
594670

671+
func mutatorFactoryForTestDefaulter(scheme *runtime.Scheme) admission.HandlerFactory {
672+
return func(obj runtime.Object, _ admission.Decoder) admission.Handler {
673+
return admission.WithCustomDefaulter(scheme, obj, &TestCustomDefaulter{}).Handler
674+
}
675+
}
676+
595677
// TestCustomValidator.
596678

597679
type TestCustomValidator struct{}

pkg/webhook/admission/webhook.go

Lines changed: 11 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,9 @@ func (r *Response) Complete(req Request) error {
9596
return nil
9697
}
9798

99+
// HandlerFactory can create a Handler for the given type.
100+
type HandlerFactory func(obj runtime.Object, decoder Decoder) Handler
101+
98102
// Handler can handle an AdmissionRequest.
99103
type Handler interface {
100104
// Handle yields a response to an AdmissionRequest.
@@ -114,6 +118,13 @@ func (f HandlerFunc) Handle(ctx context.Context, req Request) Response {
114118
return f(ctx, req)
115119
}
116120

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

0 commit comments

Comments
 (0)