diff --git a/docs/book/SUMMARY.md b/docs/book/SUMMARY.md index 44e8ce3bc1..78c87b1603 100644 --- a/docs/book/SUMMARY.md +++ b/docs/book/SUMMARY.md @@ -35,6 +35,9 @@ * [Controllers For Core Resources](beyond_basics/controllers_for_core_resources.md) * [Controller Watch Functions](beyond_basics/controller_watches.md) * [Creating Events](beyond_basics/creating_events.md) +* Webhooks + * [What is a Webhook](beyond_basics/what_is_a_webhook.md) + * [Webhook Example](beyond_basics/sample_webhook.md) * Deployment Workflow * [Deploying the manager in Cluster](beyond_basics/deploying_controller.md) diff --git a/docs/book/beyond_basics/sample_webhook.md b/docs/book/beyond_basics/sample_webhook.md new file mode 100644 index 0000000000..508d77d904 --- /dev/null +++ b/docs/book/beyond_basics/sample_webhook.md @@ -0,0 +1,229 @@ +# Webhook Example + +This chapter walks through a simple webhook implementation. + +It uses the [controller-runtime](https://godoc.org/sigs.k8s.io/controller-runtime/pkg/webhook) libraries to implement +a Webhook Server and Manager. + +Same as controllers, a Webhook Server is a +[`Runable`](https://godoc.org/sigs.k8s.io/controller-runtime/pkg/manager#Runnable) which needs to be registered to a manager. +Arbitrary number of `Runable`s can be registered to a manager, +so a webhook server can run with other controllers in the same manager. +They will share the same dependencies provided by the manager. For example, shared cache, client, scheme, etc. + +## Setup + +#### Way to Deploy your Webhook Server + +There are various ways to deploy the webhook server in terms of + +1. Where the serving certificates live. +1. In what environment the webhook server runs, in a pod or directly on a VM, etc. +1. If in a pod, on what type of node, worker nodes or master node. + +The recommended way to deploy the webhook server is + +1. Run the webhook server as a regular pod on worker nodes through a workload API, e.g. Deployment or StatefulSet. +1. Put the certificate in a k8s secret in the same namespace as the webhook server +1. Mount the secret as a volume in the pod +1. Create a k8s service to front the webhook server. + +#### Creating a Handler + +{% method %} + +The business logic for a Webhook exists in a Handler. +A Handler implements the `admission.Handler` interface, which contains a single `Handle` method. + +If a Handler implements `inject.Client` and `inject.Decoder` interfaces, +the manager will automatically inject the client and the decoder into the Handler. + +Note: The `client.Client` provided by the manager reads from a cache which is lazily initialized. +To eagerly initialize the cache, perform a read operation with the client before starting the server. + +`podAnnotator` is a Handler, which implements the `admission.Handler`, `inject.Client` and `inject.Decoder` interfaces. + +Details about how to implement an admission webhook podAnnotator is covered in a later section. + +{% sample lang="go" %} +```go +type podAnnotator struct { + client client.Client + decoder types.Decoder +} + +// podAnnotator implements admission.Handler. +var _ admission.Handler = &podAnnotator{} + +func (a *podAnnotator) Handle(ctx context.Context, req types.Request) types.Response { + ... +} + +// podAnnotator implements inject.Client. +var _ inject.Client = &podAnnotator{} + +// InjectClient injects the client into the podAnnotator +func (a *podAnnotator) InjectClient(c client.Client) error { + a.client = c + return nil +} + +// podAnnotator implements inject.Decoder. +var _ inject.Decoder = &podAnnotator{} + +// InjectDecoder injects the decoder into the podAnnotator +func (a *podAnnotator) InjectDecoder(d types.Decoder) error { + a.decoder = d + return nil +} +``` +{% endmethod %} + + +#### Configuring a Webhook and Registering the Handler + +{% method %} + +A Webhook configures what type of requests the Handler should accept from the apiserver. Options include: +- The type of the Operations (CRUD) +- The type of the Targets (Deployment, Pod, etc) +- The type of the Handler (Mutating, Validating) + +When the Server starts, it will register all Webhook Configurations with the apiserver to start accepting and +routing requests to the Handlers. + +[controller-runtime](https://godoc.org/sigs.k8s.io/controller-runtime/pkg/webhook/admission/builder) provides a useful package for +building a webhook. +You can incrementally set the configuration of a webhook and then invoke `Build` to complete building a webhook. + +If you want to specify the name and(or) path for your webhook instead of using the default, you can invoke +`Name("yourname")` and `Path("/yourpath")` respectively. + +{% sample lang="go" %} +```go +wh, err := builder.NewWebhookBuilder(). + Mutating(). + Operations(admissionregistrationv1beta1.Create). + ForType(&corev1.Pod{}). + Handlers(&podAnnotator{}). + WithManager(mgr). + Build() +if err != nil { + // handle error +} +``` +{% endmethod %} + + +#### Creating a Server + +{% method %} + +A Server registers Webhook Configuration with the apiserver and creates an HTTP server to route requests to the handlers. + +The server is behind a Kubernetes Service and provides a certificate to the apiserver when serving requests. + +The Server depends on a Kubernetes Secret containing this certificate to be mounted under `CertDir`. + +If the Secret is empty, during bootstrapping the Server will generate a certificate and write it into the Secret. + +A new webhook server can be created by invoking `webhook.NewServer`. +The Server will be registered to the provided manager. +You can specify `Port`, `CertDir` and various `BootstrapOptions`. +For the full list of Server options, please see [GoDoc](https://godoc.org/sigs.k8s.io/controller-runtime/pkg/webhook). + +{% sample lang="go" %} +```go +svr, err := webhook.NewServer("foo-admission-server", mgr, webhook.ServerOptions{ + CertDir: "/tmp/cert", + BootstrapOptions: &webhook.BootstrapOptions{ + Secret: &types.NamespacedName{ + Namespace: "default", + Name: "foo-admission-server-secret", + }, + + Service: &webhook.Service{ + Namespace: "default", + Name: "foo-admission-server-service", + // Selectors should select the pods that runs this webhook server. + Selectors: map[string]string{ + "app": "foo-admission-server", + }, + }, + }, +}) +if err != nil { + // handle error +} +``` +{% endmethod %} + +#### Registering a Webhook with the Server + +You can register webhook(s) in the webhook server by invoking `svr.Register(wh)`. + + +## Implementing Webhook Handler + + +#### Implementing the Handler Business Logic + +{% method %} + +`decoder types.Decoder` is a decoder that knows how the decode all core type and your CRD types. + +`client client.Client` is a client that knows how to talk to the API server. + +The guideline of returning HTTP status code is that: +- If the server decides to admit the request, it should return 200 and set +[`Allowed`](https://github.com/kubernetes/api/blob/f456898a08e4bbc5891694118f3819f324de12ff/admission/v1beta1/types.go#L86-L87) +to `true`. +- If the server rejects the request due to an admission policy reason, it should return 200, set +[`Allowed`](https://github.com/kubernetes/api/blob/f456898a08e4bbc5891694118f3819f324de12ff/admission/v1beta1/types.go#L86-L87) +to `false` and provide an informational message as reason. +- If the request is not well formatted, the server should reject it with 400 (Bad Request) and an error message. +- If the server encounters an unexpected error during processing, it should reject the request with 500 (Internal Error). + +`controller-runtime` provides various helper methods for constructing Response. +- `ErrorResponse` for rejecting a request due to an error. +- `PatchResponse` for mutating webook to admit a request with patches. +- `ValidationResponse` for admitting or rejecting a request with a reason message. + +{% sample lang="go" %} +```go +type podAnnotator struct { + client client.Client + decoder types.Decoder +} + +// podAnnotator Iimplements admission.Handler. +var _ admission.Handler = &podAnnotator{} + +// podAnnotator adds an annotation to every incoming pods. +func (a *podAnnotator) Handle(ctx context.Context, req types.Request) types.Response { + pod := &corev1.Pod{} + + err := a.decoder.Decode(req, pod) + if err != nil { + return admission.ErrorResponse(http.StatusBadRequest, err) + } + copy := pod.DeepCopy() + + err = a.mutatePodsFn(ctx, copy) + if err != nil { + return admission.ErrorResponse(http.StatusInternalServerError, err) + } + // admission.PatchResponse generates a Response containing patches. + return admission.PatchResponse(pod, copy) +} + +// mutatePodsFn add an annotation to the given pod +func (a *podAnnotator) mutatePodsFn(ctx context.Context, pod *corev1.Pod) error { + if pod.Annotations == nil { + pod.Annotations = map[string]string{} + } + pod.Annotations["example-mutating-admission-webhook"] = "foo" + return nil +} +``` +{% endmethod %} diff --git a/docs/book/beyond_basics/what_is_a_webhook.md b/docs/book/beyond_basics/what_is_a_webhook.md new file mode 100644 index 0000000000..2a70c40f9d --- /dev/null +++ b/docs/book/beyond_basics/what_is_a_webhook.md @@ -0,0 +1,87 @@ +# Webhook + +Webhooks are HTTP callbacks, providing a way for notifications to be delivered to an external web server. +A web application implementing webhooks will send an HTTP request (typically POST) to other application when certain event happens. +In the kubernetes world, there are 3 kinds of webhooks: +[admission webhook](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#admission-webhooks), +[authorization webhook](https://kubernetes.io/docs/reference/access-authn-authz/webhook/) and CRD conversion webhook. + +In [controller-runtime](https://godoc.org/sigs.k8s.io/controller-runtime/pkg/webhook) libraries, +currently we only support admission webhooks. +CRD conversion webhooks will be supported after it is released in kubernetes 1.12. + +## Admission Webhook + +Admission webhooks are HTTP callbacks that receive admission requests, process them and return admission responses. +There are two types of admission webhooks: mutating admission webhook and validating admission webhook. +With mutating admission webhooks, you may change the request object before it is stored (e.g. for implementing defaulting of fields) +With validating admission webhooks, you may not change the request, but you can reject it (e.g. for implementing validation of the request). + +#### Why Admission Webhooks are Important + +Admission webhooks are the mechanism to enable kubernetes extensibility through CRD. +- Mutating admission webhook is the only way to do defaulting for CRDs. +- Validating admission webhook allows for more complex validation than pure schema-based validation. +e.g. cross-field validation or cross-object validation. + +It can also be used to add custom logic in the core kubernetes API. + +#### Mutating Admission Webhook + +A mutating admission webhook receives an admission request which contains an object. +The webhook can either decline the request directly or returning JSON patches for modifying the original object. +- If admitting the request, the webhook is responsible for generating JSON patches and send them back in the +admission response. +- If declining the request, a reason message should be returned in the admission response. + +#### Validating Admission Webhook + +A validating admission webhook receives an admission request which contains an object. +The webhook can either admit or decline the request. +A reason message should be returned in the admission response if declining the request. + +#### Authentication + +The apiserver by default doesn't authenticate itself to the webhooks. +That means the webhooks don't authenticate the identities of the clients. + +But if you want to authenticate the clients, you need to configure the apiserver to use basic auth, bearer token, +or a cert to authenticate itself to the webhooks. You can find detailed steps +[here](https://kubernetes.io/docs/reference/access-authn-authz/extensible-admission-controllers/#authenticate-apiservers). + +#### Configure Admission Webhooks Dynamically + +{% method %} + +Admission webhooks can be configured dynamically via the `admissionregistration.k8s.io/v1beta1` API. +So your cluster must be 1.9 or later and has enabled the API. + +You can do CRUD operations on WebhookConfiguration objects as on other k8s objects. + +{% sample lang="yaml" %} + +```yaml +apiVersion: admissionregistration.k8s.io/v1beta1 +kind: ValidatingWebhookConfiguration +metadata: + name: +webhooks: +- name: + rules: + - apiGroups: + - apps + apiVersions: + - v1 + operations: + - CREATE + resources: + - deployments + clientConfig: + service: + namespace: + name: + caBundle: +``` + +{% endmethod %} +