ReplicationController: Add declarative validation test suite

Introduce a test suite that ensures declarative test cases
are fully tested and that validation errors are compared
with handwritten validation to ensure consistency.

Co-authored-by: Tim Hockin <thockin@google.com>
Co-authored-by: Aaron Prindle <aprindle@google.com>
Co-authored-by: Yongrui Lin <yongrlin@google.com>
This commit is contained in:
Joe Betz 2025-03-12 18:59:28 -04:00
parent 5a5ed81e1f
commit b5bc283808

View File

@ -0,0 +1,164 @@
/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package replicationcontroller
import (
"testing"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/util/validation/field"
genericapirequest "k8s.io/apiserver/pkg/endpoints/request"
utilfeature "k8s.io/apiserver/pkg/util/feature"
featuregatetesting "k8s.io/component-base/featuregate/testing"
podtest "k8s.io/kubernetes/pkg/api/pod/testing"
apitesting "k8s.io/kubernetes/pkg/api/testing"
api "k8s.io/kubernetes/pkg/apis/core"
"k8s.io/kubernetes/pkg/features"
)
func TestDeclarativeValidateForDeclarative(t *testing.T) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "",
APIVersion: "v1",
})
testCases := map[string]struct {
input api.ReplicationController
expectedErrs field.ErrorList
}{
"empty resource": {
input: mkValidReplicationController(),
},
// TODO: Add test cases
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
var declarativeTakeoverErrs field.ErrorList
var imperativeErrs field.ErrorList
for _, gateVal := range []bool{true, false} {
// We only need to test both gate enabled and disabled together, because
// 1) the DeclarativeValidationTakeover won't take effect if DeclarativeValidation is disabled.
// 2) the validation output, when only DeclarativeValidation is enabled, is the same as when both gates are disabled.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidation, gateVal)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationTakeover, gateVal)
errs := Strategy.Validate(ctx, &tc.input)
if gateVal {
declarativeTakeoverErrs = errs
} else {
imperativeErrs = errs
}
// The errOutputMatcher is used to verify the output matches the expected errors in test cases.
errOutputMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
if len(tc.expectedErrs) > 0 {
errOutputMatcher.Test(t, tc.expectedErrs, errs)
} else if len(errs) != 0 {
t.Errorf("expected no errors, but got: %v", errs)
}
}
// The equivalenceMatcher is used to verify the output errors from hand-written imperative validation
// are equivalent to the output errors when DeclarativeValidationTakeover is enabled.
equivalenceMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
equivalenceMatcher.Test(t, imperativeErrs, declarativeTakeoverErrs)
apitesting.VerifyVersionedValidationEquivalence(t, &tc.input, nil)
})
}
}
func TestValidateUpdateForDeclarative(t *testing.T) {
ctx := genericapirequest.WithRequestInfo(genericapirequest.NewDefaultContext(), &genericapirequest.RequestInfo{
APIGroup: "",
APIVersion: "v1",
})
testCases := map[string]struct {
old api.ReplicationController
update api.ReplicationController
expectedErrs field.ErrorList
}{
// TODO: Add test cases
}
for k, tc := range testCases {
t.Run(k, func(t *testing.T) {
tc.old.ObjectMeta.ResourceVersion = "1"
tc.update.ObjectMeta.ResourceVersion = "1"
var declarativeTakeoverErrs field.ErrorList
var imperativeErrs field.ErrorList
for _, gateVal := range []bool{true, false} {
// We only need to test both gate enabled and disabled together, because
// 1) the DeclarativeValidationTakeover won't take effect if DeclarativeValidation is disabled.
// 2) the validation output, when only DeclarativeValidation is enabled, is the same as when both gates are disabled.
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidation, gateVal)
featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.DeclarativeValidationTakeover, gateVal)
errs := Strategy.ValidateUpdate(ctx, &tc.update, &tc.old)
if gateVal {
declarativeTakeoverErrs = errs
} else {
imperativeErrs = errs
}
// The errOutputMatcher is used to verify the output matches the expected errors in test cases.
errOutputMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
if len(tc.expectedErrs) > 0 {
errOutputMatcher.Test(t, tc.expectedErrs, errs)
} else if len(errs) != 0 {
t.Errorf("expected no errors, but got: %v", errs)
}
}
// The equivalenceMatcher is used to verify the output errors from hand-written imperative validation
// are equivalent to the output errors when DeclarativeValidationTakeover is enabled.
equivalenceMatcher := field.ErrorMatcher{}.ByType().ByField().ByOrigin()
// TODO: remove this once ErrorMatcher has been extended to handle this form of deduplication.
dedupedImperativeErrs := field.ErrorList{}
for _, err := range imperativeErrs {
found := false
for _, existingErr := range dedupedImperativeErrs {
if equivalenceMatcher.Matches(existingErr, err) {
found = true
break
}
}
if !found {
dedupedImperativeErrs = append(dedupedImperativeErrs, err)
}
}
equivalenceMatcher.Test(t, dedupedImperativeErrs, declarativeTakeoverErrs)
apitesting.VerifyVersionedValidationEquivalence(t, &tc.update, &tc.old)
})
}
}
// Helper function for RC tests.
func mkValidReplicationController(tweaks ...func(rc *api.ReplicationController)) api.ReplicationController {
rc := api.ReplicationController{
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
Spec: api.ReplicationControllerSpec{
Replicas: 1,
Selector: map[string]string{"a": "b"},
Template: &api.PodTemplateSpec{
ObjectMeta: metav1.ObjectMeta{
Labels: map[string]string{"a": "b"},
},
Spec: podtest.MakePodSpec(),
},
},
}
for _, tweak := range tweaks {
tweak(&rc)
}
return rc
}