Skip to content

Commit 0080723

Browse files
committed
Add custom path option for webhooks
1 parent aaaefb4 commit 0080723

File tree

2 files changed

+184
-0
lines changed

2 files changed

+184
-0
lines changed

pkg/builder/webhook.go

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ import (
2020
"errors"
2121
"net/http"
2222
"net/url"
23+
"path"
24+
"regexp"
2325
"strings"
2426

2527
"github.com/go-logr/logr"
@@ -39,6 +41,7 @@ type WebhookBuilder struct {
3941
apiType runtime.Object
4042
customDefaulter admission.CustomDefaulter
4143
customValidator admission.CustomValidator
44+
customPath string
4245
gvk schema.GroupVersionKind
4346
mgr manager.Manager
4447
config *rest.Config
@@ -90,6 +93,12 @@ func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder {
9093
return blder
9194
}
9295

96+
// WithCustomPath overrides the webhook's default path by the customPath
97+
func (blder *WebhookBuilder) WithCustomPath(customPath string) *WebhookBuilder {
98+
blder.customPath = customPath
99+
return blder
100+
}
101+
93102
// Complete builds the webhook.
94103
func (blder *WebhookBuilder) Complete() error {
95104
// Set the Config
@@ -156,6 +165,11 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() {
156165
if mwh != nil {
157166
mwh.LogConstructor = blder.logConstructor
158167
path := generateMutatePath(blder.gvk)
168+
if blder.customPath != "" {
169+
if generateCustomPath(blder.customPath) != "" {
170+
path = generateCustomPath(blder.customPath)
171+
}
172+
}
159173

160174
// Checking if the path is already registered.
161175
// If so, just skip it.
@@ -185,6 +199,11 @@ func (blder *WebhookBuilder) registerValidatingWebhook() {
185199
if vwh != nil {
186200
vwh.LogConstructor = blder.logConstructor
187201
path := generateValidatePath(blder.gvk)
202+
if blder.customPath != "" {
203+
if generateCustomPath(blder.customPath) != "" {
204+
path = generateCustomPath(blder.customPath)
205+
}
206+
}
188207

189208
// Checking if the path is already registered.
190209
// If so, just skip it.
@@ -251,3 +270,11 @@ func generateValidatePath(gvk schema.GroupVersionKind) string {
251270
return "/validate-" + strings.ReplaceAll(gvk.Group, ".", "-") + "-" +
252271
gvk.Version + "-" + strings.ToLower(gvk.Kind)
253272
}
273+
274+
func generateCustomPath(customPath string) string {
275+
validPathRegex := regexp.MustCompile(`^((/[a-zA-Z0-9-_]+)+|/)$`)
276+
if !validPathRegex.MatchString(customPath) {
277+
return ""
278+
}
279+
return path.Clean(customPath)
280+
}

pkg/builder/webhook_test.go

Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,84 @@ func runTests(admissionReviewVersion string) {
153153
ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
154154
})
155155

156+
It("should scaffold a custom defaulting webhook with a custom path", func() {
157+
By("creating a controller manager")
158+
m, err := manager.New(cfg, manager.Options{})
159+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
160+
161+
By("registering the type in the Scheme")
162+
builder := scheme.Builder{GroupVersion: testDefaulterGVK.GroupVersion()}
163+
builder.Register(&TestDefaulter{}, &TestDefaulterList{})
164+
err = builder.AddToScheme(m.GetScheme())
165+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
166+
167+
customPath := "/custom-defaulting-path"
168+
err = WebhookManagedBy(m).
169+
For(&TestDefaulter{}).
170+
WithDefaulter(&TestCustomDefaulter{}).
171+
WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
172+
return admission.DefaultLogConstructor(testingLogger, req)
173+
}).
174+
WithCustomPath(customPath).
175+
Complete()
176+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
177+
svr := m.GetWebhookServer()
178+
ExpectWithOffset(1, svr).NotTo(BeNil())
179+
180+
reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
181+
"request":{
182+
"uid":"07e52e8d-4513-11e9-a716-42010a800270",
183+
"kind":{
184+
"group":"foo.test.org",
185+
"version":"v1",
186+
"kind":"TestDefaulter"
187+
},
188+
"resource":{
189+
"group":"foo.test.org",
190+
"version":"v1",
191+
"resource":"testdefaulter"
192+
},
193+
"namespace":"default",
194+
"name":"foo",
195+
"operation":"CREATE",
196+
"object":{
197+
"replica":1
198+
},
199+
"oldObject":null
200+
}
201+
}`)
202+
203+
ctx, cancel := context.WithCancel(context.Background())
204+
cancel()
205+
err = svr.Start(ctx)
206+
if err != nil && !os.IsNotExist(err) {
207+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
208+
}
209+
210+
By("sending a request to a mutating webhook path")
211+
path := generateCustomPath(customPath)
212+
req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
213+
req.Header.Add("Content-Type", "application/json")
214+
w := httptest.NewRecorder()
215+
svr.WebhookMux().ServeHTTP(w, req)
216+
ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
217+
By("sanity checking the response contains reasonable fields")
218+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":true`))
219+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"patch":`))
220+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":200`))
221+
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"`))
222+
223+
By("sending a request to a mutating webhook path that have been overrided by the custom path")
224+
path = generateMutatePath(testDefaulterGVK)
225+
_, err = reader.Seek(0, 0)
226+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
227+
req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
228+
req.Header.Add("Content-Type", "application/json")
229+
w = httptest.NewRecorder()
230+
svr.WebhookMux().ServeHTTP(w, req)
231+
ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
232+
})
233+
156234
It("should scaffold a custom defaulting webhook which recovers from panics", func() {
157235
By("creating a controller manager")
158236
m, err := manager.New(cfg, manager.Options{})
@@ -294,6 +372,85 @@ func runTests(admissionReviewVersion string) {
294372
EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
295373
})
296374

375+
It("should scaffold a custom validating webhook with a custom path", func() {
376+
By("creating a controller manager")
377+
m, err := manager.New(cfg, manager.Options{})
378+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
379+
380+
By("registering the type in the Scheme")
381+
builder := scheme.Builder{GroupVersion: testValidatorGVK.GroupVersion()}
382+
builder.Register(&TestValidator{}, &TestValidatorList{})
383+
err = builder.AddToScheme(m.GetScheme())
384+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
385+
386+
customPath := "/custom-validating-path"
387+
err = WebhookManagedBy(m).
388+
For(&TestValidator{}).
389+
WithValidator(&TestCustomValidator{}).
390+
WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
391+
return admission.DefaultLogConstructor(testingLogger, req)
392+
}).
393+
WithCustomPath(customPath).
394+
Complete()
395+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
396+
svr := m.GetWebhookServer()
397+
ExpectWithOffset(1, svr).NotTo(BeNil())
398+
399+
reader := strings.NewReader(admissionReviewGV + admissionReviewVersion + `",
400+
"request":{
401+
"uid":"07e52e8d-4513-11e9-a716-42010a800270",
402+
"kind":{
403+
"group":"foo.test.org",
404+
"version":"v1",
405+
"kind":"TestValidator"
406+
},
407+
"resource":{
408+
"group":"foo.test.org",
409+
"version":"v1",
410+
"resource":"testvalidator"
411+
},
412+
"namespace":"default",
413+
"name":"foo",
414+
"operation":"UPDATE",
415+
"object":{
416+
"replica":1
417+
},
418+
"oldObject":{
419+
"replica":2
420+
}
421+
}
422+
}`)
423+
424+
ctx, cancel := context.WithCancel(context.Background())
425+
cancel()
426+
err = svr.Start(ctx)
427+
if err != nil && !os.IsNotExist(err) {
428+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
429+
}
430+
431+
By("sending a request to a mutating webhook path that have been overrided by a custom path")
432+
path := generateValidatePath(testValidatorGVK)
433+
req := httptest.NewRequest("POST", svcBaseAddr+path, reader)
434+
req.Header.Add("Content-Type", "application/json")
435+
w := httptest.NewRecorder()
436+
svr.WebhookMux().ServeHTTP(w, req)
437+
ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
438+
439+
By("sending a request to a validating webhook path")
440+
path = generateCustomPath(customPath)
441+
_, err = reader.Seek(0, 0)
442+
ExpectWithOffset(1, err).NotTo(HaveOccurred())
443+
req = httptest.NewRequest("POST", svcBaseAddr+path, reader)
444+
req.Header.Add("Content-Type", "application/json")
445+
w = httptest.NewRecorder()
446+
svr.WebhookMux().ServeHTTP(w, req)
447+
ExpectWithOffset(1, w.Code).To(Equal(http.StatusOK))
448+
By("sanity checking the response contains reasonable field")
449+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
450+
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
451+
EventuallyWithOffset(1, logBuffer).Should(gbytes.Say(`"msg":"Validating object","object":{"name":"foo","namespace":"default"},"namespace":"default","name":"foo","resource":{"group":"foo.test.org","version":"v1","resource":"testvalidator"},"user":"","requestID":"07e52e8d-4513-11e9-a716-42010a800270"`))
452+
})
453+
297454
It("should scaffold a custom validating webhook which recovers from panics", func() {
298455
By("creating a controller manager")
299456
m, err := manager.New(cfg, manager.Options{})

0 commit comments

Comments
 (0)