Skip to content

Commit

Permalink
update and supplement webhook docs
Browse files Browse the repository at this point in the history
  • Loading branch information
camilamacedo86 committed Aug 23, 2024
1 parent 94d8fce commit 63eee41
Showing 1 changed file with 265 additions and 28 deletions.
293 changes: 265 additions & 28 deletions docs/book/src/reference/webhook-for-core-types.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,45 +9,89 @@ in controller-runtime.
It is suggested to use kubebuilder to initialize a project, and then you can
follow the steps below to add admission webhooks for core types.

## Implement Your Handler

You need to have your handler implements the
[admission.Handler](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook/admission?tab=doc#Handler)
interface.

## Implementing Your Handler Using `Handle`

Your handler must implement the [admission.Handler](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook/admission#Handler) interface. This function is responsible for both mutating and validating the incoming resource.

### Update your webhook:

**Example**

Following an example of its implementation.

```go
package v1

import (
"context"
"encoding/json"
"net/http"
"sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
corev1 "k8s.io/api/core/v1"
)

// **Note**: in order to have controller-gen generate the webhook configuration for you, you need to add markers. For example:

// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io

type podAnnotator struct {
Client client.Client
decoder *admission.Decoder
Client client.Client
decoder *admission.Decoder
}

func (a *podAnnotator) Handle(ctx context.Context, req admission.Request) admission.Response {
pod := &corev1.Pod{}
err := a.decoder.Decode(req, pod)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}

// mutate the fields in pod

marshaledPod, err := json.Marshal(pod)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
pod := &corev1.Pod{}
err := a.decoder.Decode(req, pod)
if err != nil {
return admission.Errored(http.StatusBadRequest, err)
}

// Mutate the fields in pod
pod.Annotations["example.com/mutated"] = "true"

marshaledPod, err := json.Marshal(pod)
if err != nil {
return admission.Errored(http.StatusInternalServerError, err)
}
return admission.Patched(req.Object.Raw, marshaledPod)
}
```
<aside class="note">
<h1>Markers for Webhooks</h1>

Notice that we use kubebuilder markers to generate webhook manifests.
This marker is responsible for generating a mutating webhook manifest.

The meaning of each marker can be found [here](./markers/webhook.md).

To have controller-gen automatically generate the webhook configuration for you, you need to add the appropriate markers in your code. These markers should follow a specific format, especially when defining the webhook path.

The format for the webhook path is as follows:

```go
/mutate-<group>-<version>-<kind>
```

**Note**: in order to have controller-gen generate the webhook configuration for
you, you need to add markers. For example,
`// +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io`
Since this documentation example uses Pod from the core API group, the group should be an empty string.

For example, the marker for a mutating webhook for Pod might look like this:

```go
// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io
```
</aside>

## Update main.go

Now you need to register your handler in the webhook server.

```go
mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{Handler: &podAnnotator{Client: mgr.GetClient()}})
mgr.GetWebhookServer().Register("/mutate--v1-pod", &webhook.Admission{
Handler: &podAnnotator{Client: mgr.GetClient()},
})
```

You need to ensure the path here match the path in the marker.
Expand All @@ -57,14 +101,176 @@ You need to ensure the path here match the path in the marker.
If you need a client and/or decoder, just pass them in at struct construction time.

```go
mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{
Handler: &podAnnotator{
Client: mgr.GetClient(),
decoder: admission.NewDecoder(mgr.GetScheme()),
},
mgr.GetWebhookServer().Register("/mutate--v1-pod", &webhook.Admission{
Handler: &podAnnotator{
Client: mgr.GetClient(),
decoder: admission.NewDecoder(mgr.GetScheme()),
},
})
```


## By using Custom interfaces instead of Handle

### Update your webhook:

**Example**

Following an example of its implementation.

```go
package v1

import (
"context"
"fmt"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
ctrl "sigs.k8s.io/controller-runtime"
logf "sigs.k8s.io/controller-runtime/pkg/log"
"sigs.k8s.io/controller-runtime/pkg/webhook"
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
)

// log is for logging in this package.
var podlog = logf.Log.WithName("pod-webhook")

// SetupWebhookWithManager sets up the webhook with the manager for both the defaulter and validator
func (r *corev1.Pod) SetupWebhookWithManager(mgr ctrl.Manager) error {
return ctrl.NewWebhookManagedBy(mgr).
For(r).
WithValidator(&PodCustomValidator{}).
WithDefaulter(&PodCustomDefaulter{}).
Complete()
}

// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io,admissionReviewVersions=v1

// PodCustomDefaulter handles defaulting Pods
type PodCustomDefaulter struct{}

var _ webhook.CustomDefaulter = &PodCustomDefaulter{}

// Default implements webhook.CustomDefaulter so a webhook will be registered for the type
func (d *PodCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
podlog.Info("CustomDefaulter for corev1.Pod")
req, err := admission.RequestFromContext(ctx)
if err != nil {
return fmt.Errorf("expected admission.Request in ctx: %w", err)
}
if req.Kind.Kind != "Pod" {
return fmt.Errorf("expected Kind Pod got %q", req.Kind.Kind)
}
castedObj, ok := obj.(*corev1.Pod)
if !ok {
return fmt.Errorf("expected a Pod object but got %T", obj)
}
podlog.Info("default", "name", castedObj.GetName())

// Mutate the fields in Pod (e.g., adding an annotation)
if castedObj.Annotations == nil {
castedObj.Annotations = map[string]string{}
}
castedObj.Annotations["example.com/mutated"] = "true"

return nil
}

// +kubebuilder:webhook:path=/validate--v1-pod,mutating=false,failurePolicy=fail,groups="",resources=pods,verbs=create;update;delete,versions=v1,name=vpod.kb.io,admissionReviewVersions=v1

// PodCustomValidator handles validating Pods
type PodCustomHandler struct{}

var _ webhook.CustomValidator = &PodCustomValidator{}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
func (v *PodCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
podlog.Info("Creation Validation for corev1.Pod")

req, err := admission.RequestFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
}
if req.Kind.Kind != "Pod" {
return nil, fmt.Errorf("expected Kind Pod got %q", req.Kind.Kind)
}
castedObj, ok := obj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod object but got %T", obj)
}
podlog.Info("validate create", "name", castedObj.GetName())

// Ensure the Pod has at least one container
if len(castedObj.Spec.Containers) == 0 {
return nil, fmt.Errorf("pod must have at least one container")
}

return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
func (v *PodCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
podlog.Info("Update Validation for corev1.Pod")

req, err := admission.RequestFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
}
if req.Kind.Kind != "Pod" {
return nil, fmt.Errorf("expected Kind Pod got %q", req.Kind.Kind)
}
castedObj, ok := newObj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod object but got %T", newObj)
}
podlog.Info("validate update", "name", castedObj.GetName())

// Prevent changing a specific annotation
if oldObj.(*corev1.Pod).Annotations["example.com/protected"] != castedObj.Annotations["example.com/protected"] {
return nil, fmt.Errorf("the annotation 'example.com/protected' cannot be changed")
}

return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
func (v *PodCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
podlog.Info("Deletion Validation for corev1.Pod")

req, err := admission.RequestFromContext(ctx)
if err != nil {
return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
}
if req.Kind.Kind != "Pod" {
return nil, fmt.Errorf("expected Kind Pod got %q", req.Kind.Kind)
}
castedObj, ok := obj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod object but got %T", obj)
}
podlog.Info("validate delete", "name", castedObj.GetName())

// Prevent deletion of protected Pods
if castedObj.Annotations["example.com/protected"] == "true" {
return nil, fmt.Errorf("protected pods cannot be deleted")
}

return nil, nil
}
```

### Update the main.go

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

## Deploy

Deploying it is just like deploying a webhook server for CRD. You need to
Expand All @@ -73,5 +279,36 @@ Deploying it is just like deploying a webhook server for CRD. You need to

You can follow the [tutorial](/cronjob-tutorial/running.md).

## What are `Handle` and Custom Interfaces?

In the context of Kubernetes admission webhooks, the `Handle` function and the custom interfaces (`CustomValidator` and `CustomDefaulter`) are two different approaches to implementing webhook logic. Each serves specific purposes, and the choice between them depends on the needs of your webhook.

## Purpose of the `Handle` Function

The `Handle` function is a core part of the admission webhook process. It is responsible for directly processing the incoming admission request and returning an `admission.Response`. This function is particularly useful when you need to handle both validation and mutation within the same function.

### Mutation

If your webhook needs to modify the resource (e.g., add or change annotations, labels, or other fields), the `Handle` function is where you would implement this logic. Mutation involves altering the resource before it is persisted in Kubernetes.

### Response Construction

The `Handle` function is also responsible for constructing the `admission.Response`, which determines whether the request should be allowed or denied, or if the resource should be patched (mutated). The `Handle` function gives you full control over how the response is built and what changes are applied to the resource.

## Purpose of Custom Interfaces (`CustomValidator` and `CustomDefaulter`)

The `CustomValidator` and `CustomDefaulter` interfaces provide a more modular approach to implementing webhook logic. They allow you to separate validation and defaulting (mutation) into distinct methods, making the code easier to maintain and reason about.

## When to Use Each Approach

- **Use `Handle` when**:
- You need to both mutate and validate the resource in a single function.
- You want direct control over how the admission response is constructed and returned.
- Your webhook logic is simple and doesn’t require a clear separation of concerns.

- **Use `CustomValidator` and `CustomDefaulter` when**:
- You want to separate validation and defaulting logic for better modularity.
- Your webhook logic is complex, and separating concerns makes the code easier to manage.
- You don’t need to perform mutation and validation in the same function.

[cronjob-tutorial]: /cronjob-tutorial/cronjob-tutorial.md

0 comments on commit 63eee41

Please sign in to comment.