From 64140b838542ca7470ced477adc62cbd2726e093 Mon Sep 17 00:00:00 2001 From: yongruilin Date: Thu, 29 Jan 2026 10:29:30 +0000 Subject: [PATCH 1/2] fuzz internal objects in versioned validation tests Fuzzing must be performed on the internal version of objects because custom fuzzing functions are typically registered for internal types. This ensures that all fields are properly initialized with random values before being converted to various API versions for validation. Move fuzzing logic into VerifyVersionedValidationEquivalence via a new WithFuzzer option. This also fixes a panic that occurred when attempting to fuzz types without an internal version (e.g., APIGroupList) by adding a nil check after internal conversion. --- pkg/api/testing/validation.go | 21 +++++++++++++++++++++ pkg/api/testing/validation_test.go | 11 ++++++----- 2 files changed, 27 insertions(+), 5 deletions(-) diff --git a/pkg/api/testing/validation.go b/pkg/api/testing/validation.go index aab847ece4b..7bdd19fc64e 100644 --- a/pkg/api/testing/validation.go +++ b/pkg/api/testing/validation.go @@ -31,6 +31,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. @@ -88,6 +89,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 { @@ -100,6 +108,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...) } @@ -205,6 +217,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 { @@ -225,6 +240,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..62bee0bcef5 100644 --- a/pkg/api/testing/validation_test.go +++ b/pkg/api/testing/validation_test.go @@ -77,7 +77,6 @@ func TestVersionedValidationByFuzzing(t *testing.T) { if err != nil { t.Fatalf("could not create a %v: %s", kind, err) } - f.Fill(obj) var opts []ValidationTestConfig // TODO(API group level configuration): Consider configuring normalization rules at the @@ -85,9 +84,11 @@ 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)) + + // Scale subresource needs to be specified explicitly. + if gv.Group == "autoscaling" && kind == "Scale" { + opts = append(opts, WithSubResources("scale")) } VerifyVersionedValidationEquivalence(t, obj, nil, opts...) @@ -96,7 +97,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...) } }) From fbefdc816fc1fac4eaed95b6ce62b9c81a4cbbdc Mon Sep 17 00:00:00 2001 From: yongruilin Date: Thu, 29 Jan 2026 10:29:30 +0000 Subject: [PATCH 2/2] refactor subresource handling in versioned validation fuzz tests Refactor TestVersionedValidationByFuzzing to use a declarative map, subresourceOnly, for resources that must be validated via a specific subresource path (like autoscaling/Scale). GVKs not in this map default to root-level validation (""), which is sufficient for resources that share validation logic between their root and subresources. This replaces the previous ad-hoc special-casing with a cleaner, extensible mapping. --- pkg/api/testing/validation_test.go | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/pkg/api/testing/validation_test.go b/pkg/api/testing/validation_test.go index 62bee0bcef5..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) @@ -78,6 +90,11 @@ func TestVersionedValidationByFuzzing(t *testing.T) { t.Fatalf("could not create a %v: %s", kind, err) } + subresource := "" + if specific, ok := subresourceOnly[gvk]; ok { + subresource = specific + } + var opts []ValidationTestConfig // TODO(API group level configuration): Consider configuring normalization rules at the // API group level to avoid potential collisions when multiple rule sets are combined. @@ -85,10 +102,8 @@ func TestVersionedValidationByFuzzing(t *testing.T) { allRules := append([]field.NormalizationRule{}, resourcevalidation.ResourceNormalizationRules...) allRules = append(allRules, nodevalidation.NodeNormalizationRules...) opts = append(opts, WithNormalizationRules(allRules...), WithFuzzer(f)) - - // Scale subresource needs to be specified explicitly. - if gv.Group == "autoscaling" && kind == "Scale" { - opts = append(opts, WithSubResources("scale")) + if subresource != "" { + opts = append(opts, WithSubResources(subresource)) } VerifyVersionedValidationEquivalence(t, obj, nil, opts...)