Skip to content

Commit

Permalink
Add custom path option for webhooks
Browse files Browse the repository at this point in the history
  • Loading branch information
damsien committed Oct 30, 2024
1 parent 3eade21 commit c25544e
Show file tree
Hide file tree
Showing 2 changed files with 184 additions and 0 deletions.
27 changes: 27 additions & 0 deletions pkg/builder/webhook.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import (
"errors"
"net/http"
"net/url"
"path"
"regexp"
"strings"

"github.com/go-logr/logr"
Expand All @@ -39,6 +41,7 @@ type WebhookBuilder struct {
apiType runtime.Object
customDefaulter admission.CustomDefaulter
customValidator admission.CustomValidator
customPath string
gvk schema.GroupVersionKind
mgr manager.Manager
config *rest.Config
Expand Down Expand Up @@ -90,6 +93,12 @@ func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder {
return blder
}

// WithCustomPath overrides the webhook's default path by the customPath
func (blder *WebhookBuilder) WithCustomPath(customPath string) *WebhookBuilder {
blder.customPath = customPath
return blder
}

// Complete builds the webhook.
func (blder *WebhookBuilder) Complete() error {
// Set the Config
Expand Down Expand Up @@ -156,6 +165,11 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() {
if mwh != nil {
mwh.LogConstructor = blder.logConstructor
path := generateMutatePath(blder.gvk)
if blder.customPath != "" {
if generateCustomPath(blder.customPath) != "" {
path = generateCustomPath(blder.customPath)
}
}

// Checking if the path is already registered.
// If so, just skip it.
Expand Down Expand Up @@ -185,6 +199,11 @@ func (blder *WebhookBuilder) registerValidatingWebhook() {
if vwh != nil {
vwh.LogConstructor = blder.logConstructor
path := generateValidatePath(blder.gvk)
if blder.customPath != "" {
if generateCustomPath(blder.customPath) != "" {
path = generateCustomPath(blder.customPath)
}
}

// Checking if the path is already registered.
// If so, just skip it.
Expand Down Expand Up @@ -251,3 +270,11 @@ func generateValidatePath(gvk schema.GroupVersionKind) string {
return "/validate-" + strings.ReplaceAll(gvk.Group, ".", "-") + "-" +
gvk.Version + "-" + strings.ToLower(gvk.Kind)
}

func generateCustomPath(customPath string) string {
validPathRegex := regexp.MustCompile(`^((/[a-zA-Z0-9-_]+)+|/)$`)
if !validPathRegex.MatchString(customPath) {
return ""
}
return path.Clean(customPath)
}
157 changes: 157 additions & 0 deletions pkg/builder/webhook_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,84 @@ func runTests(admissionReviewVersion string) {
ExpectWithOffset(1, w.Code).To(Equal(http.StatusNotFound))
})

It("should scaffold a custom defaulting webhook with a custom path", 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())

customPath := "/custom-defaulting-path"
err = WebhookManagedBy(m).
For(&TestDefaulter{}).
WithDefaulter(&TestCustomDefaulter{}).
WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
return admission.DefaultLogConstructor(testingLogger, req)
}).
WithCustomPath(customPath).
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 := generateCustomPath(customPath)
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 mutating webhook path that have been overrided by the custom path")
path = generateMutatePath(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 defaulting webhook which recovers from panics", func() {
By("creating a controller manager")
m, err := manager.New(cfg, manager.Options{})
Expand Down Expand Up @@ -294,6 +372,85 @@ func runTests(admissionReviewVersion string) {
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"`))
})

It("should scaffold a custom validating webhook with a custom path", 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: testValidatorGVK.GroupVersion()}
builder.Register(&TestValidator{}, &TestValidatorList{})
err = builder.AddToScheme(m.GetScheme())
ExpectWithOffset(1, err).NotTo(HaveOccurred())

customPath := "/custom-validating-path"
err = WebhookManagedBy(m).
For(&TestValidator{}).
WithValidator(&TestCustomValidator{}).
WithLogConstructor(func(base logr.Logger, req *admission.Request) logr.Logger {
return admission.DefaultLogConstructor(testingLogger, req)
}).
WithCustomPath(customPath).
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":"TestValidator"
},
"resource":{
"group":"foo.test.org",
"version":"v1",
"resource":"testvalidator"
},
"namespace":"default",
"name":"foo",
"operation":"UPDATE",
"object":{
"replica":1
},
"oldObject":{
"replica":2
}
}
}`)

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 that have been overrided by a custom path")
path := generateValidatePath(testValidatorGVK)
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))

By("sending a request to a validating webhook path")
path = generateCustomPath(customPath)
_, 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.StatusOK))
By("sanity checking the response contains reasonable field")
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"allowed":false`))
ExpectWithOffset(1, w.Body).To(ContainSubstring(`"code":403`))
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"`))
})

It("should scaffold a custom validating webhook which recovers from panics", func() {
By("creating a controller manager")
m, err := manager.New(cfg, manager.Options{})
Expand Down

0 comments on commit c25544e

Please sign in to comment.