In my last post we saw that admitting objects in a ValidatigAdmissionWebhook can be expressed as a pure function i.e.,
func Admit(req *AdmissionRequest) *AdmissionResponse
This time, I'd like to argue that abstracting the Admit func into an Admitter interface can help us make the webhook code more testable.
Testing the Admit func
A little aside before getting to the Admitter interface, an additional benefit of wrapping our admission logic in an Admit func, this way, is that it makes it really easy to write a test for it.
func TestAdmit(t *testing.T) {
// setup req and expected resp
admResp := Admit(req)
assert.Equal(t, expectedAdmResp, admResp)
}
The Admitter Interface
So our admission logic can now be tesed but what about the HTTP machinery, there's some hairy encoding/decoding logic in our Handler func, so being able to test would be really comforting. That's where the Admitter interface comes in!
Here's the Admit func again:
func Admit(req *AdmissionRequest) *AdmissionResponse
And here's what the Admitter interface could look like:
type Admitter interface {
// Name() is helpful for logging and metrics
Name() string
// Admit() is where the pure admission logic lives
Admit(req *admissionsv1.AdmissionRequest) (*admissionsv1.AdmissionResponse, error)
}
Our Handler func is going to change just as well to accomodate. The Handler func originally:
func Handler() {
return func(){
// read req
validate()
// write resp
}
}
And we call it like:
handler.Handle("/deployments", admission.Handler())
The Handler func now:
func Handler(admitter Admitter) {
return func(){
// read req
admitter.Admit()
// write resp
}
}
Calling the Handler func would now look like:
// DeploymentAdmitter implements the Admitter interface
depAdmitter := admission.DeploymentAdmitter{}
handler.Handle("/deployments", admission.Handler(depAdmitter))
Implementing the Admitter Interface
As the last section hinted, each resource that the webhook must validate can just implement the Admitter interface. For example, here's the DeploymentAdmitter:
type DeploymentAdmitter struct {
}
func (d DeploymentAdmitter) Name() string {
return "deployments"
}
func (d DeploymentAdmitter) Admit(req *admissionsv1.AdmissionRequest) (*admissionsv1.AdmissionResponse, error) {
// deny if `application` label is missing
}
And guess what, we can just implement the Admitter interface in our tests to test the Handler. So cool, right!
Here's an example testAdmitter
type testAdmitter struct {
}
func (t TestAdmitter) Name() string {
return "testAdmitter"
}
func (t testAdmitter) Admit(req *admissionsv1.AdmissionRequest) (*admissionsv1.AdmissionResponse, error) {
// allow/deny objects depending on the test
}
Testing the Handler
At last, with the actual validation logic abstracted away, testing the Handler is really just as simple as calling the handler with a test request and the testAdmitter:
func TestHandler(t *testing.T) {
// setup AdmissionReview.Request and expected AdmissionReview.Response objects
adm = testAdmitter{}
Handler(req, resp)
// Test that
// 1. when an object should be allowed Handler allows it
// 2. when an object should be denied Handler denys it
assert.Equal(t, expectedResp, resp)
}
Thanks to the Admitter interface, we can now test parsing the admission requests and validating them!
Further Resources
This series is based on my experience adding a ValidatingAdmissionWebhook to Skipper, modern HTTP proxy.
Note: While the series uses validating
Deploymentsas an example, the webhook in Skipper validatesRouteGroupsa Custom Resource used in Skipper. Hope that's not too confusing!
You can find the complete source code for the
ValidatigAdmissionWebhookhereSpecifically, the tests can be found in
admission_test.go
Top comments (0)