Admission Webhooks

Create a validating or mutating Admission Webhook

An admission webhook is an HTTP callback that is registered with Kubernetes, and will be called by Kubernetes to validate or mutate a resource before being stored. There are two types of admission webhooks, validating and mutating. Validating webhooks can be used to perform validations that go beyond the capabilities of OpenAPI schema validation, such as ensuring a field is immutable after creation or higher level permissions checks based on the user that is making the request to the API server. Mutating webhooks are most frequently used for defaulting, by adding default values for unset fields in the resource on creation.

For more background on Admission webhooks, refer to the Kubebuilder documentation or the official Kubernetes documentation on the topic. You can also refer to the Kubebuilder webhook walkthrough, which is similar in content to this guide. Kubebuilder also has a guide that walks through implementing webhooks for their example CronJob resource.

To add a webhook to your Operator SDK project, first you must scaffold out the webhooks with the following command.

$ operator-sdk create webhook --group cache --version v1alpha1 --kind Memcached --defaulting --programmatic-validation

The --defaulting flag will scaffold the resources required for a mutating webhook, and the --programmatic-validation flag will scaffold the resources required for a validating webhook. In this case we scaffolded both.

To implement the actual webhook logic, edit the api/v1alpha1/memcached_webhook.go file. The file will contain some boilerplate to set up the logger and register your webhook with the controller manager, as well as a variety of unimplemented methods (marked with TODOs). The mutating webhook implementation belongs in the Default function. The validating webhook implementation will be split between the ValidateCreate, ValidateUpdate, and ValidateDelete functions, which allows you to perform different validations based on the operation being performed, ie, preventing a field from being changed on Update.

The memcached operator we are building in this example is too simple to require defaulting or additional validations, but as an example, we can reinforce that the default value for spec.size should be 3, by adding the following logic to the Default function (note that this is already handled by the CRD defaulting and is technically completely superfluous):

if r.Spec.Size == 0 {
    r.Spec.Size = 3
}

For validation, we can enforce that the size of the cluster follows a rule that is difficult or impossible to describe with OpenAPI, for example, that the size of the cluster always remain an odd number.

To do so, we can simply implement a new function in api/v1alpha1/memcached_webhook.go, which performs this check:

func validateOdd(n int32) error {
	if n%2 == 0 {
		return errors.New("Cluster size must be an odd number")
	}
	return nil
}

And then, call that method from the ValidateCreate and ValidateUpdate functions:

// ValidateCreate implements webhook.Validator so a webhook will be registered for the type
func (r *Memcached) ValidateCreate() error {
	memcachedlog.Info("validate create", "name", r.Name)
	return validateOdd(r.Spec.Size)
}

// ValidateUpdate implements webhook.Validator so a webhook will be registered for the type
func (r *Memcached) ValidateUpdate(old runtime.Object) error {
	memcachedlog.Info("validate update", "name", r.Name)
	return validateOdd(r.Spec.Size)
}

Generate webhook manifests and enable webhook deployment

Once your webhooks are implemented, all that’s left is to create the WebhookConfiguration manifests required to register your webhooks with Kubernetes:

$ make manifests

You will need to enable cert-manager and webhook deployment in order to deploy these webhooks properly. To do so, edit the config/default/kustomize.yaml and uncomment the sections marked by [WEBHOOK] and [CERTMANAGER] comments. More detail on this step can be found in the Kubebuilder documentation.

Update main.go so that running locally works

To ensure that running locally continues working, ensure that there is a check that prevents the webhooks from being started when the ENABLE_WEBHOOKS flag is set to false. To do so, edit main.go, and add the following check around the call to SetupWebhookWithManager if it’s not already present:

if os.Getenv("ENABLE_WEBHOOKS") != "false" {
	if err = (&cachev1alpha1.Memcached{}).SetupWebhookWithManager(mgr); err != nil {
		setupLog.Error(err, "unable to create webhook", "webhook", "Memcached")
		os.Exit(1)
	}
}

Run your operator and webhooks

Run locally

Technically, The webhooks can be run locally, but for it to work you need to generate certificates for the webhook server and store them at /tmp/k8s-webhook-server/serving-certs/tls.{crt,key}. Generally it’s easier to just disable them locally and test the webhooks when running in a cluster.

If your certificates are properly configured, you should be able to start your operator by running:

$ make run ENABLE_WEBHOOKS=true

Run as a Deployment inside the cluster

For instructions on deploying your operator into a cluster, refer to the tutorial instructions. Adding webhooks does not alter this step.

Create a Memcached CR to exercise your webhook

First, follow the instructions for creating your Memcached CR in the [tutorial][tutorial_create_a_cr].

Once you have completed this step, you should have a Memcached CR with a size of 3 and a Memcached deployment with 3 replicas in your cluster.

To ensure that you are in the correct state, run the following commands and verify that the output roughly matches:

$ kubectl get memcached/memcached-sample -o yaml
apiVersion: cache.example.com/v1alpha1
kind: Memcached
metadata:
  clusterName: ""
  creationTimestamp: 2018-03-31T22:51:08Z
  generation: 0
  name: memcached-sample
  namespace: default
  resourceVersion: "245453"
  selfLink: /apis/cache.example.com/v1alpha1/namespaces/default/memcacheds/memcached-sample
  uid: 0026cc97-3536-11e8-bd83-0800274106a1
spec:
  size: 3
status:
  nodes:
  - memcached-sample-6fd7c98d8-7dqdr
  - memcached-sample-6fd7c98d8-g5k7v
  - memcached-sample-6fd7c98d8-m7vn7
$ kubectl get deployment
NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
memcached-operator-controller-manager   1/1     1            1           8m
memcached-sample                        3/3     3            3           1m

Update the size

Update config/samples/cache_v1alpha1_memcached.yaml to change the spec.size field in the Memcached CR from 3 to 5:

$ kubectl patch memcached memcached-sample -p '{"spec":{"size": 5}}' --type=merge

Confirm that the operator changes the deployment size:

$ kubectl get deployment
NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
memcached-operator-controller-manager   1/1     1            1           10m
memcached-sample                        5/5     5            5           3m

Update the size to an even number

Update config/samples/cache_v1alpha1_memcached.yaml to change the spec.size field in the Memcached CR from 3 to 4:

$ kubectl patch memcached memcached-sample -p '{"spec":{"size": 4}}' --type=merge

The request should fail, and you should see a response like this:

Error from server (Cluster size must be an odd number): admission webhook "vmemcached.kb.io" denied the request: Cluster size must be an odd number

This means that your update was rejected by your webhook. If you inspect the cluster state, you will see that your resources are unchanged:

$ kubectl get deployment
NAME                                    READY   UP-TO-DATE   AVAILABLE   AGE
memcached-operator-controller-manager   1/1     1            1           10m
memcached-sample                        5/5     5            5           3m