Skip to content

Instantly share code, notes, and snippets.

@aaron-prindle
Created January 18, 2025 22:27
Show Gist options
  • Save aaron-prindle/b106b24f74770218b3e84005ddeb1bca to your computer and use it in GitHub Desktop.
Save aaron-prindle/b106b24f74770218b3e84005ddeb1bca to your computer and use it in GitHub Desktop.
Declarative Validation: Example of migration changes for pkg/apis/core/validation/validation_test.go
diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go
index 295f8425eab..8a9fdb68661 100644
--- a/pkg/apis/core/validation/validation_test.go
+++ b/pkg/apis/core/validation/validation_test.go
@@ -1,27 +1,32 @@
/*
-Copyright 2014 The Kubernetes Authors.
+ Copyright 2014 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
+ 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
+ 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.
+ 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 validation
import (
"bytes"
+ "context"
"fmt"
"math"
+ "math/rand"
"reflect"
+ "regexp"
"runtime"
+ "sort"
+ "strconv"
"strings"
"testing"
"time"
@@ -31,21 +36,31 @@ import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"google.golang.org/protobuf/proto"
-
v1 "k8s.io/api/core/v1"
+ "k8s.io/apimachinery/pkg/api/apitesting/fuzzer"
+ "k8s.io/apimachinery/pkg/api/apitesting/roundtrip"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+ k8sruntime "k8s.io/apimachinery/pkg/runtime"
+ "k8s.io/apimachinery/pkg/runtime/schema"
+ runtimetest "k8s.io/apimachinery/pkg/runtime/testing"
"k8s.io/apimachinery/pkg/util/intstr"
"k8s.io/apimachinery/pkg/util/sets"
"k8s.io/apimachinery/pkg/util/validation"
"k8s.io/apimachinery/pkg/util/validation/field"
"k8s.io/apimachinery/pkg/util/version"
+ "k8s.io/apiserver/pkg/endpoints/request"
+ "k8s.io/apiserver/pkg/registry/rest"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/component-base/featuregate"
featuregatetesting "k8s.io/component-base/featuregate/testing"
kubeletapis "k8s.io/kubelet/pkg/apis"
+ "k8s.io/kubernetes/pkg/api/legacyscheme"
podtest "k8s.io/kubernetes/pkg/api/pod/testing"
+ apitest "k8s.io/kubernetes/pkg/api/testing"
"k8s.io/kubernetes/pkg/apis/core"
+ _ "k8s.io/kubernetes/pkg/apis/core/install" // register types with scheme for GVK discovery
+ v1util "k8s.io/kubernetes/pkg/apis/core/v1"
"k8s.io/kubernetes/pkg/capabilities"
"k8s.io/kubernetes/pkg/features"
utilpointer "k8s.io/utils/pointer"
@@ -53,7 +68,7 @@ import (
)
const (
- dnsLabelErrMsg = "must consist of lower-case alphanumeric characters or '-'"
+ dnsLabelErrMsg = "must contain only lower-case alphanumeric characters or '-'"
dnsSubdomainLabelErrMsg = "a lowercase RFC 1123 subdomain"
envVarNameErrMsg = "a valid environment variable name must consist of"
relaxedEnvVarNameFmtErrMsg string = "a valid environment variable name must consist only of printable ASCII characters other than '='"
@@ -3770,7 +3785,7 @@ func TestValidateVolumes(t *testing.T) {
errs: []verr{{
etype: field.ErrorTypeInvalid,
field: "name",
- detail: "must consist of lower-case alphanumeric characters or '-'",
+ detail: "must contain only lower-case alphanumeric characters or '-'",
}},
}, {
name: "name not a DNS label",
@@ -12280,7 +12295,7 @@ func TestValidatePod(t *testing.T) {
),
},
"final PVC name for ephemeral volume must be valid": {
- expectedError: "spec.volumes[1].name: Invalid value: \"" + longVolName + "\": PVC name \"" + longPodName + "-" + longVolName + "\": must be no more than 253 characters",
+ expectedError: "spec.volumes[1].name: Invalid value: \"" + longVolName + "\": PVC name \"" + longPodName + "-" + longVolName + "\": must be no more than 253 bytes",
spec: *podtest.MakePod(longPodName,
podtest.SetVolumes(
core.Volume{Name: "pvc", VolumeSource: core.VolumeSource{PersistentVolumeClaim: &core.PersistentVolumeClaimVolumeSource{ClaimName: "my-pvc"}}},
@@ -16529,7 +16544,7 @@ func TestValidateReplicationControllerStatusUpdate(t *testing.T) {
update: core.ReplicationController{
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
Spec: core.ReplicationControllerSpec{
- Replicas: 3,
+ Replicas: ptr.To[int32](3),
Selector: validSelector,
Template: &validPodTemplate.Template,
},
@@ -16561,7 +16576,7 @@ func TestValidateReplicationControllerStatusUpdate(t *testing.T) {
update: core.ReplicationController{
ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
Spec: core.ReplicationControllerSpec{
- Replicas: 2,
+ Replicas: ptr.To[int32](2),
Selector: validSelector,
Template: &validPodTemplate.Template,
},
@@ -16580,391 +16595,488 @@ func TestValidateReplicationControllerStatusUpdate(t *testing.T) {
}
-func TestValidateReplicationControllerUpdate(t *testing.T) {
- validSelector := map[string]string{"a": "b"}
- validPodTemplate := core.PodTemplate{
- Template: core.PodTemplateSpec{
- ObjectMeta: metav1.ObjectMeta{
- Labels: validSelector,
- },
- Spec: podtest.MakePodSpec(),
- },
- }
- readWriteVolumePodTemplate := core.PodTemplate{
- Template: core.PodTemplateSpec{
- ObjectMeta: metav1.ObjectMeta{
- Labels: validSelector,
+// Helper function for RC tests.
+func mkValidReplicationController(tweaks ...func(rc *core.ReplicationController)) core.ReplicationController {
+ rc := core.ReplicationController{
+ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
+ Spec: core.ReplicationControllerSpec{
+ Replicas: ptr.To[int32](1),
+ Selector: map[string]string{"a": "b"},
+ Template: &core.PodTemplateSpec{
+ ObjectMeta: metav1.ObjectMeta{
+ Labels: map[string]string{"a": "b"},
+ },
+ Spec: podtest.MakePodSpec(),
},
- Spec: podtest.MakePodSpec(
- podtest.SetVolumes(core.Volume{Name: "gcepd", VolumeSource: core.VolumeSource{GCEPersistentDisk: &core.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}),
- ),
},
}
- invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
- invalidPodTemplate := core.PodTemplate{
- Template: core.PodTemplateSpec{
- ObjectMeta: metav1.ObjectMeta{
- Labels: invalidSelector,
- },
- Spec: podtest.MakePodSpec(),
- },
+ for _, tweak := range tweaks {
+ tweak(&rc)
}
- type rcUpdateTest struct {
+ return rc
+}
+
+func TestValidateReplicationControllerUpdate(t *testing.T) {
+ successCases := []struct {
old core.ReplicationController
update core.ReplicationController
- }
- successCases := []rcUpdateTest{{
- old: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: 3,
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
+ }{{
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Replicas = ptr.To[int32](0)
+ }),
}, {
- old: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: 1,
- Selector: validSelector,
- Template: &readWriteVolumePodTemplate.Template,
- },
- },
- },
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Replicas = ptr.To[int32](3)
+ }),
+ }, {
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.MinReadySeconds = 0
+ }),
+ }, {
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.MinReadySeconds = 3
+ }),
+ }, {
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Replicas = ptr.To[int32](2)
+ rc.Spec.Template.Spec = podtest.MakePodSpec(
+ podtest.SetVolumes(
+ core.Volume{
+ Name: "gcepd",
+ VolumeSource: core.VolumeSource{
+ GCEPersistentDisk: &core.GCEPersistentDiskVolumeSource{
+ PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false,
+ },
+ },
+ }))
+ }),
+ }}
+ for _, tc := range successCases {
+ tc.old.ObjectMeta.ResourceVersion = "1"
+ tc.update.ObjectMeta.ResourceVersion = "1"
+ for _, gateVal := range []bool{false, true} {
+ gate := features.DeclarativeValidation
+ featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, gateVal)
+
+ errs := ValidateReplicationControllerUpdate(&tc.update, &tc.old, PodValidationOptions{})
+ if utilfeature.DefaultFeatureGate.Enabled(gate) {
+ // If declarative validation is enabled, it's the union of
+ // managed and declarative validation that we are testing.
+ newVersioned := v1.ReplicationController{}
+ if err := v1util.Convert_core_ReplicationController_To_v1_ReplicationController(&tc.update, &newVersioned, nil); err != nil {
+ t.Fatalf("failed to convert to v1: %v", err)
+ }
+ oldVersioned := v1.ReplicationController{}
+ if err := v1util.Convert_core_ReplicationController_To_v1_ReplicationController(&tc.old, &oldVersioned, nil); err != nil {
+ t.Fatalf("failed to convert to v1: %v", err)
+ }
+ ctx := request.WithRequestInfo(context.Background(), &request.RequestInfo{
+ APIGroup: "",
+ APIVersion: "v1",
+ })
+ errs = append(errs, rest.ValidateUpdateDeclaratively(ctx, nil, legacyscheme.Scheme, &newVersioned, &oldVersioned)...)
+ }
+ if len(errs) != 0 {
+ t.Errorf("expected success: %v", errs)
+ }
+ }
+
+ verifyVersionedValidationEquivalence(t, &tc.update, &tc.old)
}
- for _, successCase := range successCases {
- successCase.old.ObjectMeta.ResourceVersion = "1"
- successCase.update.ObjectMeta.ResourceVersion = "1"
- if errs := ValidateReplicationControllerUpdate(&successCase.update, &successCase.old, PodValidationOptions{}); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
+
+ mkErrs := func(strs ...string) []*regexp.Regexp {
+ out := make([]*regexp.Regexp, 0, len(strs))
+ for _, s := range strs {
+ out = append(out, regexp.MustCompile(s))
}
+ return out
}
- errorCases := map[string]rcUpdateTest{
- "more than one read/write": {
- old: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: 2,
- Selector: validSelector,
- Template: &readWriteVolumePodTemplate.Template,
- },
- },
+
+ errorCases := map[string]struct {
+ old core.ReplicationController
+ update core.ReplicationController
+ errs []*regexp.Regexp
+ }{
+ "unmatched selector": {
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Selector["another"] = "value"
+ }),
+ errs: mkErrs("^spec.template.metadata.labels: Invalid value.*does not match template"),
},
"invalid selector": {
- old: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: 2,
- Selector: invalidSelector,
- Template: &validPodTemplate.Template,
- },
- },
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ invalid := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
+ rc.Spec.Template.Labels = invalid
+ rc.Spec.Selector = invalid
+ }),
+ errs: mkErrs("^spec.template.labels: Invalid value.*name part must consist of"),
},
"invalid pod": {
- old: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: 2,
- Selector: validSelector,
- Template: &invalidPodTemplate.Template,
- },
- },
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Template = nil
+ }),
+ errs: mkErrs("^spec.template: Required value"),
},
"negative replicas": {
- old: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
- update: core.ReplicationController{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: -1,
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- },
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Replicas = ptr.To[int32](-1)
+ }),
+ errs: mkErrs("^spec.replicas: Invalid value"),
},
- }
- for testName, errorCase := range errorCases {
- // TODO: migrate
- if errs := ValidateReplicationControllerUpdate(&errorCase.update, &errorCase.old, PodValidationOptions{}); len(errs) == 0 {
- t.Errorf("expected failure: %s", testName)
+ "nil replicas": {
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Replicas = nil
+ }),
+ errs: mkErrs("^spec.replicas: Required value"),
+ },
+ "negative minReadySeconds": {
+ old: mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ update: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.MinReadySeconds = -1
+ }),
+ errs: mkErrs("^spec.minReadySeconds: Invalid value"),
+ },
+ }
+ for k, tc := range errorCases {
+ tc.old.ObjectMeta.ResourceVersion = "1"
+ tc.update.ObjectMeta.ResourceVersion = "1"
+ for _, gateVal := range []bool{false, true} {
+ gate := features.DeclarativeValidation
+ featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, gateVal)
+
+ errs := ValidateReplicationControllerUpdate(&tc.update, &tc.old, PodValidationOptions{})
+ if utilfeature.DefaultFeatureGate.Enabled(gate) {
+ // If declarative validation is enabled, it's the union of
+ // managed and declarative validation that we are testing.
+ newVersioned := v1.ReplicationController{}
+ if err := v1util.Convert_core_ReplicationController_To_v1_ReplicationController(&tc.update, &newVersioned, nil); err != nil {
+ t.Fatalf("failed to convert to v1: %v", err)
+ }
+ oldVersioned := v1.ReplicationController{}
+ if err := v1util.Convert_core_ReplicationController_To_v1_ReplicationController(&tc.old, &oldVersioned, nil); err != nil {
+ t.Fatalf("failed to convert to v1: %v", err)
+ }
+ ctx := request.WithRequestInfo(context.Background(), &request.RequestInfo{
+ APIGroup: "",
+ APIVersion: "v1",
+ })
+ errs = append(errs, rest.ValidateUpdateDeclaratively(ctx, nil, legacyscheme.Scheme, &newVersioned, &oldVersioned)...)
+ }
+ if len(errs) == 0 {
+ t.Errorf("case %q(gate=%v): expected failure", k, gateVal)
+ } else if len(errs) != len(tc.errs) {
+ t.Errorf("case %q(gate=%v): expected %d failures, got %d:\n%v", k, gateVal, len(tc.errs), len(errs), fmtErrs(errs))
+ } else {
+ for i, re := range tc.errs {
+ if want, got := re.String(), errs[i].Error(); !re.MatchString(got) {
+ t.Errorf("case %q(gate=%v): wrong error:\n\texpected: %q\n\t got: %q", k, gateVal, want, got)
+ }
+ }
+ }
}
+
+ verifyVersionedValidationEquivalence(t, &tc.update, &tc.old)
}
}
func TestValidateReplicationController(t *testing.T) {
- validSelector := map[string]string{"a": "b"}
- validPodTemplate := core.PodTemplate{
- Template: core.PodTemplateSpec{
- ObjectMeta: metav1.ObjectMeta{
- Labels: validSelector,
- },
- Spec: podtest.MakePodSpec(),
- },
- }
- readWriteVolumePodTemplate := core.PodTemplate{
- Template: core.PodTemplateSpec{
- ObjectMeta: metav1.ObjectMeta{
- Labels: validSelector,
- },
- Spec: podtest.MakePodSpec(
- podtest.SetVolumes(core.Volume{Name: "gcepd", VolumeSource: core.VolumeSource{GCEPersistentDisk: &core.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}),
- ),
- },
- }
- hostnetPodTemplate := core.PodTemplate{
- Template: core.PodTemplateSpec{
- ObjectMeta: metav1.ObjectMeta{
- Labels: validSelector,
- },
- Spec: podtest.MakePodSpec(
- podtest.SetSecurityContext(&core.PodSecurityContext{
- HostNetwork: true,
- }),
- podtest.SetContainers(podtest.MakeContainer("abc", podtest.SetContainerPorts(
- core.ContainerPort{
- ContainerPort: 12345,
- Protocol: core.ProtocolTCP,
+ successCases := []core.ReplicationController{
+ mkValidReplicationController(func(rc *core.ReplicationController) {}),
+ mkValidReplicationController(func(rc *core.ReplicationController) { rc.Name = "abc-123" }),
+ mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Replicas = ptr.To[int32](2)
+ rc.Spec.Template.Spec = podtest.MakePodSpec(
+ podtest.SetVolumes(
+ core.Volume{
+ Name: "gcepd",
+ VolumeSource: core.VolumeSource{
+ GCEPersistentDisk: &core.GCEPersistentDiskVolumeSource{
+ PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false,
+ },
+ },
+ }))
+ }),
+ mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Template.Spec = podtest.MakePodSpec(
+ podtest.SetSecurityContext(&core.PodSecurityContext{HostNetwork: true}),
+ podtest.SetContainers(podtest.MakeContainer("abc",
+ podtest.SetContainerPorts(core.ContainerPort{
+ ContainerPort: 12345, Protocol: core.ProtocolTCP,
}))),
- ),
- },
+ )
+ }),
+ mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.Replicas = ptr.To[int32](0) }),
+ mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.Replicas = ptr.To[int32](1) }),
+ mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.Replicas = ptr.To[int32](100) }),
+ mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.MinReadySeconds = 0 }),
+ mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.MinReadySeconds = 1 }),
+ mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.MinReadySeconds = 100 }),
}
- invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"}
- invalidPodTemplate := core.PodTemplate{
- Template: core.PodTemplateSpec{
- Spec: podtest.MakePodSpec(),
- ObjectMeta: metav1.ObjectMeta{
- Labels: invalidSelector,
- },
- },
+ for _, tc := range successCases {
+ for _, gateVal := range []bool{false, true} {
+ gate := features.DeclarativeValidation
+ featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, gateVal)
+
+ errs := ValidateReplicationController(&tc, PodValidationOptions{})
+ if utilfeature.DefaultFeatureGate.Enabled(gate) {
+ // If declarative validation is enabled, it's the union of
+ // managed and declarative validation that we are testing.
+ versioned := v1.ReplicationController{}
+ if err := v1util.Convert_core_ReplicationController_To_v1_ReplicationController(&tc, &versioned, nil); err != nil {
+ t.Fatalf("failed to convert to v1: %v", err)
+ }
+ ctx := request.WithRequestInfo(context.Background(), &request.RequestInfo{
+ APIGroup: "",
+ APIVersion: "v1",
+ })
+ errs = append(errs, rest.ValidateDeclaratively(ctx, nil, legacyscheme.Scheme, &versioned)...)
+ }
+ if len(errs) != 0 {
+ t.Errorf("expected success: %v", errs)
+ }
+ }
+
+ verifyVersionedValidationEquivalence(t, &tc, nil)
}
- successCases := []core.ReplicationController{{
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- }, {
- ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
- }, {
- ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: 1,
- Selector: validSelector,
- Template: &readWriteVolumePodTemplate.Template,
- },
- }, {
- ObjectMeta: metav1.ObjectMeta{Name: "hostnet", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: 1,
- Selector: validSelector,
- Template: &hostnetPodTemplate.Template,
- },
- }}
- for _, successCase := range successCases {
- if errs := ValidateReplicationController(&successCase, PodValidationOptions{}); len(errs) != 0 {
- t.Errorf("expected success: %v", errs)
+
+ mkErrs := func(strs ...string) []*regexp.Regexp {
+ out := make([]*regexp.Regexp, 0, len(strs))
+ for _, s := range strs {
+ out = append(out, regexp.MustCompile(s))
}
+ return out
}
- errorCases := map[string]core.ReplicationController{
- "zero-length ID": {
- ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
+ errorCases := map[string]struct {
+ input core.ReplicationController
+ errs []*regexp.Regexp
+ }{
+ "missing name": {
+ input: mkValidReplicationController(func(rc *core.ReplicationController) { rc.Name = "" }),
+ errs: mkErrs("^metadata.name: Required value"),
},
- "missing-namespace": {
- ObjectMeta: metav1.ObjectMeta{Name: "abc-123"},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
+ "missing namespace": {
+ input: mkValidReplicationController(func(rc *core.ReplicationController) { rc.Namespace = "" }),
+ errs: mkErrs("^metadata.namespace: Required value"),
},
"empty selector": {
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Template: &validPodTemplate.Template,
- },
+ input: mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.Selector = nil }),
+ errs: mkErrs("^spec.selector: Required value"),
},
- "selector_doesnt_match": {
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: map[string]string{"foo": "bar"},
- Template: &validPodTemplate.Template,
- },
+ "selector doesnt match": {
+ input: mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.Selector = map[string]string{"foo": "bar"} }),
+ errs: mkErrs("^spec.template.metadata.labels: Invalid value"),
},
"invalid manifest": {
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- },
+ input: mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.Template = nil }),
+ errs: mkErrs("^spec.template: Required value"),
},
- "read-write persistent disk with > 1 pod": {
- ObjectMeta: metav1.ObjectMeta{Name: "abc"},
- Spec: core.ReplicationControllerSpec{
- Replicas: 2,
- Selector: validSelector,
- Template: &readWriteVolumePodTemplate.Template,
- },
+ "negative replicas": {
+ input: mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.Replicas = ptr.To[int32](-1) }),
+ errs: mkErrs("^spec.replicas: Invalid value"),
},
- "negative_replicas": {
- ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: -1,
- Selector: validSelector,
- },
+ "negative minReadySeconds": {
+ input: mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.MinReadySeconds = -1 }),
+ errs: mkErrs("^spec.minReadySeconds: Invalid value"),
},
- "invalid_label": {
- ObjectMeta: metav1.ObjectMeta{
- Name: "abc-123",
- Namespace: metav1.NamespaceDefault,
- Labels: map[string]string{
+ "nil replicas": {
+ input: mkValidReplicationController(func(rc *core.ReplicationController) { rc.Spec.Replicas = nil }),
+ errs: mkErrs("^spec.replicas: Required value"),
+ },
+ "invalid label": {
+ input: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Labels = map[string]string{
"NoUppercaseOrSpecialCharsLike=Equals": "bar",
- },
- },
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
+ }
+ }),
+ errs: mkErrs("^metadata.labels: Invalid value.*name part must consist of"),
},
- "invalid_label 2": {
- ObjectMeta: metav1.ObjectMeta{
- Name: "abc-123",
- Namespace: metav1.NamespaceDefault,
- Labels: map[string]string{
+ "invalid label 2": {
+ input: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Template.Labels = map[string]string{
"NoUppercaseOrSpecialCharsLike=Equals": "bar",
- },
- },
- Spec: core.ReplicationControllerSpec{
- Template: &invalidPodTemplate.Template,
- },
+ }
+ }),
+ errs: mkErrs("^spec.template.metadata.labels: Invalid value.*does not match template",
+ "^spec.template.labels: Invalid value.*name part must consist of"),
},
- "invalid_annotation": {
- ObjectMeta: metav1.ObjectMeta{
- Name: "abc-123",
- Namespace: metav1.NamespaceDefault,
- Annotations: map[string]string{
+ "invalid annotation": {
+ input: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Annotations = map[string]string{
"NoUppercaseOrSpecialCharsLike=Equals": "bar",
- },
- },
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &validPodTemplate.Template,
- },
+ }
+ }),
+ errs: mkErrs("^metadata.annotations: Invalid value.*name part must consist of"),
},
"invalid restart policy 1": {
- ObjectMeta: metav1.ObjectMeta{
- Name: "abc-123",
- Namespace: metav1.NamespaceDefault,
- },
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &core.PodTemplateSpec{
- Spec: podtest.MakePodSpec(podtest.SetRestartPolicy(core.RestartPolicyOnFailure)),
- ObjectMeta: metav1.ObjectMeta{
- Labels: validSelector,
- },
- },
- },
+ input: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Template.Spec.RestartPolicy = core.RestartPolicyOnFailure
+ }),
+ errs: mkErrs("^spec.template.spec.restartPolicy: Unsupported value"),
},
"invalid restart policy 2": {
- ObjectMeta: metav1.ObjectMeta{
- Name: "abc-123",
- Namespace: metav1.NamespaceDefault,
- },
- Spec: core.ReplicationControllerSpec{
- Selector: validSelector,
- Template: &core.PodTemplateSpec{
- Spec: podtest.MakePodSpec(podtest.SetRestartPolicy(core.RestartPolicyNever)),
- ObjectMeta: metav1.ObjectMeta{
- Labels: validSelector,
- },
- },
- },
+ input: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Template.Spec.RestartPolicy = core.RestartPolicyNever
+ }),
+ errs: mkErrs("^spec.template.spec.restartPolicy: Unsupported value"),
},
"template may not contain ephemeral containers": {
- ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault},
- Spec: core.ReplicationControllerSpec{
- Replicas: 1,
- Selector: validSelector,
- Template: &core.PodTemplateSpec{
- ObjectMeta: metav1.ObjectMeta{
- Labels: validSelector,
- },
- Spec: podtest.MakePodSpec(
- podtest.SetEphemeralContainers(core.EphemeralContainer{EphemeralContainerCommon: core.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}),
- ),
- },
- },
+ input: mkValidReplicationController(func(rc *core.ReplicationController) {
+ rc.Spec.Template.Spec = podtest.MakePodSpec(
+ podtest.SetEphemeralContainers(
+ core.EphemeralContainer{
+ EphemeralContainerCommon: core.EphemeralContainerCommon{
+ Name: "debug",
+ Image: "image",
+ ImagePullPolicy: "IfNotPresent",
+ TerminationMessagePolicy: "File",
+ },
+ }))
+ }),
+ errs: mkErrs("^spec.template.spec.ephemeralContainers: Forbidden: ephemeral containers not allowed in pod template"),
},
}
- for k, v := range errorCases {
- // TODO: migrate
- errs := ValidateReplicationController(&v, PodValidationOptions{})
- if len(errs) == 0 {
- t.Errorf("expected failure for %s", k)
+ for k, tc := range errorCases {
+ for _, gateVal := range []bool{false, true} {
+ gate := features.DeclarativeValidation
+ featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, gate, gateVal)
+
+ errs := ValidateReplicationController(&tc.input, PodValidationOptions{})
+ if utilfeature.DefaultFeatureGate.Enabled(gate) {
+ // If declarative validation is enabled, it's the union of
+ // managed and declarative validation that we are testing.
+ versioned := v1.ReplicationController{}
+ if err := v1util.Convert_core_ReplicationController_To_v1_ReplicationController(&tc.input, &versioned, nil); err != nil {
+ t.Fatalf("failed to convert to v1: %v", err)
+ }
+ ctx := request.WithRequestInfo(context.Background(), &request.RequestInfo{
+ APIGroup: "",
+ APIVersion: "v1",
+ })
+ errs = append(errs, rest.ValidateDeclaratively(ctx, nil, legacyscheme.Scheme, &versioned)...)
+ }
+ if len(errs) == 0 {
+ t.Errorf("case %q(gate=%v): expected failure", k, gateVal)
+ } else if len(errs) != len(tc.errs) {
+ t.Errorf("case %q(gate=%v): expected %d failures, got %d:\n%v", k, gateVal, len(tc.errs), len(errs), fmtErrs(errs))
+ } else {
+ for i, re := range tc.errs {
+ if want, got := re.String(), errs[i].Error(); !re.MatchString(got) {
+ t.Errorf("case %q(gate=%v): wrong error:\n\texpected: %q\n\t got: %q", k, gateVal, want, got)
+ }
+ }
+ }
}
- for i := range errs {
- field := errs[i].Field
- if !strings.HasPrefix(field, "spec.template.") &&
- field != "metadata.name" &&
- field != "metadata.namespace" &&
- field != "spec.selector" &&
- field != "spec.template" &&
- field != "GCEPersistentDisk.ReadOnly" &&
- field != "spec.replicas" &&
- field != "spec.template.labels" &&
- field != "metadata.annotations" &&
- field != "metadata.labels" &&
- field != "status.replicas" {
- t.Errorf("%s: missing prefix for: %v", k, errs[i])
+
+ verifyVersionedValidationEquivalence(t, &tc.input, nil)
+ }
+}
+
+// helper for nicer output
+func fmtErrs(errs field.ErrorList) string {
+ if len(errs) == 0 {
+ return "<no errors>"
+ }
+ if len(errs) == 1 {
+ return strconv.Quote(errs[0].Error())
+ }
+ buf := bytes.Buffer{}
+ for _, e := range errs {
+ buf.WriteString("\n")
+ buf.WriteString(strconv.Quote(e.Error()))
+ }
+
+ return buf.String()
+}
+
+func verifyVersionedValidationEquivalence(t *testing.T, obj, old k8sruntime.Object) {
+ t.Helper()
+
+ // Accumulate errors from all versioned validation, per version.
+ all := map[string]field.ErrorList{}
+ accumulate := func(t *testing.T, gv string, errs field.ErrorList) {
+ all[gv] = errs
+ }
+ if old == nil {
+ runtimetest.RunValidationForEachVersion(t, legacyscheme.Scheme, sets.Set[string]{}, obj, accumulate)
+ } else {
+ runtimetest.RunUpdateValidationForEachVersion(t, legacyscheme.Scheme, sets.Set[string]{}, obj, old, accumulate)
+ }
+
+ // Make a copy so we can modify it.
+ other := map[string]field.ErrorList{}
+ // Index for nicer output.
+ keys := []string{}
+ for k, v := range all {
+ other[k] = v
+ keys = append(keys, k)
+ }
+ sort.Strings(keys)
+
+ // Compare each lhs to each rhs.
+ for _, lk := range keys {
+ lv := all[lk]
+ // remove lk since to prevent comparison to itself and because this
+ // iteration will compare it to any version it has not yet been
+ // compared to. e.g. [1, 2, 3] vs. [1, 2, 3] yields:
+ // 1 vs. 2
+ // 1 vs. 3
+ // 2 vs. 3
+ delete(other, lk)
+ // don't compare to ourself
+ for _, rk := range keys {
+ rv, found := other[rk]
+ if !found {
+ continue // done already
+ }
+ if len(lv) != len(rv) {
+ t.Errorf("different error count (%d vs. %d)\n%s: %v\n%s: %v", len(lv), len(rv), lk, fmtErrs(lv), rk, fmtErrs(rv))
+ continue
+ }
+ next := false
+ for i := range lv {
+ if l, r := lv[i], rv[i]; l.Type != r.Type || l.Detail != r.Detail {
+ t.Errorf("different errors\n%s: %v\n%s: %v", lk, fmtErrs(lv), rk, fmtErrs(rv))
+ next = true
+ break
+ }
+ }
+ if next {
+ continue
+ }
+ }
+ }
+}
+
+// FIXME: move somewhere generic - pkg/api/testing?
+func TestVersionedValidationByFuzzing(t *testing.T) {
+ for i := 0; i < *roundtrip.FuzzIters; i++ {
+ gv := schema.GroupVersion{Group: "", Version: "v1"}
+ f := fuzzer.FuzzerFor(apitest.FuzzerFuncs, rand.NewSource(rand.Int63()), legacyscheme.Codecs)
+ for kind := range legacyscheme.Scheme.KnownTypes(gv) {
+ obj, err := legacyscheme.Scheme.New(gv.WithKind(kind))
+ if err != nil {
+ t.Fatalf("could not create a %v: %s", kind, err)
}
+ f.Fuzz(obj)
+ verifyVersionedValidationEquivalence(t, obj, nil)
+
+ old, err := legacyscheme.Scheme.New(gv.WithKind(kind))
+ if err != nil {
+ t.Fatalf("could not create a %v: %s", kind, err)
+ }
+ f.Fuzz(old)
+ verifyVersionedValidationEquivalence(t, obj, old)
}
}
}
@@ -17041,23 +17153,23 @@ func TestValidateNode(t *testing.T) {
Name: "abc",
Annotations: map[string]string{
core.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "podSignature": {
- "podController": {
- "apiVersion": "v1",
- "kind": "ReplicationController",
- "name": "foo",
- "uid": "abcdef123456",
- "controller": true
- }
- },
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
+ {
+ "preferAvoidPods": [
+ {
+ "podSignature": {
+ "podController": {
+ "apiVersion": "v1",
+ "kind": "ReplicationController",
+ "name": "foo",
+ "uid": "abcdef123456",
+ "controller": true
+ }
+ },
+ "reason": "some reason",
+ "message": "some message"
+ }
+ ]
+ }`,
},
},
Status: core.NodeStatus{
@@ -17208,14 +17320,14 @@ func TestValidateNode(t *testing.T) {
Name: "abc-123",
Annotations: map[string]string{
core.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
+ {
+ "preferAvoidPods": [
+ {
+ "reason": "some reason",
+ "message": "some message"
+ }
+ ]
+ }`,
},
},
Status: core.NodeStatus{
@@ -17231,23 +17343,23 @@ func TestValidateNode(t *testing.T) {
Name: "abc-123",
Annotations: map[string]string{
core.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "podSignature": {
- "podController": {
- "apiVersion": "v1",
- "kind": "ReplicationController",
- "name": "foo",
- "uid": "abcdef123456",
- "controller": false
- }
- },
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
+ {
+ "preferAvoidPods": [
+ {
+ "podSignature": {
+ "podController": {
+ "apiVersion": "v1",
+ "kind": "ReplicationController",
+ "name": "foo",
+ "uid": "abcdef123456",
+ "controller": false
+ }
+ },
+ "reason": "some reason",
+ "message": "some message"
+ }
+ ]
+ }`,
},
},
Status: core.NodeStatus{
@@ -17529,23 +17641,23 @@ func TestValidateNodeUpdate(t *testing.T) {
Name: "foo",
Annotations: map[string]string{
core.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "podSignature": {
- "podController": {
- "apiVersion": "v1",
- "kind": "ReplicationController",
- "name": "foo",
- "uid": "abcdef123456",
- "controller": true
- }
- },
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
+ {
+ "preferAvoidPods": [
+ {
+ "podSignature": {
+ "podController": {
+ "apiVersion": "v1",
+ "kind": "ReplicationController",
+ "name": "foo",
+ "uid": "abcdef123456",
+ "controller": true
+ }
+ },
+ "reason": "some reason",
+ "message": "some message"
+ }
+ ]
+ }`,
},
},
Spec: core.NodeSpec{
@@ -17561,14 +17673,14 @@ func TestValidateNodeUpdate(t *testing.T) {
Name: "foo",
Annotations: map[string]string{
core.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
+ {
+ "preferAvoidPods": [
+ {
+ "reason": "some reason",
+ "message": "some message"
+ }
+ ]
+ }`,
},
},
}, false},
@@ -17581,23 +17693,23 @@ func TestValidateNodeUpdate(t *testing.T) {
Name: "foo",
Annotations: map[string]string{
core.PreferAvoidPodsAnnotationKey: `
- {
- "preferAvoidPods": [
- {
- "podSignature": {
- "podController": {
- "apiVersion": "v1",
- "kind": "ReplicationController",
- "name": "foo",
- "uid": "abcdef123456",
- "controller": false
- }
- },
- "reason": "some reason",
- "message": "some message"
- }
- ]
- }`,
+ {
+ "preferAvoidPods": [
+ {
+ "podSignature": {
+ "podController": {
+ "apiVersion": "v1",
+ "kind": "ReplicationController",
+ "name": "foo",
+ "uid": "abcdef123456",
+ "controller": false
+ }
+ },
+ "reason": "some reason",
+ "message": "some message"
+ }
+ ]
+ }`,
},
},
}, false},
@@ -20860,7 +20972,7 @@ func TestValidateEndpointsCreate(t *testing.T) {
for k, v := range errorCases {
t.Run(k, func(t *testing.T) {
if errs := ValidateEndpointsCreate(&v.endpoints); len(errs) == 0 || errs[0].Type != v.errorType || !strings.Contains(errs[0].Detail, v.errorDetail) {
- t.Errorf("Expected error type %s with detail %q, got %v", v.errorType, v.errorDetail, errs)
+ t.Errorf("expected error type %q with detail %q\ngot %v", v.errorType, v.errorDetail, errs)
}
})
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment