Skip to content

Commit b518d9b

Browse files
committed
Adds a client that enables strict field validation for all requests
1 parent 2c61cfe commit b518d9b

File tree

3 files changed

+289
-0
lines changed

3 files changed

+289
-0
lines changed

pkg/client/fieldvalidation.go

Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
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+
17+
package client
18+
19+
import (
20+
"context"
21+
22+
"k8s.io/apimachinery/pkg/api/meta"
23+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
24+
"k8s.io/apimachinery/pkg/runtime"
25+
"k8s.io/apimachinery/pkg/runtime/schema"
26+
)
27+
28+
// WithStrictFieldValidation wraps a Client and configures strict field
29+
// validation, by default, for all write requests from this client. Users
30+
// can override the field validation for individual requests.
31+
func WithStrictFieldValidation(c Client) Client {
32+
return &clientWithFieldValidation{
33+
validation: metav1.FieldValidationStrict,
34+
c: c,
35+
Reader: c,
36+
}
37+
}
38+
39+
type clientWithFieldValidation struct {
40+
validation string
41+
c Client
42+
Reader
43+
}
44+
45+
func (f *clientWithFieldValidation) Create(ctx context.Context, obj Object, opts ...CreateOption) error {
46+
return f.c.Create(ctx, obj, append([]CreateOption{FieldValidation(f.validation)}, opts...)...)
47+
}
48+
49+
func (f *clientWithFieldValidation) Update(ctx context.Context, obj Object, opts ...UpdateOption) error {
50+
return f.c.Update(ctx, obj, append([]UpdateOption{FieldValidation(f.validation)}, opts...)...)
51+
}
52+
53+
func (f *clientWithFieldValidation) Patch(ctx context.Context, obj Object, patch Patch, opts ...PatchOption) error {
54+
return f.c.Patch(ctx, obj, patch, append([]PatchOption{FieldValidation(f.validation)}, opts...)...)
55+
}
56+
57+
func (f *clientWithFieldValidation) Delete(ctx context.Context, obj Object, opts ...DeleteOption) error {
58+
return f.c.Delete(ctx, obj, opts...)
59+
}
60+
61+
func (f *clientWithFieldValidation) DeleteAllOf(ctx context.Context, obj Object, opts ...DeleteAllOfOption) error {
62+
return f.c.DeleteAllOf(ctx, obj, opts...)
63+
}
64+
65+
func (f *clientWithFieldValidation) Scheme() *runtime.Scheme { return f.c.Scheme() }
66+
func (f *clientWithFieldValidation) RESTMapper() meta.RESTMapper { return f.c.RESTMapper() }
67+
func (f *clientWithFieldValidation) GroupVersionKindFor(obj runtime.Object) (schema.GroupVersionKind, error) {
68+
return f.c.GroupVersionKindFor(obj)
69+
}
70+
71+
func (f *clientWithFieldValidation) IsObjectNamespaced(obj runtime.Object) (bool, error) {
72+
return f.c.IsObjectNamespaced(obj)
73+
}
74+
75+
func (f *clientWithFieldValidation) Status() StatusWriter {
76+
return &subresourceClientWithFieldValidation{
77+
validation: f.validation,
78+
subresourceWriter: f.c.Status(),
79+
}
80+
}
81+
82+
func (f *clientWithFieldValidation) SubResource(subresource string) SubResourceClient {
83+
c := f.c.SubResource(subresource)
84+
return &subresourceClientWithFieldValidation{
85+
validation: f.validation,
86+
subresourceWriter: c,
87+
SubResourceReader: c,
88+
}
89+
}
90+
91+
type subresourceClientWithFieldValidation struct {
92+
validation string
93+
subresourceWriter SubResourceWriter
94+
SubResourceReader
95+
}
96+
97+
func (f *subresourceClientWithFieldValidation) Create(ctx context.Context, obj Object, subresource Object, opts ...SubResourceCreateOption) error {
98+
return f.subresourceWriter.Create(ctx, obj, subresource, append([]SubResourceCreateOption{FieldValidation(f.validation)}, opts...)...)
99+
}
100+
101+
func (f *subresourceClientWithFieldValidation) Update(ctx context.Context, obj Object, opts ...SubResourceUpdateOption) error {
102+
return f.subresourceWriter.Update(ctx, obj, append([]SubResourceUpdateOption{FieldValidation(f.validation)}, opts...)...)
103+
}
104+
105+
func (f *subresourceClientWithFieldValidation) Patch(ctx context.Context, obj Object, patch Patch, opts ...SubResourcePatchOption) error {
106+
return f.subresourceWriter.Patch(ctx, obj, patch, append([]SubResourcePatchOption{FieldValidation(f.validation)}, opts...)...)
107+
}

pkg/client/fieldvalidation_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
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+
17+
package client_test
18+
19+
import (
20+
"context"
21+
"testing"
22+
23+
corev1 "k8s.io/api/core/v1"
24+
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
25+
"sigs.k8s.io/controller-runtime/pkg/client"
26+
"sigs.k8s.io/controller-runtime/pkg/client/fake"
27+
"sigs.k8s.io/controller-runtime/pkg/client/interceptor"
28+
)
29+
30+
func TestWithStrictFieldValidation(t *testing.T) {
31+
calls := 0
32+
fakeClient := testFieldValidationClient(t, metav1.FieldValidationStrict, func() { calls++ })
33+
wrappedClient := client.WithStrictFieldValidation(fakeClient)
34+
35+
ctx := context.Background()
36+
dummyObj := &corev1.Namespace{}
37+
38+
_ = wrappedClient.Create(ctx, dummyObj)
39+
_ = wrappedClient.Update(ctx, dummyObj)
40+
_ = wrappedClient.Patch(ctx, dummyObj, nil)
41+
_ = wrappedClient.Status().Create(ctx, dummyObj, dummyObj)
42+
_ = wrappedClient.Status().Update(ctx, dummyObj)
43+
_ = wrappedClient.Status().Patch(ctx, dummyObj, nil)
44+
_ = wrappedClient.SubResource("some-subresource").Create(ctx, dummyObj, dummyObj)
45+
_ = wrappedClient.SubResource("some-subresource").Update(ctx, dummyObj)
46+
_ = wrappedClient.SubResource("some-subresource").Patch(ctx, dummyObj, nil)
47+
48+
if expectedCalls := 9; calls != expectedCalls {
49+
t.Fatalf("wrong number of calls to assertions: expected=%d; got=%d", expectedCalls, calls)
50+
}
51+
}
52+
53+
func TestWithStrictFieldValidationOverridden(t *testing.T) {
54+
calls := 0
55+
56+
fakeClient := testFieldValidationClient(t, metav1.FieldValidationWarn, func() { calls++ })
57+
wrappedClient := client.WithStrictFieldValidation(fakeClient)
58+
59+
ctx := context.Background()
60+
dummyObj := &corev1.Namespace{}
61+
62+
_ = wrappedClient.Create(ctx, dummyObj, client.FieldValidation(metav1.FieldValidationWarn))
63+
_ = wrappedClient.Update(ctx, dummyObj, client.FieldValidation(metav1.FieldValidationWarn))
64+
_ = wrappedClient.Patch(ctx, dummyObj, nil, client.FieldValidation(metav1.FieldValidationWarn))
65+
_ = wrappedClient.Status().Create(ctx, dummyObj, dummyObj, client.FieldValidation(metav1.FieldValidationWarn))
66+
_ = wrappedClient.Status().Update(ctx, dummyObj, client.FieldValidation(metav1.FieldValidationWarn))
67+
_ = wrappedClient.Status().Patch(ctx, dummyObj, nil, client.FieldValidation(metav1.FieldValidationWarn))
68+
_ = wrappedClient.SubResource("some-subresource").Create(ctx, dummyObj, dummyObj, client.FieldValidation(metav1.FieldValidationWarn))
69+
_ = wrappedClient.SubResource("some-subresource").Update(ctx, dummyObj, client.FieldValidation(metav1.FieldValidationWarn))
70+
_ = wrappedClient.SubResource("some-subresource").Patch(ctx, dummyObj, nil, client.FieldValidation(metav1.FieldValidationWarn))
71+
72+
if expectedCalls := 9; calls != expectedCalls {
73+
t.Fatalf("wrong number of calls to assertions: expected=%d; got=%d", expectedCalls, calls)
74+
}
75+
}
76+
77+
// testFieldValidationClient is a helper function that checks if calls have the expected field validation,
78+
// and calls the callback function on each intercepted call.
79+
func testFieldValidationClient(t *testing.T, expectedFieldValidation string, callback func()) client.Client {
80+
// TODO: we could use the dummyClient in interceptor pkg if we move it to an internal pkg
81+
return fake.NewClientBuilder().WithInterceptorFuncs(interceptor.Funcs{
82+
Create: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.CreateOption) error {
83+
callback()
84+
out := &client.CreateOptions{}
85+
for _, f := range opts {
86+
f.ApplyToCreate(out)
87+
}
88+
if got := out.FieldValidation; expectedFieldValidation != got {
89+
t.Fatalf("wrong field validation: expected=%q; got=%q", expectedFieldValidation, got)
90+
}
91+
return nil
92+
},
93+
Update: func(ctx context.Context, c client.WithWatch, obj client.Object, opts ...client.UpdateOption) error {
94+
callback()
95+
out := &client.UpdateOptions{}
96+
for _, f := range opts {
97+
f.ApplyToUpdate(out)
98+
}
99+
if got := out.FieldValidation; expectedFieldValidation != got {
100+
t.Fatalf("wrong field validation: expected=%q; got=%q", expectedFieldValidation, got)
101+
}
102+
return nil
103+
},
104+
Patch: func(ctx context.Context, c client.WithWatch, obj client.Object, patch client.Patch, opts ...client.PatchOption) error {
105+
callback()
106+
out := &client.PatchOptions{}
107+
for _, f := range opts {
108+
f.ApplyToPatch(out)
109+
}
110+
if got := out.FieldValidation; expectedFieldValidation != got {
111+
t.Fatalf("wrong field validation: expected=%q; got=%q", expectedFieldValidation, got)
112+
}
113+
return nil
114+
},
115+
SubResourceCreate: func(ctx context.Context, c client.Client, subResourceName string, obj client.Object, subResource client.Object, opts ...client.SubResourceCreateOption) error {
116+
callback()
117+
out := &client.SubResourceCreateOptions{}
118+
for _, f := range opts {
119+
f.ApplyToSubResourceCreate(out)
120+
}
121+
if got := out.FieldValidation; expectedFieldValidation != got {
122+
t.Fatalf("wrong field validation: expected=%q; got=%q", expectedFieldValidation, got)
123+
}
124+
return nil
125+
},
126+
SubResourceUpdate: func(ctx context.Context, c client.Client, subResourceName string, obj client.Object, opts ...client.SubResourceUpdateOption) error {
127+
callback()
128+
out := &client.SubResourceUpdateOptions{}
129+
for _, f := range opts {
130+
f.ApplyToSubResourceUpdate(out)
131+
}
132+
if got := out.FieldValidation; expectedFieldValidation != got {
133+
t.Fatalf("wrong field validation: expected=%q; got=%q", expectedFieldValidation, got)
134+
}
135+
return nil
136+
},
137+
SubResourcePatch: func(ctx context.Context, c client.Client, subResourceName string, obj client.Object, patch client.Patch, opts ...client.SubResourcePatchOption) error {
138+
callback()
139+
out := &client.SubResourcePatchOptions{}
140+
for _, f := range opts {
141+
f.ApplyToSubResourcePatch(out)
142+
}
143+
if got := out.FieldValidation; expectedFieldValidation != got {
144+
t.Fatalf("wrong field validation: expected=%q; got=%q", expectedFieldValidation, got)
145+
}
146+
return nil
147+
},
148+
}).Build()
149+
}

pkg/client/options.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,39 @@ func (f FieldOwner) ApplyToSubResourceUpdate(opts *SubResourceUpdateOptions) {
169169
opts.FieldManager = string(f)
170170
}
171171

172+
// FieldValidation configures field validation for the given requests.
173+
type FieldValidation string
174+
175+
// ApplyToPatch applies this configuration to the given patch options.
176+
func (f FieldValidation) ApplyToPatch(opts *PatchOptions) {
177+
opts.FieldValidation = string(f)
178+
}
179+
180+
// ApplyToCreate applies this configuration to the given create options.
181+
func (f FieldValidation) ApplyToCreate(opts *CreateOptions) {
182+
opts.FieldValidation = string(f)
183+
}
184+
185+
// ApplyToUpdate applies this configuration to the given update options.
186+
func (f FieldValidation) ApplyToUpdate(opts *UpdateOptions) {
187+
opts.FieldValidation = string(f)
188+
}
189+
190+
// ApplyToSubResourcePatch applies this configuration to the given patch options.
191+
func (f FieldValidation) ApplyToSubResourcePatch(opts *SubResourcePatchOptions) {
192+
opts.FieldValidation = string(f)
193+
}
194+
195+
// ApplyToSubResourceCreate applies this configuration to the given create options.
196+
func (f FieldValidation) ApplyToSubResourceCreate(opts *SubResourceCreateOptions) {
197+
opts.FieldValidation = string(f)
198+
}
199+
200+
// ApplyToSubResourceUpdate applies this configuration to the given update options.
201+
func (f FieldValidation) ApplyToSubResourceUpdate(opts *SubResourceUpdateOptions) {
202+
opts.FieldValidation = string(f)
203+
}
204+
172205
// }}}
173206

174207
// {{{ Create Options

0 commit comments

Comments
 (0)