diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index a04b827ff9d..88290c664c9 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -53,13 +53,19 @@ var portNameErrorMsg string = fmt.Sprintf(`must be an IANA_SVC_NAME (at most 15 const totalAnnotationSizeLimitB int = 64 * (1 << 10) // 64 kB +func ValidateLabelName(labelName, fieldName string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if !util.IsQualifiedName(labelName) { + allErrs = append(allErrs, errs.NewFieldInvalid(fieldName, labelName, qualifiedNameErrorMsg)) + } + return allErrs +} + // ValidateLabels validates that a set of labels are correctly defined. func ValidateLabels(labels map[string]string, field string) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} for k, v := range labels { - if !util.IsQualifiedName(k) { - allErrs = append(allErrs, errs.NewFieldInvalid(field, k, qualifiedNameErrorMsg)) - } + allErrs = append(allErrs, ValidateLabelName(k, field)...) if !util.IsValidLabelValue(v) { allErrs = append(allErrs, errs.NewFieldInvalid(field, v, labelValueErrorMsg)) } @@ -202,6 +208,15 @@ func NameIsDNS952Label(name string, prefix bool) (bool, string) { return false, DNS952LabelErrorMsg } +// Validates that given value is not negative. +func ValidatePositiveField(value int64, fieldName string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if value < 0 { + allErrs = append(allErrs, errs.NewFieldInvalid(fieldName, value, isNegativeErrorMsg)) + } + return allErrs +} + // ValidateObjectMeta validates an object's metadata on creation. It expects that name generation has already // been performed. func ValidateObjectMeta(meta *api.ObjectMeta, requiresNamespace bool, nameFn ValidateNameFunc) errs.ValidationErrorList { @@ -224,9 +239,7 @@ func ValidateObjectMeta(meta *api.ObjectMeta, requiresNamespace bool, nameFn Val allErrs = append(allErrs, errs.NewFieldInvalid("name", meta.Name, qualifier)) } } - if meta.Generation < 0 { - allErrs = append(allErrs, errs.NewFieldInvalid("generation", meta.Generation, isNegativeErrorMsg)) - } + allErrs = append(allErrs, ValidatePositiveField(meta.Generation, "generation")...) if requiresNamespace { if len(meta.Namespace) == 0 { allErrs = append(allErrs, errs.NewFieldRequired("namespace")) @@ -1206,34 +1219,49 @@ func ValidateReplicationControllerUpdate(oldController, controller *api.Replicat return allErrs } +// Validates that the given selector is non-empty. +func ValidateNonEmptySelector(selectorMap map[string]string, fieldName string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + selector := labels.Set(selectorMap).AsSelector() + if selector.Empty() { + allErrs = append(allErrs, errs.NewFieldRequired(fieldName)) + } + return allErrs +} + +// Validates the given template and ensures that it is in accordance with the desrired selector and replicas. +func ValidatePodTemplateSpecForRC(template *api.PodTemplateSpec, selectorMap map[string]string, replicas int, fieldName string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if template == nil { + allErrs = append(allErrs, errs.NewFieldRequired(fieldName)) + } else { + selector := labels.Set(selectorMap).AsSelector() + if !selector.Empty() { + // Verify that the RC selector matches the labels in template. + labels := labels.Set(template.Labels) + if !selector.Matches(labels) { + allErrs = append(allErrs, errs.NewFieldInvalid(fieldName+".metadata.labels", template.Labels, "selector does not match labels in "+fieldName)) + } + } + allErrs = append(allErrs, ValidatePodTemplateSpec(template).Prefix(fieldName)...) + if replicas > 1 { + allErrs = append(allErrs, ValidateReadOnlyPersistentDisks(template.Spec.Volumes).Prefix(fieldName+".spec.volumes")...) + } + // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). + if template.Spec.RestartPolicy != api.RestartPolicyAlways { + allErrs = append(allErrs, errs.NewFieldValueNotSupported(fieldName+".spec.restartPolicy", template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) + } + } + return allErrs +} + // ValidateReplicationControllerSpec tests if required fields in the replication controller spec are set. func ValidateReplicationControllerSpec(spec *api.ReplicationControllerSpec) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} - selector := labels.Set(spec.Selector).AsSelector() - if selector.Empty() { - allErrs = append(allErrs, errs.NewFieldRequired("selector")) - } - if spec.Replicas < 0 { - allErrs = append(allErrs, errs.NewFieldInvalid("replicas", spec.Replicas, isNegativeErrorMsg)) - } - - if spec.Template == nil { - allErrs = append(allErrs, errs.NewFieldRequired("template")) - } else { - labels := labels.Set(spec.Template.Labels) - if !selector.Matches(labels) { - allErrs = append(allErrs, errs.NewFieldInvalid("template.metadata.labels", spec.Template.Labels, "selector does not match template")) - } - allErrs = append(allErrs, ValidatePodTemplateSpec(spec.Template).Prefix("template")...) - if spec.Replicas > 1 { - allErrs = append(allErrs, ValidateReadOnlyPersistentDisks(spec.Template.Spec.Volumes).Prefix("template.spec.volumes")...) - } - // RestartPolicy has already been first-order validated as per ValidatePodTemplateSpec(). - if spec.Template.Spec.RestartPolicy != api.RestartPolicyAlways { - allErrs = append(allErrs, errs.NewFieldValueNotSupported("template.spec.restartPolicy", spec.Template.Spec.RestartPolicy, []string{string(api.RestartPolicyAlways)})) - } - } + allErrs = append(allErrs, ValidateNonEmptySelector(spec.Selector, "selector")...) + allErrs = append(allErrs, ValidatePositiveField(int64(spec.Replicas), "replicas")...) + allErrs = append(allErrs, ValidatePodTemplateSpecForRC(spec.Template, spec.Selector, spec.Replicas, "template")...) return allErrs } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index b694f2a4588..0c0a5280983 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -234,7 +234,6 @@ func TestValidateAnnotations(t *testing.T) { } func testVolume(name string, namespace string, spec api.PersistentVolumeSpec) *api.PersistentVolume { - objMeta := api.ObjectMeta{Name: name} if namespace != "" { objMeta.Namespace = namespace @@ -247,7 +246,6 @@ func testVolume(name string, namespace string, spec api.PersistentVolumeSpec) *a } func TestValidatePersistentVolumes(t *testing.T) { - scenarios := map[string]struct { isExpectedFailure bool volume *api.PersistentVolume @@ -358,7 +356,6 @@ func testVolumeClaim(name string, namespace string, spec api.PersistentVolumeCla } func TestValidatePersistentVolumeClaim(t *testing.T) { - scenarios := map[string]struct { isExpectedFailure bool claim *api.PersistentVolumeClaim @@ -802,7 +799,6 @@ func TestValidatePullPolicy(t *testing.T) { t.Errorf("case[%s] expected policy %v, got %v", k, v.ExpectedPolicy, ctr.ImagePullPolicy) } } - } func getResourceLimits(cpu, memory string) api.ResourceList { @@ -2144,7 +2140,6 @@ func TestValidateReplicationControllerUpdate(t *testing.T) { t.Errorf("expected failure: %s", testName) } } - } func TestValidateReplicationController(t *testing.T) { diff --git a/pkg/expapi/deep_copy_generated.go b/pkg/expapi/deep_copy_generated.go index dc955d12fe5..92cc7532ed5 100644 --- a/pkg/expapi/deep_copy_generated.go +++ b/pkg/expapi/deep_copy_generated.go @@ -787,12 +787,7 @@ func deepCopy_expapi_DeploymentList(in DeploymentList, out *DeploymentList, c *c } func deepCopy_expapi_DeploymentSpec(in DeploymentSpec, out *DeploymentSpec, c *conversion.Cloner) error { - if in.Replicas != nil { - out.Replicas = new(int) - *out.Replicas = *in.Replicas - } else { - out.Replicas = nil - } + out.Replicas = in.Replicas if in.Selector != nil { out.Selector = make(map[string]string) for key, val := range in.Selector { diff --git a/pkg/expapi/types.go b/pkg/expapi/types.go index 52375606558..2cb481d8a0a 100644 --- a/pkg/expapi/types.go +++ b/pkg/expapi/types.go @@ -179,9 +179,8 @@ type Deployment struct { } type DeploymentSpec struct { - // Number of desired pods. This is a pointer to distinguish between explicit - // zero and not specified. Defaults to 1. - Replicas *int `json:"replicas,omitempty"` + // Number of desired pods. + Replicas int `json:"replicas,omitempty"` // Label selector for pods. Existing ReplicationControllers whose pods are // selected by this will be scaled down. diff --git a/pkg/expapi/v1/conversion.go b/pkg/expapi/v1/conversion.go index 381b04a1e39..1522625d0c5 100644 --- a/pkg/expapi/v1/conversion.go +++ b/pkg/expapi/v1/conversion.go @@ -22,6 +22,7 @@ import ( "k8s.io/kubernetes/pkg/api" v1 "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/conversion" + "k8s.io/kubernetes/pkg/expapi" ) func addConversionFuncs() { @@ -29,6 +30,9 @@ func addConversionFuncs() { err := api.Scheme.AddConversionFuncs( convert_api_PodSpec_To_v1_PodSpec, convert_v1_PodSpec_To_api_PodSpec, + convert_expapi_DeploymentSpec_To_v1_DeploymentSpec, + convert_v1_DeploymentSpec_To_expapi_DeploymentSpec, + convert_v1_DeploymentStrategy_To_expapi_DeploymentStrategy, ) if err != nil { // If one of the conversion functions is malformed, detect it immediately. @@ -169,3 +173,86 @@ func convert_v1_PodSpec_To_api_PodSpec(in *v1.PodSpec, out *api.PodSpec, s conve } return nil } + +func convert_expapi_DeploymentSpec_To_v1_DeploymentSpec(in *expapi.DeploymentSpec, out *DeploymentSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*expapi.DeploymentSpec))(in) + } + out.Replicas = new(int) + *out.Replicas = in.Replicas + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(v1.PodTemplateSpec) + if err := convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(in.Template, out.Template, s); err != nil { + return err + } + } else { + out.Template = nil + } + if err := convert_expapi_DeploymentStrategy_To_v1_DeploymentStrategy(&in.Strategy, &out.Strategy, s); err != nil { + return err + } + if in.UniqueLabelKey != nil { + out.UniqueLabelKey = new(string) + *out.UniqueLabelKey = *in.UniqueLabelKey + } else { + out.UniqueLabelKey = nil + } + return nil +} + +func convert_v1_DeploymentSpec_To_expapi_DeploymentSpec(in *DeploymentSpec, out *expapi.DeploymentSpec, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*DeploymentSpec))(in) + } + out.Replicas = *in.Replicas + if in.Selector != nil { + out.Selector = make(map[string]string) + for key, val := range in.Selector { + out.Selector[key] = val + } + } else { + out.Selector = nil + } + if in.Template != nil { + out.Template = new(api.PodTemplateSpec) + if err := convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(in.Template, out.Template, s); err != nil { + return err + } + } else { + out.Template = nil + } + if err := convert_v1_DeploymentStrategy_To_expapi_DeploymentStrategy(&in.Strategy, &out.Strategy, s); err != nil { + return err + } + if in.UniqueLabelKey != nil { + out.UniqueLabelKey = new(string) + *out.UniqueLabelKey = *in.UniqueLabelKey + } else { + out.UniqueLabelKey = nil + } + return nil +} + +func convert_v1_DeploymentStrategy_To_expapi_DeploymentStrategy(in *DeploymentStrategy, out *expapi.DeploymentStrategy, s conversion.Scope) error { + if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { + defaulting.(func(*DeploymentStrategy))(in) + } + out.Type = expapi.DeploymentType(in.Type) + if in.RollingUpdate != nil { + out.RollingUpdate = new(expapi.RollingUpdateDeployment) + if err := convert_v1_RollingUpdateDeployment_To_expapi_RollingUpdateDeployment(in.RollingUpdate, out.RollingUpdate, s); err != nil { + return err + } + } else { + out.RollingUpdate = nil + } + return nil +} diff --git a/pkg/expapi/v1/conversion_generated.go b/pkg/expapi/v1/conversion_generated.go index 1165f892831..fe8a0a19b27 100644 --- a/pkg/expapi/v1/conversion_generated.go +++ b/pkg/expapi/v1/conversion_generated.go @@ -1512,44 +1512,6 @@ func convert_expapi_DeploymentList_To_v1_DeploymentList(in *expapi.DeploymentLis return nil } -func convert_expapi_DeploymentSpec_To_v1_DeploymentSpec(in *expapi.DeploymentSpec, out *DeploymentSpec, s conversion.Scope) error { - if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { - defaulting.(func(*expapi.DeploymentSpec))(in) - } - if in.Replicas != nil { - out.Replicas = new(int) - *out.Replicas = *in.Replicas - } else { - out.Replicas = nil - } - if in.Selector != nil { - out.Selector = make(map[string]string) - for key, val := range in.Selector { - out.Selector[key] = val - } - } else { - out.Selector = nil - } - if in.Template != nil { - out.Template = new(v1.PodTemplateSpec) - if err := convert_api_PodTemplateSpec_To_v1_PodTemplateSpec(in.Template, out.Template, s); err != nil { - return err - } - } else { - out.Template = nil - } - if err := convert_expapi_DeploymentStrategy_To_v1_DeploymentStrategy(&in.Strategy, &out.Strategy, s); err != nil { - return err - } - if in.UniqueLabelKey != nil { - out.UniqueLabelKey = new(string) - *out.UniqueLabelKey = *in.UniqueLabelKey - } else { - out.UniqueLabelKey = nil - } - return nil -} - func convert_expapi_DeploymentStatus_To_v1_DeploymentStatus(in *expapi.DeploymentStatus, out *DeploymentStatus, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*expapi.DeploymentStatus))(in) @@ -1929,44 +1891,6 @@ func convert_v1_DeploymentList_To_expapi_DeploymentList(in *DeploymentList, out return nil } -func convert_v1_DeploymentSpec_To_expapi_DeploymentSpec(in *DeploymentSpec, out *expapi.DeploymentSpec, s conversion.Scope) error { - if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { - defaulting.(func(*DeploymentSpec))(in) - } - if in.Replicas != nil { - out.Replicas = new(int) - *out.Replicas = *in.Replicas - } else { - out.Replicas = nil - } - if in.Selector != nil { - out.Selector = make(map[string]string) - for key, val := range in.Selector { - out.Selector[key] = val - } - } else { - out.Selector = nil - } - if in.Template != nil { - out.Template = new(api.PodTemplateSpec) - if err := convert_v1_PodTemplateSpec_To_api_PodTemplateSpec(in.Template, out.Template, s); err != nil { - return err - } - } else { - out.Template = nil - } - if err := convert_v1_DeploymentStrategy_To_expapi_DeploymentStrategy(&in.Strategy, &out.Strategy, s); err != nil { - return err - } - if in.UniqueLabelKey != nil { - out.UniqueLabelKey = new(string) - *out.UniqueLabelKey = *in.UniqueLabelKey - } else { - out.UniqueLabelKey = nil - } - return nil -} - func convert_v1_DeploymentStatus_To_expapi_DeploymentStatus(in *DeploymentStatus, out *expapi.DeploymentStatus, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*DeploymentStatus))(in) @@ -1976,22 +1900,6 @@ func convert_v1_DeploymentStatus_To_expapi_DeploymentStatus(in *DeploymentStatus return nil } -func convert_v1_DeploymentStrategy_To_expapi_DeploymentStrategy(in *DeploymentStrategy, out *expapi.DeploymentStrategy, s conversion.Scope) error { - if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { - defaulting.(func(*DeploymentStrategy))(in) - } - out.Type = expapi.DeploymentType(in.Type) - if in.RollingUpdate != nil { - out.RollingUpdate = new(expapi.RollingUpdateDeployment) - if err := convert_v1_RollingUpdateDeployment_To_expapi_RollingUpdateDeployment(in.RollingUpdate, out.RollingUpdate, s); err != nil { - return err - } - } else { - out.RollingUpdate = nil - } - return nil -} - func convert_v1_HorizontalPodAutoscaler_To_expapi_HorizontalPodAutoscaler(in *HorizontalPodAutoscaler, out *expapi.HorizontalPodAutoscaler, s conversion.Scope) error { if defaulting, found := s.DefaultingInterface(reflect.TypeOf(*in)); found { defaulting.(func(*HorizontalPodAutoscaler))(in) @@ -2262,7 +2170,6 @@ func init() { convert_expapi_DaemonStatus_To_v1_DaemonStatus, convert_expapi_Daemon_To_v1_Daemon, convert_expapi_DeploymentList_To_v1_DeploymentList, - convert_expapi_DeploymentSpec_To_v1_DeploymentSpec, convert_expapi_DeploymentStatus_To_v1_DeploymentStatus, convert_expapi_DeploymentStrategy_To_v1_DeploymentStrategy, convert_expapi_Deployment_To_v1_Deployment, @@ -2289,9 +2196,7 @@ func init() { convert_v1_DaemonStatus_To_expapi_DaemonStatus, convert_v1_Daemon_To_expapi_Daemon, convert_v1_DeploymentList_To_expapi_DeploymentList, - convert_v1_DeploymentSpec_To_expapi_DeploymentSpec, convert_v1_DeploymentStatus_To_expapi_DeploymentStatus, - convert_v1_DeploymentStrategy_To_expapi_DeploymentStrategy, convert_v1_Deployment_To_expapi_Deployment, convert_v1_EmptyDirVolumeSource_To_api_EmptyDirVolumeSource, convert_v1_EnvVarSource_To_api_EnvVarSource, diff --git a/pkg/expapi/validation/validation.go b/pkg/expapi/validation/validation.go index 7a402272ed7..2bc02d80e5a 100644 --- a/pkg/expapi/validation/validation.go +++ b/pkg/expapi/validation/validation.go @@ -17,6 +17,8 @@ limitations under the License. package validation import ( + "strconv" + "k8s.io/kubernetes/pkg/api" apivalidation "k8s.io/kubernetes/pkg/api/validation" "k8s.io/kubernetes/pkg/expapi" @@ -156,3 +158,102 @@ func ValidateDaemonSpec(spec *expapi.DaemonSpec) errs.ValidationErrorList { func ValidateDaemonName(name string, prefix bool) (bool, string) { return apivalidation.NameIsDNSSubdomain(name, prefix) } + +// Validates that the given name can be used as a deployment name. +func ValidateDeploymentName(name string, prefix bool) (bool, string) { + return apivalidation.NameIsDNSSubdomain(name, prefix) +} + +func ValidatePositiveIntOrPercent(intOrPercent util.IntOrString, fieldName string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if intOrPercent.Kind == util.IntstrString { + if !util.IsValidPercent(intOrPercent.StrVal) { + allErrs = append(allErrs, errs.NewFieldInvalid(fieldName, intOrPercent, "value should be int(5) or percentage(5%)")) + } + + } else if intOrPercent.Kind == util.IntstrInt { + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(intOrPercent.IntVal), fieldName)...) + } + return allErrs +} + +func getPercentValue(intOrStringValue util.IntOrString) (int, bool) { + if intOrStringValue.Kind != util.IntstrString || !util.IsValidPercent(intOrStringValue.StrVal) { + return 0, false + } + value, _ := strconv.Atoi(intOrStringValue.StrVal[:len(intOrStringValue.StrVal)-1]) + return value, true +} + +func getIntOrPercentValue(intOrStringValue util.IntOrString) int { + value, isPercent := getPercentValue(intOrStringValue) + if isPercent { + return value + } + return intOrStringValue.IntVal +} + +func IsNotMoreThan100Percent(intOrStringValue util.IntOrString, fieldName string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + value, isPercent := getPercentValue(intOrStringValue) + if !isPercent || value <= 100 { + return nil + } + allErrs = append(allErrs, errs.NewFieldInvalid(fieldName, intOrStringValue, "should not be more than 100%")) + return allErrs +} + +func ValidateRollingUpdateDeployment(rollingUpdate *expapi.RollingUpdateDeployment, fieldName string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxUnavailable, fieldName+"maxUnavailable")...) + allErrs = append(allErrs, ValidatePositiveIntOrPercent(rollingUpdate.MaxSurge, fieldName+".maxSurge")...) + if getIntOrPercentValue(rollingUpdate.MaxUnavailable) == 0 && getIntOrPercentValue(rollingUpdate.MaxSurge) == 0 { + // Both MaxSurge and MaxUnavailable cannot be zero. + allErrs = append(allErrs, errs.NewFieldInvalid(fieldName+".maxUnavailable", rollingUpdate.MaxUnavailable, "cannot be 0 when maxSurge is 0 as well")) + } + // Validate that MaxUnavailable is not more than 100%. + allErrs = append(allErrs, IsNotMoreThan100Percent(rollingUpdate.MaxUnavailable, fieldName+".maxUnavailable")...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(rollingUpdate.MinReadySeconds), fieldName+".minReadySeconds")...) + return allErrs +} + +func ValidateDeploymentStrategy(strategy *expapi.DeploymentStrategy, fieldName string) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + if strategy.RollingUpdate == nil { + return allErrs + } + switch strategy.Type { + case expapi.DeploymentRecreate: + allErrs = append(allErrs, errs.NewFieldForbidden("rollingUpdate", "rollingUpdate should be nil when strategy type is "+expapi.DeploymentRecreate)) + case expapi.DeploymentRollingUpdate: + allErrs = append(allErrs, ValidateRollingUpdateDeployment(strategy.RollingUpdate, "rollingUpdate")...) + } + return allErrs +} + +// Validates given deployment spec. +func ValidateDeploymentSpec(spec *expapi.DeploymentSpec) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, apivalidation.ValidateNonEmptySelector(spec.Selector, "selector")...) + allErrs = append(allErrs, apivalidation.ValidatePositiveField(int64(spec.Replicas), "replicas")...) + allErrs = append(allErrs, apivalidation.ValidatePodTemplateSpecForRC(spec.Template, spec.Selector, spec.Replicas, "template")...) + allErrs = append(allErrs, ValidateDeploymentStrategy(&spec.Strategy, "strategy")...) + if spec.UniqueLabelKey != nil { + allErrs = append(allErrs, apivalidation.ValidateLabelName(*spec.UniqueLabelKey, "uniqueLabel")...) + } + return allErrs +} + +func ValidateDeploymentUpdate(old, update *expapi.Deployment) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMetaUpdate(&update.ObjectMeta, &old.ObjectMeta).Prefix("metadata")...) + allErrs = append(allErrs, ValidateDeploymentSpec(&update.Spec).Prefix("spec")...) + return allErrs +} + +func ValidateDeployment(obj *expapi.Deployment) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, apivalidation.ValidateObjectMeta(&obj.ObjectMeta, true, ValidateDeploymentName).Prefix("metadata")...) + allErrs = append(allErrs, ValidateDeploymentSpec(&obj.Spec).Prefix("spec")...) + return allErrs +} diff --git a/pkg/expapi/validation/validation_test.go b/pkg/expapi/validation/validation_test.go index 082a84c0139..4aaf939481f 100644 --- a/pkg/expapi/validation/validation_test.go +++ b/pkg/expapi/validation/validation_test.go @@ -23,6 +23,7 @@ import ( "k8s.io/kubernetes/pkg/api" "k8s.io/kubernetes/pkg/api/resource" "k8s.io/kubernetes/pkg/expapi" + "k8s.io/kubernetes/pkg/util" errors "k8s.io/kubernetes/pkg/util/fielderrors" ) @@ -540,3 +541,120 @@ func TestValidateDaemon(t *testing.T) { } } } + +func validDeployment() *expapi.Deployment { + return &expapi.Deployment{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: api.NamespaceDefault, + }, + Spec: expapi.DeploymentSpec{ + Selector: map[string]string{ + "name": "abc", + }, + Template: &api.PodTemplateSpec{ + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: api.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, + }, + }, + }, + }, + }, + } +} + +func TestValidateDeployment(t *testing.T) { + successCases := []*expapi.Deployment{ + validDeployment(), + } + for _, successCase := range successCases { + if errs := ValidateDeployment(successCase); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + } + + errorCases := map[string]*expapi.Deployment{} + errorCases["metadata.name: required value"] = &expapi.Deployment{ + ObjectMeta: api.ObjectMeta{ + Namespace: api.NamespaceDefault, + }, + } + // selector should match the labels in pod template. + invalidSelectorDeployment := validDeployment() + invalidSelectorDeployment.Spec.Selector = map[string]string{ + "name": "def", + } + errorCases["selector does not match labels"] = invalidSelectorDeployment + + // RestartPolicy should be always. + invalidRestartPolicyDeployment := validDeployment() + invalidRestartPolicyDeployment.Spec.Template.Spec.RestartPolicy = api.RestartPolicyNever + errorCases["unsupported value 'Never'"] = invalidRestartPolicyDeployment + + // invalid unique label key. + invalidUniqueLabelDeployment := validDeployment() + invalidUniqueLabel := "abc/def/ghi" + invalidUniqueLabelDeployment.Spec.UniqueLabelKey = &invalidUniqueLabel + errorCases["spec.uniqueLabel: invalid value"] = invalidUniqueLabelDeployment + + // rollingUpdate should be nil for recreate. + invalidRecreateDeployment := validDeployment() + invalidRecreateDeployment.Spec.Strategy = expapi.DeploymentStrategy{ + Type: expapi.DeploymentRecreate, + RollingUpdate: &expapi.RollingUpdateDeployment{}, + } + errorCases["rollingUpdate should be nil when strategy type is Recreate"] = invalidRecreateDeployment + + // MaxSurge should be in the form of 20%. + invalidMaxSurgeDeployment := validDeployment() + invalidMaxSurgeDeployment.Spec.Strategy = expapi.DeploymentStrategy{ + Type: expapi.DeploymentRollingUpdate, + RollingUpdate: &expapi.RollingUpdateDeployment{ + MaxSurge: util.NewIntOrStringFromString("20Percent"), + }, + } + errorCases["value should be int(5) or percentage(5%)"] = invalidMaxSurgeDeployment + + // MaxSurge and MaxUnavailable cannot both be zero. + invalidRollingUpdateDeployment := validDeployment() + invalidRollingUpdateDeployment.Spec.Strategy = expapi.DeploymentStrategy{ + Type: expapi.DeploymentRollingUpdate, + RollingUpdate: &expapi.RollingUpdateDeployment{ + MaxSurge: util.NewIntOrStringFromString("0%"), + MaxUnavailable: util.NewIntOrStringFromInt(0), + }, + } + errorCases["cannot be 0 when maxSurge is 0 as well"] = invalidRollingUpdateDeployment + + // MaxUnavailable should not be more than 100%. + invalidMaxUnavailableDeployment := validDeployment() + invalidMaxUnavailableDeployment.Spec.Strategy = expapi.DeploymentStrategy{ + Type: expapi.DeploymentRollingUpdate, + RollingUpdate: &expapi.RollingUpdateDeployment{ + MaxUnavailable: util.NewIntOrStringFromString("110%"), + }, + } + errorCases["should not be more than 100%"] = invalidMaxUnavailableDeployment + + for k, v := range errorCases { + errs := ValidateDeployment(v) + if len(errs) == 0 { + t.Errorf("expected failure for %s", k) + } else if !strings.Contains(errs[0].Error(), k) { + t.Errorf("unexpected error: %v, expected: %s", errs[0], k) + } + } +} diff --git a/pkg/util/validation.go b/pkg/util/validation.go index 66010f5c8f0..20f68317201 100644 --- a/pkg/util/validation.go +++ b/pkg/util/validation.go @@ -139,3 +139,11 @@ func IsValidPortName(port string) bool { func IsValidIPv4(value string) bool { return net.ParseIP(value) != nil && net.ParseIP(value).To4() != nil } + +const percentFmt string = "[0-9]+%" + +var percentRegexp = regexp.MustCompile("^" + percentFmt + "$") + +func IsValidPercent(percent string) bool { + return percentRegexp.MatchString(percent) +}