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.
This commit is contained in:
Lalit Chauhan
2025-12-29 22:15:32 +00:00
parent 4c5746c0bc
commit e83723ce2c
3 changed files with 31 additions and 12 deletions

View File

@@ -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.

View File

@@ -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...)

View File

@@ -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)
}
}