How to create validating webhook with operator-sdk

Vincent Hou
5 min readApr 2, 2021

--

Kubernetes provides admission controllers for users to leverage the security capabilities. Kubernetes admission controllers are plugins that govern and enforce how the cluster is used. They are gatekeepers that intercept (authenticated) API requests and may change the request object or deny the request altogether. The admission control process has two phases: the mutating phase is executed first, followed by the validating phase as shown in the diagram:

We have done the official definition. Let’s get to the hard core. What you need to remember is that mutating webhooks are called in series, before the validating webhooks called in parallel. Kubernetes has a variety of admission webhooks available, but speaking of the Kubernetes Operators, there are three of them to master: validating webhook, mutating webhook and conversion webhook, which I have given a thorough instruction on. We will focus on validating webhook in this article.

In my opinion, conversion webhook is the most difficult one among three of these key webhooks. If you have followed and understood how the conversion webhook works, conquering the other two webhooks is just another piece of cake. Validating webhook is self-explainable, that we mostly leverage it to validate the attributes of the objects, including the custom resources defined for the operator.

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 validating webhook, or skip the command to create the conversion webhook.

Next, let’s walk through how to create the validating 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 validating webhook:

operator-sdk create webhook --version v1beta1 --kind Memcached --group cache --programmatic-validation

You have the Golang code for the validating webhook and the manifests generated. If you run into the error message:

FATA[0000] failed to create webhook with "go.kubebuilder.io/v3": webhook resource already exists

It means you have already created the validating webhook. You can append the flag

--force

to the command overwriting the existing webhook.

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:

// TODO(user): change verbs to "verbs=create;update;delete" if you want to enable deletion validation.
// +kubebuilder:webhook:path=/validate-cache-example-com-v1beta1-memcached,mutating=false,failurePolicy=fail,sideEffects=None,groups=cache.example.com,resources=memcacheds,verbs=create;update,versions=v1beta1,name=vmemcached.kb.io,admissionReviewVersions={v1alpha1,v1beta1}

var _ webhook.Validator = &Memcached{}

// 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)

// TODO(user): fill in your validation logic upon object creation.
return nil
}

// 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)

// TODO(user): fill in your validation logic upon object update.
return nil
}

// ValidateDelete implements webhook.Validator so a webhook will be registered for the type
func (r *Memcached) ValidateDelete() error {
memcachedlog.Info("validate delete", "name", r.Name)

// TODO(user): fill in your validation logic upon object deletion.
return nil
}

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

There are three functions for validating webhook: ValidateCreate, ValidateUpdate and ValidateDelete. They are called respectively, when the CR is created, updated and deleted.

Usually, the function ValidateDelete is left empty, because there is no need to validate the CR, when deleting it. The functions ValidateCreate and ValidateUpdate, may share the same implementation, because they work on the same CR with the same attributes, unless there are different requirements for CR attributes during the creation and update.

How to raise the error, if the field is invalid?

During the validation, we can go through the field/attribute one by one to validate. If they are all valid, the function returns nil in the end. If one field is invalid, the function should return an error.

As in this example, we can check if the field replicaSize is an integer. If it is not an integer, the functions ValidateCreate and ValidateUpdate would return an error.

There is plenty of ways to manage the errors. After a glance of many operators, you can choose any of the following options:

If you need to manage the errors in a more systematic way, I personally recommend the last option, because it will validate all the fields and return the consolidated error. The other two just return the error, when they hit one, regardless the rest of the fields.

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

Go to the file config/default/webhookcainjection_patch.yaml. Comment out the following lines:

apiVersion: admissionregistration.k8s.io/v1
kind: MutatingWebhookConfiguration
metadata:
name: mutating-webhook-configuration
annotations:
cert-manager.io/inject-ca-from: $(CERTIFICATE_NAMESPACE)/$(CERTIFICATE_NAME)

If you have created the mutating webhook, you do not need to comment them.

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
spec:
replicaSize: "3.2"

Set the field replicaSize into a non-integer to see how the validating webhook work. The request will be rejected.

Check the log with the command:

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

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.