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.
This commit is contained in:
yongruilin
2026-01-29 10:29:30 +00:00
parent 6dfae1df46
commit 64140b8385
2 changed files with 27 additions and 5 deletions

View File

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

View File

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