Skip to content

Commit d4697d5

Browse files
authored
Validate AdmissionWebhookAdapter (#597)
Defines and calls Validate() on AdmissionWebhookAdapter when building the webhook. Calls validate with nested validation inside AdmissionWebhookTestCase. AdmissionWebhookAdapter#Build is now deprecated in favor of AdmissionWebhookAdapter#BuildWithContext which accepts a context and returns an error when validation fails. The previous Build will use a TODO context and panic on validation errors. AdmissionWebhookTestSuite#Run, AdmissionWebhookTests#Run, and AdmissionWebhookTestCase#Run are deprecated in favor of RunWithContext. The existing Run method delegates to RunWithContext, but requires users to manually configure nested validation (if desired). Signed-off-by: Scott Andrews <scott@andrews.me>
1 parent b2bb2eb commit d4697d5

4 files changed

Lines changed: 175 additions & 17 deletions

File tree

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,8 @@ Backwards support may be removed in a future release, users are encouraged to mi
11691169
- status `InitializeConditions()` is deprecated in favor of `InitializeConditions(context.Context)`.
11701170
- `ConditionSet#Manage` is deprecated in favor of `ConditionSet#ManageWithContext`.
11711171
- `HaltSubReconcilers` is deprecated in favor of `ErrHaltSubReconcilers`.
1172+
- `AdmissionWebhookAdapter#Build` is deprecated in favor of `AdmissionWebhookAdapter#BuildWithContext`.
1173+
- `AdmissionWebhookTestSuite#Run`, `AdmissionWebhookTests#Run`, and `AdmissionWebhookTestCase#Run` are deprecated in favor of `#RunWithContext`.
11721174

11731175
## Community
11741176

reconcilers/webhook.go

Lines changed: 52 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ import (
3333
"k8s.io/apimachinery/pkg/types"
3434
"reconciler.io/runtime/internal"
3535
rtime "reconciler.io/runtime/time"
36+
"reconciler.io/runtime/validation"
3637
"sigs.k8s.io/controller-runtime/pkg/client"
3738
crlog "sigs.k8s.io/controller-runtime/pkg/log"
3839
"sigs.k8s.io/controller-runtime/pkg/webhook"
@@ -115,30 +116,72 @@ func (r *AdmissionWebhookAdapter[T]) init() {
115116
})
116117
}
117118

119+
func (r *AdmissionWebhookAdapter[T]) Validate(ctx context.Context) error {
120+
r.init()
121+
122+
// validate Reconciler value
123+
if r.Reconciler == nil {
124+
return fmt.Errorf("AdmissionWebhookAdapter %q must define Reconciler", r.Name)
125+
}
126+
if validation.IsRecursive(ctx) {
127+
if v, ok := r.Reconciler.(validation.Validator); ok {
128+
if err := v.Validate(ctx); err != nil {
129+
return fmt.Errorf("AdmissionWebhookAdapter %q must have a valid Reconciler: %w", r.Name, err)
130+
}
131+
}
132+
}
133+
134+
return nil
135+
}
136+
137+
// Deprecated use BuildWithContext
118138
func (r *AdmissionWebhookAdapter[T]) Build() *admission.Webhook {
139+
webhook, err := r.BuildWithContext(context.TODO())
140+
if err != nil {
141+
panic(err)
142+
}
143+
return webhook
144+
}
145+
146+
func (r *AdmissionWebhookAdapter[T]) BuildWithContext(ctx context.Context) (*admission.Webhook, error) {
119147
r.init()
148+
149+
if err := r.Validate(r.withContext(ctx)); err != nil {
150+
return nil, err
151+
}
152+
120153
return &admission.Webhook{
121154
Handler: r,
122155
WithContextFunc: func(ctx context.Context, req *http.Request) context.Context {
156+
ctx = r.withContext(ctx)
157+
123158
log := crlog.FromContext(ctx).
124-
WithName("controller-runtime.webhook.webhooks").
125-
WithName(r.Name).
126159
WithValues(
127160
"webhook", req.URL.Path,
128161
)
129162
ctx = logr.NewContext(ctx, log)
130163

131-
ctx = WithStash(ctx)
132-
133-
ctx = StashConfig(ctx, r.Config)
134-
ctx = StashOriginalConfig(ctx, r.Config)
135-
ctx = StashResourceType(ctx, r.Type)
136-
ctx = StashOriginalResourceType(ctx, r.Type)
137164
ctx = StashHTTPRequest(ctx, req)
138165

139166
return ctx
140167
},
141-
}
168+
}, nil
169+
}
170+
171+
func (r *AdmissionWebhookAdapter[T]) withContext(ctx context.Context) context.Context {
172+
log := crlog.FromContext(ctx).
173+
WithName("controller-runtime.webhook.webhooks").
174+
WithName(r.Name)
175+
ctx = logr.NewContext(ctx, log)
176+
177+
ctx = WithStash(ctx)
178+
179+
ctx = StashConfig(ctx, r.Config)
180+
ctx = StashOriginalConfig(ctx, r.Config)
181+
ctx = StashResourceType(ctx, r.Type)
182+
ctx = StashOriginalResourceType(ctx, r.Type)
183+
184+
return ctx
142185
}
143186

144187
// Handle implements admission.Handler

reconcilers/webhook_test.go

Lines changed: 83 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import (
4040
"reconciler.io/runtime/reconcilers"
4141
rtesting "reconciler.io/runtime/testing"
4242
"reconciler.io/runtime/tracker"
43+
"reconciler.io/runtime/validation"
4344
"sigs.k8s.io/controller-runtime/pkg/client"
4445
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
4546
)
@@ -351,7 +352,7 @@ func TestAdmissionWebhookAdapter(t *testing.T) {
351352
AdmissionResponse: response.DieRelease(),
352353
},
353354
Prepare: func(t *testing.T, ctx context.Context, tc *rtesting.AdmissionWebhookTestCase) (context.Context, error) {
354-
key := "test-key"
355+
key := reconcilers.StashKey("test-key")
355356
value := "test-value"
356357
ctx = context.WithValue(ctx, key, value)
357358

@@ -375,13 +376,92 @@ func TestAdmissionWebhookAdapter(t *testing.T) {
375376
return ctx, nil
376377
},
377378
},
379+
"invalid nested reconciler": {
380+
Skip: true,
381+
Request: &admission.Request{
382+
AdmissionRequest: request.
383+
Object(resource.DieReleaseRawExtension()).
384+
DieRelease(),
385+
},
386+
ExpectedResponse: admission.Response{
387+
AdmissionResponse: response.DieRelease(),
388+
},
389+
Metadata: map[string]interface{}{
390+
"SubReconciler": func(t *testing.T, c reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource] {
391+
return &reconcilers.SyncReconciler[*resources.TestResource]{
392+
// Sync: func(ctx context.Context, resource *resources.TestResource) error {
393+
// return nil
394+
// },
395+
}
396+
},
397+
},
398+
},
378399
}
379400

380-
wts.Run(t, scheme, func(t *testing.T, wtc *rtesting.AdmissionWebhookTestCase, c reconcilers.Config) *admission.Webhook {
401+
wts.RunWithContext(t, scheme, func(t *testing.T, ctx context.Context, wtc *rtesting.AdmissionWebhookTestCase, c reconcilers.Config) (*admission.Webhook, error) {
381402
return (&reconcilers.AdmissionWebhookAdapter[*resources.TestResource]{
382403
Type: &resources.TestResource{},
383404
Reconciler: wtc.Metadata["SubReconciler"].(func(*testing.T, reconcilers.Config) reconcilers.SubReconciler[*resources.TestResource])(t, c),
384405
Config: c,
385-
}).Build()
406+
}).BuildWithContext(ctx)
386407
})
387408
}
409+
410+
func TestAdmissionWebhookAdapter_Validate(t *testing.T) {
411+
tests := []struct {
412+
name string
413+
webhook *reconcilers.AdmissionWebhookAdapter[*resources.TestResource]
414+
validateNested bool
415+
shouldErr string
416+
}{
417+
{
418+
name: "valid",
419+
webhook: &reconcilers.AdmissionWebhookAdapter[*resources.TestResource]{
420+
Reconciler: reconcilers.Sequence[*resources.TestResource]{},
421+
},
422+
},
423+
{
424+
name: "missing reconciler",
425+
webhook: &reconcilers.AdmissionWebhookAdapter[*resources.TestResource]{
426+
Name: "missing reconciler",
427+
},
428+
shouldErr: `AdmissionWebhookAdapter "missing reconciler" must define Reconciler`,
429+
},
430+
{
431+
name: "valid reconciler",
432+
webhook: &reconcilers.AdmissionWebhookAdapter[*resources.TestResource]{
433+
Reconciler: &reconcilers.SyncReconciler[*resources.TestResource]{
434+
Sync: func(ctx context.Context, resource *resources.TestResource) error {
435+
return nil
436+
},
437+
},
438+
},
439+
validateNested: true,
440+
},
441+
{
442+
name: "invalid reconciler",
443+
webhook: &reconcilers.AdmissionWebhookAdapter[*resources.TestResource]{
444+
Reconciler: &reconcilers.SyncReconciler[*resources.TestResource]{
445+
// Sync: func(ctx context.Context, resource *resources.TestResource) error {
446+
// return nil
447+
// },
448+
},
449+
},
450+
validateNested: true,
451+
shouldErr: `AdmissionWebhookAdapter "TestResourceAdmissionWebhookAdapter" must have a valid Reconciler: SyncReconciler "SyncReconciler" must implement Sync or SyncWithResult`,
452+
},
453+
}
454+
455+
for _, c := range tests {
456+
t.Run(c.name, func(t *testing.T) {
457+
ctx := context.TODO()
458+
if c.validateNested {
459+
ctx = validation.WithRecursive(ctx)
460+
}
461+
err := c.webhook.Validate(ctx)
462+
if (err != nil) != (c.shouldErr != "") || (c.shouldErr != "" && c.shouldErr != err.Error()) {
463+
t.Errorf("validate() error = %q, shouldErr %q", err, c.shouldErr)
464+
}
465+
})
466+
}
467+
}

testing/webhook.go

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import (
2828
"k8s.io/apimachinery/pkg/runtime"
2929
"reconciler.io/runtime/reconcilers"
3030
rtime "reconciler.io/runtime/time"
31+
"reconciler.io/runtime/validation"
3132
"sigs.k8s.io/controller-runtime/pkg/client"
3233
"sigs.k8s.io/controller-runtime/pkg/client/fake"
3334
"sigs.k8s.io/controller-runtime/pkg/webhook/admission"
@@ -122,7 +123,7 @@ type AdmissionWebhookTestCase struct {
122123
// test case. Test cases are executed in random order.
123124
type AdmissionWebhookTests map[string]AdmissionWebhookTestCase
124125

125-
// Run executes the test cases.
126+
// Deprecated use RunWithContext instead
126127
func (wt AdmissionWebhookTests) Run(t *testing.T, scheme *runtime.Scheme, factory AdmissionWebhookFactory) {
127128
t.Helper()
128129
wts := AdmissionWebhookTestSuite{}
@@ -133,12 +134,31 @@ func (wt AdmissionWebhookTests) Run(t *testing.T, scheme *runtime.Scheme, factor
133134
wts.Run(t, scheme, factory)
134135
}
135136

137+
// Run executes the test cases.
138+
func (wt AdmissionWebhookTests) RunWithContext(t *testing.T, scheme *runtime.Scheme, factory AdmissionWebhookFactoryWithContext) {
139+
t.Helper()
140+
wts := AdmissionWebhookTestSuite{}
141+
for name, wtc := range wt {
142+
wtc.Name = name
143+
wts = append(wts, wtc)
144+
}
145+
wts.RunWithContext(t, scheme, factory)
146+
}
147+
136148
// AdmissionWebhookTestSuite represents a list of webhook test cases. The test cases are
137149
// executed in order.
138150
type AdmissionWebhookTestSuite []AdmissionWebhookTestCase
139151

140-
// Run executes the test case.
152+
// Deprecated use RunWithContext instead
141153
func (tc *AdmissionWebhookTestCase) Run(t *testing.T, scheme *runtime.Scheme, factory AdmissionWebhookFactory) {
154+
tc.RunWithContext(t, scheme, func(t *testing.T, ctx context.Context, wtc *AdmissionWebhookTestCase, c reconcilers.Config) (*admission.Webhook, error) {
155+
webhook := factory(t, wtc, c)
156+
return webhook, nil
157+
})
158+
}
159+
160+
// Run executes the test case.
161+
func (tc *AdmissionWebhookTestCase) RunWithContext(t *testing.T, scheme *runtime.Scheme, factory AdmissionWebhookFactoryWithContext) {
142162
t.Helper()
143163
if tc.Skip {
144164
t.SkipNow()
@@ -155,6 +175,7 @@ func (tc *AdmissionWebhookTestCase) Run(t *testing.T, scheme *runtime.Scheme, fa
155175
ctx, cancel = context.WithDeadline(ctx, deadline)
156176
defer cancel()
157177
}
178+
ctx = validation.WithRecursive(ctx)
158179

159180
if tc.Metadata == nil {
160181
tc.Metadata = map[string]interface{}{}
@@ -199,7 +220,10 @@ func (tc *AdmissionWebhookTestCase) Run(t *testing.T, scheme *runtime.Scheme, fa
199220
}
200221

201222
c := expectConfig.Config()
202-
r := factory(t, tc, c)
223+
r, err := factory(t, ctx, tc, c)
224+
if err != nil {
225+
t.Fatalf("Webhook factory failed: %s", err)
226+
}
203227

204228
// Run the Reconcile we're testing.
205229
response := func() admission.Response {
@@ -239,8 +263,16 @@ func (tc *AdmissionWebhookTestCase) Run(t *testing.T, scheme *runtime.Scheme, fa
239263
expectConfig.AssertExpectations(t)
240264
}
241265

242-
// Run executes the webhook test suite.
266+
// Deprecated use RunWithContext instead
243267
func (ts AdmissionWebhookTestSuite) Run(t *testing.T, scheme *runtime.Scheme, factory AdmissionWebhookFactory) {
268+
ts.RunWithContext(t, scheme, func(t *testing.T, ctx context.Context, wtc *AdmissionWebhookTestCase, c reconcilers.Config) (*admission.Webhook, error) {
269+
webhook := factory(t, wtc, c)
270+
return webhook, nil
271+
})
272+
}
273+
274+
// Run executes the webhook test suite.
275+
func (ts AdmissionWebhookTestSuite) RunWithContext(t *testing.T, scheme *runtime.Scheme, factory AdmissionWebhookFactoryWithContext) {
244276
t.Helper()
245277
focused := AdmissionWebhookTestSuite{}
246278
for _, test := range ts {
@@ -256,7 +288,7 @@ func (ts AdmissionWebhookTestSuite) Run(t *testing.T, scheme *runtime.Scheme, fa
256288
for _, test := range testsToExecute {
257289
t.Run(test.Name, func(t *testing.T) {
258290
t.Helper()
259-
test.Run(t, scheme, factory)
291+
test.RunWithContext(t, scheme, factory)
260292
})
261293
}
262294
if len(focused) > 0 {
@@ -265,3 +297,4 @@ func (ts AdmissionWebhookTestSuite) Run(t *testing.T, scheme *runtime.Scheme, fa
265297
}
266298

267299
type AdmissionWebhookFactory func(t *testing.T, wtc *AdmissionWebhookTestCase, c reconcilers.Config) *admission.Webhook
300+
type AdmissionWebhookFactoryWithContext func(t *testing.T, ctx context.Context, wtc *AdmissionWebhookTestCase, c reconcilers.Config) (*admission.Webhook, error)

0 commit comments

Comments
 (0)