Skip to content

Commit 3cbfa0c

Browse files
authored
✨ Adds a client that enables strict field validation by default. (#2860)
* Adds fieldValidation field to create, patch, and update request options * Adds a client that enables strict field validation for all requests * fixup! Adds fieldValidation field to create, patch, and update request options Remove "+optional" tag. * fixup! Adds a client that enables strict field validation for all requests Construct client wrapper using validation as a parameter.
1 parent 89bb86e commit 3cbfa0c

File tree

3 files changed

+342
-0
lines changed

3 files changed

+342
-0
lines changed

pkg/client/fieldvalidation.go

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

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.WithFieldValidation(fakeClient, metav1.FieldValidationStrict)
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.WithFieldValidation(fakeClient, metav1.FieldValidationStrict)
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: 87 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
@@ -187,6 +220,24 @@ type CreateOptions struct {
187220
// this request. It must be set with server-side apply.
188221
FieldManager string
189222

223+
// fieldValidation instructs the server on how to handle
224+
// objects in the request (POST/PUT/PATCH) containing unknown
225+
// or duplicate fields. Valid values are:
226+
// - Ignore: This will ignore any unknown fields that are silently
227+
// dropped from the object, and will ignore all but the last duplicate
228+
// field that the decoder encounters. This is the default behavior
229+
// prior to v1.23.
230+
// - Warn: This will send a warning via the standard warning response
231+
// header for each unknown field that is dropped from the object, and
232+
// for each duplicate field that is encountered. The request will
233+
// still succeed if there are no other errors, and will only persist
234+
// the last of any duplicate fields. This is the default in v1.23+
235+
// - Strict: This will fail the request with a BadRequest error if
236+
// any unknown fields would be dropped from the object, or if any
237+
// duplicate fields are present. The error returned from the server
238+
// will contain all unknown and duplicate fields encountered.
239+
FieldValidation string
240+
190241
// Raw represents raw CreateOptions, as passed to the API server.
191242
Raw *metav1.CreateOptions
192243
}
@@ -679,6 +730,24 @@ type UpdateOptions struct {
679730
// this request. It must be set with server-side apply.
680731
FieldManager string
681732

733+
// fieldValidation instructs the server on how to handle
734+
// objects in the request (POST/PUT/PATCH) containing unknown
735+
// or duplicate fields. Valid values are:
736+
// - Ignore: This will ignore any unknown fields that are silently
737+
// dropped from the object, and will ignore all but the last duplicate
738+
// field that the decoder encounters. This is the default behavior
739+
// prior to v1.23.
740+
// - Warn: This will send a warning via the standard warning response
741+
// header for each unknown field that is dropped from the object, and
742+
// for each duplicate field that is encountered. The request will
743+
// still succeed if there are no other errors, and will only persist
744+
// the last of any duplicate fields. This is the default in v1.23+
745+
// - Strict: This will fail the request with a BadRequest error if
746+
// any unknown fields would be dropped from the object, or if any
747+
// duplicate fields are present. The error returned from the server
748+
// will contain all unknown and duplicate fields encountered.
749+
FieldValidation string
750+
682751
// Raw represents raw UpdateOptions, as passed to the API server.
683752
Raw *metav1.UpdateOptions
684753
}
@@ -745,6 +814,24 @@ type PatchOptions struct {
745814
// this request. It must be set with server-side apply.
746815
FieldManager string
747816

817+
// fieldValidation instructs the server on how to handle
818+
// objects in the request (POST/PUT/PATCH) containing unknown
819+
// or duplicate fields. Valid values are:
820+
// - Ignore: This will ignore any unknown fields that are silently
821+
// dropped from the object, and will ignore all but the last duplicate
822+
// field that the decoder encounters. This is the default behavior
823+
// prior to v1.23.
824+
// - Warn: This will send a warning via the standard warning response
825+
// header for each unknown field that is dropped from the object, and
826+
// for each duplicate field that is encountered. The request will
827+
// still succeed if there are no other errors, and will only persist
828+
// the last of any duplicate fields. This is the default in v1.23+
829+
// - Strict: This will fail the request with a BadRequest error if
830+
// any unknown fields would be dropped from the object, or if any
831+
// duplicate fields are present. The error returned from the server
832+
// will contain all unknown and duplicate fields encountered.
833+
FieldValidation string
834+
748835
// Raw represents raw PatchOptions, as passed to the API server.
749836
Raw *metav1.PatchOptions
750837
}

0 commit comments

Comments
 (0)