/* Copyright 2016 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 validation import ( "fmt" "strings" "testing" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" "k8s.io/apimachinery/pkg/api/resource" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/dump" "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/validation/field" utilfeature "k8s.io/apiserver/pkg/util/feature" featuregatetesting "k8s.io/component-base/featuregate/testing" "k8s.io/kubernetes/pkg/api/pod" "k8s.io/kubernetes/pkg/apis/apps" api "k8s.io/kubernetes/pkg/apis/core" corevalidation "k8s.io/kubernetes/pkg/apis/core/validation" "k8s.io/kubernetes/pkg/features" "k8s.io/utils/ptr" ) type statefulSetTweak func(ss *apps.StatefulSet) func mkStatefulSet(template *api.PodTemplate, tweaks ...statefulSetTweak) apps.StatefulSet { ss := apps.StatefulSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.StatefulSetSpec{ PodManagementPolicy: apps.OrderedReadyPodManagement, Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"a": "b"}}, Template: template.Template, UpdateStrategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType}, }, } for _, tw := range tweaks { tw(&ss) } return ss } func tweakName(name string) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.ObjectMeta.Name = name } } func tweakNamespace(ns string) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.ObjectMeta.Namespace = ns } } func tweakLabels(key string, value string) statefulSetTweak { return func(ss *apps.StatefulSet) { if ss.ObjectMeta.Labels == nil { ss.ObjectMeta.Labels = map[string]string{} } ss.ObjectMeta.Labels[key] = value } } func tweakAnnotations(key string, value string) statefulSetTweak { return func(ss *apps.StatefulSet) { if ss.ObjectMeta.Annotations == nil { ss.ObjectMeta.Annotations = map[string]string{} } ss.ObjectMeta.Annotations[key] = value } } func tweakFinalizers(finalizers ...string) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.ObjectMeta.Finalizers = finalizers } } func tweakPodManagementPolicy(policy apps.PodManagementPolicyType) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.Spec.PodManagementPolicy = policy } } func tweakReplicas(replicas int32) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.Spec.Replicas = replicas } } func tweakSelectorLabels(labels map[string]string) statefulSetTweak { return func(ss *apps.StatefulSet) { if labels == nil { ss.Spec.Selector = nil } else { ss.Spec.Selector = &metav1.LabelSelector{MatchLabels: labels} } } } func tweakTemplateRestartPolicy(rp api.RestartPolicy) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.Spec.Template.Spec.RestartPolicy = rp } } func tweakMinReadySeconds(t int32) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.Spec.MinReadySeconds = t } } func tweakOrdinalsStart(s int32) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.Spec.Ordinals = &apps.StatefulSetOrdinals{Start: s} } } func tweakPVCTemplate(pvc ...api.PersistentVolumeClaim) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.Spec.VolumeClaimTemplates = pvc } } func tweakUpdateStrategyType(t apps.StatefulSetUpdateStrategyType) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.Spec.UpdateStrategy.Type = t } } func tweakRollingUpdatePartition(partition int32) statefulSetTweak { return func(ss *apps.StatefulSet) { if ss.Spec.UpdateStrategy.RollingUpdate == nil { ss.Spec.UpdateStrategy.RollingUpdate = &apps.RollingUpdateStatefulSetStrategy{} } ss.Spec.UpdateStrategy.RollingUpdate.Partition = partition } } func tweakMaxUnavailable(mu intstr.IntOrString) statefulSetTweak { return func(ss *apps.StatefulSet) { if ss.Spec.UpdateStrategy.RollingUpdate == nil { ss.Spec.UpdateStrategy.RollingUpdate = &apps.RollingUpdateStatefulSetStrategy{} } ss.Spec.UpdateStrategy.RollingUpdate.MaxUnavailable = ptr.To(mu) } } func tweakPVCPolicy(policy *apps.StatefulSetPersistentVolumeClaimRetentionPolicy) statefulSetTweak { return func(ss *apps.StatefulSet) { ss.Spec.PersistentVolumeClaimRetentionPolicy = policy } } type pvcPolicyTweak func(policy *apps.StatefulSetPersistentVolumeClaimRetentionPolicy) func mkPVCPolicy(tweaks ...pvcPolicyTweak) *apps.StatefulSetPersistentVolumeClaimRetentionPolicy { policy := &apps.StatefulSetPersistentVolumeClaimRetentionPolicy{} for _, tw := range tweaks { tw(policy) } return policy } func tweakPVCDeletedPolicy(t apps.PersistentVolumeClaimRetentionPolicyType) pvcPolicyTweak { return func(policy *apps.StatefulSetPersistentVolumeClaimRetentionPolicy) { policy.WhenDeleted = t } } func tweakPVCScalePolicy(t apps.PersistentVolumeClaimRetentionPolicyType) pvcPolicyTweak { return func(policy *apps.StatefulSetPersistentVolumeClaimRetentionPolicy) { policy.WhenScaled = t } } func TestValidateStatefulSet(t *testing.T) { validLabels := map[string]string{"a": "b"} validPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, } validHostNetPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostNetwork: true, }, RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{ Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", Ports: []api.ContainerPort{{ ContainerPort: 12345, Protocol: api.ProtocolTCP, }}, }}, }, }, } invalidLabels := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: metav1.ObjectMeta{ Labels: invalidLabels, }, }, } invalidTime := int64(60) invalidPodTemplate2 := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, ActiveDeadlineSeconds: &invalidTime, }, }, } const enableStatefulSetAutoDeletePVC = "[enable StatefulSetAutoDeletePVC]" type testCase struct { name string set apps.StatefulSet errs field.ErrorList } successCases := []testCase{{ name: "alpha name", set: mkStatefulSet(&validPodTemplate, tweakName("abc")), }, { name: "alphanumeric name", set: mkStatefulSet(&validPodTemplate, tweakName("abc-123")), }, { name: "hostNetwork true", set: mkStatefulSet(&validHostNetPodTemplate), }, { name: "parallel pod management", set: mkStatefulSet(&validPodTemplate, tweakPodManagementPolicy(apps.ParallelPodManagement)), }, { name: "ordered ready pod management", set: mkStatefulSet(&validPodTemplate, tweakPodManagementPolicy(apps.OrderedReadyPodManagement)), }, { name: "update strategy", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakUpdateStrategyType(apps.RollingUpdateStatefulSetStrategyType), tweakRollingUpdatePartition(2), ), }, { name: "PVC policy " + enableStatefulSetAutoDeletePVC, set: mkStatefulSet(&validPodTemplate, tweakPVCPolicy(mkPVCPolicy( tweakPVCDeletedPolicy(apps.DeletePersistentVolumeClaimRetentionPolicyType), tweakPVCScalePolicy(apps.RetainPersistentVolumeClaimRetentionPolicyType), )), ), }, { name: "maxUnavailable with parallel pod management", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakUpdateStrategyType(apps.RollingUpdateStatefulSetStrategyType), tweakRollingUpdatePartition(2), tweakMaxUnavailable(intstr.FromInt32(2)), ), }, { name: "ordinals.start positive value", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakOrdinalsStart(2), ), }, } errorCases := []testCase{{ name: "zero-length name", set: mkStatefulSet(&validPodTemplate, tweakName("")), errs: field.ErrorList{ field.Required(field.NewPath("metadata", "name"), ""), }, }, { name: "name-with-dots", set: mkStatefulSet(&validPodTemplate, tweakName("abc.123")), errs: field.ErrorList{ field.Invalid(field.NewPath("metadata", "name"), "abc.123", ""), }, }, { name: "long name", set: mkStatefulSet(&validPodTemplate, tweakName(strings.Repeat("a", 64))), errs: field.ErrorList{ field.Invalid(field.NewPath("metadata", "name"), strings.Repeat("a", 64), ""), }, }, { name: "missing-namespace", set: mkStatefulSet(&validPodTemplate, tweakNamespace("")), errs: field.ErrorList{ field.Required(field.NewPath("metadata", "namespace"), ""), }, }, { name: "empty selector", set: mkStatefulSet(&validPodTemplate, tweakSelectorLabels(nil)), errs: field.ErrorList{ field.Required(field.NewPath("spec", "selector"), ""), field.Invalid(field.NewPath("spec", "template", "metadata", "labels"), nil, ""), // selector is empty, labels are not, so select doesn't match labels }, }, { name: "selector_doesnt_match", set: mkStatefulSet(&validPodTemplate, tweakSelectorLabels(map[string]string{"foo": "bar"})), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "template", "metadata", "labels"), nil, ""), }, }, { name: "negative_replicas", set: mkStatefulSet(&validPodTemplate, tweakReplicas(-1)), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "replicas"), nil, ""), }, }, { name: "invalid_label", set: mkStatefulSet(&validPodTemplate, tweakLabels("NoUppercaseOrSpecialCharsLike=Equals", "bar"), ), errs: field.ErrorList{ field.Invalid(field.NewPath("metadata", "labels"), nil, ""), }, }, { name: "invalid_label 2", set: mkStatefulSet(&invalidPodTemplate, tweakLabels("NoUppercaseOrSpecialCharsLike=Equals", "bar"), tweakSelectorLabels(invalidLabels), ), errs: field.ErrorList{ field.Invalid(field.NewPath("metadata", "labels"), nil, ""), field.Invalid(field.NewPath("spec", "selector"), nil, ""), field.Invalid(field.NewPath("spec", "selector", "matchLabels"), nil, ""), }, }, { name: "invalid_annotation", set: mkStatefulSet(&validPodTemplate, tweakAnnotations("NoUppercaseOrSpecialCharsLike=Equals", "bar"), ), errs: field.ErrorList{ field.Invalid(field.NewPath("metadata", "annotations"), nil, ""), }, }, { name: "invalid restart policy 1", set: mkStatefulSet(&validPodTemplate, tweakTemplateRestartPolicy(api.RestartPolicyOnFailure)), errs: field.ErrorList{ field.NotSupported[string](field.NewPath("spec", "template", "spec", "restartPolicy"), nil, nil), }, }, { name: "invalid restart policy 2", set: mkStatefulSet(&validPodTemplate, tweakTemplateRestartPolicy(api.RestartPolicyNever)), errs: field.ErrorList{ field.NotSupported[string](field.NewPath("spec", "template", "spec", "restartPolicy"), nil, nil), }, }, { name: "empty restart policy", set: mkStatefulSet(&validPodTemplate, tweakTemplateRestartPolicy("")), errs: field.ErrorList{ field.NotSupported[string](field.NewPath("spec", "template", "spec", "restartPolicy"), nil, nil), }, }, { name: "invalid update strategy", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakUpdateStrategyType("foo"), ), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "updateStrategy"), nil, ""), }, }, { name: "empty update strategy", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakUpdateStrategyType(""), ), errs: field.ErrorList{ field.Required(field.NewPath("spec", "updateStrategy"), ""), }, }, { name: "invalid rolling update", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakUpdateStrategyType(apps.OnDeleteStatefulSetStrategyType), tweakRollingUpdatePartition(1), ), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "updateStrategy", "rollingUpdate"), nil, ""), }, }, { name: "negative parition", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakRollingUpdatePartition(-1), ), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "updateStrategy", "rollingUpdate", "partition"), nil, ""), }, }, { name: "empty pod management policy", set: mkStatefulSet(&validPodTemplate, tweakPodManagementPolicy(""), tweakReplicas(3), ), errs: field.ErrorList{ field.Required(field.NewPath("spec", "podManagementPolicy"), ""), }, }, { name: "invalid pod management policy", set: mkStatefulSet(&validPodTemplate, tweakPodManagementPolicy("foo")), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "podManagementPolicy"), nil, ""), }, }, { name: "set active deadline seconds", set: mkStatefulSet(&invalidPodTemplate2, tweakReplicas(3)), errs: field.ErrorList{ field.Forbidden(field.NewPath("spec", "template", "spec", "activeDeadlineSeconds"), ""), }, }, { name: "empty PersistentVolumeClaimRetentionPolicy " + enableStatefulSetAutoDeletePVC, set: mkStatefulSet(&validPodTemplate, tweakPVCPolicy(mkPVCPolicy()), ), errs: field.ErrorList{ field.NotSupported[string](field.NewPath("spec", "persistentVolumeClaimRetentionPolicy", "whenDeleted"), nil, nil), field.NotSupported[string](field.NewPath("spec", "persistentVolumeClaimRetentionPolicy", "whenScaled"), nil, nil), }, }, { name: "invalid PersistentVolumeClaimRetentionPolicy " + enableStatefulSetAutoDeletePVC, set: mkStatefulSet(&validPodTemplate, tweakPVCPolicy(mkPVCPolicy( tweakPVCDeletedPolicy("invalid-retention-policy"), tweakPVCScalePolicy("invalid-retention-policy"), )), ), errs: field.ErrorList{ field.NotSupported[string](field.NewPath("spec", "persistentVolumeClaimRetentionPolicy", "whenDeleted"), nil, nil), field.NotSupported[string](field.NewPath("spec", "persistentVolumeClaimRetentionPolicy", "whenScaled"), nil, nil), }, }, { name: "zero maxUnavailable", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakUpdateStrategyType(apps.RollingUpdateStatefulSetStrategyType), tweakMaxUnavailable(intstr.FromInt32(0)), ), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "updateStrategy", "rollingUpdate", "maxUnavailable"), nil, ""), }, }, { name: "zero percent maxUnavailable", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakUpdateStrategyType(apps.RollingUpdateStatefulSetStrategyType), tweakMaxUnavailable(intstr.FromString("0%")), ), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "updateStrategy", "rollingUpdate", "maxUnavailable"), nil, ""), }, }, { name: "greater than 100 percent maxUnavailable", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakUpdateStrategyType(apps.RollingUpdateStatefulSetStrategyType), tweakMaxUnavailable(intstr.FromString("101%")), ), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "updateStrategy", "rollingUpdate", "maxUnavailable"), nil, ""), }, }, { name: "invalid ordinals.start", set: mkStatefulSet(&validPodTemplate, tweakReplicas(3), tweakOrdinalsStart(-2), ), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "ordinals.start"), nil, ""), }, }, } cmpOpts := []cmp.Option{cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail"), cmpopts.SortSlices(func(a, b *field.Error) bool { return a.Error() < b.Error() })} for _, testCase := range append(successCases, errorCases...) { name := testCase.name var testTitle string if len(testCase.errs) == 0 { testTitle = fmt.Sprintf("success case %s", name) } else { testTitle = fmt.Sprintf("error case %s", name) } t.Run(testTitle, func(t *testing.T) { if strings.Contains(name, enableStatefulSetAutoDeletePVC) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.StatefulSetAutoDeletePVC, true)() } errs := ValidateStatefulSet(&testCase.set, pod.GetValidationOptionsFromPodTemplate(&testCase.set.Spec.Template, nil)) wantErrs := testCase.errs if diff := cmp.Diff(wantErrs, errs, cmpOpts...); diff != "" { t.Errorf("Unexpected validation errors (-want,+got):\n%s", diff) } }) } } // generateStatefulSetSpec generates a valid StatefulSet spec func generateStatefulSetSpec(minSeconds int32) *apps.StatefulSetSpec { labels := map[string]string{"a": "b"} podTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: labels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, } ss := &apps.StatefulSetSpec{ PodManagementPolicy: "OrderedReady", Selector: &metav1.LabelSelector{MatchLabels: labels}, Template: podTemplate.Template, Replicas: 3, UpdateStrategy: apps.StatefulSetUpdateStrategy{Type: apps.RollingUpdateStatefulSetStrategyType}, MinReadySeconds: minSeconds, } return ss } // TestValidateStatefulSetMinReadySeconds tests the StatefulSet Spec's minReadySeconds field func TestValidateStatefulSetMinReadySeconds(t *testing.T) { testCases := map[string]struct { ss *apps.StatefulSetSpec expectErr bool }{ "valid : minReadySeconds enabled, zero": { ss: generateStatefulSetSpec(0), expectErr: false, }, "invalid : minReadySeconds enabled, negative": { ss: generateStatefulSetSpec(-1), expectErr: true, }, "valid : minReadySeconds enabled, very large value": { ss: generateStatefulSetSpec(2147483647), expectErr: false, }, "invalid : minReadySeconds enabled, large negative": { ss: generateStatefulSetSpec(-2147483648), expectErr: true, }, } for tcName, tc := range testCases { t.Run(tcName, func(t *testing.T) { errs := ValidateStatefulSetSpec(tc.ss, field.NewPath("spec", "minReadySeconds"), corevalidation.PodValidationOptions{}) if tc.expectErr && len(errs) == 0 { t.Errorf("Unexpected success") } if !tc.expectErr && len(errs) != 0 { t.Errorf("Unexpected error(s): %v", errs) } }) } } func TestValidateStatefulSetStatus(t *testing.T) { observedGenerationMinusOne := int64(-1) collisionCountMinusOne := int32(-1) tests := []struct { name string replicas int32 readyReplicas int32 currentReplicas int32 updatedReplicas int32 availableReplicas int32 observedGeneration *int64 collisionCount *int32 expectedErr bool }{{ name: "valid status", replicas: 3, readyReplicas: 3, currentReplicas: 2, updatedReplicas: 1, expectedErr: false, }, { name: "invalid replicas", replicas: -1, readyReplicas: 3, currentReplicas: 2, updatedReplicas: 1, expectedErr: true, }, { name: "invalid readyReplicas", replicas: 3, readyReplicas: -1, currentReplicas: 2, updatedReplicas: 1, expectedErr: true, }, { name: "invalid currentReplicas", replicas: 3, readyReplicas: 3, currentReplicas: -1, updatedReplicas: 1, expectedErr: true, }, { name: "invalid updatedReplicas", replicas: 3, readyReplicas: 3, currentReplicas: 2, updatedReplicas: -1, expectedErr: true, }, { name: "invalid observedGeneration", replicas: 3, readyReplicas: 3, currentReplicas: 2, updatedReplicas: 1, observedGeneration: &observedGenerationMinusOne, expectedErr: true, }, { name: "invalid collisionCount", replicas: 3, readyReplicas: 3, currentReplicas: 2, updatedReplicas: 1, collisionCount: &collisionCountMinusOne, expectedErr: true, }, { name: "readyReplicas greater than replicas", replicas: 3, readyReplicas: 4, currentReplicas: 2, updatedReplicas: 1, expectedErr: true, }, { name: "currentReplicas greater than replicas", replicas: 3, readyReplicas: 3, currentReplicas: 4, updatedReplicas: 1, expectedErr: true, }, { name: "updatedReplicas greater than replicas", replicas: 3, readyReplicas: 3, currentReplicas: 2, updatedReplicas: 4, expectedErr: true, }, { name: "invalid: number of available replicas", replicas: 3, readyReplicas: 3, currentReplicas: 2, availableReplicas: int32(-1), expectedErr: true, }, { name: "invalid: available replicas greater than replicas", replicas: 3, readyReplicas: 3, currentReplicas: 2, availableReplicas: int32(4), expectedErr: true, }, { name: "invalid: available replicas greater than ready replicas", replicas: 3, readyReplicas: 2, currentReplicas: 2, availableReplicas: int32(3), expectedErr: true, }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { status := apps.StatefulSetStatus{ Replicas: test.replicas, ReadyReplicas: test.readyReplicas, CurrentReplicas: test.currentReplicas, UpdatedReplicas: test.updatedReplicas, ObservedGeneration: test.observedGeneration, CollisionCount: test.collisionCount, AvailableReplicas: test.availableReplicas, } errs := ValidateStatefulSetStatus(&status, field.NewPath("status")) if hasErr := len(errs) > 0; hasErr != test.expectedErr { t.Errorf("%s: expected error: %t, got error: %t\nerrors: %s", test.name, test.expectedErr, hasErr, errs.ToAggregate().Error()) } }) } } func TestValidateStatefulSetUpdate(t *testing.T) { validLabels := map[string]string{"a": "b"} validLabels2 := map[string]string{"c": "d"} validPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, } validPodTemplate2 := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels2, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent"}}, }, }, } storageClass := "storage-class1" storageClass2 := "storage-class2" validPVCTemplate := api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "pvc-abc", }, Spec: api.PersistentVolumeClaimSpec{ StorageClassName: &storageClass, Resources: api.VolumeResourceRequirements{ Requests: api.ResourceList{ api.ResourceStorage: resource.MustParse("1Gi"), }, }, }, } validPVCTemplateChangedSize := *validPVCTemplate.DeepCopy() validPVCTemplateChangedSize.Spec.Resources.Requests[api.ResourceStorage] = resource.MustParse("2Gi") validPVCTemplateChangedClass := *validPVCTemplate.DeepCopy() validPVCTemplateChangedClass.Spec.StorageClassName = &storageClass2 validPVCTemplate2 := api.PersistentVolumeClaim{ ObjectMeta: metav1.ObjectMeta{ Name: "pvc-abc2", }, Spec: api.PersistentVolumeClaimSpec{ StorageClassName: &storageClass2, Resources: api.VolumeResourceRequirements{ Requests: api.ResourceList{ api.ResourceStorage: resource.MustParse("2Gi"), }, }, }, } addContainersValidTemplate := validPodTemplate.DeepCopy() addContainersValidTemplate.Template.Spec.Containers = append(addContainersValidTemplate.Template.Spec.Containers, api.Container{Name: "def", Image: "image2", ImagePullPolicy: "IfNotPresent"}) if len(addContainersValidTemplate.Template.Spec.Containers) != len(validPodTemplate.Template.Spec.Containers)+1 { t.Errorf("failure during test setup: template %v should have more containers than template %v", addContainersValidTemplate, validPodTemplate) } type testCase struct { name string old apps.StatefulSet update apps.StatefulSet errs field.ErrorList } successCases := []testCase{{ name: "update replica count", old: mkStatefulSet(&validPodTemplate), update: mkStatefulSet(&validPodTemplate, tweakReplicas(3)), }, { name: "update containers 1", old: mkStatefulSet(&validPodTemplate), update: mkStatefulSet(addContainersValidTemplate), }, { name: "update containers 2", old: mkStatefulSet(addContainersValidTemplate), update: mkStatefulSet(&validPodTemplate), }, { name: "update containers and pvc retention policy 1", old: mkStatefulSet(addContainersValidTemplate), update: mkStatefulSet(&validPodTemplate, tweakPVCPolicy(mkPVCPolicy( tweakPVCDeletedPolicy(apps.RetainPersistentVolumeClaimRetentionPolicyType), tweakPVCScalePolicy(apps.RetainPersistentVolumeClaimRetentionPolicyType), )), ), }, { name: "update containers and pvc retention policy 2", old: mkStatefulSet(&validPodTemplate, tweakPVCPolicy(mkPVCPolicy( tweakPVCScalePolicy(apps.RetainPersistentVolumeClaimRetentionPolicyType), )), ), update: mkStatefulSet(&validPodTemplate), }, { name: "update update strategy", old: mkStatefulSet(&validPodTemplate), update: mkStatefulSet(&validPodTemplate, tweakUpdateStrategyType(apps.OnDeleteStatefulSetStrategyType), ), }, { name: "update min ready seconds 1", old: mkStatefulSet(&validPodTemplate), update: mkStatefulSet(&validPodTemplate, tweakMinReadySeconds(10)), }, { name: "update min ready seconds 2", old: mkStatefulSet(&validPodTemplate, tweakMinReadySeconds(5)), update: mkStatefulSet(&validPodTemplate, tweakMinReadySeconds(10)), }, { name: "update existing instance with now-invalid name", old: mkStatefulSet(&validPodTemplate, tweakFinalizers("final")), update: mkStatefulSet(&validPodTemplate, tweakFinalizers()), }, { name: "update existing instance with .spec.ordinals.start", old: mkStatefulSet(&validPodTemplate), update: mkStatefulSet(&validPodTemplate, tweakOrdinalsStart(3)), }, } errorCases := []testCase{{ name: "update name", old: mkStatefulSet(&validPodTemplate, tweakName("abc")), update: mkStatefulSet(&validPodTemplate, tweakName("abc2")), errs: field.ErrorList{ field.Invalid(field.NewPath("metadata", "name"), nil, ""), }, }, { name: "update namespace", old: mkStatefulSet(&validPodTemplate, tweakNamespace(metav1.NamespaceDefault)), update: mkStatefulSet(&validPodTemplate, tweakNamespace(metav1.NamespaceDefault+"1")), errs: field.ErrorList{ field.Invalid(field.NewPath("metadata", "namespace"), nil, ""), }, }, { name: "update selector", old: mkStatefulSet(&validPodTemplate, tweakSelectorLabels(validLabels)), update: mkStatefulSet(&validPodTemplate2, tweakSelectorLabels(validLabels2), ), errs: field.ErrorList{ field.Forbidden(field.NewPath("spec"), ""), }, }, { name: "update pod management policy 1", old: mkStatefulSet(&validPodTemplate, tweakPodManagementPolicy("")), update: mkStatefulSet(&validPodTemplate, tweakPodManagementPolicy(apps.OrderedReadyPodManagement)), errs: field.ErrorList{ field.Forbidden(field.NewPath("spec"), ""), }, }, { name: "update pod management policy 2", old: mkStatefulSet(&validPodTemplate, tweakPodManagementPolicy(apps.ParallelPodManagement)), update: mkStatefulSet(&validPodTemplate, tweakPodManagementPolicy(apps.OrderedReadyPodManagement)), errs: field.ErrorList{ field.Forbidden(field.NewPath("spec"), ""), }, }, { name: "update to negative replicas", old: mkStatefulSet(&validPodTemplate), update: mkStatefulSet(&validPodTemplate, tweakReplicas(-1)), errs: field.ErrorList{ field.Invalid(field.NewPath("spec", "replicas"), nil, ""), }, }, { name: "update pvc template size", old: mkStatefulSet(&validPodTemplate, tweakPVCTemplate(validPVCTemplate)), update: mkStatefulSet(&validPodTemplate, tweakPVCTemplate(validPVCTemplateChangedSize)), errs: field.ErrorList{ field.Forbidden(field.NewPath("spec"), ""), }, }, { name: "update pvc template storage class", old: mkStatefulSet(&validPodTemplate, tweakPVCTemplate(validPVCTemplate)), update: mkStatefulSet(&validPodTemplate, tweakPVCTemplate(validPVCTemplateChangedClass)), errs: field.ErrorList{ field.Forbidden(field.NewPath("spec"), ""), }, }, { name: "add new pvc template", old: mkStatefulSet(&validPodTemplate, tweakPVCTemplate(validPVCTemplate)), update: mkStatefulSet(&validPodTemplate, tweakPVCTemplate(validPVCTemplate, validPVCTemplate2)), errs: field.ErrorList{ field.Forbidden(field.NewPath("spec"), ""), }, }, } cmpOpts := []cmp.Option{ cmpopts.IgnoreFields(field.Error{}, "BadValue", "Detail"), cmpopts.SortSlices(func(a, b *field.Error) bool { return a.Error() < b.Error() }), cmpopts.EquateEmpty(), } for _, testCase := range append(successCases, errorCases...) { name := testCase.name var testTitle string if len(testCase.errs) == 0 { testTitle = fmt.Sprintf("success case %s", name) } else { testTitle = fmt.Sprintf("error case %s", name) } t.Run(testTitle, func(t *testing.T) { testCase.old.ObjectMeta.ResourceVersion = "1" testCase.update.ObjectMeta.ResourceVersion = "1" errs := ValidateStatefulSetUpdate(&testCase.update, &testCase.old, pod.GetValidationOptionsFromPodTemplate(&testCase.update.Spec.Template, &testCase.old.Spec.Template)) wantErrs := testCase.errs if diff := cmp.Diff(wantErrs, errs, cmpOpts...); diff != "" { t.Errorf("Unexpected validation errors (-want,+got):\n%s", diff) } }) } } func TestValidateControllerRevision(t *testing.T) { newControllerRevision := func(name, namespace string, data runtime.Object, revision int64) apps.ControllerRevision { return apps.ControllerRevision{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, }, Data: data, Revision: revision, } } podTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, } ss := mkStatefulSet(&podTemplate) var ( valid = newControllerRevision("validname", "validns", &ss, 0) badRevision = newControllerRevision("validname", "validns", &ss, -1) emptyName = newControllerRevision("", "validns", &ss, 0) invalidName = newControllerRevision("NoUppercaseOrSpecialCharsLike=Equals", "validns", &ss, 0) emptyNs = newControllerRevision("validname", "", &ss, 100) invalidNs = newControllerRevision("validname", "NoUppercaseOrSpecialCharsLike=Equals", &ss, 100) nilData = newControllerRevision("validname", "NoUppercaseOrSpecialCharsLike=Equals", nil, 100) ) tests := map[string]struct { history apps.ControllerRevision isValid bool }{ "valid": {valid, true}, "negative revision": {badRevision, false}, "empty name": {emptyName, false}, "invalid name": {invalidName, false}, "empty namespace": {emptyNs, false}, "invalid namespace": {invalidNs, false}, "nil data": {nilData, false}, } for name, tc := range tests { t.Run(name, func(t *testing.T) { errs := ValidateControllerRevision(&tc.history) if tc.isValid && len(errs) > 0 { t.Errorf("%v: unexpected error: %v", name, errs) } if !tc.isValid && len(errs) == 0 { t.Errorf("%v: unexpected non-error", name) } }) } } func TestValidateControllerRevisionUpdate(t *testing.T) { newControllerRevision := func(version, name, namespace string, data runtime.Object, revision int64) apps.ControllerRevision { return apps.ControllerRevision{ ObjectMeta: metav1.ObjectMeta{ Name: name, Namespace: namespace, ResourceVersion: version, }, Data: data, Revision: revision, } } podTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: metav1.ObjectMeta{ Labels: map[string]string{"a": "b"}, }, }, } ss := mkStatefulSet(&podTemplate, tweakName("abc")) modifiedss := mkStatefulSet(&podTemplate, tweakName("cdf")) var ( valid = newControllerRevision("1", "validname", "validns", &ss, 0) noVersion = newControllerRevision("", "validname", "validns", &ss, 0) changedData = newControllerRevision("1", "validname", "validns", &modifiedss, 0) changedRevision = newControllerRevision("1", "validname", "validns", &ss, 1) ) cases := []struct { name string newHistory apps.ControllerRevision oldHistory apps.ControllerRevision isValid bool }{{ name: "valid", newHistory: valid, oldHistory: valid, isValid: true, }, { name: "invalid", newHistory: noVersion, oldHistory: valid, isValid: false, }, { name: "changed data", newHistory: changedData, oldHistory: valid, isValid: false, }, { name: "changed revision", newHistory: changedRevision, oldHistory: valid, isValid: true, }, } for _, tc := range cases { t.Run(tc.name, func(t *testing.T) { errs := ValidateControllerRevisionUpdate(&tc.newHistory, &tc.oldHistory) if tc.isValid && len(errs) > 0 { t.Errorf("%v: unexpected error: %v", tc.name, errs) } if !tc.isValid && len(errs) == 0 { t.Errorf("%v: unexpected non-error", tc.name) } }) } } func TestValidateDaemonSetStatusUpdate(t *testing.T) { type dsUpdateTest struct { old apps.DaemonSet update apps.DaemonSet } successCases := []dsUpdateTest{{ old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 1, DesiredNumberScheduled: 3, NumberReady: 1, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, }, } for _, successCase := range successCases { successCase.old.ObjectMeta.ResourceVersion = "1" successCase.update.ObjectMeta.ResourceVersion = "1" if errs := ValidateDaemonSetStatusUpdate(&successCase.update, &successCase.old); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } errorCases := map[string]dsUpdateTest{ "negative values": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: -1, NumberMisscheduled: -1, DesiredNumberScheduled: -3, NumberReady: -1, ObservedGeneration: -3, UpdatedNumberScheduled: -1, NumberAvailable: -1, NumberUnavailable: -2, }, }, }, "negative CurrentNumberScheduled": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: -1, NumberMisscheduled: 1, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, }, "negative NumberMisscheduled": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: -1, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, }, "negative DesiredNumberScheduled": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 1, DesiredNumberScheduled: -3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, }, "negative NumberReady": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 1, DesiredNumberScheduled: 3, NumberReady: -1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, }, "negative ObservedGeneration": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 1, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: -3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, }, "negative UpdatedNumberScheduled": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 1, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: -1, NumberAvailable: 1, NumberUnavailable: 2, }, }, }, "negative NumberAvailable": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 1, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: -1, NumberUnavailable: 2, }, }, }, "negative NumberUnavailable": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 2, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: 2, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, ResourceVersion: "10", }, Status: apps.DaemonSetStatus{ CurrentNumberScheduled: 1, NumberMisscheduled: 1, DesiredNumberScheduled: 3, NumberReady: 1, ObservedGeneration: 3, UpdatedNumberScheduled: 1, NumberAvailable: 1, NumberUnavailable: -2, }, }, }, } for testName, errorCase := range errorCases { if errs := ValidateDaemonSetStatusUpdate(&errorCase.update, &errorCase.old); len(errs) == 0 { t.Errorf("expected failure: %s", testName) } } } func TestValidateDaemonSetUpdate(t *testing.T) { validSelector := map[string]string{"a": "b"} validSelector2 := map[string]string{"c": "d"} invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} validPodSpecAbc := api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, } validPodSpecDef := api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "def", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, } validPodSpecNodeSelector := api.PodSpec{ NodeSelector: validSelector, NodeName: "xyz", RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, } validPodSpecVolume := api.PodSpec{ Volumes: []api.Volume{{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}}, RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, } validPodTemplateAbc := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, Spec: validPodSpecAbc, }, } validPodTemplateAbcSemanticallyEqual := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, Spec: validPodSpecAbc, }, } validPodTemplateAbcSemanticallyEqual.Template.Spec.ImagePullSecrets = []api.LocalObjectReference{} validPodTemplateNodeSelector := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, Spec: validPodSpecNodeSelector, }, } validPodTemplateAbc2 := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector2, }, Spec: validPodSpecAbc, }, } validPodTemplateDef := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector2, }, Spec: validPodSpecDef, }, } invalidPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ Spec: api.PodSpec{ // no containers specified RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, }, } readWriteVolumePodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, Spec: validPodSpecVolume, }, } type dsUpdateTest struct { old apps.DaemonSet update apps.DaemonSet expectedErrNum int enableSkipReadOnlyValidationGCE bool } successCases := map[string]dsUpdateTest{ "no change": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, }, "change template and selector": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 2, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector2}, TemplateGeneration: 3, Template: validPodTemplateAbc2.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, }, "change template": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 3, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 4, Template: validPodTemplateNodeSelector.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, }, "change container image name": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector2}, TemplateGeneration: 2, Template: validPodTemplateDef.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, }, "change update strategy": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 4, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 4, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.RollingUpdateDaemonSetStrategyType, RollingUpdate: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromInt32(1), }, }, }, }, }, "unchanged templateGeneration upon semantically equal template update": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 4, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 4, Template: validPodTemplateAbcSemanticallyEqual.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.RollingUpdateDaemonSetStrategyType, RollingUpdate: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromInt32(1), }, }, }, }, }, "Read-write volume verification": { enableSkipReadOnlyValidationGCE: true, old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 2, Template: readWriteVolumePodTemplate.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, }, } for testName, successCase := range successCases { t.Run(testName, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SkipReadOnlyValidationGCE, successCase.enableSkipReadOnlyValidationGCE)() // ResourceVersion is required for updates. successCase.old.ObjectMeta.ResourceVersion = "1" successCase.update.ObjectMeta.ResourceVersion = "2" // Check test setup if successCase.expectedErrNum > 0 { t.Errorf("%q has incorrect test setup with expectedErrNum %d, expected no error", testName, successCase.expectedErrNum) } if len(successCase.old.ObjectMeta.ResourceVersion) == 0 || len(successCase.update.ObjectMeta.ResourceVersion) == 0 { t.Errorf("%q has incorrect test setup with no resource version set", testName) } if errs := ValidateDaemonSetUpdate(&successCase.update, &successCase.old, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("%q expected no error, but got: %v", testName, errs) } }) } errorCases := map[string]dsUpdateTest{ "change daemon name": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, expectedErrNum: 1, }, "invalid selector": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: invalidSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, expectedErrNum: 1, }, "invalid pod": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 2, Template: invalidPodTemplate.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, expectedErrNum: 1, }, "invalid read-write volume": { enableSkipReadOnlyValidationGCE: false, old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 2, Template: readWriteVolumePodTemplate.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, expectedErrNum: 1, }, "invalid update strategy": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: 1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: "Random", }, }, }, expectedErrNum: 1, }, "negative templateGeneration": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: -1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, TemplateGeneration: -1, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, expectedErrNum: 1, }, "decreased templateGeneration": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ TemplateGeneration: 2, Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ TemplateGeneration: 1, Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, expectedErrNum: 1, }, "unchanged templateGeneration upon template update": { old: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ TemplateGeneration: 2, Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validPodTemplateAbc.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, update: apps.DaemonSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ TemplateGeneration: 2, Selector: &metav1.LabelSelector{MatchLabels: validSelector2}, Template: validPodTemplateAbc2.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, expectedErrNum: 1, }, } for testName, errorCase := range errorCases { t.Run(testName, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SkipReadOnlyValidationGCE, errorCase.enableSkipReadOnlyValidationGCE)() // ResourceVersion is required for updates. errorCase.old.ObjectMeta.ResourceVersion = "1" errorCase.update.ObjectMeta.ResourceVersion = "2" // Check test setup if errorCase.expectedErrNum <= 0 { t.Errorf("%q has incorrect test setup with expectedErrNum %d, expected at least one error", testName, errorCase.expectedErrNum) } if len(errorCase.old.ObjectMeta.ResourceVersion) == 0 || len(errorCase.update.ObjectMeta.ResourceVersion) == 0 { t.Errorf("%q has incorrect test setup with no resource version set", testName) } // Run the tests if errs := ValidateDaemonSetUpdate(&errorCase.update, &errorCase.old, corevalidation.PodValidationOptions{}); len(errs) != errorCase.expectedErrNum { t.Errorf("%q expected %d errors, but got %d error: %v", testName, errorCase.expectedErrNum, len(errs), errs) } else { t.Logf("(PASS) %q got errors %v", testName, errs) } }) } } func TestValidateDaemonSet(t *testing.T) { validSelector := map[string]string{"a": "b"} validPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, }, } validHostNetPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostNetwork: true, }, RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{ Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile, Ports: []api.ContainerPort{{ ContainerPort: 12345, Protocol: api.ProtocolTCP, }}, }}, }, }, } invalidSelector := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: metav1.ObjectMeta{ Labels: invalidSelector, }, }, } successCases := []apps.DaemonSet{{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validPodTemplate.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validPodTemplate.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "hostnet", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validHostNetPodTemplate.Template, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, } for _, successCase := range successCases { if errs := ValidateDaemonSet(&successCase, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } errorCases := map[string]apps.DaemonSet{ "zero-length ID": { ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validPodTemplate.Template, }, }, "missing-namespace": { ObjectMeta: metav1.ObjectMeta{Name: "abc-123"}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validPodTemplate.Template, }, }, "nil selector": { ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Template: validPodTemplate.Template, }, }, "empty selector": { ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{}, Template: validPodTemplate.Template, }, }, "selector_doesnt_match": { ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, Template: validPodTemplate.Template, }, }, "invalid template": { ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, }, }, "invalid_label": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, Labels: map[string]string{ "NoUppercaseOrSpecialCharsLike=Equals": "bar", }, }, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validPodTemplate.Template, }, }, "invalid_label 2": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, Labels: map[string]string{ "NoUppercaseOrSpecialCharsLike=Equals": "bar", }, }, Spec: apps.DaemonSetSpec{ Template: invalidPodTemplate.Template, }, }, "invalid_annotation": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, Annotations: map[string]string{ "NoUppercaseOrSpecialCharsLike=Equals": "bar", }, }, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: validPodTemplate.Template, }, }, "invalid restart policy 1": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, }, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, }, }, }, "invalid restart policy 2": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, }, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyNever, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, }, }, }, "template may not contain ephemeral containers": { ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DaemonSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validSelector}, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validSelector, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, EphemeralContainers: []api.EphemeralContainer{{EphemeralContainerCommon: api.EphemeralContainerCommon{Name: "debug", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}}}, }, }, UpdateStrategy: apps.DaemonSetUpdateStrategy{ Type: apps.OnDeleteDaemonSetStrategyType, }, }, }, } for k, v := range errorCases { errs := ValidateDaemonSet(&v, corevalidation.PodValidationOptions{}) if len(errs) == 0 { t.Errorf("expected failure for %s", k) } for i := range errs { field := errs[i].Field if !strings.HasPrefix(field, "spec.template.") && !strings.HasPrefix(field, "spec.updateStrategy") && field != "metadata.name" && field != "metadata.namespace" && field != "spec.selector" && field != "spec.template" && field != "GCEPersistentDisk.ReadOnly" && field != "spec.template.labels" && field != "metadata.annotations" && field != "metadata.labels" { t.Errorf("%s: missing prefix for: %v", k, errs[i]) } } } } func validDeployment(tweaks ...func(d *apps.Deployment)) *apps.Deployment { d := &apps.Deployment{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, }, Spec: apps.DeploymentSpec{ Selector: &metav1.LabelSelector{ MatchLabels: map[string]string{ "name": "abc", }, }, Strategy: apps.DeploymentStrategy{ Type: apps.RollingUpdateDeploymentStrategyType, RollingUpdate: &apps.RollingUpdateDeployment{ MaxSurge: intstr.FromInt32(1), MaxUnavailable: intstr.FromInt32(1), }, }, Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Name: "abc", Namespace: metav1.NamespaceDefault, Labels: map[string]string{ "name": "abc", }, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSDefault, Containers: []api.Container{{ Name: "nginx", Image: "image", ImagePullPolicy: api.PullNever, TerminationMessagePolicy: api.TerminationMessageReadFile, }}, }, }, RollbackTo: &apps.RollbackConfig{ Revision: 1, }, }, } for _, tweak := range tweaks { tweak(d) } return d } func TestValidateDeployment(t *testing.T) { successCases := []*apps.Deployment{ validDeployment(), validDeployment(func(d *apps.Deployment) { d.Spec.Template.Spec.SecurityContext = &api.PodSecurityContext{ HostNetwork: true, } d.Spec.Template.Spec.Containers[0].Ports = []api.ContainerPort{{ ContainerPort: 12345, Protocol: api.ProtocolTCP, }} }), } for _, successCase := range successCases { if errs := ValidateDeployment(successCase, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } errorCases := map[string]*apps.Deployment{} errorCases["metadata.name: Required value"] = &apps.Deployment{ ObjectMeta: metav1.ObjectMeta{ Namespace: metav1.NamespaceDefault, }, } // selector should match the labels in pod template. invalidSelectorDeployment := validDeployment() invalidSelectorDeployment.Spec.Selector = &metav1.LabelSelector{ MatchLabels: map[string]string{ "name": "def", }, } errorCases["`selector` does not match template `labels`"] = invalidSelectorDeployment // RestartPolicy should be always. invalidRestartPolicyDeployment := validDeployment() invalidRestartPolicyDeployment.Spec.Template.Spec.RestartPolicy = api.RestartPolicyNever errorCases["Unsupported value: \"Never\""] = invalidRestartPolicyDeployment // must have valid strategy type invalidStrategyDeployment := validDeployment() invalidStrategyDeployment.Spec.Strategy.Type = apps.DeploymentStrategyType("randomType") errorCases[`supported values: "Recreate", "RollingUpdate"`] = invalidStrategyDeployment // rollingUpdate should be nil for recreate. invalidRecreateDeployment := validDeployment() invalidRecreateDeployment.Spec.Strategy = apps.DeploymentStrategy{ Type: apps.RecreateDeploymentStrategyType, RollingUpdate: &apps.RollingUpdateDeployment{}, } errorCases["may not be specified when strategy `type` is 'Recreate'"] = invalidRecreateDeployment // MaxSurge should be in the form of 20%. invalidMaxSurgeDeployment := validDeployment() invalidMaxSurgeDeployment.Spec.Strategy = apps.DeploymentStrategy{ Type: apps.RollingUpdateDeploymentStrategyType, RollingUpdate: &apps.RollingUpdateDeployment{ MaxSurge: intstr.FromString("20Percent"), }, } errorCases["a valid percent string must be"] = invalidMaxSurgeDeployment // MaxSurge and MaxUnavailable cannot both be zero. invalidRollingUpdateDeployment := validDeployment() invalidRollingUpdateDeployment.Spec.Strategy = apps.DeploymentStrategy{ Type: apps.RollingUpdateDeploymentStrategyType, RollingUpdate: &apps.RollingUpdateDeployment{ MaxSurge: intstr.FromString("0%"), MaxUnavailable: intstr.FromInt32(0), }, } errorCases["may not be 0 when `maxSurge` is 0"] = invalidRollingUpdateDeployment // MaxUnavailable should not be more than 100%. invalidMaxUnavailableDeployment := validDeployment() invalidMaxUnavailableDeployment.Spec.Strategy = apps.DeploymentStrategy{ Type: apps.RollingUpdateDeploymentStrategyType, RollingUpdate: &apps.RollingUpdateDeployment{ MaxUnavailable: intstr.FromString("110%"), }, } errorCases["must not be greater than 100%"] = invalidMaxUnavailableDeployment // Rollback.Revision must be non-negative invalidRollbackRevisionDeployment := validDeployment() invalidRollbackRevisionDeployment.Spec.RollbackTo.Revision = -3 errorCases["must be greater than or equal to 0"] = invalidRollbackRevisionDeployment // ProgressDeadlineSeconds should be greater than MinReadySeconds invalidProgressDeadlineDeployment := validDeployment() seconds := int32(600) invalidProgressDeadlineDeployment.Spec.ProgressDeadlineSeconds = &seconds invalidProgressDeadlineDeployment.Spec.MinReadySeconds = seconds errorCases["must be greater than minReadySeconds"] = invalidProgressDeadlineDeployment // Must not have ephemeral containers invalidEphemeralContainersDeployment := validDeployment() invalidEphemeralContainersDeployment.Spec.Template.Spec.EphemeralContainers = []api.EphemeralContainer{{ EphemeralContainerCommon: api.EphemeralContainerCommon{ Name: "ec", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: "File"}, }} errorCases["ephemeral containers not allowed"] = invalidEphemeralContainersDeployment for k, v := range errorCases { errs := ValidateDeployment(v, corevalidation.PodValidationOptions{}) if len(errs) == 0 { t.Errorf("[%s] expected failure", k) } else if !strings.Contains(errs[0].Error(), k) { t.Errorf("unexpected error: %q, expected: %q", errs[0].Error(), k) } } } func TestValidateDeploymentStatus(t *testing.T) { collisionCount := int32(-3) tests := []struct { name string replicas int32 updatedReplicas int32 readyReplicas int32 availableReplicas int32 observedGeneration int64 collisionCount *int32 expectedErr bool }{{ name: "valid status", replicas: 3, updatedReplicas: 3, readyReplicas: 2, availableReplicas: 1, observedGeneration: 2, expectedErr: false, }, { name: "invalid replicas", replicas: -1, updatedReplicas: 2, readyReplicas: 2, availableReplicas: 1, observedGeneration: 2, expectedErr: true, }, { name: "invalid updatedReplicas", replicas: 2, updatedReplicas: -1, readyReplicas: 2, availableReplicas: 1, observedGeneration: 2, expectedErr: true, }, { name: "invalid readyReplicas", replicas: 3, readyReplicas: -1, availableReplicas: 1, observedGeneration: 2, expectedErr: true, }, { name: "invalid availableReplicas", replicas: 3, readyReplicas: 3, availableReplicas: -1, observedGeneration: 2, expectedErr: true, }, { name: "invalid observedGeneration", replicas: 3, readyReplicas: 3, availableReplicas: 3, observedGeneration: -1, expectedErr: true, }, { name: "updatedReplicas greater than replicas", replicas: 3, updatedReplicas: 4, readyReplicas: 3, availableReplicas: 3, observedGeneration: 1, expectedErr: true, }, { name: "readyReplicas greater than replicas", replicas: 3, readyReplicas: 4, availableReplicas: 3, observedGeneration: 1, expectedErr: true, }, { name: "availableReplicas greater than replicas", replicas: 3, readyReplicas: 3, availableReplicas: 4, observedGeneration: 1, expectedErr: true, }, { name: "availableReplicas greater than readyReplicas", replicas: 3, readyReplicas: 2, availableReplicas: 3, observedGeneration: 1, expectedErr: true, }, { name: "invalid collisionCount", replicas: 3, observedGeneration: 1, collisionCount: &collisionCount, expectedErr: true, }, } for _, test := range tests { status := apps.DeploymentStatus{ Replicas: test.replicas, UpdatedReplicas: test.updatedReplicas, ReadyReplicas: test.readyReplicas, AvailableReplicas: test.availableReplicas, ObservedGeneration: test.observedGeneration, CollisionCount: test.collisionCount, } errs := ValidateDeploymentStatus(&status, field.NewPath("status")) if hasErr := len(errs) > 0; hasErr != test.expectedErr { errString := dump.Pretty(errs) t.Errorf("%s: expected error: %t, got error: %t\nerrors: %s", test.name, test.expectedErr, hasErr, errString) } } } func TestValidateDeploymentStatusUpdate(t *testing.T) { collisionCount := int32(1) otherCollisionCount := int32(2) tests := []struct { name string from, to apps.DeploymentStatus expectedErr bool }{{ name: "increase: valid update", from: apps.DeploymentStatus{ CollisionCount: nil, }, to: apps.DeploymentStatus{ CollisionCount: &collisionCount, }, expectedErr: false, }, { name: "stable: valid update", from: apps.DeploymentStatus{ CollisionCount: &collisionCount, }, to: apps.DeploymentStatus{ CollisionCount: &collisionCount, }, expectedErr: false, }, { name: "unset: invalid update", from: apps.DeploymentStatus{ CollisionCount: &collisionCount, }, to: apps.DeploymentStatus{ CollisionCount: nil, }, expectedErr: true, }, { name: "decrease: invalid update", from: apps.DeploymentStatus{ CollisionCount: &otherCollisionCount, }, to: apps.DeploymentStatus{ CollisionCount: &collisionCount, }, expectedErr: true, }, } for _, test := range tests { meta := metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault, ResourceVersion: "1"} from := &apps.Deployment{ ObjectMeta: meta, Status: test.from, } to := &apps.Deployment{ ObjectMeta: meta, Status: test.to, } errs := ValidateDeploymentStatusUpdate(to, from) if hasErr := len(errs) > 0; hasErr != test.expectedErr { errString := dump.Pretty(errs) t.Errorf("%s: expected error: %t, got error: %t\nerrors: %s", test.name, test.expectedErr, hasErr, errString) } } } func validDeploymentRollback() *apps.DeploymentRollback { return &apps.DeploymentRollback{ Name: "abc", UpdatedAnnotations: map[string]string{ "created-by": "abc", }, RollbackTo: apps.RollbackConfig{ Revision: 1, }, } } func TestValidateDeploymentUpdate(t *testing.T) { validLabels := map[string]string{"a": "b"} validPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, }, } readWriteVolumePodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, Volumes: []api.Volume{{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}}, }, }, } invalidLabels := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ Spec: api.PodSpec{ // no containers specified RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: metav1.ObjectMeta{ Labels: invalidLabels, }, }, } type depUpdateTest struct { old apps.Deployment update apps.Deployment expectedErrNum int enableSkipReadOnlyValidationGCE bool } successCases := map[string]depUpdateTest{ "positive replicas": { old: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, update: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Replicas: 1, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: readWriteVolumePodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, }, "Read-write volume verification": { enableSkipReadOnlyValidationGCE: true, old: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, update: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Replicas: 2, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: readWriteVolumePodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, }, } for testName, successCase := range successCases { t.Run(testName, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SkipReadOnlyValidationGCE, successCase.enableSkipReadOnlyValidationGCE)() // ResourceVersion is required for updates. successCase.old.ObjectMeta.ResourceVersion = "1" successCase.update.ObjectMeta.ResourceVersion = "2" // Check test setup if successCase.expectedErrNum > 0 { t.Errorf("%q has incorrect test setup with expectedErrNum %d, expected no error", testName, successCase.expectedErrNum) } if len(successCase.old.ObjectMeta.ResourceVersion) == 0 || len(successCase.update.ObjectMeta.ResourceVersion) == 0 { t.Errorf("%q has incorrect test setup with no resource version set", testName) } // Run the tests if errs := ValidateDeploymentUpdate(&successCase.update, &successCase.old, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("%q expected no error, but got: %v", testName, errs) } }) errorCases := map[string]depUpdateTest{ "more than one read/write": { old: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, update: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Replicas: 2, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: readWriteVolumePodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, expectedErrNum: 2, }, "invalid selector": { old: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, update: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Replicas: 2, Selector: &metav1.LabelSelector{MatchLabels: invalidLabels}, Template: validPodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, expectedErrNum: 3, }, "invalid pod": { old: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, update: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Replicas: 2, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: invalidPodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, expectedErrNum: 4, }, "negative replicas": { old: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, update: apps.Deployment{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.DeploymentSpec{ Replicas: -1, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: readWriteVolumePodTemplate.Template, Strategy: apps.DeploymentStrategy{Type: apps.RecreateDeploymentStrategyType}, }, }, expectedErrNum: 1, }, } for testName, errorCase := range errorCases { t.Run(testName, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SkipReadOnlyValidationGCE, errorCase.enableSkipReadOnlyValidationGCE)() // ResourceVersion is required for updates. errorCase.old.ObjectMeta.ResourceVersion = "1" errorCase.update.ObjectMeta.ResourceVersion = "2" // Check test setup if errorCase.expectedErrNum <= 0 { t.Errorf("%q has incorrect test setup with expectedErrNum %d, expected at least one error", testName, errorCase.expectedErrNum) } if len(errorCase.old.ObjectMeta.ResourceVersion) == 0 || len(errorCase.update.ObjectMeta.ResourceVersion) == 0 { t.Errorf("%q has incorrect test setup with no resource version set", testName) } // Run the tests if errs := ValidateDeploymentUpdate(&errorCase.update, &errorCase.old, corevalidation.PodValidationOptions{}); len(errs) != errorCase.expectedErrNum { t.Errorf("%q expected %d errors, but got %d error: %v", testName, errorCase.expectedErrNum, len(errs), errs) } else { t.Logf("(PASS) %q got errors %v", testName, errs) } }) } } } func TestValidateDeploymentRollback(t *testing.T) { noAnnotation := validDeploymentRollback() noAnnotation.UpdatedAnnotations = nil successCases := []*apps.DeploymentRollback{ validDeploymentRollback(), noAnnotation, } for _, successCase := range successCases { if errs := ValidateDeploymentRollback(successCase); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } errorCases := map[string]*apps.DeploymentRollback{} invalidNoName := validDeploymentRollback() invalidNoName.Name = "" errorCases["name: Required value"] = invalidNoName for k, v := range errorCases { errs := ValidateDeploymentRollback(v) if len(errs) == 0 { t.Errorf("[%s] expected failure", k) } else if !strings.Contains(errs[0].Error(), k) { t.Errorf("unexpected error: %q, expected: %q", errs[0].Error(), k) } } } func TestValidateReplicaSetStatus(t *testing.T) { tests := []struct { name string replicas int32 fullyLabeledReplicas int32 readyReplicas int32 availableReplicas int32 observedGeneration int64 expectedErr bool }{{ name: "valid status", replicas: 3, fullyLabeledReplicas: 3, readyReplicas: 2, availableReplicas: 1, observedGeneration: 2, expectedErr: false, }, { name: "invalid replicas", replicas: -1, fullyLabeledReplicas: 3, readyReplicas: 2, availableReplicas: 1, observedGeneration: 2, expectedErr: true, }, { name: "invalid fullyLabeledReplicas", replicas: 3, fullyLabeledReplicas: -1, readyReplicas: 2, availableReplicas: 1, observedGeneration: 2, expectedErr: true, }, { name: "invalid readyReplicas", replicas: 3, fullyLabeledReplicas: 3, readyReplicas: -1, availableReplicas: 1, observedGeneration: 2, expectedErr: true, }, { name: "invalid availableReplicas", replicas: 3, fullyLabeledReplicas: 3, readyReplicas: 3, availableReplicas: -1, observedGeneration: 2, expectedErr: true, }, { name: "invalid observedGeneration", replicas: 3, fullyLabeledReplicas: 3, readyReplicas: 3, availableReplicas: 3, observedGeneration: -1, expectedErr: true, }, { name: "fullyLabeledReplicas greater than replicas", replicas: 3, fullyLabeledReplicas: 4, readyReplicas: 3, availableReplicas: 3, observedGeneration: 1, expectedErr: true, }, { name: "readyReplicas greater than replicas", replicas: 3, fullyLabeledReplicas: 3, readyReplicas: 4, availableReplicas: 3, observedGeneration: 1, expectedErr: true, }, { name: "availableReplicas greater than replicas", replicas: 3, fullyLabeledReplicas: 3, readyReplicas: 3, availableReplicas: 4, observedGeneration: 1, expectedErr: true, }, { name: "availableReplicas greater than readyReplicas", replicas: 3, fullyLabeledReplicas: 3, readyReplicas: 2, availableReplicas: 3, observedGeneration: 1, expectedErr: true, }, } for _, test := range tests { status := apps.ReplicaSetStatus{ Replicas: test.replicas, FullyLabeledReplicas: test.fullyLabeledReplicas, ReadyReplicas: test.readyReplicas, AvailableReplicas: test.availableReplicas, ObservedGeneration: test.observedGeneration, } if hasErr := len(ValidateReplicaSetStatus(status, field.NewPath("status"))) > 0; hasErr != test.expectedErr { t.Errorf("%s: expected error: %t, got error: %t", test.name, test.expectedErr, hasErr) } } } func TestValidateReplicaSetStatusUpdate(t *testing.T) { validLabels := map[string]string{"a": "b"} validPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, }, } type rcUpdateTest struct { old apps.ReplicaSet update apps.ReplicaSet } successCases := []rcUpdateTest{{ old: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, Status: apps.ReplicaSetStatus{ Replicas: 2, }, }, update: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: 3, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, Status: apps.ReplicaSetStatus{ Replicas: 4, }, }, }, } for _, successCase := range successCases { successCase.old.ObjectMeta.ResourceVersion = "1" successCase.update.ObjectMeta.ResourceVersion = "1" if errs := ValidateReplicaSetStatusUpdate(&successCase.update, &successCase.old); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } errorCases := map[string]rcUpdateTest{ "negative replicas": { old: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, Status: apps.ReplicaSetStatus{ Replicas: 3, }, }, update: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: 2, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, Status: apps.ReplicaSetStatus{ Replicas: -3, }, }, }, } for testName, errorCase := range errorCases { if errs := ValidateReplicaSetStatusUpdate(&errorCase.update, &errorCase.old); len(errs) == 0 { t.Errorf("expected failure: %s", testName) } } } func TestValidateReplicaSetUpdate(t *testing.T) { validLabels := map[string]string{"a": "b"} validPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, }, } readWriteVolumePodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, Volumes: []api.Volume{{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}}, }, }, } invalidLabels := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: metav1.ObjectMeta{ Labels: invalidLabels, }, }, } type rcUpdateTest struct { old apps.ReplicaSet update apps.ReplicaSet expectedErrNum int enableSkipReadOnlyValidationGCE bool } successCases := map[string]rcUpdateTest{ "positive replicas": { old: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, update: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: 3, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, }, "Read-write volume verification": { enableSkipReadOnlyValidationGCE: true, old: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, update: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: 3, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: readWriteVolumePodTemplate.Template, }, }, }, } for testName, successCase := range successCases { t.Run(testName, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SkipReadOnlyValidationGCE, successCase.enableSkipReadOnlyValidationGCE)() // ResourceVersion is required for updates. successCase.old.ObjectMeta.ResourceVersion = "1" successCase.update.ObjectMeta.ResourceVersion = "2" // Check test setup if successCase.expectedErrNum > 0 { t.Errorf("%q has incorrect test setup with expectedErrNum %d, expected no error", testName, successCase.expectedErrNum) } if len(successCase.old.ObjectMeta.ResourceVersion) == 0 || len(successCase.update.ObjectMeta.ResourceVersion) == 0 { t.Errorf("%q has incorrect test setup with no resource version set", testName) } // Run the tests if errs := ValidateReplicaSetUpdate(&successCase.update, &successCase.old, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("%q expected no error, but got: %v", testName, errs) } }) } errorCases := map[string]rcUpdateTest{ "more than one read/write": { old: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, update: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: 2, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: readWriteVolumePodTemplate.Template, }, }, expectedErrNum: 2, }, "invalid selector": { old: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, update: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: 2, Selector: &metav1.LabelSelector{MatchLabels: invalidLabels}, Template: validPodTemplate.Template, }, }, expectedErrNum: 3, }, "invalid pod": { old: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, update: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: 2, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: invalidPodTemplate.Template, }, }, expectedErrNum: 4, }, "negative replicas": { old: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, update: apps.ReplicaSet{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: -1, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, expectedErrNum: 1, }, } for testName, errorCase := range errorCases { t.Run(testName, func(t *testing.T) { defer featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.SkipReadOnlyValidationGCE, errorCase.enableSkipReadOnlyValidationGCE)() // ResourceVersion is required for updates. errorCase.old.ObjectMeta.ResourceVersion = "1" errorCase.update.ObjectMeta.ResourceVersion = "2" // Check test setup if errorCase.expectedErrNum <= 0 { t.Errorf("%q has incorrect test setup with expectedErrNum %d, expected at least one error", testName, errorCase.expectedErrNum) } if len(errorCase.old.ObjectMeta.ResourceVersion) == 0 || len(errorCase.update.ObjectMeta.ResourceVersion) == 0 { t.Errorf("%q has incorrect test setup with no resource version set", testName) } // Run the tests if errs := ValidateReplicaSetUpdate(&errorCase.update, &errorCase.old, corevalidation.PodValidationOptions{}); len(errs) != errorCase.expectedErrNum { t.Errorf("%q expected %d errors, but got %d error: %v", testName, errorCase.expectedErrNum, len(errs), errs) } else { t.Logf("(PASS) %q got errors %v", testName, errs) } }) } } func TestValidateReplicaSet(t *testing.T) { validLabels := map[string]string{"a": "b"} validPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, }, } validHostNetPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ SecurityContext: &api.PodSecurityContext{ HostNetwork: true, }, RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{ Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile, Ports: []api.ContainerPort{{ ContainerPort: 12345, Protocol: api.ProtocolTCP, }}, }}, }, }, } readWriteVolumePodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, Spec: api.PodSpec{ Volumes: []api.Volume{{Name: "gcepd", VolumeSource: api.VolumeSource{GCEPersistentDisk: &api.GCEPersistentDiskVolumeSource{PDName: "my-PD", FSType: "ext4", Partition: 1, ReadOnly: false}}}}, RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "abc", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, }, } invalidLabels := map[string]string{"NoUppercaseOrSpecialCharsLike=Equals": "b"} invalidPodTemplate := api.PodTemplate{ Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyAlways, DNSPolicy: api.DNSClusterFirst, }, ObjectMeta: metav1.ObjectMeta{ Labels: invalidLabels, }, }, } successCases := []apps.ReplicaSet{{ ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "hostnet", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validHostNetPodTemplate.Template, }, }, { ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: 1, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: readWriteVolumePodTemplate.Template, }, }, } for _, successCase := range successCases { if errs := ValidateReplicaSet(&successCase, corevalidation.PodValidationOptions{}); len(errs) != 0 { t.Errorf("expected success: %v", errs) } } errorCases := map[string]apps.ReplicaSet{ "zero-length ID": { ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, "missing-namespace": { ObjectMeta: metav1.ObjectMeta{Name: "abc-123"}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, "empty selector": { ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Template: validPodTemplate.Template, }, }, "selector_doesnt_match": { ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, Template: validPodTemplate.Template, }, }, "invalid manifest": { ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, }, }, "read-write persistent disk with > 1 pod": { ObjectMeta: metav1.ObjectMeta{Name: "abc"}, Spec: apps.ReplicaSetSpec{ Replicas: 2, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: readWriteVolumePodTemplate.Template, }, }, "negative_replicas": { ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, Spec: apps.ReplicaSetSpec{ Replicas: -1, Selector: &metav1.LabelSelector{MatchLabels: validLabels}, }, }, "invalid_label": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, Labels: map[string]string{ "NoUppercaseOrSpecialCharsLike=Equals": "bar", }, }, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, "invalid_label 2": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, Labels: map[string]string{ "NoUppercaseOrSpecialCharsLike=Equals": "bar", }, }, Spec: apps.ReplicaSetSpec{ Template: invalidPodTemplate.Template, }, }, "invalid_annotation": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, Annotations: map[string]string{ "NoUppercaseOrSpecialCharsLike=Equals": "bar", }, }, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: validPodTemplate.Template, }, }, "invalid restart policy 1": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, }, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyOnFailure, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, }, }, }, "invalid restart policy 2": { ObjectMeta: metav1.ObjectMeta{ Name: "abc-123", Namespace: metav1.NamespaceDefault, }, Spec: apps.ReplicaSetSpec{ Selector: &metav1.LabelSelector{MatchLabels: validLabels}, Template: api.PodTemplateSpec{ Spec: api.PodSpec{ RestartPolicy: api.RestartPolicyNever, DNSPolicy: api.DNSClusterFirst, Containers: []api.Container{{Name: "ctr", Image: "image", ImagePullPolicy: "IfNotPresent", TerminationMessagePolicy: api.TerminationMessageReadFile}}, }, ObjectMeta: metav1.ObjectMeta{ Labels: validLabels, }, }, }, }, } for k, v := range errorCases { errs := ValidateReplicaSet(&v, corevalidation.PodValidationOptions{}) if len(errs) == 0 { t.Errorf("expected failure for %s", k) } 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]) } } } } func TestDaemonSetUpdateMaxSurge(t *testing.T) { testCases := map[string]struct { ds *apps.RollingUpdateDaemonSet expectError bool }{ "invalid: unset": { ds: &apps.RollingUpdateDaemonSet{}, expectError: true, }, "invalid: zero percent": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("0%"), }, expectError: true, }, "invalid: zero": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromInt32(0), }, expectError: true, }, "valid: one": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromInt32(1), }, }, "valid: one percent": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("1%"), }, }, "valid: 100%": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("100%"), }, }, "invalid: greater than 100%": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("101%"), }, expectError: true, }, "valid: surge and unavailable set": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("1%"), MaxSurge: intstr.FromString("1%"), }, expectError: true, }, "invalid: surge enabled, unavailable zero percent": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("0%"), }, expectError: true, }, "invalid: surge enabled, unavailable zero": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromInt32(0), }, expectError: true, }, "valid: surge enabled, unavailable one": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromInt32(1), }, }, "valid: surge enabled, unavailable one percent": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("1%"), }, }, "valid: surge enabled, unavailable 100%": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("100%"), }, }, "invalid: surge enabled, unavailable greater than 100%": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("101%"), }, expectError: true, }, "invalid: surge enabled, surge zero percent": { ds: &apps.RollingUpdateDaemonSet{ MaxSurge: intstr.FromString("0%"), }, expectError: true, }, "invalid: surge enabled, surge zero": { ds: &apps.RollingUpdateDaemonSet{ MaxSurge: intstr.FromInt32(0), }, expectError: true, }, "valid: surge enabled, surge one": { ds: &apps.RollingUpdateDaemonSet{ MaxSurge: intstr.FromInt32(1), }, }, "valid: surge enabled, surge one percent": { ds: &apps.RollingUpdateDaemonSet{ MaxSurge: intstr.FromString("1%"), }, }, "valid: surge enabled, surge 100%": { ds: &apps.RollingUpdateDaemonSet{ MaxSurge: intstr.FromString("100%"), }, }, "invalid: surge enabled, surge greater than 100%": { ds: &apps.RollingUpdateDaemonSet{ MaxSurge: intstr.FromString("101%"), }, expectError: true, }, "invalid: surge enabled, surge and unavailable set": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("1%"), MaxSurge: intstr.FromString("1%"), }, expectError: true, }, "invalid: surge enabled, surge and unavailable zero percent": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromString("0%"), MaxSurge: intstr.FromString("0%"), }, expectError: true, }, "invalid: surge enabled, surge and unavailable zero": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromInt32(0), MaxSurge: intstr.FromInt32(0), }, expectError: true, }, "invalid: surge enabled, surge and unavailable mixed zero": { ds: &apps.RollingUpdateDaemonSet{ MaxUnavailable: intstr.FromInt32(0), MaxSurge: intstr.FromString("0%"), }, expectError: true, }, } for tcName, tc := range testCases { t.Run(tcName, func(t *testing.T) { errs := ValidateRollingUpdateDaemonSet(tc.ds, field.NewPath("spec", "updateStrategy", "rollingUpdate")) if tc.expectError && len(errs) == 0 { t.Errorf("Unexpected success") } if !tc.expectError && len(errs) != 0 { t.Errorf("Unexpected error(s): %v", errs) } }) } }