Skip to content

Commit d97a77b

Browse files
📖 update and supplement webhook documentation with example to work with core types (#4061)
update and supplement webhook docs
1 parent b7b009d commit d97a77b

File tree

1 file changed

+279
-28
lines changed

1 file changed

+279
-28
lines changed

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

Lines changed: 279 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -9,45 +9,85 @@ 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+
## Implementing Your Handler Using `Handle`
1313

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.
14+
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.
15+
16+
### Update your webhook:
17+
18+
**Example**
1719

1820
```go
21+
package v1
22+
23+
import (
24+
"context"
25+
"encoding/json"
26+
"net/http"
27+
"sigs.k8s.io/controller-runtime/pkg/client"
28+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
29+
corev1 "k8s.io/api/core/v1"
30+
)
31+
32+
// **Note**: in order to have controller-gen generate the webhook configuration for you, you need to add markers. For example:
33+
34+
// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io
35+
1936
type podAnnotator struct {
20-
Client client.Client
21-
decoder *admission.Decoder
37+
Client client.Client
38+
decoder *admission.Decoder
2239
}
2340

2441
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)
42+
pod := &corev1.Pod{}
43+
err := a.decoder.Decode(req, pod)
44+
if err != nil {
45+
return admission.Errored(http.StatusBadRequest, err)
46+
}
47+
48+
// Mutate the fields in pod
49+
pod.Annotations["example.com/mutated"] = "true"
50+
51+
marshaledPod, err := json.Marshal(pod)
52+
if err != nil {
53+
return admission.Errored(http.StatusInternalServerError, err)
54+
}
55+
return admission.Patched(req.Object.Raw, marshaledPod)
3856
}
3957
```
58+
<aside class="note">
59+
<h1>Markers for Webhooks</h1>
60+
61+
Notice that we use kubebuilder markers to generate webhook manifests.
62+
This marker is responsible for generating a mutating webhook manifest.
63+
64+
The meaning of each marker can be found [here](./markers/webhook.md).
65+
66+
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.
4067

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`
68+
The format for the webhook path is as follows:
69+
70+
```go
71+
/mutate-<group>-<version>-<kind>
72+
```
73+
74+
Since this documentation example uses Pod from the core API group, the group should be an empty string.
75+
76+
For example, the marker for a mutating webhook for Pod might look like this:
77+
78+
```go
79+
// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io
80+
```
81+
</aside>
4482

4583
## Update main.go
4684

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

4987
```go
50-
mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{Handler: &podAnnotator{Client: mgr.GetClient()}})
88+
mgr.GetWebhookServer().Register("/mutate--v1-pod", &webhook.Admission{
89+
Handler: &podAnnotator{Client: mgr.GetClient()},
90+
})
5191
```
5292

5393
You need to ensure the path here match the path in the marker.
@@ -57,14 +97,194 @@ You need to ensure the path here match the path in the marker.
5797
If you need a client and/or decoder, just pass them in at struct construction time.
5898

5999
```go
60-
mgr.GetWebhookServer().Register("/mutate-v1-pod", &webhook.Admission{
61-
Handler: &podAnnotator{
62-
Client: mgr.GetClient(),
63-
decoder: admission.NewDecoder(mgr.GetScheme()),
64-
},
100+
mgr.GetWebhookServer().Register("/mutate--v1-pod", &webhook.Admission{
101+
Handler: &podAnnotator{
102+
Client: mgr.GetClient(),
103+
decoder: admission.NewDecoder(mgr.GetScheme()),
104+
},
65105
})
66106
```
67107

108+
## By using Custom interfaces instead of Handle
109+
110+
### Update your webhook:
111+
112+
**Example**
113+
114+
```go
115+
package v1
116+
117+
import (
118+
"context"
119+
"fmt"
120+
121+
corev1 "k8s.io/api/core/v1"
122+
"k8s.io/apimachinery/pkg/runtime"
123+
ctrl "sigs.k8s.io/controller-runtime"
124+
logf "sigs.k8s.io/controller-runtime/pkg/log"
125+
"sigs.k8s.io/controller-runtime/pkg/webhook"
126+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
127+
)
128+
129+
// log is for logging in this package.
130+
var podlog = logf.Log.WithName("pod-resource")
131+
132+
// SetupWebhookWithManager will setup the manager to manage the webhooks
133+
func (r *corev1.Pod) SetupWebhookWithManager(mgr ctrl.Manager) error {
134+
runAsNonRoot := true
135+
allowPrivilegeEscalation := false
136+
137+
return ctrl.NewWebhookManagedBy(mgr).
138+
For(r).
139+
WithValidator(&PodCustomValidator{}).
140+
WithDefaulter(&PodCustomDefaulter{
141+
DefaultSecurityContext: &corev1.SecurityContext{
142+
RunAsNonRoot: &runAsNonRoot, // Set to true
143+
AllowPrivilegeEscalation: &allowPrivilegeEscalation, // Set to false
144+
},
145+
}).
146+
Complete()
147+
}
148+
149+
// +kubebuilder:webhook:path=/mutate--v1-pod,mutating=true,failurePolicy=fail,groups="",resources=pods,verbs=create;update,versions=v1,name=mpod.kb.io,admissionReviewVersions=v1
150+
151+
// +kubebuilder:object:generate=false
152+
// PodCustomDefaulter struct is responsible for setting default values on the Pod resource
153+
// when it is created or updated.
154+
//
155+
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
156+
// as it is used only for temporary operations and does not need to be deeply copied.
157+
type PodCustomDefaulter struct {
158+
// Default security context to be applied to Pods
159+
DefaultSecurityContext *corev1.SecurityContext
160+
161+
// TODO: Add more fields as needed for defaulting
162+
}
163+
164+
var _ webhook.CustomDefaulter = &PodCustomDefaulter{}
165+
166+
// Default implements webhook.CustomDefaulter so a webhook will be registered for the type Pod
167+
func (d *PodCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
168+
pod, ok := obj.(*corev1.Pod)
169+
if !ok {
170+
return fmt.Errorf("expected a Pod object but got %T", obj)
171+
}
172+
podlog.Info("CustomDefaulter for corev1.Pod", "name", pod.GetName())
173+
174+
// Apply the default security context if it's not set
175+
for i := range pod.Spec.Containers {
176+
if pod.Spec.Containers[i].SecurityContext == nil {
177+
pod.Spec.Containers[i].SecurityContext = d.DefaultSecurityContext
178+
}
179+
}
180+
181+
// Mutate the fields in Pod (e.g., adding an annotation)
182+
if pod.Annotations == nil {
183+
pod.Annotations = map[string]string{}
184+
}
185+
pod.Annotations["example.com/mutated"] = "true"
186+
187+
// TODO: Add any additional defaulting logic here.
188+
189+
return nil
190+
}
191+
192+
// +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
193+
194+
// +kubebuilder:object:generate=false
195+
// PodCustomValidator struct is responsible for validating the Pod resource
196+
// when it is created, updated, or deleted.
197+
//
198+
// NOTE: The +kubebuilder:object:generate=false marker prevents controller-gen from generating DeepCopy methods,
199+
// as this struct is used only for temporary operations and does not need to be deeply copied.
200+
type PodCustomValidator struct {
201+
}
202+
203+
var _ webhook.CustomValidator = &PodCustomValidator{}
204+
205+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type Pod
206+
func (v *PodCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
207+
pod, ok := obj.(*corev1.Pod)
208+
if !ok {
209+
return nil, fmt.Errorf("expected a Pod object but got %T", obj)
210+
}
211+
podlog.Info("Validation for corev1.Pod upon creation", "name", pod.GetName())
212+
213+
// Ensure the Pod has at least one container
214+
if len(pod.Spec.Containers) == 0 {
215+
return nil, fmt.Errorf("pod must have at least one container")
216+
}
217+
218+
// TODO: Add any additional creation validation logic here.
219+
220+
return nil, nil
221+
}
222+
223+
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type Pod
224+
func (v *PodCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
225+
pod, ok := newObj.(*corev1.Pod)
226+
if !ok {
227+
return nil, fmt.Errorf("expected a Pod object but got %T", newObj)
228+
}
229+
podlog.Info("Validation for corev1.Pod upon Update", "name", pod.GetName())
230+
231+
oldPod := oldObj.(*corev1.Pod)
232+
// Prevent changing a specific annotation
233+
if oldPod.Annotations["example.com/protected"] != pod.Annotations["example.com/protected"] {
234+
return nil, fmt.Errorf("the annotation 'example.com/protected' cannot be changed")
235+
}
236+
237+
// Prevent changing the security context after creation
238+
for i := range pod.Spec.Containers {
239+
if !equalSecurityContexts(oldPod.Spec.Containers[i].SecurityContext, pod.Spec.Containers[i].SecurityContext) {
240+
return nil, fmt.Errorf("security context of containers cannot be changed after creation")
241+
}
242+
}
243+
244+
// TODO: Add any additional update validation logic here.
245+
246+
return nil, nil
247+
}
248+
249+
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type Pod
250+
func (v *PodCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
251+
pod, ok := obj.(*corev1.Pod)
252+
if !ok {
253+
return nil, fmt.Errorf("expected a Pod object but got %T", obj)
254+
}
255+
podlog.Info("Deletion for corev1.Pod upon Update", "name", pod.GetName())
256+
257+
// Prevent deletion of protected Pods
258+
if pod.Annotations["example.com/protected"] == "true" {
259+
return nil, fmt.Errorf("protected pods cannot be deleted")
260+
}
261+
262+
// TODO: Add any additional deletion validation logic here.
263+
264+
return nil, nil
265+
}
266+
267+
// equalSecurityContexts checks if two SecurityContexts are equal
268+
func equalSecurityContexts(a, b *corev1.SecurityContext) bool {
269+
// Implement your logic to compare SecurityContexts here
270+
// For example, you can compare specific fields:
271+
return a.RunAsNonRoot == b.RunAsNonRoot &&
272+
a.AllowPrivilegeEscalation == b.AllowPrivilegeEscalation
273+
}
274+
275+
```
276+
277+
### Update the main.go
278+
279+
```go
280+
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
281+
if err := (&corev1.Pod{}).SetupWebhookWithManager(mgr); err != nil {
282+
setupLog.Error(err, "unable to create webhook", "webhook", "corev1.Pod")
283+
os.Exit(1)
284+
}
285+
}
286+
```
287+
68288
## Deploy
69289

70290
Deploying it is just like deploying a webhook server for CRD. You need to
@@ -73,5 +293,36 @@ Deploying it is just like deploying a webhook server for CRD. You need to
73293

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

296+
## What are `Handle` and Custom Interfaces?
297+
298+
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.
299+
300+
## Purpose of the `Handle` Function
301+
302+
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.
303+
304+
### Mutation
305+
306+
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.
307+
308+
### Response Construction
309+
310+
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.
311+
312+
## Purpose of Custom Interfaces (`CustomValidator` and `CustomDefaulter`)
313+
314+
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.
315+
316+
## When to Use Each Approach
317+
318+
- **Use `Handle` when**:
319+
- You need to both mutate and validate the resource in a single function.
320+
- You want direct control over how the admission response is constructed and returned.
321+
- Your webhook logic is simple and doesn’t require a clear separation of concerns.
322+
323+
- **Use `CustomValidator` and `CustomDefaulter` when**:
324+
- You want to separate validation and defaulting logic for better modularity.
325+
- Your webhook logic is complex, and separating concerns makes the code easier to manage.
326+
- You don’t need to perform mutation and validation in the same function.
76327

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

0 commit comments

Comments
 (0)