diff --git a/pkg/client/client.go b/pkg/client/client.go index 1a14fcc5e2..ad946daeaa 100644 --- a/pkg/client/client.go +++ b/pkg/client/client.go @@ -61,6 +61,16 @@ type Options struct { // This default can be overridden for a specific call by passing a [FieldOwner] option // to the method. FieldOwner string + + // FieldValidation sets the field validation strategy for all mutating operations performed by this client + // and subresource clients created from it. + // The exception are apply requests which are always strict, regardless of the FieldValidation setting. + // Available values for this option can be found in "k8s.io/apimachinery/pkg/apis/meta/v1" package and are: + // - FieldValidationIgnore + // - FieldValidationWarn + // - FieldValidationStrict + // For more details, see: https://kubernetes.io/docs/reference/using-api/api-concepts/#field-validation + FieldValidation string } // CacheOptions are options for creating a cache-backed client. @@ -111,6 +121,9 @@ func New(config *rest.Config, options Options) (c Client, err error) { if fo := options.FieldOwner; fo != "" { c = WithFieldOwner(c, fo) } + if fv := options.FieldValidation; fv != "" { + c = WithFieldValidation(c, FieldValidation(fv)) + } return c, err } diff --git a/pkg/client/client_test.go b/pkg/client/client_test.go index d52f043692..b2890c385d 100644 --- a/pkg/client/client_test.go +++ b/pkg/client/client_test.go @@ -43,6 +43,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apimachinery/pkg/types" + "k8s.io/apimachinery/pkg/util/rand" appsv1applyconfigurations "k8s.io/client-go/applyconfigurations/apps/v1" autoscaling1applyconfigurations "k8s.io/client-go/applyconfigurations/autoscaling/v1" corev1applyconfigurations "k8s.io/client-go/applyconfigurations/core/v1" @@ -378,6 +379,52 @@ U5wwSivyi7vmegHKmblOzNVKA5qPO8zWzqBC Expect(actual.ManagedFields).To(HaveLen(1)) Expect(actual.ManagedFields[0].Manager).To(Equal("test-owner")) }) + + Context("with the FieldValidation option", func() { + It("should log warnings with FieldValidation equal to Warn", func(ctx SpecContext) { + restCfg := rest.CopyConfig(cfg) + var testLog bytes.Buffer + restCfg.WarningHandler = rest.NewWarningWriter(&testLog, rest.WarningWriterOptions{}) + + warnClient, err := client.New(restCfg, client.Options{FieldValidation: metav1.FieldValidationWarn}) + Expect(err).NotTo(HaveOccurred()) + Expect(warnClient).NotTo(BeNil()) + + unstrContent, err := runtime.DefaultUnstructuredConverter.ToUnstructured( + corev1applyconfigurations.ConfigMap("test-cm-"+rand.String(3), "default"). + WithData(map[string]string{"foo": "bar"}), + ) + Expect(err).NotTo(HaveOccurred()) + unstrContent["additionalField"] = "test" + cm := &unstructured.Unstructured{Object: unstrContent} + + err = warnClient.Create(ctx, cm) + Expect(err).NotTo(HaveOccurred()) + Expect(testLog.String()).To(ContainSubstring(`Warning: unknown field "additionalField"`)) + + }) + It("should fail write operation if FieldValidation equals Strict", func(ctx SpecContext) { + restCfg := rest.CopyConfig(cfg) + var testLog bytes.Buffer + restCfg.WarningHandler = rest.NewWarningWriter(&testLog, rest.WarningWriterOptions{}) + strictClient, err := client.New(restCfg, client.Options{FieldValidation: metav1.FieldValidationStrict}) + Expect(err).NotTo(HaveOccurred()) + + unstrContent, err := runtime.DefaultUnstructuredConverter.ToUnstructured( + corev1applyconfigurations.ConfigMap("test-cm-"+rand.String(3), "default"). + WithData(map[string]string{"foo": "bar"}), + ) + Expect(err).NotTo(HaveOccurred()) + unstrContent["additionalField"] = "test" + cm := &unstructured.Unstructured{Object: unstrContent} + + err = strictClient.Create(ctx, cm) + Expect(err).To(HaveOccurred()) + Expect(err).To(MatchError(ContainSubstring("unknown field \"additionalField\""))) + Expect(err).To(MatchError(ContainSubstring("strict decoding error"))) + Expect(testLog.String()).To(BeEmpty()) + }) + }) }) Describe("Create", func() {