Skip to content

Commit 2dfddc9

Browse files
Remove the usage of deprecated functions for webhooks
Motivation: kubernetes-sigs/controller-runtime#2877
1 parent b5235ef commit 2dfddc9

File tree

32 files changed

+1633
-229
lines changed

32 files changed

+1633
-229
lines changed
Lines changed: 277 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,277 @@
1+
/*
2+
Copyright 2024 The Kubernetes authors.
3+
4+
Licensed under the Apache License, Version 2.0 (the "License");
5+
you may not use this file except in compliance with the License.
6+
You may obtain a copy of the License at
7+
8+
http://www.apache.org/licenses/LICENSE-2.0
9+
10+
Unless required by applicable law or agreed to in writing, software
11+
distributed under the License is distributed on an "AS IS" BASIS,
12+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
See the License for the specific language governing permissions and
14+
limitations under the License.
15+
*/
16+
// +kubebuilder:docs-gen:collapse=Apache License
17+
18+
package v1
19+
20+
import (
21+
"context"
22+
"fmt"
23+
"github.com/robfig/cron"
24+
apierrors "k8s.io/apimachinery/pkg/api/errors"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
validationutils "k8s.io/apimachinery/pkg/util/validation"
27+
"k8s.io/apimachinery/pkg/util/validation/field"
28+
29+
"k8s.io/apimachinery/pkg/runtime"
30+
ctrl "sigs.k8s.io/controller-runtime"
31+
logf "sigs.k8s.io/controller-runtime/pkg/log"
32+
"sigs.k8s.io/controller-runtime/pkg/webhook"
33+
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
34+
)
35+
36+
// +kubebuilder:docs-gen:collapse=Go imports
37+
38+
/*
39+
Next, we'll setup a logger for the webhooks.
40+
*/
41+
42+
var cronjoblog = logf.Log.WithName("cronjob-resource")
43+
44+
/*
45+
Then, we set up the webhook with the manager.
46+
*/
47+
48+
// SetupWebhookWithManager will setup the manager to manage the webhooks
49+
func (r *CronJob) SetupWebhookWithManager(mgr ctrl.Manager) error {
50+
return ctrl.NewWebhookManagedBy(mgr).
51+
For(r).
52+
WithValidator(&CronJobCustomValidator{}).
53+
WithDefaulter(&CronJobCustomDefaulter{}).
54+
Complete()
55+
}
56+
57+
/*
58+
Notice that we use kubebuilder markers to generate webhook manifests.
59+
This marker is responsible for generating a mutating webhook manifest.
60+
61+
The meaning of each marker can be found [here](/reference/markers/webhook.md).
62+
*/
63+
64+
// +kubebuilder:webhook:path=/mutate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=true,failurePolicy=fail,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,verbs=create;update,versions=v1,name=mcronjob.kb.io,sideEffects=None,admissionReviewVersions=v1
65+
66+
/*
67+
We use the `webhook.Defaulter` interface to set defaults to our CRD.
68+
A webhook will automatically be served that calls this defaulting.
69+
70+
The `Default` method is expected to mutate the receiver, setting the defaults.
71+
*/
72+
73+
type CronJobCustomDefaulter struct{}
74+
75+
var _ webhook.CustomDefaulter = &CronJobCustomDefaulter{}
76+
77+
// Default implements webhook.CustomDefaulter so a webhook will be registered for the type
78+
func (d *CronJobCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
79+
cronjoblog.Info("CustomDefaulter for CronJob")
80+
req, err := admission.RequestFromContext(ctx)
81+
if err != nil {
82+
return fmt.Errorf("expected admission.Request in ctx: %w", err)
83+
}
84+
if req.Kind.Kind != "CronJob" {
85+
return fmt.Errorf("expected Kind CronJob got %q", req.Kind.Kind)
86+
}
87+
castedObj, ok := obj.(*CronJob)
88+
if !ok {
89+
return fmt.Errorf("expected a CronJob object but got %T", obj)
90+
}
91+
cronjoblog.Info("default", "name", castedObj.GetName())
92+
93+
if castedObj.Spec.ConcurrencyPolicy == "" {
94+
castedObj.Spec.ConcurrencyPolicy = AllowConcurrent
95+
}
96+
if castedObj.Spec.Suspend == nil {
97+
castedObj.Spec.Suspend = new(bool)
98+
}
99+
if castedObj.Spec.SuccessfulJobsHistoryLimit == nil {
100+
castedObj.Spec.SuccessfulJobsHistoryLimit = new(int32)
101+
*castedObj.Spec.SuccessfulJobsHistoryLimit = 3
102+
}
103+
if castedObj.Spec.FailedJobsHistoryLimit == nil {
104+
castedObj.Spec.FailedJobsHistoryLimit = new(int32)
105+
*castedObj.Spec.FailedJobsHistoryLimit = 1
106+
}
107+
108+
return nil
109+
}
110+
111+
/*
112+
This marker is responsible for generating a validating webhook manifest.
113+
*/
114+
115+
// +kubebuilder:webhook:verbs=create;update;delete,path=/validate-batch-tutorial-kubebuilder-io-v1-cronjob,mutating=false,failurePolicy=fail,groups=batch.tutorial.kubebuilder.io,resources=cronjobs,versions=v1,name=vcronjob.kb.io,sideEffects=None,admissionReviewVersions=v1
116+
117+
/*
118+
We can validate our CRD beyond what's possible with declarative
119+
validation. Generally, declarative validation should be sufficient, but
120+
sometimes more advanced use cases call for complex validation.
121+
122+
For instance, we'll see below that we use this to validate a well-formed cron
123+
schedule without making up a long regular expression.
124+
125+
If `webhook.Validator` interface is implemented, a webhook will automatically be
126+
served that calls the validation.
127+
128+
The `ValidateCreate`, `ValidateUpdate` and `ValidateDelete` methods are expected
129+
to validate its receiver upon creation, update and deletion respectively.
130+
We separate out ValidateCreate from ValidateUpdate to allow behavior like making
131+
certain fields immutable, so that they can only be set on creation.
132+
ValidateDelete is also separated from ValidateUpdate to allow different
133+
validation behavior on deletion.
134+
Here, however, we just use the same shared validation for `ValidateCreate` and
135+
`ValidateUpdate`. And we do nothing in `ValidateDelete`, since we don't need to
136+
validate anything on deletion.
137+
*/
138+
139+
type CronJobCustomValidator struct{}
140+
141+
var _ webhook.CustomValidator = &CronJobCustomValidator{}
142+
143+
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type
144+
func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
145+
cronjoblog.Info("Creation Validation for CronJob")
146+
147+
req, err := admission.RequestFromContext(ctx)
148+
if err != nil {
149+
return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
150+
}
151+
if req.Kind.Kind != "CronJob" {
152+
return nil, fmt.Errorf("expected Kind CronJob got %q", req.Kind.Kind)
153+
}
154+
castedObj, ok := obj.(*CronJob)
155+
if !ok {
156+
return nil, fmt.Errorf("expected a CronJob object but got %T", obj)
157+
}
158+
cronjoblog.Info("default", "name", castedObj.GetName())
159+
160+
return nil, v.validateCronJob(castedObj)
161+
}
162+
163+
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type
164+
func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
165+
cronjoblog.Info("Update Validation for CronJob")
166+
req, err := admission.RequestFromContext(ctx)
167+
if err != nil {
168+
return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
169+
}
170+
if req.Kind.Kind != "CronJob" {
171+
return nil, fmt.Errorf("expected Kind CronJob got %q", req.Kind.Kind)
172+
}
173+
castedObj, ok := newObj.(*CronJob)
174+
if !ok {
175+
return nil, fmt.Errorf("expected a CronJob object but got %T", newObj)
176+
}
177+
cronjoblog.Info("default", "name", castedObj.GetName())
178+
179+
return nil, v.validateCronJob(castedObj)
180+
}
181+
182+
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type
183+
func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
184+
cronjoblog.Info("Deletion Validation for CronJob")
185+
req, err := admission.RequestFromContext(ctx)
186+
if err != nil {
187+
return nil, fmt.Errorf("expected admission.Request in ctx: %w", err)
188+
}
189+
if req.Kind.Kind != "CronJob" {
190+
return nil, fmt.Errorf("expected Kind CronJob got %q", req.Kind.Kind)
191+
}
192+
castedObj, ok := obj.(*CronJob)
193+
if !ok {
194+
return nil, fmt.Errorf("expected a CronJob object but got %T", obj)
195+
}
196+
cronjoblog.Info("default", "name", castedObj.GetName())
197+
198+
// TODO(user): fill in your validation logic upon object deletion.
199+
200+
return nil, nil
201+
}
202+
203+
/*
204+
We validate the name and the spec of the CronJob.
205+
*/
206+
207+
func (v *CronJobCustomValidator) validateCronJob(castedObj *CronJob) error {
208+
var allErrs field.ErrorList
209+
if err := v.validateCronJobName(castedObj); err != nil {
210+
allErrs = append(allErrs, err)
211+
}
212+
if err := v.validateCronJobSpec(castedObj); err != nil {
213+
allErrs = append(allErrs, err)
214+
}
215+
if len(allErrs) == 0 {
216+
return nil
217+
}
218+
219+
return apierrors.NewInvalid(
220+
schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"},
221+
castedObj.Name, allErrs)
222+
}
223+
224+
225+
/*
226+
Some fields are declaratively validated by OpenAPI schema.
227+
You can find kubebuilder validation markers (prefixed
228+
with `// +kubebuilder:validation`) in the
229+
[Designing an API](api-design.md) section.
230+
You can find all of the kubebuilder supported markers for
231+
declaring validation by running `controller-gen crd -w`,
232+
or [here](/reference/markers/crd-validation.md).
233+
*/
234+
235+
func (v *CronJobCustomValidator) validateCronJobSpec(castedObj *CronJob) *field.Error {
236+
// The field helpers from the kubernetes API machinery help us return nicely
237+
// structured validation errors.
238+
return validateScheduleFormat(
239+
castedObj.Spec.Schedule,
240+
field.NewPath("spec").Child("schedule"))
241+
}
242+
243+
/*
244+
We'll need to validate the [cron](https://en.wikipedia.org/wiki/Cron) schedule
245+
is well-formatted.
246+
*/
247+
248+
func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error {
249+
if _, err := cron.ParseStandard(schedule); err != nil {
250+
return field.Invalid(fldPath, schedule, err.Error())
251+
}
252+
return nil
253+
}
254+
255+
/*
256+
Validating the length of a string field can be done declaratively by
257+
the validation schema.
258+
259+
But the `ObjectMeta.Name` field is defined in a shared package under
260+
the apimachinery repo, so we can't declaratively validate it using
261+
the validation schema.
262+
*/
263+
264+
func (v *CronJobCustomValidator) validateCronJobName(castedObj *CronJob) *field.Error {
265+
if len(castedObj.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 {
266+
// The job name length is 63 characters like all Kubernetes objects
267+
// (which must fit in a DNS subdomain). The cronjob controller appends
268+
// a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating
269+
// a job. The job name length limit is 63 characters. Therefore cronjob
270+
// names must have length <= 63-11=52. If we don't validate this here,
271+
// then job creation will fail later.
272+
return field.Invalid(field.NewPath("metadata").Child("name"), castedObj.Name, "must be no more than 52 characters")
273+
}
274+
return nil
275+
}
276+
277+
// +kubebuilder:docs-gen:collapse=Validate object name

0 commit comments

Comments
 (0)