Skip to content

Commit 86dce22

Browse files
⚠️ decouple webhooks from APIs
1 parent bca81f3 commit 86dce22

File tree

88 files changed

+836
-1553
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

88 files changed

+836
-1553
lines changed

docs/book/src/cronjob-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/book/src/cronjob-tutorial/testdata/project/cmd/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import (
3838

3939
batchv1 "tutorial.kubebuilder.io/project/api/v1"
4040
"tutorial.kubebuilder.io/project/internal/controller"
41+
webhooksv1 "tutorial.kubebuilder.io/project/internal/webhooks/v1"
4142
// +kubebuilder:scaffold:imports
4243
)
4344

@@ -183,7 +184,7 @@ func main() {
183184
*/
184185
// nolint:goconst
185186
if os.Getenv("ENABLE_WEBHOOKS") != "false" {
186-
if err = (&batchv1.CronJob{}).SetupWebhookWithManager(mgr); err != nil {
187+
if err = webhooksv1.SetupCronJobWebhookWithManager(mgr); err != nil {
187188
setupLog.Error(err, "unable to create webhook", "webhook", "CronJob")
188189
os.Exit(1)
189190
}

docs/book/src/cronjob-tutorial/testdata/project/config/webhook/manifests.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ webhooks:
3232
namespace: system
3333
path: /mutate-batch-tutorial-kubebuilder-io-v1-cronjob
3434
failurePolicy: Fail
35-
name: mcronjob.kb.io
35+
name: mcronjob-v1.kb.io
3636
rules:
3737
- apiGroups:
3838
- batch.tutorial.kubebuilder.io
@@ -67,6 +67,7 @@ webhooks:
6767
operations:
6868
- CREATE
6969
- UPDATE
70+
- DELETE
7071
resources:
7172
- cronjobs
7273
sideEffects: None
@@ -78,7 +79,7 @@ webhooks:
7879
namespace: system
7980
path: /validate-batch-tutorial-kubebuilder-io-v1-cronjob
8081
failurePolicy: Fail
81-
name: vcronjob.kb.io
82+
name: vcronjob-v1.kb.io
8283
rules:
8384
- apiGroups:
8485
- batch.tutorial.kubebuilder.io
@@ -87,7 +88,6 @@ webhooks:
8788
operations:
8889
- CREATE
8990
- UPDATE
90-
- DELETE
9191
resources:
9292
- cronjobs
9393
sideEffects: None

docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_webhook.go renamed to docs/book/src/cronjob-tutorial/testdata/project/internal/webhooks/v1/cronjob_webhook.go

Lines changed: 44 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@ import (
3131
logf "sigs.k8s.io/controller-runtime/pkg/log"
3232
"sigs.k8s.io/controller-runtime/pkg/webhook"
3333
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
34+
35+
batchv1 "tutorial.kubebuilder.io/project/api/v1"
3436
)
3537

3638
// +kubebuilder:docs-gen:collapse=Go imports
@@ -45,13 +47,12 @@ var cronjoblog = logf.Log.WithName("cronjob-resource")
4547
Then, we set up the webhook with the manager.
4648
*/
4749

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).
50+
// SetupCronJobWebhookWithManager registers the webhook for CronJob in the manager.
51+
func SetupCronJobWebhookWithManager(mgr ctrl.Manager) error {
52+
return ctrl.NewWebhookManagedBy(mgr).For(&batchv1.CronJob{}).
5253
WithValidator(&CronJobCustomValidator{}).
5354
WithDefaulter(&CronJobCustomDefaulter{
54-
DefaultConcurrencyPolicy: AllowConcurrent,
55+
DefaultConcurrencyPolicy: batchv1.AllowConcurrent,
5556
DefaultSuspend: false,
5657
DefaultSuccessfulJobsHistoryLimit: 3,
5758
DefaultFailedJobsHistoryLimit: 1,
@@ -66,7 +67,7 @@ This marker is responsible for generating a mutating webhook manifest.
6667
The meaning of each marker can be found [here](/reference/markers/webhook.md).
6768
*/
6869

69-
// +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
70+
// +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-v1.kb.io,sideEffects=None,admissionReviewVersions=v1
7071

7172
/*
7273
We use the `webhook.CustomDefaulter` interface to set defaults to our CRD.
@@ -86,7 +87,7 @@ The `Default` method is expected to mutate the receiver, setting the defaults.
8687
type CronJobCustomDefaulter struct {
8788

8889
// Default values for various CronJob fields
89-
DefaultConcurrencyPolicy ConcurrencyPolicy
90+
DefaultConcurrencyPolicy batchv1.ConcurrencyPolicy
9091
DefaultSuspend bool
9192
DefaultSuccessfulJobsHistoryLimit int32
9293
DefaultFailedJobsHistoryLimit int32
@@ -96,40 +97,42 @@ var _ webhook.CustomDefaulter = &CronJobCustomDefaulter{}
9697

9798
// Default implements webhook.CustomDefaulter so a webhook will be registered for the Kind CronJob.
9899
func (d *CronJobCustomDefaulter) Default(ctx context.Context, obj runtime.Object) error {
99-
cronjob, ok := obj.(*CronJob)
100+
cronjob, ok := obj.(*batchv1.CronJob)
101+
100102
if !ok {
101103
return fmt.Errorf("expected an CronJob object but got %T", obj)
102104
}
103105
cronjoblog.Info("Defaulting for CronJob", "name", cronjob.GetName())
104106

105107
// Set default values
106-
cronjob.Default()
107-
108+
d.applyDefaults(cronjob)
108109
return nil
109110
}
110111

111-
func (r *CronJob) Default() {
112-
if r.Spec.ConcurrencyPolicy == "" {
113-
r.Spec.ConcurrencyPolicy = AllowConcurrent
112+
// applyDefaults applies default values to CronJob fields.
113+
func (d *CronJobCustomDefaulter) applyDefaults(cronJob *batchv1.CronJob) {
114+
if cronJob.Spec.ConcurrencyPolicy == "" {
115+
cronJob.Spec.ConcurrencyPolicy = d.DefaultConcurrencyPolicy
114116
}
115-
if r.Spec.Suspend == nil {
116-
r.Spec.Suspend = new(bool)
117+
if cronJob.Spec.Suspend == nil {
118+
cronJob.Spec.Suspend = new(bool)
119+
*cronJob.Spec.Suspend = d.DefaultSuspend
117120
}
118-
if r.Spec.SuccessfulJobsHistoryLimit == nil {
119-
r.Spec.SuccessfulJobsHistoryLimit = new(int32)
120-
*r.Spec.SuccessfulJobsHistoryLimit = 3
121+
if cronJob.Spec.SuccessfulJobsHistoryLimit == nil {
122+
cronJob.Spec.SuccessfulJobsHistoryLimit = new(int32)
123+
*cronJob.Spec.SuccessfulJobsHistoryLimit = d.DefaultSuccessfulJobsHistoryLimit
121124
}
122-
if r.Spec.FailedJobsHistoryLimit == nil {
123-
r.Spec.FailedJobsHistoryLimit = new(int32)
124-
*r.Spec.FailedJobsHistoryLimit = 1
125+
if cronJob.Spec.FailedJobsHistoryLimit == nil {
126+
cronJob.Spec.FailedJobsHistoryLimit = new(int32)
127+
*cronJob.Spec.FailedJobsHistoryLimit = d.DefaultFailedJobsHistoryLimit
125128
}
126129
}
127130

128131
/*
129132
This marker is responsible for generating a validating webhook manifest.
130133
*/
131134

132-
// +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
135+
// +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-v1.kb.io,sideEffects=None,admissionReviewVersions=v1
133136

134137
/*
135138
We can validate our CRD beyond what's possible with declarative
@@ -171,29 +174,29 @@ var _ webhook.CustomValidator = &CronJobCustomValidator{}
171174

172175
// ValidateCreate implements webhook.CustomValidator so a webhook will be registered for the type CronJob.
173176
func (v *CronJobCustomValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
174-
cronjob, ok := obj.(*CronJob)
177+
cronjob, ok := obj.(*batchv1.CronJob)
175178
if !ok {
176179
return nil, fmt.Errorf("expected a CronJob object but got %T", obj)
177180
}
178181
cronjoblog.Info("Validation for CronJob upon creation", "name", cronjob.GetName())
179182

180-
return nil, cronjob.validateCronJob()
183+
return nil, validateCronJob(cronjob)
181184
}
182185

183186
// ValidateUpdate implements webhook.CustomValidator so a webhook will be registered for the type CronJob.
184187
func (v *CronJobCustomValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
185-
cronjob, ok := newObj.(*CronJob)
188+
cronjob, ok := newObj.(*batchv1.CronJob)
186189
if !ok {
187-
return nil, fmt.Errorf("expected a CronJob object but got %T", newObj)
190+
return nil, fmt.Errorf("expected a CronJob object for the newObj but got got %T", newObj)
188191
}
189192
cronjoblog.Info("Validation for CronJob upon update", "name", cronjob.GetName())
190193

191-
return nil, cronjob.validateCronJob()
194+
return nil, validateCronJob(cronjob)
192195
}
193196

194197
// ValidateDelete implements webhook.CustomValidator so a webhook will be registered for the type CronJob.
195198
func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
196-
cronjob, ok := obj.(*CronJob)
199+
cronjob, ok := obj.(*batchv1.CronJob)
197200
if !ok {
198201
return nil, fmt.Errorf("expected a CronJob object but got %T", obj)
199202
}
@@ -208,21 +211,19 @@ func (v *CronJobCustomValidator) ValidateDelete(ctx context.Context, obj runtime
208211
We validate the name and the spec of the CronJob.
209212
*/
210213

211-
func (r *CronJob) validateCronJob() error {
214+
// validateCronJob validates the fields of a CronJob object.
215+
func validateCronJob(cronjob *batchv1.CronJob) error {
212216
var allErrs field.ErrorList
213-
if err := r.validateCronJobName(); err != nil {
217+
if err := validateCronJobName(cronjob); err != nil {
214218
allErrs = append(allErrs, err)
215219
}
216-
if err := r.validateCronJobSpec(); err != nil {
220+
if err := validateCronJobSpec(cronjob); err != nil {
217221
allErrs = append(allErrs, err)
218222
}
219223
if len(allErrs) == 0 {
220224
return nil
221225
}
222-
223-
return apierrors.NewInvalid(
224-
schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"},
225-
r.Name, allErrs)
226+
return apierrors.NewInvalid(schema.GroupKind{Group: "batch.tutorial.kubebuilder.io", Kind: "CronJob"}, cronjob.Name, allErrs)
226227
}
227228

228229
/*
@@ -235,19 +236,17 @@ declaring validation by running `controller-gen crd -w`,
235236
or [here](/reference/markers/crd-validation.md).
236237
*/
237238

238-
func (r *CronJob) validateCronJobSpec() *field.Error {
239-
// The field helpers from the kubernetes API machinery help us return nicely
240-
// structured validation errors.
241-
return validateScheduleFormat(
242-
r.Spec.Schedule,
243-
field.NewPath("spec").Child("schedule"))
239+
// validateCronJobSpec validates the schedule field of the CronJob spec.
240+
func validateCronJobSpec(cronjob *batchv1.CronJob) *field.Error {
241+
return validateScheduleFormat(cronjob.Spec.Schedule, field.NewPath("spec").Child("schedule"))
244242
}
245243

246244
/*
247245
We'll need to validate the [cron](https://en.wikipedia.org/wiki/Cron) schedule
248246
is well-formatted.
249247
*/
250248

249+
// validateScheduleFormat validates that the schedule field follows the cron format.
251250
func validateScheduleFormat(schedule string, fldPath *field.Path) *field.Error {
252251
if _, err := cron.ParseStandard(schedule); err != nil {
253252
return field.Invalid(fldPath, schedule, err.Error())
@@ -264,15 +263,10 @@ the apimachinery repo, so we can't declaratively validate it using
264263
the validation schema.
265264
*/
266265

267-
func (r *CronJob) validateCronJobName() *field.Error {
268-
if len(r.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 {
269-
// The job name length is 63 characters like all Kubernetes objects
270-
// (which must fit in a DNS subdomain). The cronjob controller appends
271-
// a 11-character suffix to the cronjob (`-$TIMESTAMP`) when creating
272-
// a job. The job name length limit is 63 characters. Therefore cronjob
273-
// names must have length <= 63-11=52. If we don't validate this here,
274-
// then job creation will fail later.
275-
return field.Invalid(field.NewPath("metadata").Child("name"), r.ObjectMeta.Name, "must be no more than 52 characters")
266+
// validateCronJobName validates the name of the CronJob object.
267+
func validateCronJobName(cronjob *batchv1.CronJob) *field.Error {
268+
if len(cronjob.ObjectMeta.Name) > validationutils.DNS1035LabelMaxLength-11 {
269+
return field.Invalid(field.NewPath("metadata").Child("name"), cronjob.ObjectMeta.Name, "must be no more than 52 characters")
276270
}
277271
return nil
278272
}

docs/book/src/cronjob-tutorial/testdata/project/api/v1/cronjob_webhook_test.go renamed to docs/book/src/cronjob-tutorial/testdata/project/internal/webhooks/v1/cronjob_webhook_test.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -19,32 +19,35 @@ package v1
1919
import (
2020
. "github.com/onsi/ginkgo/v2"
2121
. "github.com/onsi/gomega"
22+
23+
batchv1 "tutorial.kubebuilder.io/project/api/v1"
2224
// TODO (user): Add any additional imports if needed
2325
)
2426

2527
var _ = Describe("CronJob Webhook", func() {
2628
var (
27-
obj *CronJob
28-
oldObj *CronJob
29+
obj = &batchv1.CronJob{}
30+
oldObj = &batchv1.CronJob{}
2931
validator CronJobCustomValidator
32+
defaulter CronJobCustomDefaulter
3033
)
3134

3235
BeforeEach(func() {
33-
obj = &CronJob{
34-
Spec: CronJobSpec{
36+
obj = &batchv1.CronJob{
37+
Spec: batchv1.CronJobSpec{
3538
Schedule: "*/5 * * * *",
36-
ConcurrencyPolicy: AllowConcurrent,
39+
ConcurrencyPolicy: batchv1.AllowConcurrent,
3740
SuccessfulJobsHistoryLimit: new(int32),
3841
FailedJobsHistoryLimit: new(int32),
3942
},
4043
}
4144
*obj.Spec.SuccessfulJobsHistoryLimit = 3
4245
*obj.Spec.FailedJobsHistoryLimit = 1
4346

44-
oldObj = &CronJob{
45-
Spec: CronJobSpec{
47+
oldObj = &batchv1.CronJob{
48+
Spec: batchv1.CronJobSpec{
4649
Schedule: "*/5 * * * *",
47-
ConcurrencyPolicy: AllowConcurrent,
50+
ConcurrencyPolicy: batchv1.AllowConcurrent,
4851
SuccessfulJobsHistoryLimit: new(int32),
4952
FailedJobsHistoryLimit: new(int32),
5053
},
@@ -53,6 +56,12 @@ var _ = Describe("CronJob Webhook", func() {
5356
*oldObj.Spec.FailedJobsHistoryLimit = 1
5457

5558
validator = CronJobCustomValidator{}
59+
defaulter = CronJobCustomDefaulter{
60+
DefaultConcurrencyPolicy: batchv1.AllowConcurrent,
61+
DefaultSuspend: false,
62+
DefaultSuccessfulJobsHistoryLimit: 3,
63+
DefaultFailedJobsHistoryLimit: 1,
64+
}
5665

5766
Expect(obj).NotTo(BeNil(), "Expected obj to be initialized")
5867
Expect(oldObj).NotTo(BeNil(), "Expected oldObj to be initialized")
@@ -71,18 +80,18 @@ var _ = Describe("CronJob Webhook", func() {
7180
obj.Spec.FailedJobsHistoryLimit = nil // This should default to 1
7281

7382
By("calling the Default method to apply defaults")
74-
obj.Default()
83+
defaulter.Default(ctx, obj)
7584

7685
By("checking that the default values are set")
77-
Expect(obj.Spec.ConcurrencyPolicy).To(Equal(AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent")
86+
Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.AllowConcurrent), "Expected ConcurrencyPolicy to default to AllowConcurrent")
7887
Expect(*obj.Spec.Suspend).To(BeFalse(), "Expected Suspend to default to false")
7988
Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(3)), "Expected SuccessfulJobsHistoryLimit to default to 3")
8089
Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(1)), "Expected FailedJobsHistoryLimit to default to 1")
8190
})
8291

8392
It("Should not overwrite fields that are already set", func() {
8493
By("setting fields that would normally get a default")
85-
obj.Spec.ConcurrencyPolicy = ForbidConcurrent
94+
obj.Spec.ConcurrencyPolicy = batchv1.ForbidConcurrent
8695
obj.Spec.Suspend = new(bool)
8796
*obj.Spec.Suspend = true
8897
obj.Spec.SuccessfulJobsHistoryLimit = new(int32)
@@ -91,10 +100,10 @@ var _ = Describe("CronJob Webhook", func() {
91100
*obj.Spec.FailedJobsHistoryLimit = 2
92101

93102
By("calling the Default method to apply defaults")
94-
obj.Default()
103+
defaulter.Default(ctx, obj)
95104

96105
By("checking that the fields were not overwritten")
97-
Expect(obj.Spec.ConcurrencyPolicy).To(Equal(ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value")
106+
Expect(obj.Spec.ConcurrencyPolicy).To(Equal(batchv1.ForbidConcurrent), "Expected ConcurrencyPolicy to retain its set value")
98107
Expect(*obj.Spec.Suspend).To(BeTrue(), "Expected Suspend to retain its set value")
99108
Expect(*obj.Spec.SuccessfulJobsHistoryLimit).To(Equal(int32(5)), "Expected SuccessfulJobsHistoryLimit to retain its set value")
100109
Expect(*obj.Spec.FailedJobsHistoryLimit).To(Equal(int32(2)), "Expected FailedJobsHistoryLimit to retain its set value")

docs/book/src/cronjob-tutorial/testdata/project/api/v1/webhook_suite_test.go renamed to docs/book/src/cronjob-tutorial/testdata/project/internal/webhooks/v1/webhook_suite_test.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,7 +89,7 @@ var _ = BeforeSuite(func() {
8989
Expect(cfg).NotTo(BeNil())
9090

9191
scheme := apimachineryruntime.NewScheme()
92-
err = AddToScheme(scheme)
92+
err = admissionv1.AddToScheme(scheme)
9393
Expect(err).NotTo(HaveOccurred())
9494

9595
err = admissionv1.AddToScheme(scheme)
@@ -115,7 +115,7 @@ var _ = BeforeSuite(func() {
115115
})
116116
Expect(err).NotTo(HaveOccurred())
117117

118-
err = (&CronJob{}).SetupWebhookWithManager(mgr)
118+
err = SetupCronJobWebhookWithManager(mgr)
119119
Expect(err).NotTo(HaveOccurred())
120120

121121
// +kubebuilder:scaffold:webhook

docs/book/src/multiversion-tutorial/testdata/project/api/v1/zz_generated.deepcopy.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/book/src/multiversion-tutorial/testdata/project/api/v2/zz_generated.deepcopy.go

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)