How to use mutating webhook for the operator with operator-sdk

Vincent Hou
3 min readApr 5, 2021

After reading the instructions on how to create the conversion webhook and the validating webhook, there is no way for the mutating webhook to escape. The custom resource is the source of truth to configure the operand for the Kubernetes operator. Once we have got the content of the CR ready, it does not necessarily need to change. I have done some researches on the use cases of the mutating webhook. The common one is to fill in the fields with the default values, if they are empty. I have not found any other proper use case. Feel free to share it with me, if you find some.

We can set the default values to the CR fields, by implementing the Defaulter interface, the controller-runtime framework provides.

Question for audience: If an operator has enabled all the types of webhooks: conversion webhook, validating webhook and mutating webhook(defaulting webhook), what is the correct order to run all of them, when there is an incoming CR request?

I will give the answer at the end of this article.

Make sure you have done the following steps:

  • Prerequisites
  • Prepare your workstation
  • Create the operator. Follow all the step below this section. You can either choose to finish the creation and configuration of webhook, because that’s also necessary for the defaulting webhook, or skip the command to create the conversion webhook.

Next, let’s walk through how to create the defaulting webhook.

Open the terminal, and go to the home directory of memcached-operator.

cd $GOPATH/src/github.com/example/memcached-operator

There are two versions for the APIs. We will create the validating webhook for the version v1beta1.

Run the following command to create the defaulting webhook:

operator-sdk create webhook --version v1beta1 --kind Memcached --group cache --defaulting --force

The flag

--force 

in the end will force to create the defaulting webhook, even if there is any.

What has been scaffolded after this command? Go to the file memcached_webhook.go under api/v1beta1, and you can see the following section is added:

//+kubebuilder:webhook:path=/mutate-cache-example-com-v1beta1-memcached,mutating=true,failurePolicy=fail,sideEffects=None,groups=cache.example.com,resources=memcacheds,verbs=create;update,versions=v1beta1,name=mmemcached.kb.io,admissionReviewVersions={v1alpha1,v1beta1}

var _ webhook.Defaulter = &Memcached{}

// Default implements webhook.Defaulter so a webhook will be registered for the type
func (r *Memcached) Default() {
memcachedlog.Info("default", "name", r.Name)

// TODO(user): fill in your defaulting logic.
}

Change the annotation admissionReviewVersions={v1,v1beta1} into admissionReviewVersions={v1alpha1,v1beta1}, since we have create the CRDs with v1alpha1 and v1beta1.

In this function Default(), specify the CR fields with the default value if they are empty.

For example, we would like to set the number of replicas into 2, if it is empty.

Implement the function like this:

func (r *Memcached) Default() {
memcachedlog.Info("default", "name", r.Name)
if r.Spec.ReplicaSize == 0 {
r.Spec.ReplicaSize = 2
}
}

Update the generated code and regenerate the CRD manifests:

make generate
make manifests

Enable the webhook and certificate manager in manifests:

Go through a few kustomization.yaml files under config/crd, config/default, and config/webhook, and do a few uncomment and comment actions.

For config/crd/kustomization.yaml, uncomment the following lines:

#- patches/webhook_in_memcacheds.yaml
#- patches/cainjection_in_memcacheds.yaml

For config/default/kustomization.yaml, uncomment the following lines:

#- ../webhook
#- ../certmanager
#- manager_webhook_patch.yaml
#- webhookcainjection_patch.yaml

and all the line below vars:

#- name: CERTIFICATE_NAMESPACE # namespace of the certificate CR
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
# fieldref:
# fieldpath: metadata.namespace
#- name: CERTIFICATE_NAME
# objref:
# kind: Certificate
# group: cert-manager.io
# version: v1
# name: serving-cert # this name should match the one in certificate.yaml
#- name: SERVICE_NAMESPACE # namespace of the service
# objref:
# kind: Service
# version: v1
# name: webhook-service
# fieldref:
# fieldpath: metadata.namespace
#- name: SERVICE_NAME
# objref:
# kind: Service
# version: v1
# name: webhook-service

Build the image for v1beta1:

export USER=<name>
make docker-build docker-push IMG=docker.io/$USER/memcached-operator:v0.0.2

We use the tag v0.0.2 for v1beta1 CRD. Replace <name> with your name registered with docker.io.

Deploy the operator with v1beta1 resource:

make deploy IMG=docker.io/$USER/memcached-operator:v0.0.2

Change the CR sample at config/samples/cache_v1beta1_memcached.yaml into the following contents:

apiVersion: v1
kind: Namespace
metadata:
name: memcached-sample
---
apiVersion: cache.example.com/v1beta1
kind: Memcached
metadata:
name: memcached-sample
namespace: memcached-sample

The field replicaSize is left empty. The defaulting webhook will set it into 2 automatically.

Check the log with the command:

kubectl logs -f deploy/memcached-operator-controller-manager -n memcached-operator-system -c manager

Answer to the question: The order of running the webhooks is conversion webhook first, mutating(defaulting webhook) second, and validating webhook last.

Follow Vincent, you won’t derail!

--

--

Vincent Hou

A Chinese software engineer, used to study in Belgium and currently working in US, as Knative & Tekton Operator Lead and Istio Operator Contributor.