Skip to content

Commit 0f049b8

Browse files
authored
⚠️ Generic Validator and Defaulter (#3360)
* Type defaulter and validator * Builder * Introduce WebhookFor * Update existing WebhookManagedBy * Linting * Preserve alias for NewWebhookManagedBy * Use WithDefaulter/WithValidator going forward * Test defaulter * TestValidatorBuilder * Mixed * Custom and Typed validator/defaulter are mutually exclusive * Type new in validator * Simplify builder * Re-add aliases * feedback
1 parent 0ddbc52 commit 0f049b8

File tree

11 files changed

+885
-673
lines changed

11 files changed

+885
-673
lines changed

alias.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package controllerruntime
1818

1919
import (
2020
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
21+
"k8s.io/apimachinery/pkg/runtime"
2122
"k8s.io/apimachinery/pkg/runtime/schema"
2223
"sigs.k8s.io/controller-runtime/pkg/builder"
2324
"sigs.k8s.io/controller-runtime/pkg/client/config"
@@ -104,9 +105,6 @@ var (
104105
// NewControllerManagedBy returns a new controller builder that will be started by the provided Manager.
105106
NewControllerManagedBy = builder.ControllerManagedBy
106107

107-
// NewWebhookManagedBy returns a new webhook builder that will be started by the provided Manager.
108-
NewWebhookManagedBy = builder.WebhookManagedBy
109-
110108
// NewManager returns a new Manager for creating Controllers.
111109
// Note that if ContentType in the given config is not set, "application/vnd.kubernetes.protobuf"
112110
// will be used for all built-in resources of Kubernetes, and "application/json" is for other types
@@ -155,3 +153,8 @@ var (
155153
// SetLogger sets a concrete logging implementation for all deferred Loggers.
156154
SetLogger = log.SetLogger
157155
)
156+
157+
// NewWebhookManagedBy returns a new webhook builder for the provided type T.
158+
func NewWebhookManagedBy[T runtime.Object](mgr manager.Manager, obj T) *builder.WebhookBuilder[T] {
159+
return builder.WebhookManagedBy(mgr, obj)
160+
}

examples/builtins/main.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ func main() {
6060
os.Exit(1)
6161
}
6262

63-
if err := ctrl.NewWebhookManagedBy(mgr).
64-
For(&corev1.Pod{}).
63+
if err := ctrl.NewWebhookManagedBy(mgr, &corev1.Pod{}).
6564
WithDefaulter(&podAnnotator{}).
6665
WithValidator(&podValidator{}).
6766
Complete(); err != nil {

examples/builtins/mutatingwebhook.go

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,8 @@ package main
1818

1919
import (
2020
"context"
21-
"fmt"
2221

2322
corev1 "k8s.io/api/core/v1"
24-
"k8s.io/apimachinery/pkg/runtime"
2523

2624
logf "sigs.k8s.io/controller-runtime/pkg/log"
2725
)
@@ -31,12 +29,8 @@ import (
3129
// podAnnotator annotates Pods
3230
type podAnnotator struct{}
3331

34-
func (a *podAnnotator) Default(ctx context.Context, obj runtime.Object) error {
32+
func (a *podAnnotator) Default(ctx context.Context, pod *corev1.Pod) error {
3533
log := logf.FromContext(ctx)
36-
pod, ok := obj.(*corev1.Pod)
37-
if !ok {
38-
return fmt.Errorf("expected a Pod but got a %T", obj)
39-
}
4034

4135
if pod.Annotations == nil {
4236
pod.Annotations = map[string]string{}

examples/builtins/validatingwebhook.go

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@ import (
2121
"fmt"
2222

2323
corev1 "k8s.io/api/core/v1"
24-
"k8s.io/apimachinery/pkg/runtime"
2524

2625
logf "sigs.k8s.io/controller-runtime/pkg/log"
2726
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -33,12 +32,8 @@ import (
3332
type podValidator struct{}
3433

3534
// validate admits a pod if a specific annotation exists.
36-
func (v *podValidator) validate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
35+
func (v *podValidator) validate(ctx context.Context, pod *corev1.Pod) (admission.Warnings, error) {
3736
log := logf.FromContext(ctx)
38-
pod, ok := obj.(*corev1.Pod)
39-
if !ok {
40-
return nil, fmt.Errorf("expected a Pod but got a %T", obj)
41-
}
4237

4338
log.Info("Validating Pod")
4439
key := "example-mutating-admission-webhook"
@@ -53,14 +48,14 @@ func (v *podValidator) validate(ctx context.Context, obj runtime.Object) (admiss
5348
return nil, nil
5449
}
5550

56-
func (v *podValidator) ValidateCreate(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
51+
func (v *podValidator) ValidateCreate(ctx context.Context, obj *corev1.Pod) (admission.Warnings, error) {
5752
return v.validate(ctx, obj)
5853
}
5954

60-
func (v *podValidator) ValidateUpdate(ctx context.Context, oldObj, newObj runtime.Object) (admission.Warnings, error) {
55+
func (v *podValidator) ValidateUpdate(ctx context.Context, oldObj, newObj *corev1.Pod) (admission.Warnings, error) {
6156
return v.validate(ctx, newObj)
6257
}
6358

64-
func (v *podValidator) ValidateDelete(ctx context.Context, obj runtime.Object) (admission.Warnings, error) {
59+
func (v *podValidator) ValidateDelete(ctx context.Context, obj *corev1.Pod) (admission.Warnings, error) {
6560
return v.validate(ctx, obj)
6661
}

examples/crd/main.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,7 @@ func main() {
129129
os.Exit(1)
130130
}
131131

132-
err = ctrl.NewWebhookManagedBy(mgr).
133-
For(&api.ChaosPod{}).
132+
err = ctrl.NewWebhookManagedBy(mgr, &api.ChaosPod{}).
134133
Complete()
135134
if err != nil {
136135
setupLog.Error(err, "unable to create webhook")

pkg/builder/example_webhook_test.go

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,7 @@ func ExampleWebhookBuilder() {
4040
}
4141

4242
err = builder.
43-
WebhookManagedBy(mgr). // Create the WebhookManagedBy
44-
For(&examplegroup.ChaosPod{}). // ChaosPod is a CRD.
43+
WebhookManagedBy(mgr, &examplegroup.ChaosPod{}). // Create the WebhookManagedBy
4544
Complete()
4645
if err != nil {
4746
log.Error(err, "could not create webhook")

pkg/builder/webhook.go

Lines changed: 74 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,13 @@ import (
3737
)
3838

3939
// WebhookBuilder builds a Webhook.
40-
type WebhookBuilder struct {
40+
type WebhookBuilder[T runtime.Object] struct {
4141
apiType runtime.Object
4242
customDefaulter admission.CustomDefaulter
43+
defaulter admission.Defaulter[T]
4344
customDefaulterOpts []admission.DefaulterOption
4445
customValidator admission.CustomValidator
46+
validator admission.Validator[T]
4547
customPath string
4648
customValidatorCustomPath string
4749
customDefaulterCustomPath string
@@ -56,59 +58,61 @@ type WebhookBuilder struct {
5658
}
5759

5860
// WebhookManagedBy returns a new webhook builder.
59-
func WebhookManagedBy(m manager.Manager) *WebhookBuilder {
60-
return &WebhookBuilder{mgr: m}
61+
func WebhookManagedBy[T runtime.Object](m manager.Manager, object T) *WebhookBuilder[T] {
62+
return &WebhookBuilder[T]{mgr: m, apiType: object}
6163
}
6264

63-
// TODO(droot): update the GoDoc for conversion.
64-
65-
// For takes a runtime.Object which should be a CR.
66-
// If the given object implements the admission.Defaulter interface, a MutatingWebhook will be wired for this type.
67-
// If the given object implements the admission.Validator interface, a ValidatingWebhook will be wired for this type.
68-
func (blder *WebhookBuilder) For(apiType runtime.Object) *WebhookBuilder {
69-
if blder.apiType != nil {
70-
blder.err = errors.New("For(...) should only be called once, could not assign multiple objects for webhook registration")
71-
}
72-
blder.apiType = apiType
65+
// WithCustomDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption)
66+
// will be wired for this type.
67+
// Deprecated: Use WithDefaulter instead.
68+
func (blder *WebhookBuilder[T]) WithCustomDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder[T] {
69+
blder.customDefaulter = defaulter
70+
blder.customDefaulterOpts = opts
7371
return blder
7472
}
7573

76-
// WithDefaulter takes an admission.CustomDefaulter interface, a MutatingWebhook with the provided opts (admission.DefaulterOption)
77-
// will be wired for this type.
78-
func (blder *WebhookBuilder) WithDefaulter(defaulter admission.CustomDefaulter, opts ...admission.DefaulterOption) *WebhookBuilder {
79-
blder.customDefaulter = defaulter
74+
// WithDefaulter sets up the provided admission.Defaulter in a defaulting webhook.
75+
func (blder *WebhookBuilder[T]) WithDefaulter(defaulter admission.Defaulter[T], opts ...admission.DefaulterOption) *WebhookBuilder[T] {
76+
blder.defaulter = defaulter
8077
blder.customDefaulterOpts = opts
8178
return blder
8279
}
8380

84-
// WithValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type.
85-
func (blder *WebhookBuilder) WithValidator(validator admission.CustomValidator) *WebhookBuilder {
81+
// WithCustomValidator takes a admission.CustomValidator interface, a ValidatingWebhook will be wired for this type.
82+
// Deprecated: Use WithValidator instead.
83+
func (blder *WebhookBuilder[T]) WithCustomValidator(validator admission.CustomValidator) *WebhookBuilder[T] {
8684
blder.customValidator = validator
8785
return blder
8886
}
8987

88+
// WithValidator sets up the provided admission.Validator in a validating webhook.
89+
func (blder *WebhookBuilder[T]) WithValidator(validator admission.Validator[T]) *WebhookBuilder[T] {
90+
blder.validator = validator
91+
return blder
92+
}
93+
9094
// WithConverter takes a func that constructs a converter.Converter.
91-
// The Converter will then be used by the conversion endpoint for the type passed into For().
92-
func (blder *WebhookBuilder) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder {
95+
// The Converter will then be used by the conversion endpoint for the type passed into NewWebhookManagedBy()
96+
func (blder *WebhookBuilder[T]) WithConverter(converterConstructor func(*runtime.Scheme) (conversion.Converter, error)) *WebhookBuilder[T] {
9397
blder.converterConstructor = converterConstructor
9498
return blder
9599
}
96100

97101
// WithLogConstructor overrides the webhook's LogConstructor.
98-
func (blder *WebhookBuilder) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder {
102+
func (blder *WebhookBuilder[T]) WithLogConstructor(logConstructor func(base logr.Logger, req *admission.Request) logr.Logger) *WebhookBuilder[T] {
99103
blder.logConstructor = logConstructor
100104
return blder
101105
}
102106

103107
// WithContextFunc overrides the webhook's WithContextFunc.
104-
func (blder *WebhookBuilder) WithContextFunc(contextFunc func(context.Context, *http.Request) context.Context) *WebhookBuilder {
108+
func (blder *WebhookBuilder[T]) WithContextFunc(contextFunc func(context.Context, *http.Request) context.Context) *WebhookBuilder[T] {
105109
blder.contextFunc = contextFunc
106110
return blder
107111
}
108112

109113
// RecoverPanic indicates whether panics caused by the webhook should be recovered.
110114
// Defaults to true.
111-
func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder {
115+
func (blder *WebhookBuilder[T]) RecoverPanic(recoverPanic bool) *WebhookBuilder[T] {
112116
blder.recoverPanic = &recoverPanic
113117
return blder
114118
}
@@ -117,25 +121,25 @@ func (blder *WebhookBuilder) RecoverPanic(recoverPanic bool) *WebhookBuilder {
117121
//
118122
// Deprecated: WithCustomPath should not be used anymore.
119123
// Please use WithValidatorCustomPath or WithDefaulterCustomPath instead.
120-
func (blder *WebhookBuilder) WithCustomPath(customPath string) *WebhookBuilder {
124+
func (blder *WebhookBuilder[T]) WithCustomPath(customPath string) *WebhookBuilder[T] {
121125
blder.customPath = customPath
122126
return blder
123127
}
124128

125129
// WithValidatorCustomPath overrides the path of the Validator.
126-
func (blder *WebhookBuilder) WithValidatorCustomPath(customPath string) *WebhookBuilder {
130+
func (blder *WebhookBuilder[T]) WithValidatorCustomPath(customPath string) *WebhookBuilder[T] {
127131
blder.customValidatorCustomPath = customPath
128132
return blder
129133
}
130134

131135
// WithDefaulterCustomPath overrides the path of the Defaulter.
132-
func (blder *WebhookBuilder) WithDefaulterCustomPath(customPath string) *WebhookBuilder {
136+
func (blder *WebhookBuilder[T]) WithDefaulterCustomPath(customPath string) *WebhookBuilder[T] {
133137
blder.customDefaulterCustomPath = customPath
134138
return blder
135139
}
136140

137141
// Complete builds the webhook.
138-
func (blder *WebhookBuilder) Complete() error {
142+
func (blder *WebhookBuilder[T]) Complete() error {
139143
// Set the Config
140144
blder.loadRestConfig()
141145

@@ -146,13 +150,13 @@ func (blder *WebhookBuilder) Complete() error {
146150
return blder.registerWebhooks()
147151
}
148152

149-
func (blder *WebhookBuilder) loadRestConfig() {
153+
func (blder *WebhookBuilder[T]) loadRestConfig() {
150154
if blder.config == nil {
151155
blder.config = blder.mgr.GetConfig()
152156
}
153157
}
154158

155-
func (blder *WebhookBuilder) setLogConstructor() {
159+
func (blder *WebhookBuilder[T]) setLogConstructor() {
156160
if blder.logConstructor == nil {
157161
blder.logConstructor = func(base logr.Logger, req *admission.Request) logr.Logger {
158162
log := base.WithValues(
@@ -172,11 +176,11 @@ func (blder *WebhookBuilder) setLogConstructor() {
172176
}
173177
}
174178

175-
func (blder *WebhookBuilder) isThereCustomPathConflict() bool {
179+
func (blder *WebhookBuilder[T]) isThereCustomPathConflict() bool {
176180
return (blder.customPath != "" && blder.customDefaulter != nil && blder.customValidator != nil) || (blder.customPath != "" && blder.customDefaulterCustomPath != "") || (blder.customPath != "" && blder.customValidatorCustomPath != "")
177181
}
178182

179-
func (blder *WebhookBuilder) registerWebhooks() error {
183+
func (blder *WebhookBuilder[T]) registerWebhooks() error {
180184
typ, err := blder.getType()
181185
if err != nil {
182186
return err
@@ -217,8 +221,11 @@ func (blder *WebhookBuilder) registerWebhooks() error {
217221
}
218222

219223
// registerDefaultingWebhook registers a defaulting webhook if necessary.
220-
func (blder *WebhookBuilder) registerDefaultingWebhook() error {
221-
mwh := blder.getDefaultingWebhook()
224+
func (blder *WebhookBuilder[T]) registerDefaultingWebhook() error {
225+
mwh, err := blder.getDefaultingWebhook()
226+
if err != nil {
227+
return err
228+
}
222229
if mwh != nil {
223230
mwh.LogConstructor = blder.logConstructor
224231
mwh.WithContextFunc = blder.contextFunc
@@ -244,20 +251,28 @@ func (blder *WebhookBuilder) registerDefaultingWebhook() error {
244251
return nil
245252
}
246253

247-
func (blder *WebhookBuilder) getDefaultingWebhook() *admission.Webhook {
248-
if defaulter := blder.customDefaulter; defaulter != nil {
249-
w := admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, defaulter, blder.customDefaulterOpts...)
250-
if blder.recoverPanic != nil {
251-
w = w.WithRecoverPanic(*blder.recoverPanic)
254+
func (blder *WebhookBuilder[T]) getDefaultingWebhook() (*admission.Webhook, error) {
255+
var w *admission.Webhook
256+
if blder.defaulter != nil {
257+
if blder.customDefaulter != nil {
258+
return nil, errors.New("only one of Defaulter or CustomDefaulter can be set")
252259
}
253-
return w
260+
w = admission.WithDefaulter(blder.mgr.GetScheme(), blder.defaulter, blder.customDefaulterOpts...)
261+
} else if blder.customDefaulter != nil {
262+
w = admission.WithCustomDefaulter(blder.mgr.GetScheme(), blder.apiType, blder.customDefaulter, blder.customDefaulterOpts...)
254263
}
255-
return nil
264+
if w != nil && blder.recoverPanic != nil {
265+
w = w.WithRecoverPanic(*blder.recoverPanic)
266+
}
267+
return w, nil
256268
}
257269

258270
// registerValidatingWebhook registers a validating webhook if necessary.
259-
func (blder *WebhookBuilder) registerValidatingWebhook() error {
260-
vwh := blder.getValidatingWebhook()
271+
func (blder *WebhookBuilder[T]) registerValidatingWebhook() error {
272+
vwh, err := blder.getValidatingWebhook()
273+
if err != nil {
274+
return err
275+
}
261276
if vwh != nil {
262277
vwh.LogConstructor = blder.logConstructor
263278
vwh.WithContextFunc = blder.contextFunc
@@ -283,18 +298,23 @@ func (blder *WebhookBuilder) registerValidatingWebhook() error {
283298
return nil
284299
}
285300

286-
func (blder *WebhookBuilder) getValidatingWebhook() *admission.Webhook {
287-
if validator := blder.customValidator; validator != nil {
288-
w := admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, validator)
289-
if blder.recoverPanic != nil {
290-
w = w.WithRecoverPanic(*blder.recoverPanic)
301+
func (blder *WebhookBuilder[T]) getValidatingWebhook() (*admission.Webhook, error) {
302+
var w *admission.Webhook
303+
if blder.validator != nil {
304+
if blder.customValidator != nil {
305+
return nil, errors.New("only one of Validator or CustomValidator can be set")
291306
}
292-
return w
307+
w = admission.WithValidator(blder.mgr.GetScheme(), blder.validator)
308+
} else if blder.customValidator != nil {
309+
w = admission.WithCustomValidator(blder.mgr.GetScheme(), blder.apiType, blder.customValidator)
293310
}
294-
return nil
311+
if w != nil && blder.recoverPanic != nil {
312+
w = w.WithRecoverPanic(*blder.recoverPanic)
313+
}
314+
return w, nil
295315
}
296316

297-
func (blder *WebhookBuilder) registerConversionWebhook() error {
317+
func (blder *WebhookBuilder[T]) registerConversionWebhook() error {
298318
if blder.converterConstructor != nil {
299319
converter, err := blder.converterConstructor(blder.mgr.GetScheme())
300320
if err != nil {
@@ -323,14 +343,14 @@ func (blder *WebhookBuilder) registerConversionWebhook() error {
323343
return nil
324344
}
325345

326-
func (blder *WebhookBuilder) getType() (runtime.Object, error) {
346+
func (blder *WebhookBuilder[T]) getType() (runtime.Object, error) {
327347
if blder.apiType != nil {
328348
return blder.apiType, nil
329349
}
330-
return nil, errors.New("For() must be called with a valid object")
350+
return nil, errors.New("NewWebhookManagedBy() must be called with a valid object")
331351
}
332352

333-
func (blder *WebhookBuilder) isAlreadyHandled(path string) bool {
353+
func (blder *WebhookBuilder[T]) isAlreadyHandled(path string) bool {
334354
if blder.mgr.GetWebhookServer().WebhookMux() == nil {
335355
return false
336356
}

0 commit comments

Comments
 (0)