Skip to content

Commit 53fbfca

Browse files
update and supplement webhook docs
1 parent b5235ef commit 53fbfca

File tree

1 file changed

+262
-27
lines changed

1 file changed

+262
-27
lines changed

docs/book/src/reference/webhook-for-core-types.md

Lines changed: 262 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,75 @@ in controller-runtime.
99
It is suggested to use kubebuilder to initialize a project, and then you can
1010
follow the steps below to add admission webhooks for core types.
1111

12-
## Implement Your Handler
12+
<aside class="note">
13+
<h1>Markers for Webhooks</h1>
1314

14-
You need to have your handler implements the
15-
[admission.Handler](https://pkg.go.dev/sigs.k8s.io/controller-runtime/pkg/webhook/admission?tab=doc#Handler)
16-
interface.
15+
Notice that we use kubebuilder markers to generate webhook manifests.
16+
This marker is responsible for generating a mutating webhook manifest.
17+
18+
The meaning of each marker can be found [here](/reference/markers/webhook.md).
19+
20+
You will find those markers in the both following examples.
21+
</aside>
22+
23+
## Implementing Your Handler Using `Handle`
24+
25+
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.
26+
27+
### Update your webhook:
28+
29+
**Example**
30+
31+
Following an example of its implementation.
1732

1833
```go
34+
package v1
35+
36+
import (
37+
"context"
38+
"encoding/json"
39+
"net/http"
40+
"sigs.k8s.io/controller-runtime/pkg/client"
41+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
42+
corev1 "k8s.io/api/core/v1"
43+
)
44+
45+
// **Note**: in order to have controller-gen generate the webhook configuration for you, you need to add markers. For example:
46+
47+
// +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io
48+
1949
type podAnnotator struct {
20-
Client client.Client
21-
decoder *admission.Decoder
50+
Client client.Client
51+
decoder *admission.Decoder
2252
}
2353

2454
func (a *podAnnotator) Handle(ctx context.Context, req admission.Request) admission.Response {
25-
pod := &corev1.Pod{}
26-
err := a.decoder.Decode(req, pod)
27-
if err != nil {
28-
return admission.Errored(http.StatusBadRequest, err)
29-
}
30-
31-
// mutate the fields in pod
32-
33-
marshaledPod, err := json.Marshal(pod)
34-
if err != nil {
35-
return admission.Errored(http.StatusInternalServerError, err)
36-
}
37-
return admission.PatchResponseFromRaw(req.Object.Raw, marshaledPod)
55+
pod := &corev1.Pod{}
56+
err := a.decoder.Decode(req, pod)
57+
if err != nil {
58+
return admission.Errored(http.StatusBadRequest, err)
59+
}
60+
61+
// Mutate the fields in pod
62+
pod.Annotations["example.com/mutated"] = "true"
63+
64+
marshaledPod, err := json.Marshal(pod)
65+
if err != nil {
66+
return admission.Errored(http.StatusInternalServerError, err)
67+
}
68+
return admission.Patched(req.Object.Raw, marshaledPod)
3869
}
3970
```
4071

41-
**Note**: in order to have controller-gen generate the webhook configuration for
42-
you, you need to add markers. For example,
43-
`// +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io`
4472

4573
## Update main.go
4674

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

4977
```go
50-
mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{Handler: &podAnnotator{Client: mgr.GetClient()}})
78+
mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{
79+
Handler: &podAnnotator{Client: mgr.GetClient()},
80+
})
5181
```
5282

5383
You need to ensure the path here match the path in the marker.
@@ -58,13 +88,175 @@ If you need a client and/or decoder, just pass them in at struct construction ti
5888

5989
```go
6090
mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{
61-
Handler: &podAnnotator{
62-
Client: mgr.GetClient(),
63-
decoder: admission.NewDecoder(mgr.GetScheme()),
64-
},
91+
Handler: &podAnnotator{
92+
Client: mgr.GetClient(),
93+
decoder: admission.NewDecoder(mgr.GetScheme()),
94+
},
6595
})
6696
```
6797

98+
99+
## By using Custom interfaces instead of Handle
100+
101+
### Update your webhook:
102+
103+
**Example**
104+
105+
Following an example of its implementation.
106+
107+
```go
108+
package v1
109+
110+
import (
111+
"context"
112+
"fmt"
113+
114+
corev1 "k8s.io/api/core/v1"
115+
"k8s.io/apimachinery/pkg/runtime"
116+
ctrl "sigs.k8s.io/controller-runtime"
117+
logf "sigs.k8s.io/controller-runtime/pkg/log"
118+
"sigs.k8s.io/controller-runtime/pkg/webhook"
119+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
120+
)
121+
122+
// log is for logging in this package.
123+
var podlog = logf.Log.WithName("pod-webhook")
124+
125+
// SetupWebhookWithManager sets up the webhook with the manager for both the defaulter and validator
126+
func (r *corev1.Pod) SetupWebhookWithManager(mgr ctrl.Manager) error {
127+
return ctrl.NewWebhookManagedBy(mgr).
128+
For(r).
129+
WithValidator(&PodCustomValidator{}).
130+
WithDefaulter(&PodCustomDefaulter{}).
131+
Complete()
132+
}
133+
134+
// +kubebuilder:webhook:path=/mutate-v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io,admissionReviewVersions=v1
135+
136+
// PodCustomDefaulter handles defaulting Pods
137+
type PodCustomDefaulter struct{}
138+
139+
var _ webhook.CustomDefaulter = &PodCustomDefaulter{}
140+
141+
// Default implements webhook.CustomDefaulter so a webhook will be registered for the type
142+
func (d *PodCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
143+
podlog.Info("CustomDefaulter for corev1.Pod")
144+
req, err := admission.RequestFromContext(ctx)
145+
if err != nil {
146+
return fmt.Errorf("expected admission.Request in ctx: %w", err)
147+
}
148+
if req.Kind.Kind != "Pod" {
149+
return fmt.Errorf("expected Kind Pod got %q", req.Kind.Kind)
150+
}
151+
castedObj, ok := obj.(*corev1.Pod)
152+
if !ok {
153+
return fmt.Errorf("expected a Pod object but got %T", obj)
154+
}
155+
podlog.Info("default", "name", castedObj.GetName())
156+
157+
// Mutate the fields in Pod (e.g., adding an annotation)
158+
if castedObj.Annotations == nil {
159+
castedObj.Annotations = map[string]string{}
160+
}
161+
castedObj.Annotations["example.com/mutated"] = "true"
162+
163+
return nil
164+
}
165+
166+
// +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
167+
168+
// PodCustomValidator handles validating Pods
169+
type PodCustomValidator struct{}
170+
171+
var _ webhook.CustomValidator = &PodCustomValidator{}
172+
173+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
174+
func (v *PodCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
175+
podlog.Info("Creation Validation for corev1.Pod")
176+
177+
req, err := admission.RequestFromContext(ctx)
178+
if err != nil {
179+
return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
180+
}
181+
if req.Kind.Kind != "Pod" {
182+
return nil, fmt.Errorf("expected Kind Pod got %q", req.Kind.Kind)
183+
}
184+
castedObj, ok := obj.(*corev1.Pod)
185+
if !ok {
186+
return nil, fmt.Errorf("expected a Pod object but got %T", obj)
187+
}
188+
podlog.Info("validate create", "name", castedObj.GetName())
189+
190+
// Ensure the Pod has at least one container
191+
if len(castedObj.Spec.Containers) == 0 {
192+
return nil, fmt.Errorf("pod must have at least one container")
193+
}
194+
195+
return nil, nil
196+
}
197+
198+
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
199+
func (v *PodCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
200+
podlog.Info("Update Validation for corev1.Pod")
201+
202+
req, err := admission.RequestFromContext(ctx)
203+
if err != nil {
204+
return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
205+
}
206+
if req.Kind.Kind != "Pod" {
207+
return nil, fmt.Errorf("expected Kind Pod got %q", req.Kind.Kind)
208+
}
209+
castedObj, ok := newObj.(*corev1.Pod)
210+
if !ok {
211+
return nil, fmt.Errorf("expected a Pod object but got %T", newObj)
212+
}
213+
podlog.Info("validate update", "name", castedObj.GetName())
214+
215+
// Prevent changing a specific annotation
216+
if oldObj.(*corev1.Pod).Annotations["example.com/protected"] != castedObj.Annotations["example.com/protected"] {
217+
return nil, fmt.Errorf("the annotation 'example.com/protected' cannot be changed")
218+
}
219+
220+
return nil, nil
221+
}
222+
223+
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
224+
func (v *PodCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
225+
podlog.Info("Deletion Validation for corev1.Pod")
226+
227+
req, err := admission.RequestFromContext(ctx)
228+
if err != nil {
229+
return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
230+
}
231+
if req.Kind.Kind != "Pod" {
232+
return nil, fmt.Errorf("expected Kind Pod got %q", req.Kind.Kind)
233+
}
234+
castedObj, ok := obj.(*corev1.Pod)
235+
if !ok {
236+
return nil, fmt.Errorf("expected a Pod object but got %T", obj)
237+
}
238+
podlog.Info("validate delete", "name", castedObj.GetName())
239+
240+
// Prevent deletion of protected Pods
241+
if castedObj.Annotations["example.com/protected"] == "true" {
242+
return nil, fmt.Errorf("protected pods cannot be deleted")
243+
}
244+
245+
return nil, nil
246+
}
247+
```
248+
249+
### Update the main.go
250+
251+
```go
252+
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
253+
if err := (&corev1.Pod{}).SetupWebhookWithManager(mgr); err != nil {
254+
setupLog.Error(err, "unable to create webhook", "webhook", "corev1.Pod")
255+
os.Exit(1)
256+
}
257+
}
258+
```
259+
68260
## Deploy
69261

70262
Deploying it is just like deploying a webhook server for CRD. You need to
@@ -73,5 +265,48 @@ Deploying it is just like deploying a webhook server for CRD. You need to
73265

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

268+
## What are `Handle` and Custom Interfaces?
269+
270+
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.
271+
272+
## Purpose of the `Handle` Function
273+
274+
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.
275+
276+
### Mutation
277+
278+
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.
279+
280+
### Response Construction
281+
282+
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.
283+
284+
## Purpose of Custom Interfaces (`CustomValidator` and `CustomDefaulter`)
285+
286+
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.
287+
288+
### Validation (`CustomValidator`)
289+
290+
The `CustomValidator` interface is used specifically for validating resources during create, update, or delete operations. It ensures that the resource meets certain criteria before the operation is allowed to proceed. The interface provides three methods:
291+
292+
- **`ValidateCreate`**: Called during resource creation.
293+
- **`ValidateUpdate`**: Called during resource updates.
294+
- **`ValidateDelete`**: Called when a resource is deleted.
295+
296+
### Defaulting (`CustomDefaulter`)
297+
298+
The `CustomDefaulter` interface is used for setting default values on a resource before it is created. This allows you to ensure that all necessary fields have valid values, even if the user did not specify them.
299+
300+
## When to Use Each Approach
301+
302+
- **Use `Handle` when**:
303+
- You need to both mutate and validate the resource in a single function.
304+
- You want direct control over how the admission response is constructed and returned.
305+
- Your webhook logic is simple and doesn’t require a clear separation of concerns.
306+
307+
- **Use `CustomValidator` and `CustomDefaulter` when**:
308+
- You want to separate validation and defaulting logic for better modularity.
309+
- Your webhook logic is complex, and separating concerns makes the code easier to manage.
310+
- You don’t need to perform mutation and validation in the same function.
76311

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

0 commit comments

Comments
 (0)