diff --git a/pkg/api/testing/validation.go b/pkg/api/testing/validation.go index 358966daa37..6bdd76c48b0 100644 --- a/pkg/api/testing/validation.go +++ b/pkg/api/testing/validation.go @@ -32,6 +32,7 @@ import ( utilfeature "k8s.io/apiserver/pkg/util/feature" featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/kubernetes/pkg/api/legacyscheme" + "sigs.k8s.io/randfill" ) // ValidateFunc is a function that runs validation. @@ -89,6 +90,13 @@ func VerifyVersionedValidationEquivalence(t *testing.T, obj, old runtime.Object, if internalObj == nil { return } + // We do fuzzing on the internal version of the object. + // This is because custom fuzzing function are only + // supported for internal objects. + // Fuzz the internal object if a fuzzer is provided. + if opts.Fuzzer != nil { + opts.Fuzzer.Fill(internalObj) + } if old == nil { runtimetest.RunValidationForEachVersion(t, legacyscheme.Scheme, []string{}, internalObj, accumulate, opts.IgnoreObjectConversionErrors, opts.SubResources...) } else { @@ -101,6 +109,10 @@ func VerifyVersionedValidationEquivalence(t *testing.T, obj, old runtime.Object, if internalOld == nil { return } + // Fuzz the internal old object if a fuzzer is provided. + if opts.Fuzzer != nil { + opts.Fuzzer.Fill(internalOld) + } runtimetest.RunUpdateValidationForEachVersion(t, legacyscheme.Scheme, []string{}, internalObj, internalOld, accumulate, opts.IgnoreObjectConversionErrors, opts.SubResources...) } @@ -206,6 +218,9 @@ type validationOption struct { // IgnoreObjectConversions skips the tests if the conversion from the internal object // to the versioned object fails. IgnoreObjectConversionErrors bool + + // Fuzzer is the fuzzer to use for generating test objects. + Fuzzer *randfill.Filler } func WithSubResources(subResources ...string) ValidationTestConfig { @@ -226,6 +241,12 @@ func WithIgnoreObjectConversionErrors() ValidationTestConfig { } } +func WithFuzzer(fuzzer *randfill.Filler) ValidationTestConfig { + return func(o *validationOption) { + o.Fuzzer = fuzzer + } +} + // 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 df8df8c8a78..2910717c73e 100644 --- a/pkg/api/testing/validation_test.go +++ b/pkg/api/testing/validation_test.go @@ -65,6 +65,18 @@ func TestVersionedValidationByFuzzing(t *testing.T) { {Group: "admissionregistration.k8s.io", Version: "v1alpha1"}, } + // subresourceOnly specifies the subresource path for types that can only be validated + // as subresources (e.g. autoscaling/Scale) and do not support root-level validation. + // For GVKs not in this map, the test defaults to fuzzing the root resource (""). + // Other resources with subresources (e.g. Pod status, exec) share validation logic with + // the root resource, so fuzzing the root is sufficient to verify validation equivalence. + subresourceOnly := map[schema.GroupVersionKind]string{ + {Group: "autoscaling", Version: "v1", Kind: "Scale"}: "scale", + {Group: "autoscaling", Version: "v1beta1", Kind: "Scale"}: "scale", + {Group: "autoscaling", Version: "v1beta2", Kind: "Scale"}: "scale", + {Group: "autoscaling", Version: "v2", Kind: "Scale"}: "scale", + } + fuzzIters := *roundtrip.FuzzIters / 10 // TODO: Find a better way to manage test running time f := fuzzer.FuzzerFor(FuzzerFuncs, rand.NewSource(rand.Int63()), legacyscheme.Codecs) @@ -77,7 +89,11 @@ func TestVersionedValidationByFuzzing(t *testing.T) { if err != nil { t.Fatalf("could not create a %v: %s", kind, err) } - f.Fill(obj) + + subresource := "" + if specific, ok := subresourceOnly[gvk]; ok { + subresource = specific + } var opts []ValidationTestConfig // TODO(API group level configuration): Consider configuring normalization rules at the @@ -85,9 +101,9 @@ func TestVersionedValidationByFuzzing(t *testing.T) { // This would allow each API group to register its own normalization rules independently. allRules := append([]field.NormalizationRule{}, resourcevalidation.ResourceNormalizationRules...) allRules = append(allRules, nodevalidation.NodeNormalizationRules...) - opts = append(opts, WithNormalizationRules(allRules...)) - if gv.Group == "autoscaling" { - opts = append(opts, WithIgnoreObjectConversionErrors()) + opts = append(opts, WithNormalizationRules(allRules...), WithFuzzer(f)) + if subresource != "" { + opts = append(opts, WithSubResources(subresource)) } VerifyVersionedValidationEquivalence(t, obj, nil, opts...) @@ -96,7 +112,7 @@ func TestVersionedValidationByFuzzing(t *testing.T) { if err != nil { t.Fatalf("could not create a %v: %s", kind, err) } - f.Fill(old) + VerifyVersionedValidationEquivalence(t, obj, old, opts...) } })