Webhooks are a way to “intercept” requests to the k8s apiserver on their way from the apiserver’s HTTP handler to etcd persistency. There are two types of webhooks:
- Admission webhooks (includes mutating and validating webhooks)
- Conversion webhooks
Like the name already says, mutating webhooks will modify submitted resources before they are persisted (e.g. filling in default values etc.). Validating webhooks will validate field values of a submitted resource but not change them. Conversion webhooks will convert between different versions of the same CRD for compatibility.
Serving Webhooks
Webhooks need to be served by a HTTP server so that the k8s apiserver can send requests to it. For admission webhooks, the POST requests have a AdmissionReview
struct serialized to JSON as body. For conversion webhooks, a ConversionReview
struct is used. Similarly, response bodies serialized the same structs to JSON.
The webhook server is a k8s Deployment
, with a service wrapping it. For admission webhooks, the service is referenced in an admission webhook configuration from the admissionregistration.k8s.io
API. This configuration also specifies invocation rules e.g. for which CRUD operation, which api groups, versions or resources an admission webhook should be used. The following configuration taken from the k8s docs is an example:
apiVersion: admissionregistration.k8s.io/v1
kind: ValidatingWebhookConfiguration
...
webhooks:
- name: my-webhook.example.com
clientConfig:
caBundle: "Ci0tLS0tQk...<base64-encoded PEM bundle containing the CA that signed the webhook's serving certificate>...tLS0K"
service:
namespace: my-service-namespace
name: my-service-name
path: /my-path
port: 1234
rules:
- operations: ["CREATE", "UPDATE"]
apiGroups: ["apps"]
apiVersions: ["v1", "v1beta1"]
resources: ["deployments", "replicasets"]
scope: "Namespaced"
...
For conversion webhooks, the service is directly referenced in the CRD and there is no separate configuration file. This is described in the next section.
Multiversion support of CRDs and Webhooks
When several versions are present, some things need to be specified in the CRD yaml:
- Is custom logic necessary for conversion between different CRD versions? If there are schema changes from one version to the other, conversion webhooks should be used to implement conversion logic.
- In which version are the resources persisted? When a resource is created, it is persisted in a version selected as the storage version. This means that the stored version of the resource can be different from the one requested via
kubectl get resource.version.group
(when no version is specified, e.g. in kubectl get resource
, kubectl will use the latest version by default). Reading from storage will not automatically changed the stored version and when a new storage version is set, there is no automatic process that converts the stored resources to the new storage version. The conversion will only happen when a stored resource is updated.
- Which versions are supported? As new versions are added, older versions can be disabled.
The following yaml taken from the k8s docs is an example CRD:
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
# name must match the spec fields below, and be in the form: <plural>.<group>
name: crontabs.example.com
spec:
# group name to use for REST API: /apis/<group>/<version>
group: example.com
# list of versions supported by this CustomResourceDefinition
versions:
- name: v1beta1
# Each version can be enabled/disabled by Served flag.
served: true
# One and only one version must be marked as the storage version.
storage: true
# A schema is required
schema:
openAPIV3Schema:
type: object
properties:
...
- name: v1
served: true
storage: false
schema:
openAPIV3Schema:
type: object
properties:
...
# The conversion section is introduced in Kubernetes 1.13+ with a default value of
# None conversion (strategy sub-field set to None).
conversion:
# None conversion assumes the same schema for all versions and only sets the apiVersion
# field of custom resources to the proper value
strategy: None
# either Namespaced or Cluster
scope: Namespaced
names:
# plural name to be used in the URL: /apis/<group>/<version>/<plural>
plural: crontabs
# singular name to be used as an alias on the CLI and for display
singular: crontab
# kind is normally the CamelCased singular type. Your resource manifests use this.
kind: CronTab
# shortNames allow shorter string to match your resource on the CLI
shortNames:
- ct
Using Webhooks with Controller-Runtime
Let us now look at the inclusion of webhooks in controller-runtime
. The starting point here is again the manager. When a manager is created, you can pass webhook-relevant config such as the port, host and certificate directory of the webhook server. After the manager is created, you add the webhook server to it:
import (
ctrl "sigs.k8s.io/controller-runtime"
)
mgr, _ := ctrl.NewManager(<config>)
ctrl.NewWebhookManagedBy(mgr).
For(<your crd>).
Complete()
Under the hood, NewWebhookManagedBy()
will return a builder that checks if your CRD implements the Defaulter
(mutating webhook) and/or Validator
(validating webhook) interfaces.
/* source code from https://github.com/kubernetes-sigs/controller-runtime/blob/release-0.7/pkg/webhook/admission/defaulter.go */
type Defaulter interface {
runtime.Object
Default()
}
/* source code from https://github.com/kubernetes-sigs/controller-runtime/blob/release-0.7/pkg/webhook/admission/validator.go */
type Validator interface {
runtime.Object
ValidateCreate() error
ValidateUpdate(old runtime.Object) error
ValidateDelete() error
}
If it does, it generates paths for your webhooks and registers the implemented interface functions as handlers to a http.ServeMux
HTTP request multiplexer.
For conversion webhooks, the builder checks if one of your CRD versions implements the Hub
interface and if all other versions implement the Convertible
interface.
/* source code from https://github.com/kubernetes-sigs/controller-runtime/blob/release-0.7/pkg/conversion/conversion.go */
type Convertible interface {
runtime.Object
ConvertTo(dst Hub) error
ConvertFrom(src Hub) error
}
type Hub interface {
runtime.Object
Hub()
}
Hubs and convertibles (or spokes) are a concept used by controller-runtime
to reduce the combinatorial complexity of conversions. Imagine we have a three CRD versions: v1
, v2
, v3
. Assume our hub version is v2
, and a resource is persisted as v1
. We now query the resource, but as v3
with kubectl get myresource.v3.mygroup
. What happens is that v1
from the storage is converted into the hub version v2
and from there to the target version v3
. To do so, we need to define how v1
and v3
can be converted to and from v2
, and that is what the interface Convertible
does. The introduction of a hub version eliminates the need to write logic for all possible version pairs.
After all the configuration is done, the manager starts up the webhook server - the webhook deployment in controller-runtime
is part of the manager deployment. All that is left to do is to create a service yaml and to refer to the service in the admission webhook configuration or CRD yaml. If you use kubebuilder
, you get the yaml files automatically.
Further Reading
- https://banzaicloud.com/blog/k8s-admission-webhooks/
- https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/
- https://kubernetes.io/docs/tasks/extend-kubernetes/custom-resources/custom-resource-definition-versioning/