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
Deployments
as an example, the webhook in Skipper validatesRouteGroups
a Custom Resource used in Skipper. Hope that's not too confusing!
You can find the complete source code for the
ValidatigAdmissionWebhook
hereSpecifically, the tests can be found in
admission_test.go
Top comments (0)