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 1169d69
Showing 1 changed file with 310 additions and 28 deletions.
338 changes: 310 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,85 @@ 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
## Implementing Your Handler Using `Handle`

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.
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**

```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.

**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`
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>
```

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 +97,225 @@ 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**

```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-resource")

// SetupWebhookWithManager will setup the manager to manage the webhooks
func (r *corev1.Pod) SetupWebhookWithManager(mgr ctrl.Manager) error {
runAsNonRoot := true
allowPrivilegeEscalation := false

return ctrl.NewWebhookManagedBy(mgr).
For(r).
WithValidator(&PodCustomValidator{}).
WithDefaulter(&PodCustomDefaulter{
DefaultSecurityContext: &corev1.SecurityContext{
RunAsNonRoot: &runAsNonRoot, // Set to true
AllowPrivilegeEscalation: &allowPrivilegeEscalation, // Set to false
},
}).
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

// +kubebuilder:object:generate=false
// PodCustomDefaulter struct is responsible for setting default values on the Pod resource
// when it is created or updated.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as it is used only for temporary operations and does not need to be deeply copied.
type PodCustomDefaulter struct {
pod *corev1.Pod

// Default security context to be applied to Pods
DefaultSecurityContext *corev1.SecurityContext

// TODO: Add more fields as needed for defaulting
}

var _ webhook.CustomDefaulter = &PodCustomDefaulter{}

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

// Assign the Pod object to the PodCustomDefaulter struct field
// so that it can be used in the defaulting logic
d.pod = pod

// Apply the default security context if it's not set
for i := range d.pod.Spec.Containers {
if d.pod.Spec.Containers[i].SecurityContext == nil {
d.pod.Spec.Containers[i].SecurityContext = d.DefaultSecurityContext
}
}

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

// TODO: Add any additional defaulting logic here.

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

// +kubebuilder:object:generate=false
// PodCustomValidator struct is responsible for validating the Pod resource
// when it is created, updated, or deleted.
//
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
// as this struct is used only for temporary operations and does not need to be deeply copied.
type PodCustomValidator struct {
pod *corev1.Pod

// Previous state of the Pod, useful during updates
PreviousPodState *corev1.Pod

// TODO: Add more fields as needed for validation
}

var _ webhook.CustomValidator = &PodCustomValidator{}

// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Pod
func (v *PodCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
podlog.Info("Creation Validation for corev1.Pod")
pod, ok := obj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod object but got %T", obj)
}
podlog.Info("validate create", "name", pod.GetName())

// Assign the Pod object to the PodCustomValidator struct field
// so that it can be used in the validation logic
v.pod = pod

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

// TODO: Add any additional creation validation logic here.

return nil, nil
}

// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Pod
func (v *PodCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
podlog.Info("Update Validation for corev1.Pod")
pod, ok := newObj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod object but got %T", newObj)
}
podlog.Info("validate update", "name", pod.GetName())

// Assign the Pod object to the PodCustomValidator struct field
// so that it can be used in the validation logic
v.pod = pod

// Assign the previous state of the Pod
v.PreviousPodState = oldObj.(*corev1.Pod)

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

// Prevent changing the security context after creation
for i := range v.pod.Spec.Containers {
if !equalSecurityContexts(v.PreviousPodState.Spec.Containers[i].SecurityContext, v.pod.Spec.Containers[i].SecurityContext) {
return nil, fmt.Errorf("security context of containers cannot be changed after creation")
}
}

// TODO: Add any additional update validation logic here.

return nil, nil
}

// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Pod
func (v *PodCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
podlog.Info("Deletion Validation for corev1.Pod")
pod, ok := obj.(*corev1.Pod)
if !ok {
return nil, fmt.Errorf("expected a Pod object but got %T", obj)
}
podlog.Info("validate delete", "name", pod.GetName())

// Assign the Pod object to the PodCustomValidator struct field
// so that it can be used in the validation logic
v.pod = pod

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

// TODO: Add any additional deletion validation logic here.

return nil, nil
}

// equalSecurityContexts checks if two SecurityContexts are equal
func equalSecurityContexts(a, b *corev1.SecurityContext) bool {
// Implement your logic to compare SecurityContexts here
// For example, you can compare specific fields:
return a.RunAsNonRoot == b.RunAsNonRoot &&
a.AllowPrivilegeEscalation == b.AllowPrivilegeEscalation
}

```

### 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 +324,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 1169d69

Please sign in to comment.