DEV Community

Muaaz Saleem
Muaaz Saleem

Posted on • Originally published at muaazsaleem.com

Making the ValidatingWebhook Testable

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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
    }
}
Enter fullscreen mode Exit fullscreen mode

And we call it like:

handler.Handle("/deployments", admission.Handler())
Enter fullscreen mode Exit fullscreen mode

The Handler func now:

func Handler(admitter Admitter) {
    return func(){
        // read req
        admitter.Admit()
        // write resp
    }
}
Enter fullscreen mode Exit fullscreen mode

Calling the Handler func would now look like:

// DeploymentAdmitter implements the Admitter interface
depAdmitter := admission.DeploymentAdmitter{}
handler.Handle("/deployments", admission.Handler(depAdmitter))
Enter fullscreen mode Exit fullscreen mode

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 
}
Enter fullscreen mode Exit fullscreen mode

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 
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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 validates RouteGroups a Custom Resource used in Skipper. Hope that's not too confusing!

  • You can find the complete source code for the ValidatigAdmissionWebhook here

  • Specifically, the tests can be found in admission_test.go

Top comments (0)