From e83723ce2ca13c3899b5e13d1d449ec0c7ef1e38 Mon Sep 17 00:00:00 2001 From: Lalit Chauhan Date: Mon, 29 Dec 2025 22:15:32 +0000 Subject: [PATCH] Add ignoreConversionErrors option to validation testing This commit adds an 'IgnoreObjectConversionErrors' option to the validation testing framework in 'k8s.io/apimachinery' and exposes it via 'pkg/api/testing'. This is useful for fuzzing tests where we might want to skip object versions that cannot be converted from the internal version (e.g. due to missing fields or incompatible types in older versions) but still want to test validation for the versions that *can* be converted. The 'autoscaling' group versions are added to 'TestVersionedValidationByFuzzing' with this option enabled. --- pkg/api/testing/validation.go | 13 ++++++++-- pkg/api/testing/validation_test.go | 5 ++++ .../pkg/runtime/testing/validation.go | 25 +++++++++++-------- 3 files changed, 31 insertions(+), 12 deletions(-) diff --git a/pkg/api/testing/validation.go b/pkg/api/testing/validation.go index e11ca144808..49f4eab260b 100644 --- a/pkg/api/testing/validation.go +++ b/pkg/api/testing/validation.go @@ -89,7 +89,7 @@ func VerifyVersionedValidationEquivalence(t *testing.T, obj, old runtime.Object, return } if old == nil { - runtimetest.RunValidationForEachVersion(t, legacyscheme.Scheme, []string{}, internalObj, accumulate, opts.SubResources...) + runtimetest.RunValidationForEachVersion(t, legacyscheme.Scheme, []string{}, internalObj, accumulate, opts.IgnoreObjectConversionErrors, opts.SubResources...) } else { // Convert old versioned object to internal format before validation. // runtimetest.RunUpdateValidationForEachVersion requires unversioned (internal) objects as input. @@ -100,7 +100,7 @@ func VerifyVersionedValidationEquivalence(t *testing.T, obj, old runtime.Object, if internalOld == nil { return } - runtimetest.RunUpdateValidationForEachVersion(t, legacyscheme.Scheme, []string{}, internalObj, internalOld, accumulate, opts.SubResources...) + runtimetest.RunUpdateValidationForEachVersion(t, legacyscheme.Scheme, []string{}, internalObj, internalOld, accumulate, opts.IgnoreObjectConversionErrors, opts.SubResources...) } // Make a copy so we can modify it. @@ -201,6 +201,9 @@ type validationOption struct { SubResources []string // NormalizationRules are the rules to apply to field paths before comparison. NormalizationRules []field.NormalizationRule + + // IgnoreObjectConversions skips the tests if the conversion between object fails. + IgnoreObjectConversionErrors bool } func WithSubResources(subResources ...string) ValidationTestConfig { @@ -215,6 +218,12 @@ func WithNormalizationRules(rules ...field.NormalizationRule) ValidationTestConf } } +func WithIgnoreObjectConversionErrors() ValidationTestConfig { + return func(o *validationOption) { + o.IgnoreObjectConversionErrors = true + } +} + // VerifyValidationEquivalence provides a helper for testing the migration from // hand-written imperative validation to declarative validation. It ensures that // the validation logic remains consistent before and after the feature is enabled. diff --git a/pkg/api/testing/validation_test.go b/pkg/api/testing/validation_test.go index de1579b41de..d79f13fff79 100644 --- a/pkg/api/testing/validation_test.go +++ b/pkg/api/testing/validation_test.go @@ -47,6 +47,10 @@ func TestVersionedValidationByFuzzing(t *testing.T) { {Group: "node.k8s.io", Version: "v1beta1"}, {Group: "node.k8s.io", Version: "v1"}, {Group: "node.k8s.io", Version: "v1alpha1"}, + {Group: "autoscaling", Version: "v1"}, + {Group: "autoscaling", Version: "v1beta1"}, + {Group: "autoscaling", Version: "v1beta2"}, + {Group: "autoscaling", Version: "v2"}, } fuzzIters := *roundtrip.FuzzIters / 10 // TODO: Find a better way to manage test running time @@ -70,6 +74,7 @@ func TestVersionedValidationByFuzzing(t *testing.T) { allRules := append([]field.NormalizationRule{}, resourcevalidation.ResourceNormalizationRules...) allRules = append(allRules, nodevalidation.NodeNormalizationRules...) opts = append(opts, WithNormalizationRules(allRules...)) + opts = append(opts, WithIgnoreObjectConversionErrors()) VerifyVersionedValidationEquivalence(t, obj, nil, opts...) diff --git a/staging/src/k8s.io/apimachinery/pkg/runtime/testing/validation.go b/staging/src/k8s.io/apimachinery/pkg/runtime/testing/validation.go index 8375d6ecfa1..2520cc7f4a4 100644 --- a/staging/src/k8s.io/apimachinery/pkg/runtime/testing/validation.go +++ b/staging/src/k8s.io/apimachinery/pkg/runtime/testing/validation.go @@ -37,16 +37,16 @@ type VersionValidationRunner func(t *testing.T, gv string, versionValidationErro // errs = append(errs, versionValidationErrors...) // generated declarative validation // // Validate that the errors are what was expected for this test case. // }) -func RunValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options []string, unversioned runtime.Object, fn VersionValidationRunner, subresources ...string) { - runValidation(t, scheme, options, unversioned, fn, subresources...) +func RunValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options []string, unversioned runtime.Object, fn VersionValidationRunner, ignoreConversionErrors bool, subresources ...string) { + runValidation(t, scheme, options, unversioned, fn, ignoreConversionErrors, subresources...) } // RunUpdateValidationForEachVersion is like RunValidationForEachVersion but for update validation. -func RunUpdateValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options []string, unversioned, unversionedOld runtime.Object, fn VersionValidationRunner, subresources ...string) { - runUpdateValidation(t, scheme, options, unversioned, unversionedOld, fn, subresources...) +func RunUpdateValidationForEachVersion(t *testing.T, scheme *runtime.Scheme, options []string, unversioned, unversionedOld runtime.Object, fn VersionValidationRunner, ignoreConversionErrors bool, subresources ...string) { + runUpdateValidation(t, scheme, options, unversioned, unversionedOld, fn, ignoreConversionErrors, subresources...) } -func runValidation(t *testing.T, scheme *runtime.Scheme, options []string, unversioned runtime.Object, fn VersionValidationRunner, subresources ...string) { +func runValidation(t *testing.T, scheme *runtime.Scheme, options []string, unversioned runtime.Object, fn VersionValidationRunner, ignoreConversionErrors bool, subresources ...string) { unversionedGVKs, _, err := scheme.ObjectKinds(unversioned) if err != nil { t.Fatal(err) @@ -66,7 +66,9 @@ func runValidation(t *testing.T, scheme *runtime.Scheme, options []string, unver t.Fatal(err) } err = scheme.Convert(unversioned, versioned, nil) - if err != nil { + if ignoreConversionErrors && err != nil { + t.Skipf("Failed to convert object from internal type to %s: %v", gvk.Version, err) + } else if err != nil { t.Fatal(err) } fn(t, gv.String(), scheme.Validate(context.Background(), options, versioned, subresources...)) @@ -76,7 +78,7 @@ func runValidation(t *testing.T, scheme *runtime.Scheme, options []string, unver } } -func runUpdateValidation(t *testing.T, scheme *runtime.Scheme, options []string, unversionedNew, unversionedOld runtime.Object, fn VersionValidationRunner, subresources ...string) { +func runUpdateValidation(t *testing.T, scheme *runtime.Scheme, options []string, unversionedNew, unversionedOld runtime.Object, fn VersionValidationRunner, ignoreConversionErrors bool, subresources ...string) { unversionedGVKs, _, err := scheme.ObjectKinds(unversionedNew) if err != nil { t.Fatal(err) @@ -96,10 +98,11 @@ func runUpdateValidation(t *testing.T, scheme *runtime.Scheme, options []string, t.Fatal(err) } err = scheme.Convert(unversionedNew, versionedNew, nil) - if err != nil { + if err != nil && ignoreConversionErrors { + t.Skipf("Failed to convert object from internal type to %s: %v", gvk.Version, err) + } else if err != nil { t.Fatal(err) } - var versionedOld runtime.Object if unversionedOld != nil { versionedOld, err = scheme.New(gvk) @@ -108,7 +111,9 @@ func runUpdateValidation(t *testing.T, scheme *runtime.Scheme, options []string, } err = scheme.Convert(unversionedOld, versionedOld, nil) - if err != nil { + if err != nil && ignoreConversionErrors { + t.Skipf("Failed to convert object from internal type to %s: %v", gvk.Version, err) + } else if err != nil { t.Fatal(err) } }