From 283e282515d3d34616e1adacb006051009b5f0ee Mon Sep 17 00:00:00 2001 From: Jordan Liggitt Date: Fri, 19 Oct 2018 12:46:53 -0400 Subject: [PATCH] Move extensions validation to apps --- pkg/apis/apps/validation/validation.go | 433 ++++ pkg/apis/apps/validation/validation_test.go | 2021 ++++++++++++++++ pkg/apis/extensions/validation/validation.go | 437 ---- .../extensions/validation/validation_test.go | 2022 ----------------- 4 files changed, 2454 insertions(+), 2459 deletions(-) diff --git a/pkg/apis/apps/validation/validation.go b/pkg/apis/apps/validation/validation.go index 5653d7cad7..7cdf4c2dc2 100644 --- a/pkg/apis/apps/validation/validation.go +++ b/pkg/apis/apps/validation/validation.go @@ -18,12 +18,15 @@ package validation import ( "fmt" + "strconv" apiequality "k8s.io/apimachinery/pkg/api/equality" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" "k8s.io/apimachinery/pkg/labels" + "k8s.io/apimachinery/pkg/util/intstr" + "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/kubernetes/pkg/apis/apps" api "k8s.io/kubernetes/pkg/apis/core" @@ -236,3 +239,433 @@ func ValidateControllerRevisionUpdate(newHistory, oldHistory *apps.ControllerRev errs = append(errs, apivalidation.ValidateImmutableField(newHistory.Data, oldHistory.Data, field.NewPath("data"))...) return errs } + +// ValidateDaemonSet tests if required fields in the DaemonSet are set. +func ValidateDaemonSet(ds *apps.DaemonSet) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&ds.ObjectMeta, true, ValidateDaemonSetName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateDaemonSetSpec(&ds.Spec, field.NewPath("spec"))...) + return allErrs +} + +// ValidateDaemonSetUpdate tests if required fields in the DaemonSet are set. +func ValidateDaemonSetUpdate(ds, oldDS *apps.DaemonSet) field.ErrorList { + allErrs := apivalidation.ValidateObjectMetaUpdate(&ds.ObjectMeta, &oldDS.ObjectMeta, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateDaemonSetSpecUpdate(&ds.Spec, &oldDS.Spec, field.NewPath("spec"))...) + allErrs = append(allErrs, ValidateDaemonSetSpec(&ds.Spec, field.NewPath("spec"))...) + return allErrs +} + +func ValidateDaemonSetSpecUpdate(newSpec, oldSpec *apps.DaemonSetSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + // TemplateGeneration shouldn't be decremented + if newSpec.TemplateGeneration < oldSpec.TemplateGeneration { + allErrs = append(allErrs, field.Invalid(fldPath.Child("templateGeneration"), newSpec.TemplateGeneration, "must not be decremented")) + } + + // TemplateGeneration should be increased when and only when template is changed + templateUpdated := !apiequality.Semantic.DeepEqual(newSpec.Template, oldSpec.Template) + if newSpec.TemplateGeneration == oldSpec.TemplateGeneration && templateUpdated { + allErrs = append(allErrs, field.Invalid(fldPath.Child("templateGeneration"), newSpec.TemplateGeneration, "must be incremented upon template update")) + } else if newSpec.TemplateGeneration > oldSpec.TemplateGeneration && !templateUpdated { + allErrs = append(allErrs, field.Invalid(fldPath.Child("templateGeneration"), newSpec.TemplateGeneration, "must not be incremented without template update")) + } + + return allErrs +} + +// validateDaemonSetStatus validates a DaemonSetStatus +func validateDaemonSetStatus(status *apps.DaemonSetStatus, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.CurrentNumberScheduled), fldPath.Child("currentNumberScheduled"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.NumberMisscheduled), fldPath.Child("numberMisscheduled"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.DesiredNumberScheduled), fldPath.Child("desiredNumberScheduled"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.NumberReady), fldPath.Child("numberReady"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(status.ObservedGeneration, fldPath.Child("observedGeneration"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.UpdatedNumberScheduled), fldPath.Child("updatedNumberScheduled"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.NumberAvailable), fldPath.Child("numberAvailable"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.NumberUnavailable), fldPath.Child("numberUnavailable"))...) + if status.CollisionCount != nil { + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*status.CollisionCount), fldPath.Child("collisionCount"))...) + } + return allErrs +} + +// ValidateDaemonSetStatus validates tests if required fields in the DaemonSet Status section +func ValidateDaemonSetStatusUpdate(ds, oldDS *apps.DaemonSet) field.ErrorList { + allErrs := apivalidation.ValidateObjectMetaUpdate(&ds.ObjectMeta, &oldDS.ObjectMeta, field.NewPath("metadata")) + allErrs = append(allErrs, validateDaemonSetStatus(&ds.Status, field.NewPath("status"))...) + if apivalidation.IsDecremented(ds.Status.CollisionCount, oldDS.Status.CollisionCount) { + value := int32(0) + if ds.Status.CollisionCount != nil { + value = *ds.Status.CollisionCount + } + allErrs = append(allErrs, field.Invalid(field.NewPath("status").Child("collisionCount"), value, "cannot be decremented")) + } + return allErrs +} + +// ValidateDaemonSetSpec tests if required fields in the DaemonSetSpec are set. +func ValidateDaemonSetSpec(spec *apps.DaemonSetSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, fldPath.Child("selector"))...) + + selector, err := metav1.LabelSelectorAsSelector(spec.Selector) + if err == nil && !selector.Matches(labels.Set(spec.Template.Labels)) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "metadata", "labels"), spec.Template.Labels, "`selector` does not match template `labels`")) + } + if spec.Selector != nil && len(spec.Selector.MatchLabels)+len(spec.Selector.MatchExpressions) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "empty selector is invalid for daemonset")) + } + + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"))...) + // Daemons typically run on more than one node, so mark Read-Write persistent disks as invalid. + allErrs = append(allErrs, apivalidation.ValidateReadOnlyPersistentDisks(spec.Template.Spec.Volumes, fldPath.Child("template", "spec", "volumes"))...) + // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). + if spec.Template.Spec.RestartPolicy != api.RestartPolicyAlways { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("template", "spec", "restartPolicy"), spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) + } + if spec.Template.Spec.ActiveDeadlineSeconds != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "spec", "activeDeadlineSeconds"), spec.Template.Spec.ActiveDeadlineSeconds, "must not be specified")) + } + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.TemplateGeneration), fldPath.Child("templateGeneration"))...) + + allErrs = append(allErrs, ValidateDaemonSetUpdateStrategy(&spec.UpdateStrategy, fldPath.Child("updateStrategy"))...) + if spec.RevisionHistoryLimit != nil { + // zero is a valid RevisionHistoryLimit + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.RevisionHistoryLimit), fldPath.Child("revisionHistoryLimit"))...) + } + return allErrs +} + +func ValidateRollingUpdateDaemonSet(rollingUpdate *apps.RollingUpdateDaemonSet, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) + if getIntOrPercentValue(rollingUpdate.MaxUnavailable) == 0 { + // MaxUnavailable cannot be 0. + allErrs = append(allErrs, field.Invalid(fldPath.Child("maxUnavailable"), rollingUpdate.MaxUnavailable, "cannot be 0")) + } + // Validate that MaxUnavailable is not more than 100%. + allErrs = append(allErrs, IsNotMoreThan100Percent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) + return allErrs +} + +func ValidateDaemonSetUpdateStrategy(strategy *apps.DaemonSetUpdateStrategy, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + switch strategy.Type { + case apps.OnDeleteDaemonSetStrategyType: + case apps.RollingUpdateDaemonSetStrategyType: + // Make sure RollingUpdate field isn't nil. + if strategy.RollingUpdate == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("rollingUpdate"), "")) + return allErrs + } + allErrs = append(allErrs, ValidateRollingUpdateDaemonSet(strategy.RollingUpdate, fldPath.Child("rollingUpdate"))...) + default: + validValues := []string{string(apps.RollingUpdateDaemonSetStrategyType), string(apps.OnDeleteDaemonSetStrategyType)} + allErrs = append(allErrs, field.NotSupported(fldPath, strategy, validValues)) + } + return allErrs +} + +// ValidateDaemonSetName can be used to check whether the given daemon set name is valid. +// Prefix indicates this name will be used as part of generation, in which case +// trailing dashes are allowed. +var ValidateDaemonSetName = apimachineryvalidation.NameIsDNSSubdomain + +// Validates that the given name can be used as a deployment name. +var ValidateDeploymentName = apimachineryvalidation.NameIsDNSSubdomain + +func ValidatePositiveIntOrPercent(intOrPercent intstr.IntOrString, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + switch intOrPercent.Type { + case intstr.String: + for _, msg := range validation.IsValidPercent(intOrPercent.StrVal) { + allErrs = append(allErrs, field.Invalid(fldPath, intOrPercent, msg)) + } + case intstr.Int: + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(intOrPercent.IntValue()), fldPath)...) + default: + allErrs = append(allErrs, field.Invalid(fldPath, intOrPercent, "must be an integer or percentage (e.g '5%%')")) + } + return allErrs +} + +func getPercentValue(intOrStringValue intstr.IntOrString) (int, bool) { + if intOrStringValue.Type != intstr.String { + return 0, false + } + if len(validation.IsValidPercent(intOrStringValue.StrVal)) != 0 { + return 0, false + } + value, _ := strconv.Atoi(intOrStringValue.StrVal[:len(intOrStringValue.StrVal)-1]) + return value, true +} + +func getIntOrPercentValue(intOrStringValue intstr.IntOrString) int { + value, isPercent := getPercentValue(intOrStringValue) + if isPercent { + return value + } + return intOrStringValue.IntValue() +} + +func IsNotMoreThan100Percent(intOrStringValue intstr.IntOrString, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + value, isPercent := getPercentValue(intOrStringValue) + if !isPercent || value <= 100 { + return nil + } + allErrs = append(allErrs, field.Invalid(fldPath, intOrStringValue, "must not be greater than 100%")) + return allErrs +} + +func ValidateRollingUpdateDeployment(rollingUpdate *apps.RollingUpdateDeployment, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) + allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxSurge, fldPath.Child("maxSurge"))...) + if getIntOrPercentValue(rollingUpdate.MaxUnavailable) == 0 && getIntOrPercentValue(rollingUpdate.MaxSurge) == 0 { + // Both MaxSurge and MaxUnavailable cannot be zero. + allErrs = append(allErrs, field.Invalid(fldPath.Child("maxUnavailable"), rollingUpdate.MaxUnavailable, "may not be 0 when `maxSurge` is 0")) + } + // Validate that MaxUnavailable is not more than 100%. + allErrs = append(allErrs, IsNotMoreThan100Percent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) + return allErrs +} + +func ValidateDeploymentStrategy(strategy *apps.DeploymentStrategy, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + switch strategy.Type { + case apps.RecreateDeploymentStrategyType: + if strategy.RollingUpdate != nil { + allErrs = append(allErrs, field.Forbidden(fldPath.Child("rollingUpdate"), "may not be specified when strategy `type` is '"+string(apps.RecreateDeploymentStrategyType+"'"))) + } + case apps.RollingUpdateDeploymentStrategyType: + // This should never happen since it's set and checked in defaults.go + if strategy.RollingUpdate == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("rollingUpdate"), "this should be defaulted and never be nil")) + } else { + allErrs = append(allErrs, ValidateRollingUpdateDeployment(strategy.RollingUpdate, fldPath.Child("rollingUpdate"))...) + } + default: + validValues := []string{string(apps.RecreateDeploymentStrategyType), string(apps.RollingUpdateDeploymentStrategyType)} + allErrs = append(allErrs, field.NotSupported(fldPath, strategy, validValues)) + } + return allErrs +} + +func ValidateRollback(rollback *apps.RollbackConfig, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + v := rollback.Revision + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(v), fldPath.Child("version"))...) + return allErrs +} + +// Validates given deployment spec. +func ValidateDeploymentSpec(spec *apps.DeploymentSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...) + + if spec.Selector == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("selector"), "")) + } else { + allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, fldPath.Child("selector"))...) + if len(spec.Selector.MatchLabels)+len(spec.Selector.MatchExpressions) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "empty selector is invalid for deployment")) + } + } + + selector, err := metav1.LabelSelectorAsSelector(spec.Selector) + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "invalid label selector")) + } else { + allErrs = append(allErrs, ValidatePodTemplateSpecForReplicaSet(&spec.Template, selector, spec.Replicas, fldPath.Child("template"))...) + } + + allErrs = append(allErrs, ValidateDeploymentStrategy(&spec.Strategy, fldPath.Child("strategy"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...) + if spec.RevisionHistoryLimit != nil { + // zero is a valid RevisionHistoryLimit + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.RevisionHistoryLimit), fldPath.Child("revisionHistoryLimit"))...) + } + if spec.RollbackTo != nil { + allErrs = append(allErrs, ValidateRollback(spec.RollbackTo, fldPath.Child("rollback"))...) + } + if spec.ProgressDeadlineSeconds != nil { + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.ProgressDeadlineSeconds), fldPath.Child("progressDeadlineSeconds"))...) + if *spec.ProgressDeadlineSeconds <= spec.MinReadySeconds { + allErrs = append(allErrs, field.Invalid(fldPath.Child("progressDeadlineSeconds"), spec.ProgressDeadlineSeconds, "must be greater than minReadySeconds")) + } + } + return allErrs +} + +// Validates given deployment status. +func ValidateDeploymentStatus(status *apps.DeploymentStatus, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(status.ObservedGeneration, fldPath.Child("observedGeneration"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Replicas), fldPath.Child("replicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.UpdatedReplicas), fldPath.Child("updatedReplicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.ReadyReplicas), fldPath.Child("readyReplicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.AvailableReplicas), fldPath.Child("availableReplicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.UnavailableReplicas), fldPath.Child("unavailableReplicas"))...) + if status.CollisionCount != nil { + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*status.CollisionCount), fldPath.Child("collisionCount"))...) + } + msg := "cannot be greater than status.replicas" + if status.UpdatedReplicas > status.Replicas { + allErrs = append(allErrs, field.Invalid(fldPath.Child("updatedReplicas"), status.UpdatedReplicas, msg)) + } + if status.ReadyReplicas > status.Replicas { + allErrs = append(allErrs, field.Invalid(fldPath.Child("readyReplicas"), status.ReadyReplicas, msg)) + } + if status.AvailableReplicas > status.Replicas { + allErrs = append(allErrs, field.Invalid(fldPath.Child("availableReplicas"), status.AvailableReplicas, msg)) + } + if status.AvailableReplicas > status.ReadyReplicas { + allErrs = append(allErrs, field.Invalid(fldPath.Child("availableReplicas"), status.AvailableReplicas, "cannot be greater than readyReplicas")) + } + return allErrs +} + +func ValidateDeploymentUpdate(update, old *apps.Deployment) field.ErrorList { + allErrs := apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateDeploymentSpec(&update.Spec, field.NewPath("spec"))...) + return allErrs +} + +func ValidateDeploymentStatusUpdate(update, old *apps.Deployment) field.ErrorList { + allErrs := apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata")) + fldPath := field.NewPath("status") + allErrs = append(allErrs, ValidateDeploymentStatus(&update.Status, fldPath)...) + if apivalidation.IsDecremented(update.Status.CollisionCount, old.Status.CollisionCount) { + value := int32(0) + if update.Status.CollisionCount != nil { + value = *update.Status.CollisionCount + } + allErrs = append(allErrs, field.Invalid(fldPath.Child("collisionCount"), value, "cannot be decremented")) + } + return allErrs +} + +func ValidateDeployment(obj *apps.Deployment) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateDeploymentName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateDeploymentSpec(&obj.Spec, field.NewPath("spec"))...) + return allErrs +} + +func ValidateDeploymentRollback(obj *apps.DeploymentRollback) field.ErrorList { + allErrs := apivalidation.ValidateAnnotations(obj.UpdatedAnnotations, field.NewPath("updatedAnnotations")) + if len(obj.Name) == 0 { + allErrs = append(allErrs, field.Required(field.NewPath("name"), "name is required")) + } + allErrs = append(allErrs, ValidateRollback(&obj.RollbackTo, field.NewPath("rollback"))...) + return allErrs +} + +// ValidateReplicaSetName can be used to check whether the given ReplicaSet +// name is valid. +// Prefix indicates this name will be used as part of generation, in which case +// trailing dashes are allowed. +var ValidateReplicaSetName = apimachineryvalidation.NameIsDNSSubdomain + +// ValidateReplicaSet tests if required fields in the ReplicaSet are set. +func ValidateReplicaSet(rs *apps.ReplicaSet) field.ErrorList { + allErrs := apivalidation.ValidateObjectMeta(&rs.ObjectMeta, true, ValidateReplicaSetName, field.NewPath("metadata")) + allErrs = append(allErrs, ValidateReplicaSetSpec(&rs.Spec, field.NewPath("spec"))...) + return allErrs +} + +// ValidateReplicaSetUpdate tests if required fields in the ReplicaSet are set. +func ValidateReplicaSetUpdate(rs, oldRs *apps.ReplicaSet) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&rs.ObjectMeta, &oldRs.ObjectMeta, field.NewPath("metadata"))...) + allErrs = append(allErrs, ValidateReplicaSetSpec(&rs.Spec, field.NewPath("spec"))...) + return allErrs +} + +// ValidateReplicaSetStatusUpdate tests if required fields in the ReplicaSet are set. +func ValidateReplicaSetStatusUpdate(rs, oldRs *apps.ReplicaSet) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&rs.ObjectMeta, &oldRs.ObjectMeta, field.NewPath("metadata"))...) + allErrs = append(allErrs, ValidateReplicaSetStatus(rs.Status, field.NewPath("status"))...) + return allErrs +} + +func ValidateReplicaSetStatus(status apps.ReplicaSetStatus, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Replicas), fldPath.Child("replicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.FullyLabeledReplicas), fldPath.Child("fullyLabeledReplicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.ReadyReplicas), fldPath.Child("readyReplicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.AvailableReplicas), fldPath.Child("availableReplicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.ObservedGeneration), fldPath.Child("observedGeneration"))...) + msg := "cannot be greater than status.replicas" + if status.FullyLabeledReplicas > status.Replicas { + allErrs = append(allErrs, field.Invalid(fldPath.Child("fullyLabeledReplicas"), status.FullyLabeledReplicas, msg)) + } + if status.ReadyReplicas > status.Replicas { + allErrs = append(allErrs, field.Invalid(fldPath.Child("readyReplicas"), status.ReadyReplicas, msg)) + } + if status.AvailableReplicas > status.Replicas { + allErrs = append(allErrs, field.Invalid(fldPath.Child("availableReplicas"), status.AvailableReplicas, msg)) + } + if status.AvailableReplicas > status.ReadyReplicas { + allErrs = append(allErrs, field.Invalid(fldPath.Child("availableReplicas"), status.AvailableReplicas, "cannot be greater than readyReplicas")) + } + return allErrs +} + +// ValidateReplicaSetSpec tests if required fields in the ReplicaSet spec are set. +func ValidateReplicaSetSpec(spec *apps.ReplicaSetSpec, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...) + allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...) + + if spec.Selector == nil { + allErrs = append(allErrs, field.Required(fldPath.Child("selector"), "")) + } else { + allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, fldPath.Child("selector"))...) + if len(spec.Selector.MatchLabels)+len(spec.Selector.MatchExpressions) == 0 { + allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "empty selector is invalid for deployment")) + } + } + + selector, err := metav1.LabelSelectorAsSelector(spec.Selector) + if err != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "invalid label selector")) + } else { + allErrs = append(allErrs, ValidatePodTemplateSpecForReplicaSet(&spec.Template, selector, spec.Replicas, fldPath.Child("template"))...) + } + return allErrs +} + +// Validates the given template and ensures that it is in accordance with the desired selector and replicas. +func ValidatePodTemplateSpecForReplicaSet(template *api.PodTemplateSpec, selector labels.Selector, replicas int32, fldPath *field.Path) field.ErrorList { + allErrs := field.ErrorList{} + if template == nil { + allErrs = append(allErrs, field.Required(fldPath, "")) + } else { + if !selector.Empty() { + // Verify that the ReplicaSet selector matches the labels in template. + labels := labels.Set(template.Labels) + if !selector.Matches(labels) { + allErrs = append(allErrs, field.Invalid(fldPath.Child("metadata", "labels"), template.Labels, "`selector` does not match template `labels`")) + } + } + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(template, fldPath)...) + if replicas > 1 { + allErrs = append(allErrs, apivalidation.ValidateReadOnlyPersistentDisks(template.Spec.Volumes, fldPath.Child("spec", "volumes"))...) + } + // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). + if template.Spec.RestartPolicy != api.RestartPolicyAlways { + allErrs = append(allErrs, field.NotSupported(fldPath.Child("spec", "restartPolicy"), template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) + } + if template.Spec.ActiveDeadlineSeconds != nil { + allErrs = append(allErrs, field.Invalid(fldPath.Child("spec", "activeDeadlineSeconds"), template.Spec.ActiveDeadlineSeconds, "must not be specified")) + } + } + return allErrs +} diff --git a/pkg/apis/apps/validation/validation_test.go b/pkg/apis/apps/validation/validation_test.go index 35b67f0182..a4c8791f03 100644 --- a/pkg/apis/apps/validation/validation_test.go +++ b/pkg/apis/apps/validation/validation_test.go @@ -21,8 +21,10 @@ import ( "strings" "testing" + "github.com/davecgh/go-spew/spew" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/validation/field" "k8s.io/kubernetes/pkg/apis/apps" api "k8s.io/kubernetes/pkg/apis/core" @@ -911,3 +913,2022 @@ func TestValidateControllerRevisionUpdate(t *testing.T) { }) } } + +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 + } + 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.FromInt(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.FromInt(1), + }, + }, + }, + }, + }, + } + for testName, successCase := range successCases { + // 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); 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": { + 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 { + // 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); 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}}, + }, + }, + } + 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, + }, + }, + }, + } + for _, successCase := range successCases { + if errs := ValidateDaemonSet(&successCase); 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, + }, + }, + }, + }, + } + for k, v := range errorCases { + errs := ValidateDaemonSet(&v) + 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() *apps.Deployment { + return &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.FromInt(1), + MaxUnavailable: intstr.FromInt(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, + }, + }, + } +} + +func TestValidateDeployment(t *testing.T) { + successCases := []*apps.Deployment{ + validDeployment(), + } + for _, successCase := range successCases { + if errs := ValidateDeployment(successCase); 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.FromInt(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 + + for k, v := range errorCases { + errs := ValidateDeployment(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 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 := spew.Sprintf("%#v", 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 := spew.Sprintf("%#v", 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 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 + } + successCases := []rcUpdateTest{ + { + 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, + }, + }, + }, + { + 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: readWriteVolumePodTemplate.Template, + }, + }, + }, + } + for _, successCase := range successCases { + successCase.old.ObjectMeta.ResourceVersion = "1" + successCase.update.ObjectMeta.ResourceVersion = "1" + if errs := ValidateReplicaSetUpdate(&successCase.update, &successCase.old); len(errs) != 0 { + t.Errorf("expected success: %v", 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, + }, + }, + }, + "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, + }, + }, + }, + "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, + }, + }, + }, + "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, + }, + }, + }, + } + for testName, errorCase := range errorCases { + if errs := ValidateReplicaSetUpdate(&errorCase.update, &errorCase.old); len(errs) == 0 { + t.Errorf("expected failure: %s", testName) + } + } +} + +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}}, + }, + }, + } + 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: "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); 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) + 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]) + } + } + } +} diff --git a/pkg/apis/extensions/validation/validation.go b/pkg/apis/extensions/validation/validation.go index 36c191c17f..0ada34ce60 100644 --- a/pkg/apis/extensions/validation/validation.go +++ b/pkg/apis/extensions/validation/validation.go @@ -19,347 +19,15 @@ package validation import ( "net" "regexp" - "strconv" "strings" - apiequality "k8s.io/apimachinery/pkg/api/equality" apimachineryvalidation "k8s.io/apimachinery/pkg/api/validation" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" - unversionedvalidation "k8s.io/apimachinery/pkg/apis/meta/v1/validation" - "k8s.io/apimachinery/pkg/labels" - "k8s.io/apimachinery/pkg/util/intstr" "k8s.io/apimachinery/pkg/util/validation" "k8s.io/apimachinery/pkg/util/validation/field" - api "k8s.io/kubernetes/pkg/apis/core" apivalidation "k8s.io/kubernetes/pkg/apis/core/validation" "k8s.io/kubernetes/pkg/apis/extensions" ) -// ValidateDaemonSet tests if required fields in the DaemonSet are set. -func ValidateDaemonSet(ds *extensions.DaemonSet) field.ErrorList { - allErrs := apivalidation.ValidateObjectMeta(&ds.ObjectMeta, true, ValidateDaemonSetName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateDaemonSetSpec(&ds.Spec, field.NewPath("spec"))...) - return allErrs -} - -// ValidateDaemonSetUpdate tests if required fields in the DaemonSet are set. -func ValidateDaemonSetUpdate(ds, oldDS *extensions.DaemonSet) field.ErrorList { - allErrs := apivalidation.ValidateObjectMetaUpdate(&ds.ObjectMeta, &oldDS.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateDaemonSetSpecUpdate(&ds.Spec, &oldDS.Spec, field.NewPath("spec"))...) - allErrs = append(allErrs, ValidateDaemonSetSpec(&ds.Spec, field.NewPath("spec"))...) - return allErrs -} - -func ValidateDaemonSetSpecUpdate(newSpec, oldSpec *extensions.DaemonSetSpec, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - // TemplateGeneration shouldn't be decremented - if newSpec.TemplateGeneration < oldSpec.TemplateGeneration { - allErrs = append(allErrs, field.Invalid(fldPath.Child("templateGeneration"), newSpec.TemplateGeneration, "must not be decremented")) - } - - // TemplateGeneration should be increased when and only when template is changed - templateUpdated := !apiequality.Semantic.DeepEqual(newSpec.Template, oldSpec.Template) - if newSpec.TemplateGeneration == oldSpec.TemplateGeneration && templateUpdated { - allErrs = append(allErrs, field.Invalid(fldPath.Child("templateGeneration"), newSpec.TemplateGeneration, "must be incremented upon template update")) - } else if newSpec.TemplateGeneration > oldSpec.TemplateGeneration && !templateUpdated { - allErrs = append(allErrs, field.Invalid(fldPath.Child("templateGeneration"), newSpec.TemplateGeneration, "must not be incremented without template update")) - } - - return allErrs -} - -// validateDaemonSetStatus validates a DaemonSetStatus -func validateDaemonSetStatus(status *extensions.DaemonSetStatus, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.CurrentNumberScheduled), fldPath.Child("currentNumberScheduled"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.NumberMisscheduled), fldPath.Child("numberMisscheduled"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.DesiredNumberScheduled), fldPath.Child("desiredNumberScheduled"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.NumberReady), fldPath.Child("numberReady"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(status.ObservedGeneration, fldPath.Child("observedGeneration"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.UpdatedNumberScheduled), fldPath.Child("updatedNumberScheduled"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.NumberAvailable), fldPath.Child("numberAvailable"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.NumberUnavailable), fldPath.Child("numberUnavailable"))...) - if status.CollisionCount != nil { - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*status.CollisionCount), fldPath.Child("collisionCount"))...) - } - return allErrs -} - -// ValidateDaemonSetStatus validates tests if required fields in the DaemonSet Status section -func ValidateDaemonSetStatusUpdate(ds, oldDS *extensions.DaemonSet) field.ErrorList { - allErrs := apivalidation.ValidateObjectMetaUpdate(&ds.ObjectMeta, &oldDS.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, validateDaemonSetStatus(&ds.Status, field.NewPath("status"))...) - if apivalidation.IsDecremented(ds.Status.CollisionCount, oldDS.Status.CollisionCount) { - value := int32(0) - if ds.Status.CollisionCount != nil { - value = *ds.Status.CollisionCount - } - allErrs = append(allErrs, field.Invalid(field.NewPath("status").Child("collisionCount"), value, "cannot be decremented")) - } - return allErrs -} - -// ValidateDaemonSetSpec tests if required fields in the DaemonSetSpec are set. -func ValidateDaemonSetSpec(spec *extensions.DaemonSetSpec, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, fldPath.Child("selector"))...) - - selector, err := metav1.LabelSelectorAsSelector(spec.Selector) - if err == nil && !selector.Matches(labels.Set(spec.Template.Labels)) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "metadata", "labels"), spec.Template.Labels, "`selector` does not match template `labels`")) - } - if spec.Selector != nil && len(spec.Selector.MatchLabels)+len(spec.Selector.MatchExpressions) == 0 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "empty selector is invalid for daemonset")) - } - - allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(&spec.Template, fldPath.Child("template"))...) - // Daemons typically run on more than one node, so mark Read-Write persistent disks as invalid. - allErrs = append(allErrs, apivalidation.ValidateReadOnlyPersistentDisks(spec.Template.Spec.Volumes, fldPath.Child("template", "spec", "volumes"))...) - // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). - if spec.Template.Spec.RestartPolicy != api.RestartPolicyAlways { - allErrs = append(allErrs, field.NotSupported(fldPath.Child("template", "spec", "restartPolicy"), spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) - } - if spec.Template.Spec.ActiveDeadlineSeconds != nil { - allErrs = append(allErrs, field.Invalid(fldPath.Child("template", "spec", "activeDeadlineSeconds"), spec.Template.Spec.ActiveDeadlineSeconds, "must not be specified")) - } - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.TemplateGeneration), fldPath.Child("templateGeneration"))...) - - allErrs = append(allErrs, ValidateDaemonSetUpdateStrategy(&spec.UpdateStrategy, fldPath.Child("updateStrategy"))...) - if spec.RevisionHistoryLimit != nil { - // zero is a valid RevisionHistoryLimit - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.RevisionHistoryLimit), fldPath.Child("revisionHistoryLimit"))...) - } - return allErrs -} - -func ValidateRollingUpdateDaemonSet(rollingUpdate *extensions.RollingUpdateDaemonSet, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) - if getIntOrPercentValue(rollingUpdate.MaxUnavailable) == 0 { - // MaxUnavailable cannot be 0. - allErrs = append(allErrs, field.Invalid(fldPath.Child("maxUnavailable"), rollingUpdate.MaxUnavailable, "cannot be 0")) - } - // Validate that MaxUnavailable is not more than 100%. - allErrs = append(allErrs, IsNotMoreThan100Percent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) - return allErrs -} - -func ValidateDaemonSetUpdateStrategy(strategy *extensions.DaemonSetUpdateStrategy, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - switch strategy.Type { - case extensions.OnDeleteDaemonSetStrategyType: - case extensions.RollingUpdateDaemonSetStrategyType: - // Make sure RollingUpdate field isn't nil. - if strategy.RollingUpdate == nil { - allErrs = append(allErrs, field.Required(fldPath.Child("rollingUpdate"), "")) - return allErrs - } - allErrs = append(allErrs, ValidateRollingUpdateDaemonSet(strategy.RollingUpdate, fldPath.Child("rollingUpdate"))...) - default: - validValues := []string{string(extensions.RollingUpdateDaemonSetStrategyType), string(extensions.OnDeleteDaemonSetStrategyType)} - allErrs = append(allErrs, field.NotSupported(fldPath, strategy, validValues)) - } - return allErrs -} - -// ValidateDaemonSetName can be used to check whether the given daemon set name is valid. -// Prefix indicates this name will be used as part of generation, in which case -// trailing dashes are allowed. -var ValidateDaemonSetName = apimachineryvalidation.NameIsDNSSubdomain - -// Validates that the given name can be used as a deployment name. -var ValidateDeploymentName = apimachineryvalidation.NameIsDNSSubdomain - -func ValidatePositiveIntOrPercent(intOrPercent intstr.IntOrString, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - switch intOrPercent.Type { - case intstr.String: - for _, msg := range validation.IsValidPercent(intOrPercent.StrVal) { - allErrs = append(allErrs, field.Invalid(fldPath, intOrPercent, msg)) - } - case intstr.Int: - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(intOrPercent.IntValue()), fldPath)...) - default: - allErrs = append(allErrs, field.Invalid(fldPath, intOrPercent, "must be an integer or percentage (e.g '5%%')")) - } - return allErrs -} - -func getPercentValue(intOrStringValue intstr.IntOrString) (int, bool) { - if intOrStringValue.Type != intstr.String { - return 0, false - } - if len(validation.IsValidPercent(intOrStringValue.StrVal)) != 0 { - return 0, false - } - value, _ := strconv.Atoi(intOrStringValue.StrVal[:len(intOrStringValue.StrVal)-1]) - return value, true -} - -func getIntOrPercentValue(intOrStringValue intstr.IntOrString) int { - value, isPercent := getPercentValue(intOrStringValue) - if isPercent { - return value - } - return intOrStringValue.IntValue() -} - -func IsNotMoreThan100Percent(intOrStringValue intstr.IntOrString, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - value, isPercent := getPercentValue(intOrStringValue) - if !isPercent || value <= 100 { - return nil - } - allErrs = append(allErrs, field.Invalid(fldPath, intOrStringValue, "must not be greater than 100%")) - return allErrs -} - -func ValidateRollingUpdateDeployment(rollingUpdate *extensions.RollingUpdateDeployment, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) - allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxSurge, fldPath.Child("maxSurge"))...) - if getIntOrPercentValue(rollingUpdate.MaxUnavailable) == 0 && getIntOrPercentValue(rollingUpdate.MaxSurge) == 0 { - // Both MaxSurge and MaxUnavailable cannot be zero. - allErrs = append(allErrs, field.Invalid(fldPath.Child("maxUnavailable"), rollingUpdate.MaxUnavailable, "may not be 0 when `maxSurge` is 0")) - } - // Validate that MaxUnavailable is not more than 100%. - allErrs = append(allErrs, IsNotMoreThan100Percent(rollingUpdate.MaxUnavailable, fldPath.Child("maxUnavailable"))...) - return allErrs -} - -func ValidateDeploymentStrategy(strategy *extensions.DeploymentStrategy, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - switch strategy.Type { - case extensions.RecreateDeploymentStrategyType: - if strategy.RollingUpdate != nil { - allErrs = append(allErrs, field.Forbidden(fldPath.Child("rollingUpdate"), "may not be specified when strategy `type` is '"+string(extensions.RecreateDeploymentStrategyType+"'"))) - } - case extensions.RollingUpdateDeploymentStrategyType: - // This should never happen since it's set and checked in defaults.go - if strategy.RollingUpdate == nil { - allErrs = append(allErrs, field.Required(fldPath.Child("rollingUpdate"), "this should be defaulted and never be nil")) - } else { - allErrs = append(allErrs, ValidateRollingUpdateDeployment(strategy.RollingUpdate, fldPath.Child("rollingUpdate"))...) - } - default: - validValues := []string{string(extensions.RecreateDeploymentStrategyType), string(extensions.RollingUpdateDeploymentStrategyType)} - allErrs = append(allErrs, field.NotSupported(fldPath, strategy, validValues)) - } - return allErrs -} - -func ValidateRollback(rollback *extensions.RollbackConfig, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - v := rollback.Revision - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(v), fldPath.Child("version"))...) - return allErrs -} - -// Validates given deployment spec. -func ValidateDeploymentSpec(spec *extensions.DeploymentSpec, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...) - - if spec.Selector == nil { - allErrs = append(allErrs, field.Required(fldPath.Child("selector"), "")) - } else { - allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, fldPath.Child("selector"))...) - if len(spec.Selector.MatchLabels)+len(spec.Selector.MatchExpressions) == 0 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "empty selector is invalid for deployment")) - } - } - - selector, err := metav1.LabelSelectorAsSelector(spec.Selector) - if err != nil { - allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "invalid label selector")) - } else { - allErrs = append(allErrs, ValidatePodTemplateSpecForReplicaSet(&spec.Template, selector, spec.Replicas, fldPath.Child("template"))...) - } - - allErrs = append(allErrs, ValidateDeploymentStrategy(&spec.Strategy, fldPath.Child("strategy"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...) - if spec.RevisionHistoryLimit != nil { - // zero is a valid RevisionHistoryLimit - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.RevisionHistoryLimit), fldPath.Child("revisionHistoryLimit"))...) - } - if spec.RollbackTo != nil { - allErrs = append(allErrs, ValidateRollback(spec.RollbackTo, fldPath.Child("rollback"))...) - } - if spec.ProgressDeadlineSeconds != nil { - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*spec.ProgressDeadlineSeconds), fldPath.Child("progressDeadlineSeconds"))...) - if *spec.ProgressDeadlineSeconds <= spec.MinReadySeconds { - allErrs = append(allErrs, field.Invalid(fldPath.Child("progressDeadlineSeconds"), spec.ProgressDeadlineSeconds, "must be greater than minReadySeconds")) - } - } - return allErrs -} - -// Validates given deployment status. -func ValidateDeploymentStatus(status *extensions.DeploymentStatus, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(status.ObservedGeneration, fldPath.Child("observedGeneration"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Replicas), fldPath.Child("replicas"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.UpdatedReplicas), fldPath.Child("updatedReplicas"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.ReadyReplicas), fldPath.Child("readyReplicas"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.AvailableReplicas), fldPath.Child("availableReplicas"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.UnavailableReplicas), fldPath.Child("unavailableReplicas"))...) - if status.CollisionCount != nil { - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(*status.CollisionCount), fldPath.Child("collisionCount"))...) - } - msg := "cannot be greater than status.replicas" - if status.UpdatedReplicas > status.Replicas { - allErrs = append(allErrs, field.Invalid(fldPath.Child("updatedReplicas"), status.UpdatedReplicas, msg)) - } - if status.ReadyReplicas > status.Replicas { - allErrs = append(allErrs, field.Invalid(fldPath.Child("readyReplicas"), status.ReadyReplicas, msg)) - } - if status.AvailableReplicas > status.Replicas { - allErrs = append(allErrs, field.Invalid(fldPath.Child("availableReplicas"), status.AvailableReplicas, msg)) - } - if status.AvailableReplicas > status.ReadyReplicas { - allErrs = append(allErrs, field.Invalid(fldPath.Child("availableReplicas"), status.AvailableReplicas, "cannot be greater than readyReplicas")) - } - return allErrs -} - -func ValidateDeploymentUpdate(update, old *extensions.Deployment) field.ErrorList { - allErrs := apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateDeploymentSpec(&update.Spec, field.NewPath("spec"))...) - return allErrs -} - -func ValidateDeploymentStatusUpdate(update, old *extensions.Deployment) field.ErrorList { - allErrs := apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta, field.NewPath("metadata")) - fldPath := field.NewPath("status") - allErrs = append(allErrs, ValidateDeploymentStatus(&update.Status, fldPath)...) - if apivalidation.IsDecremented(update.Status.CollisionCount, old.Status.CollisionCount) { - value := int32(0) - if update.Status.CollisionCount != nil { - value = *update.Status.CollisionCount - } - allErrs = append(allErrs, field.Invalid(fldPath.Child("collisionCount"), value, "cannot be decremented")) - } - return allErrs -} - -func ValidateDeployment(obj *extensions.Deployment) field.ErrorList { - allErrs := apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateDeploymentName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateDeploymentSpec(&obj.Spec, field.NewPath("spec"))...) - return allErrs -} - -func ValidateDeploymentRollback(obj *extensions.DeploymentRollback) field.ErrorList { - allErrs := apivalidation.ValidateAnnotations(obj.UpdatedAnnotations, field.NewPath("updatedAnnotations")) - if len(obj.Name) == 0 { - allErrs = append(allErrs, field.Required(field.NewPath("name"), "name is required")) - } - allErrs = append(allErrs, ValidateRollback(&obj.RollbackTo, field.NewPath("rollback"))...) - return allErrs -} - // ValidateIngress tests if required fields in the Ingress are set. func ValidateIngress(ingress *extensions.Ingress) field.ErrorList { allErrs := apivalidation.ValidateObjectMeta(&ingress.ObjectMeta, true, ValidateIngressName, field.NewPath("metadata")) @@ -502,108 +170,3 @@ func validateIngressBackend(backend *extensions.IngressBackend, fldPath *field.P allErrs = append(allErrs, apivalidation.ValidatePortNumOrName(backend.ServicePort, fldPath.Child("servicePort"))...) return allErrs } - -// ValidateReplicaSetName can be used to check whether the given ReplicaSet -// name is valid. -// Prefix indicates this name will be used as part of generation, in which case -// trailing dashes are allowed. -var ValidateReplicaSetName = apimachineryvalidation.NameIsDNSSubdomain - -// ValidateReplicaSet tests if required fields in the ReplicaSet are set. -func ValidateReplicaSet(rs *extensions.ReplicaSet) field.ErrorList { - allErrs := apivalidation.ValidateObjectMeta(&rs.ObjectMeta, true, ValidateReplicaSetName, field.NewPath("metadata")) - allErrs = append(allErrs, ValidateReplicaSetSpec(&rs.Spec, field.NewPath("spec"))...) - return allErrs -} - -// ValidateReplicaSetUpdate tests if required fields in the ReplicaSet are set. -func ValidateReplicaSetUpdate(rs, oldRs *extensions.ReplicaSet) field.ErrorList { - allErrs := field.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&rs.ObjectMeta, &oldRs.ObjectMeta, field.NewPath("metadata"))...) - allErrs = append(allErrs, ValidateReplicaSetSpec(&rs.Spec, field.NewPath("spec"))...) - return allErrs -} - -// ValidateReplicaSetStatusUpdate tests if required fields in the ReplicaSet are set. -func ValidateReplicaSetStatusUpdate(rs, oldRs *extensions.ReplicaSet) field.ErrorList { - allErrs := field.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&rs.ObjectMeta, &oldRs.ObjectMeta, field.NewPath("metadata"))...) - allErrs = append(allErrs, ValidateReplicaSetStatus(rs.Status, field.NewPath("status"))...) - return allErrs -} - -func ValidateReplicaSetStatus(status extensions.ReplicaSetStatus, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.Replicas), fldPath.Child("replicas"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.FullyLabeledReplicas), fldPath.Child("fullyLabeledReplicas"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.ReadyReplicas), fldPath.Child("readyReplicas"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.AvailableReplicas), fldPath.Child("availableReplicas"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(status.ObservedGeneration), fldPath.Child("observedGeneration"))...) - msg := "cannot be greater than status.replicas" - if status.FullyLabeledReplicas > status.Replicas { - allErrs = append(allErrs, field.Invalid(fldPath.Child("fullyLabeledReplicas"), status.FullyLabeledReplicas, msg)) - } - if status.ReadyReplicas > status.Replicas { - allErrs = append(allErrs, field.Invalid(fldPath.Child("readyReplicas"), status.ReadyReplicas, msg)) - } - if status.AvailableReplicas > status.Replicas { - allErrs = append(allErrs, field.Invalid(fldPath.Child("availableReplicas"), status.AvailableReplicas, msg)) - } - if status.AvailableReplicas > status.ReadyReplicas { - allErrs = append(allErrs, field.Invalid(fldPath.Child("availableReplicas"), status.AvailableReplicas, "cannot be greater than readyReplicas")) - } - return allErrs -} - -// ValidateReplicaSetSpec tests if required fields in the ReplicaSet spec are set. -func ValidateReplicaSetSpec(spec *extensions.ReplicaSetSpec, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.Replicas), fldPath.Child("replicas"))...) - allErrs = append(allErrs, apivalidation.ValidateNonnegativeField(int64(spec.MinReadySeconds), fldPath.Child("minReadySeconds"))...) - - if spec.Selector == nil { - allErrs = append(allErrs, field.Required(fldPath.Child("selector"), "")) - } else { - allErrs = append(allErrs, unversionedvalidation.ValidateLabelSelector(spec.Selector, fldPath.Child("selector"))...) - if len(spec.Selector.MatchLabels)+len(spec.Selector.MatchExpressions) == 0 { - allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "empty selector is invalid for deployment")) - } - } - - selector, err := metav1.LabelSelectorAsSelector(spec.Selector) - if err != nil { - allErrs = append(allErrs, field.Invalid(fldPath.Child("selector"), spec.Selector, "invalid label selector")) - } else { - allErrs = append(allErrs, ValidatePodTemplateSpecForReplicaSet(&spec.Template, selector, spec.Replicas, fldPath.Child("template"))...) - } - return allErrs -} - -// Validates the given template and ensures that it is in accordance with the desired selector and replicas. -func ValidatePodTemplateSpecForReplicaSet(template *api.PodTemplateSpec, selector labels.Selector, replicas int32, fldPath *field.Path) field.ErrorList { - allErrs := field.ErrorList{} - if template == nil { - allErrs = append(allErrs, field.Required(fldPath, "")) - } else { - if !selector.Empty() { - // Verify that the ReplicaSet selector matches the labels in template. - labels := labels.Set(template.Labels) - if !selector.Matches(labels) { - allErrs = append(allErrs, field.Invalid(fldPath.Child("metadata", "labels"), template.Labels, "`selector` does not match template `labels`")) - } - } - allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpec(template, fldPath)...) - if replicas > 1 { - allErrs = append(allErrs, apivalidation.ValidateReadOnlyPersistentDisks(template.Spec.Volumes, fldPath.Child("spec", "volumes"))...) - } - // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). - if template.Spec.RestartPolicy != api.RestartPolicyAlways { - allErrs = append(allErrs, field.NotSupported(fldPath.Child("spec", "restartPolicy"), template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) - } - if template.Spec.ActiveDeadlineSeconds != nil { - allErrs = append(allErrs, field.Invalid(fldPath.Child("spec", "activeDeadlineSeconds"), template.Spec.ActiveDeadlineSeconds, "must not be specified")) - } - } - return allErrs -} diff --git a/pkg/apis/extensions/validation/validation_test.go b/pkg/apis/extensions/validation/validation_test.go index cd56129a9e..2d99683788 100644 --- a/pkg/apis/extensions/validation/validation_test.go +++ b/pkg/apis/extensions/validation/validation_test.go @@ -21,1450 +21,12 @@ import ( "strings" "testing" - "github.com/davecgh/go-spew/spew" - metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/util/intstr" - "k8s.io/apimachinery/pkg/util/validation/field" api "k8s.io/kubernetes/pkg/apis/core" "k8s.io/kubernetes/pkg/apis/extensions" ) -func TestValidateDaemonSetStatusUpdate(t *testing.T) { - type dsUpdateTest struct { - old extensions.DaemonSet - update extensions.DaemonSet - } - - successCases := []dsUpdateTest{ - { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Status: extensions.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: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: -1, - NumberMisscheduled: -1, - DesiredNumberScheduled: -3, - NumberReady: -1, - ObservedGeneration: -3, - UpdatedNumberScheduled: -1, - NumberAvailable: -1, - NumberUnavailable: -2, - }, - }, - }, - "negative CurrentNumberScheduled": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: -1, - NumberMisscheduled: 1, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - }, - "negative NumberMisscheduled": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: -1, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - }, - "negative DesiredNumberScheduled": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 1, - DesiredNumberScheduled: -3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - }, - "negative NumberReady": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 1, - DesiredNumberScheduled: 3, - NumberReady: -1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - }, - "negative ObservedGeneration": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 1, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: -3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - }, - "negative UpdatedNumberScheduled": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 1, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: -1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - }, - "negative NumberAvailable": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 1, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: -1, - NumberUnavailable: 2, - }, - }, - }, - "negative NumberUnavailable": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.DaemonSetStatus{ - CurrentNumberScheduled: 1, - NumberMisscheduled: 2, - DesiredNumberScheduled: 3, - NumberReady: 1, - ObservedGeneration: 3, - UpdatedNumberScheduled: 1, - NumberAvailable: 1, - NumberUnavailable: 2, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - ResourceVersion: "10", - }, - Status: extensions.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 extensions.DaemonSet - update extensions.DaemonSet - expectedErrNum int - } - successCases := map[string]dsUpdateTest{ - "no change": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - }, - "change template and selector": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 2, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector2}, - TemplateGeneration: 3, - Template: validPodTemplateAbc2.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - }, - "change template": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 3, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 4, - Template: validPodTemplateNodeSelector.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - }, - "change container image name": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector2}, - TemplateGeneration: 2, - Template: validPodTemplateDef.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - }, - "change update strategy": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 4, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 4, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.RollingUpdateDaemonSetStrategyType, - RollingUpdate: &extensions.RollingUpdateDaemonSet{ - MaxUnavailable: intstr.FromInt(1), - }, - }, - }, - }, - }, - "unchanged templateGeneration upon semantically equal template update": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 4, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 4, - Template: validPodTemplateAbcSemanticallyEqual.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.RollingUpdateDaemonSetStrategyType, - RollingUpdate: &extensions.RollingUpdateDaemonSet{ - MaxUnavailable: intstr.FromInt(1), - }, - }, - }, - }, - }, - } - for testName, successCase := range successCases { - // 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); len(errs) != 0 { - t.Errorf("%q expected no error, but got: %v", testName, errs) - } - } - errorCases := map[string]dsUpdateTest{ - "change daemon name": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - expectedErrNum: 1, - }, - "invalid selector": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: invalidSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - expectedErrNum: 1, - }, - "invalid pod": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 2, - Template: invalidPodTemplate.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - expectedErrNum: 1, - }, - "invalid read-write volume": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 2, - Template: readWriteVolumePodTemplate.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - expectedErrNum: 1, - }, - "invalid update strategy": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: 1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: "Random", - }, - }, - }, - expectedErrNum: 1, - }, - "negative templateGeneration": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: -1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - TemplateGeneration: -1, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - expectedErrNum: 1, - }, - "decreased templateGeneration": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - TemplateGeneration: 2, - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - TemplateGeneration: 1, - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - expectedErrNum: 1, - }, - "unchanged templateGeneration upon template update": { - old: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - TemplateGeneration: 2, - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - Template: validPodTemplateAbc.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - update: extensions.DaemonSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - TemplateGeneration: 2, - Selector: &metav1.LabelSelector{MatchLabels: validSelector2}, - Template: validPodTemplateAbc2.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - expectedErrNum: 1, - }, - } - for testName, errorCase := range errorCases { - // 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); 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}}, - }, - }, - } - 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 := []extensions.DaemonSet{ - { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - Template: validPodTemplate.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - Template: validPodTemplate.Template, - UpdateStrategy: extensions.DaemonSetUpdateStrategy{ - Type: extensions.OnDeleteDaemonSetStrategyType, - }, - }, - }, - } - for _, successCase := range successCases { - if errs := ValidateDaemonSet(&successCase); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - } - - errorCases := map[string]extensions.DaemonSet{ - "zero-length ID": { - ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - Template: validPodTemplate.Template, - }, - }, - "missing-namespace": { - ObjectMeta: metav1.ObjectMeta{Name: "abc-123"}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - Template: validPodTemplate.Template, - }, - }, - "nil selector": { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Template: validPodTemplate.Template, - }, - }, - "empty selector": { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{}, - Template: validPodTemplate.Template, - }, - }, - "selector_doesnt_match": { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, - Template: validPodTemplate.Template, - }, - }, - "invalid template": { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.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: extensions.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: extensions.DaemonSetSpec{ - Template: invalidPodTemplate.Template, - }, - }, - "invalid_annotation": { - ObjectMeta: metav1.ObjectMeta{ - Name: "abc-123", - Namespace: metav1.NamespaceDefault, - Annotations: map[string]string{ - "NoUppercaseOrSpecialCharsLike=Equals": "bar", - }, - }, - Spec: extensions.DaemonSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validSelector}, - Template: validPodTemplate.Template, - }, - }, - "invalid restart policy 1": { - ObjectMeta: metav1.ObjectMeta{ - Name: "abc-123", - Namespace: metav1.NamespaceDefault, - }, - Spec: extensions.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: extensions.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, - }, - }, - }, - }, - } - for k, v := range errorCases { - errs := ValidateDaemonSet(&v) - 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() *extensions.Deployment { - return &extensions.Deployment{ - ObjectMeta: metav1.ObjectMeta{ - Name: "abc", - Namespace: metav1.NamespaceDefault, - }, - Spec: extensions.DeploymentSpec{ - Selector: &metav1.LabelSelector{ - MatchLabels: map[string]string{ - "name": "abc", - }, - }, - Strategy: extensions.DeploymentStrategy{ - Type: extensions.RollingUpdateDeploymentStrategyType, - RollingUpdate: &extensions.RollingUpdateDeployment{ - MaxSurge: intstr.FromInt(1), - MaxUnavailable: intstr.FromInt(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: &extensions.RollbackConfig{ - Revision: 1, - }, - }, - } -} - -func TestValidateDeployment(t *testing.T) { - successCases := []*extensions.Deployment{ - validDeployment(), - } - for _, successCase := range successCases { - if errs := ValidateDeployment(successCase); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - } - - errorCases := map[string]*extensions.Deployment{} - errorCases["metadata.name: Required value"] = &extensions.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 = extensions.DeploymentStrategyType("randomType") - errorCases[`supported values: "Recreate", "RollingUpdate"`] = invalidStrategyDeployment - - // rollingUpdate should be nil for recreate. - invalidRecreateDeployment := validDeployment() - invalidRecreateDeployment.Spec.Strategy = extensions.DeploymentStrategy{ - Type: extensions.RecreateDeploymentStrategyType, - RollingUpdate: &extensions.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 = extensions.DeploymentStrategy{ - Type: extensions.RollingUpdateDeploymentStrategyType, - RollingUpdate: &extensions.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 = extensions.DeploymentStrategy{ - Type: extensions.RollingUpdateDeploymentStrategyType, - RollingUpdate: &extensions.RollingUpdateDeployment{ - MaxSurge: intstr.FromString("0%"), - MaxUnavailable: intstr.FromInt(0), - }, - } - errorCases["may not be 0 when `maxSurge` is 0"] = invalidRollingUpdateDeployment - - // MaxUnavailable should not be more than 100%. - invalidMaxUnavailableDeployment := validDeployment() - invalidMaxUnavailableDeployment.Spec.Strategy = extensions.DeploymentStrategy{ - Type: extensions.RollingUpdateDeploymentStrategyType, - RollingUpdate: &extensions.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 - - for k, v := range errorCases { - errs := ValidateDeployment(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 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 := extensions.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 := spew.Sprintf("%#v", 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 extensions.DeploymentStatus - - expectedErr bool - }{ - { - name: "increase: valid update", - from: extensions.DeploymentStatus{ - CollisionCount: nil, - }, - to: extensions.DeploymentStatus{ - CollisionCount: &collisionCount, - }, - expectedErr: false, - }, - { - name: "stable: valid update", - from: extensions.DeploymentStatus{ - CollisionCount: &collisionCount, - }, - to: extensions.DeploymentStatus{ - CollisionCount: &collisionCount, - }, - expectedErr: false, - }, - { - name: "unset: invalid update", - from: extensions.DeploymentStatus{ - CollisionCount: &collisionCount, - }, - to: extensions.DeploymentStatus{ - CollisionCount: nil, - }, - expectedErr: true, - }, - { - name: "decrease: invalid update", - from: extensions.DeploymentStatus{ - CollisionCount: &otherCollisionCount, - }, - to: extensions.DeploymentStatus{ - CollisionCount: &collisionCount, - }, - expectedErr: true, - }, - } - - for _, test := range tests { - meta := metav1.ObjectMeta{Name: "foo", Namespace: metav1.NamespaceDefault, ResourceVersion: "1"} - from := &extensions.Deployment{ - ObjectMeta: meta, - Status: test.from, - } - to := &extensions.Deployment{ - ObjectMeta: meta, - Status: test.to, - } - - errs := ValidateDeploymentStatusUpdate(to, from) - if hasErr := len(errs) > 0; hasErr != test.expectedErr { - errString := spew.Sprintf("%#v", errs) - t.Errorf("%s: expected error: %t, got error: %t\nerrors: %s", test.name, test.expectedErr, hasErr, errString) - } - } -} - -func validDeploymentRollback() *extensions.DeploymentRollback { - return &extensions.DeploymentRollback{ - Name: "abc", - UpdatedAnnotations: map[string]string{ - "created-by": "abc", - }, - RollbackTo: extensions.RollbackConfig{ - Revision: 1, - }, - } -} - -func TestValidateDeploymentRollback(t *testing.T) { - noAnnotation := validDeploymentRollback() - noAnnotation.UpdatedAnnotations = nil - successCases := []*extensions.DeploymentRollback{ - validDeploymentRollback(), - noAnnotation, - } - for _, successCase := range successCases { - if errs := ValidateDeploymentRollback(successCase); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - } - - errorCases := map[string]*extensions.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 TestValidateIngress(t *testing.T) { defaultBackend := extensions.IngressBackend{ ServiceName: "default-backend", @@ -1730,587 +292,3 @@ func TestValidateIngressStatusUpdate(t *testing.T) { } } } - -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 := extensions.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 extensions.ReplicaSet - update extensions.ReplicaSet - } - successCases := []rcUpdateTest{ - { - old: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - Status: extensions.ReplicaSetStatus{ - Replicas: 2, - }, - }, - update: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Replicas: 3, - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - Status: extensions.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: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - Status: extensions.ReplicaSetStatus{ - Replicas: 3, - }, - }, - update: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Replicas: 2, - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - Status: extensions.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 extensions.ReplicaSet - update extensions.ReplicaSet - } - successCases := []rcUpdateTest{ - { - old: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - update: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Replicas: 3, - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - }, - { - old: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - update: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Replicas: 1, - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: readWriteVolumePodTemplate.Template, - }, - }, - }, - } - for _, successCase := range successCases { - successCase.old.ObjectMeta.ResourceVersion = "1" - successCase.update.ObjectMeta.ResourceVersion = "1" - if errs := ValidateReplicaSetUpdate(&successCase.update, &successCase.old); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - } - errorCases := map[string]rcUpdateTest{ - "more than one read/write": { - old: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - update: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Replicas: 2, - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: readWriteVolumePodTemplate.Template, - }, - }, - }, - "invalid selector": { - old: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - update: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Replicas: 2, - Selector: &metav1.LabelSelector{MatchLabels: invalidLabels}, - Template: validPodTemplate.Template, - }, - }, - }, - "invalid pod": { - old: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - update: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Replicas: 2, - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: invalidPodTemplate.Template, - }, - }, - }, - "negative replicas": { - old: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - update: extensions.ReplicaSet{ - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Replicas: -1, - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - }, - } - for testName, errorCase := range errorCases { - if errs := ValidateReplicaSetUpdate(&errorCase.update, &errorCase.old); len(errs) == 0 { - t.Errorf("expected failure: %s", testName) - } - } -} - -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}}, - }, - }, - } - 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 := []extensions.ReplicaSet{ - { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - { - ObjectMeta: metav1.ObjectMeta{Name: "abc-123", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Replicas: 1, - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: readWriteVolumePodTemplate.Template, - }, - }, - } - for _, successCase := range successCases { - if errs := ValidateReplicaSet(&successCase); len(errs) != 0 { - t.Errorf("expected success: %v", errs) - } - } - - errorCases := map[string]extensions.ReplicaSet{ - "zero-length ID": { - ObjectMeta: metav1.ObjectMeta{Name: "", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - "missing-namespace": { - ObjectMeta: metav1.ObjectMeta{Name: "abc-123"}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - "empty selector": { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Template: validPodTemplate.Template, - }, - }, - "selector_doesnt_match": { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: map[string]string{"foo": "bar"}}, - Template: validPodTemplate.Template, - }, - }, - "invalid manifest": { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - }, - }, - "read-write persistent disk with > 1 pod": { - ObjectMeta: metav1.ObjectMeta{Name: "abc"}, - Spec: extensions.ReplicaSetSpec{ - Replicas: 2, - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: readWriteVolumePodTemplate.Template, - }, - }, - "negative_replicas": { - ObjectMeta: metav1.ObjectMeta{Name: "abc", Namespace: metav1.NamespaceDefault}, - Spec: extensions.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: extensions.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: extensions.ReplicaSetSpec{ - Template: invalidPodTemplate.Template, - }, - }, - "invalid_annotation": { - ObjectMeta: metav1.ObjectMeta{ - Name: "abc-123", - Namespace: metav1.NamespaceDefault, - Annotations: map[string]string{ - "NoUppercaseOrSpecialCharsLike=Equals": "bar", - }, - }, - Spec: extensions.ReplicaSetSpec{ - Selector: &metav1.LabelSelector{MatchLabels: validLabels}, - Template: validPodTemplate.Template, - }, - }, - "invalid restart policy 1": { - ObjectMeta: metav1.ObjectMeta{ - Name: "abc-123", - Namespace: metav1.NamespaceDefault, - }, - Spec: extensions.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: extensions.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) - 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]) - } - } - } -}