Making the ValidatingWebhook Testable
Tags: kubernetes, go
Published: 2021-01-17 17:23:46

In my [last post](https://www.learningkubeonaws.com/blog/validating-admission-requests-with-a-validating-admission-webhook) we saw that admitting objects in a ValidatigAdmissionWebhook can be expressed as a pure function i.e.,

```go 
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.

```go
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:

```go 
func Admit(req *AdmissionRequest) *AdmissionResponse
```

And here's what the `Admitter` interface could look like:

```go
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:

```go
func Handler() {
    return func(){
        // read req
        validate()
        // write resp
    }
}
```

And we call it like:

```go
handler.Handle("/deployments", admission.Handler())
```

The `Handler` func now:

```go
func Handler(admitter Admitter) {
    return func(){
        // read req
        admitter.Admit()
        // write resp
    }
}
```

Calling the `Handler` func would now look like:

```go
// 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`:

```go
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`

```go
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`:

```go
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](https://www.learningkubeonaws.com/blog/the-2-main-parts-of-a-kubernetes-validating-admission-webhook) and [validating them](https://www.learningkubeonaws.com/blog/parsing-admission-requests-in-a-validating-admission-webhook)!

### Further Resources

This series is based on my experience adding a `ValidatingAdmissionWebhook` to [Skipper](https://github.com/zalando/skipper), modern HTTP proxy.

> **Note:** While the series uses validating `Deployments` as an example, the webhook in Skipper validates `RouteGroups` a [Custom Resource](https://kubernetes.io/docs/concepts/extend-kubernetes/api-extension/custom-resources/) used in Skipper. Hope that's not too confusing!

- You can find the complete source code for the `ValidatigAdmissionWebhook` [here](https://github.com/zalando/skipper/tree/master/cmd/webhook)

- Specifically, the tests can be found in [`admission_test.go`](https://github.com/zalando/skipper/blob/master/cmd/webhook/admission/admission_test.go)