diff --git a/pkg/api/helpers.go b/pkg/api/helpers.go index e113ffc2a0d..8248899d56a 100644 --- a/pkg/api/helpers.go +++ b/pkg/api/helpers.go @@ -80,20 +80,36 @@ var Semantic = conversion.EqualitiesOrDie( ) var standardResources = sets.NewString( - string(ResourceMemory), string(ResourceCPU), + string(ResourceMemory), string(ResourcePods), string(ResourceQuotas), string(ResourceServices), string(ResourceReplicationControllers), string(ResourceSecrets), string(ResourcePersistentVolumeClaims), - string(ResourceStorage)) + string(ResourceStorage), +) +// IsStandardResourceName returns true if the resource is known to the system func IsStandardResourceName(str string) bool { return standardResources.Has(str) } +var integerResources = sets.NewString( + string(ResourcePods), + string(ResourceQuotas), + string(ResourceServices), + string(ResourceReplicationControllers), + string(ResourceSecrets), + string(ResourcePersistentVolumeClaims), +) + +// IsIntegerResourceName returns true if the resource is measured in integer values +func IsIntegerResourceName(str string) bool { + return integerResources.Has(str) +} + // NewDeleteOptions returns a DeleteOptions indicating the resource should // be deleted within the specified grace period. Use zero to indicate // immediate deletion. If you would prefer to use the default grace period, diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 2ba2ab7d07a..43bfafa2d33 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -44,6 +44,7 @@ var RepairMalformedUpdates bool = true const cIdentifierErrorMsg string = `must be a C identifier (matching regex ` + validation.CIdentifierFmt + `): e.g. "my_name" or "MyName"` const isNegativeErrorMsg string = `must be non-negative` +const isNotIntegerErrorMsg string = `must be an integer` func intervalErrorMsg(lo, hi int) string { return fmt.Sprintf(`must be greater than %d and less than %d`, lo, hi) @@ -1765,15 +1766,27 @@ func ValidateResourceQuota(resourceQuota *api.ResourceQuota) errs.ValidationErro for k, v := range resourceQuota.Spec.Hard { allErrs = append(allErrs, validateResourceName(string(k), string(resourceQuota.TypeMeta.Kind))...) - allErrs = append(allErrs, ValidatePositiveQuantity(v, string(k))...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) } for k, v := range resourceQuota.Status.Hard { allErrs = append(allErrs, validateResourceName(string(k), string(resourceQuota.TypeMeta.Kind))...) - allErrs = append(allErrs, ValidatePositiveQuantity(v, string(k))...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) } for k, v := range resourceQuota.Status.Used { allErrs = append(allErrs, validateResourceName(string(k), string(resourceQuota.TypeMeta.Kind))...) - allErrs = append(allErrs, ValidatePositiveQuantity(v, string(k))...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) + } + return allErrs +} + +// validateResourceQuantityValue enforces that specified quantity is valid for specified resource +func validateResourceQuantityValue(resource string, value resource.Quantity) errs.ValidationErrorList { + allErrs := errs.ValidationErrorList{} + allErrs = append(allErrs, ValidatePositiveQuantity(value, resource)...) + if api.IsIntegerResourceName(resource) { + if value.MilliValue()%int64(1000) != int64(0) { + allErrs = append(allErrs, errs.NewFieldInvalid(resource, value, isNotIntegerErrorMsg)) + } } return allErrs } @@ -1783,8 +1796,9 @@ func ValidateResourceQuota(resourceQuota *api.ResourceQuota) errs.ValidationErro func ValidateResourceQuotaUpdate(newResourceQuota, oldResourceQuota *api.ResourceQuota) errs.ValidationErrorList { allErrs := errs.ValidationErrorList{} allErrs = append(allErrs, ValidateObjectMetaUpdate(&newResourceQuota.ObjectMeta, &oldResourceQuota.ObjectMeta).Prefix("metadata")...) - for k := range newResourceQuota.Spec.Hard { + for k, v := range newResourceQuota.Spec.Hard { allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) } newResourceQuota.Status = oldResourceQuota.Status return allErrs @@ -1798,11 +1812,13 @@ func ValidateResourceQuotaStatusUpdate(newResourceQuota, oldResourceQuota *api.R if newResourceQuota.ResourceVersion == "" { allErrs = append(allErrs, errs.NewFieldRequired("resourceVersion")) } - for k := range newResourceQuota.Status.Hard { + for k, v := range newResourceQuota.Status.Hard { allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) } - for k := range newResourceQuota.Status.Used { + for k, v := range newResourceQuota.Status.Used { allErrs = append(allErrs, validateResourceName(string(k), string(newResourceQuota.TypeMeta.Kind))...) + allErrs = append(allErrs, validateResourceQuantityValue(string(k), v)...) } newResourceQuota.Spec = oldResourceQuota.Spec return allErrs diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 426a02aa4ab..ca7b8d7bf0d 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -3244,6 +3244,21 @@ func TestValidateResourceQuota(t *testing.T) { }, } + fractionalComputeSpec := api.ResourceQuotaSpec{ + Hard: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100m"), + }, + } + + fractionalPodSpec := api.ResourceQuotaSpec{ + Hard: api.ResourceList{ + api.ResourcePods: resource.MustParse(".1"), + api.ResourceServices: resource.MustParse(".5"), + api.ResourceReplicationControllers: resource.MustParse("1.25"), + api.ResourceQuotas: resource.MustParse("2.5"), + }, + } + successCases := []api.ResourceQuota{ { ObjectMeta: api.ObjectMeta{ @@ -3252,6 +3267,13 @@ func TestValidateResourceQuota(t *testing.T) { }, Spec: spec, }, + { + ObjectMeta: api.ObjectMeta{ + Name: "abc", + Namespace: "foo", + }, + Spec: fractionalComputeSpec, + }, } for _, successCase := range successCases { @@ -3284,6 +3306,10 @@ func TestValidateResourceQuota(t *testing.T) { api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: negativeSpec}, isNegativeErrorMsg, }, + "fractional-api-resource": { + api.ResourceQuota{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: fractionalPodSpec}, + isNotIntegerErrorMsg, + }, } for k, v := range errorCases { errs := ValidateResourceQuota(&v.R)