diff --git a/api/swagger-spec/v1.json b/api/swagger-spec/v1.json index 90a7e1234e3..8240c9f9fa4 100644 --- a/api/swagger-spec/v1.json +++ b/api/swagger-spec/v1.json @@ -11575,7 +11575,15 @@ }, "default": { "type": "any", - "description": "Default usage constraints on this kind by resource name. Default values on this kind by resource name if omitted." + "description": "Default resource requirement limit value by resource name if resource limit is omitted." + }, + "defaultRequest": { + "type": "any", + "description": "DefaultRequest is the default resource requirement request value by resource name if resource request is omitted." + }, + "maxLimitRequestRatio": { + "type": "any", + "description": "MaxLimitRequestRatio if specified, the named resource must have a request and limit that are both non-zero where limit divided by request is less than or equal to the enumerated value; this represents the max burst for the named resource." } } }, diff --git a/pkg/api/deep_copy_generated.go b/pkg/api/deep_copy_generated.go index a84d50522fa..bf3b7a23fc1 100644 --- a/pkg/api/deep_copy_generated.go +++ b/pkg/api/deep_copy_generated.go @@ -689,6 +689,30 @@ func deepCopy_api_LimitRangeItem(in LimitRangeItem, out *LimitRangeItem, c *conv } else { out.Default = nil } + if in.DefaultRequest != nil { + out.DefaultRequest = make(ResourceList) + for key, val := range in.DefaultRequest { + newVal := new(resource.Quantity) + if err := deepCopy_resource_Quantity(val, newVal, c); err != nil { + return err + } + out.DefaultRequest[key] = *newVal + } + } else { + out.DefaultRequest = nil + } + if in.MaxLimitRequestRatio != nil { + out.MaxLimitRequestRatio = make(ResourceList) + for key, val := range in.MaxLimitRequestRatio { + newVal := new(resource.Quantity) + if err := deepCopy_resource_Quantity(val, newVal, c); err != nil { + return err + } + out.MaxLimitRequestRatio[key] = *newVal + } + } else { + out.MaxLimitRequestRatio = nil + } return nil } diff --git a/pkg/api/testing/fuzzer.go b/pkg/api/testing/fuzzer.go index 2ea5c8fc461..b8568e745df 100644 --- a/pkg/api/testing/fuzzer.go +++ b/pkg/api/testing/fuzzer.go @@ -183,6 +183,28 @@ func FuzzerFor(t *testing.T, version string, src rand.Source) *fuzz.Fuzzer { q.Limits[api.ResourceStorage] = *storageLimit.Copy() q.Requests[api.ResourceStorage] = *storageLimit.Copy() }, + func(q *api.LimitRangeItem, c fuzz.Continue) { + randomQuantity := func() resource.Quantity { + return *resource.NewQuantity(c.Int63n(1000), resource.DecimalExponent) + } + cpuLimit := randomQuantity() + + q.Type = api.LimitTypeContainer + q.Default = make(api.ResourceList) + q.Default[api.ResourceCPU] = *(cpuLimit.Copy()) + + q.DefaultRequest = make(api.ResourceList) + q.DefaultRequest[api.ResourceCPU] = *(cpuLimit.Copy()) + + q.Max = make(api.ResourceList) + q.Max[api.ResourceCPU] = *(cpuLimit.Copy()) + + q.Min = make(api.ResourceList) + q.Min[api.ResourceCPU] = *(cpuLimit.Copy()) + + q.MaxLimitRequestRatio = make(api.ResourceList) + q.MaxLimitRequestRatio[api.ResourceCPU] = resource.MustParse("10") + }, func(p *api.PullPolicy, c fuzz.Continue) { policies := []api.PullPolicy{api.PullAlways, api.PullNever, api.PullIfNotPresent} *p = policies[c.Rand.Intn(len(policies))] diff --git a/pkg/api/types.go b/pkg/api/types.go index 9eee47f50c9..842a90412b9 100644 --- a/pkg/api/types.go +++ b/pkg/api/types.go @@ -1942,8 +1942,12 @@ type LimitRangeItem struct { Max ResourceList `json:"max,omitempty"` // Min usage constraints on this kind by resource name Min ResourceList `json:"min,omitempty"` - // Default usage constraints on this kind by resource name + // Default resource requirement limit value by resource name. Default ResourceList `json:"default,omitempty"` + // DefaultRequest resource requirement request value by resource name. + DefaultRequest ResourceList `json:"defaultRequest,omitempty"` + // MaxLimitRequestRatio represents the max burst value for the named resource + MaxLimitRequestRatio ResourceList `json:"maxLimitRequestRatio,omitempty"` } // LimitRangeSpec defines a min/max usage limit for resources that match on kind diff --git a/pkg/api/v1/conversion_generated.go b/pkg/api/v1/conversion_generated.go index f1ea280ac2e..3b317ac1edb 100644 --- a/pkg/api/v1/conversion_generated.go +++ b/pkg/api/v1/conversion_generated.go @@ -805,6 +805,30 @@ func convert_api_LimitRangeItem_To_v1_LimitRangeItem(in *api.LimitRangeItem, out } else { out.Default = nil } + if in.DefaultRequest != nil { + out.DefaultRequest = make(ResourceList) + for key, val := range in.DefaultRequest { + newVal := resource.Quantity{} + if err := s.Convert(&val, &newVal, 0); err != nil { + return err + } + out.DefaultRequest[ResourceName(key)] = newVal + } + } else { + out.DefaultRequest = nil + } + if in.MaxLimitRequestRatio != nil { + out.MaxLimitRequestRatio = make(ResourceList) + for key, val := range in.MaxLimitRequestRatio { + newVal := resource.Quantity{} + if err := s.Convert(&val, &newVal, 0); err != nil { + return err + } + out.MaxLimitRequestRatio[ResourceName(key)] = newVal + } + } else { + out.MaxLimitRequestRatio = nil + } return nil } @@ -3170,6 +3194,30 @@ func convert_v1_LimitRangeItem_To_api_LimitRangeItem(in *LimitRangeItem, out *ap } else { out.Default = nil } + if in.DefaultRequest != nil { + out.DefaultRequest = make(api.ResourceList) + for key, val := range in.DefaultRequest { + newVal := resource.Quantity{} + if err := s.Convert(&val, &newVal, 0); err != nil { + return err + } + out.DefaultRequest[api.ResourceName(key)] = newVal + } + } else { + out.DefaultRequest = nil + } + if in.MaxLimitRequestRatio != nil { + out.MaxLimitRequestRatio = make(api.ResourceList) + for key, val := range in.MaxLimitRequestRatio { + newVal := resource.Quantity{} + if err := s.Convert(&val, &newVal, 0); err != nil { + return err + } + out.MaxLimitRequestRatio[api.ResourceName(key)] = newVal + } + } else { + out.MaxLimitRequestRatio = nil + } return nil } diff --git a/pkg/api/v1/deep_copy_generated.go b/pkg/api/v1/deep_copy_generated.go index 2b7fb503d79..06d4a281f65 100644 --- a/pkg/api/v1/deep_copy_generated.go +++ b/pkg/api/v1/deep_copy_generated.go @@ -704,6 +704,30 @@ func deepCopy_v1_LimitRangeItem(in LimitRangeItem, out *LimitRangeItem, c *conve } else { out.Default = nil } + if in.DefaultRequest != nil { + out.DefaultRequest = make(ResourceList) + for key, val := range in.DefaultRequest { + newVal := new(resource.Quantity) + if err := deepCopy_resource_Quantity(val, newVal, c); err != nil { + return err + } + out.DefaultRequest[key] = *newVal + } + } else { + out.DefaultRequest = nil + } + if in.MaxLimitRequestRatio != nil { + out.MaxLimitRequestRatio = make(ResourceList) + for key, val := range in.MaxLimitRequestRatio { + newVal := new(resource.Quantity) + if err := deepCopy_resource_Quantity(val, newVal, c); err != nil { + return err + } + out.MaxLimitRequestRatio[key] = *newVal + } + } else { + out.MaxLimitRequestRatio = nil + } return nil } diff --git a/pkg/api/v1/defaults.go b/pkg/api/v1/defaults.go index eee2a647c0c..e9d857aeac4 100644 --- a/pkg/api/v1/defaults.go +++ b/pkg/api/v1/defaults.go @@ -188,6 +188,37 @@ func addDefaultingFuncs() { } } }, + func(obj *LimitRangeItem) { + // for container limits, we apply default values + if obj.Type == LimitTypeContainer { + + if obj.Default == nil { + obj.Default = make(ResourceList) + } + if obj.DefaultRequest == nil { + obj.DefaultRequest = make(ResourceList) + } + + // If a default limit is unspecified, but the max is specified, default the limit to the max + for key, value := range obj.Max { + if _, exists := obj.Default[key]; !exists { + obj.Default[key] = *(value.Copy()) + } + } + // If a default limit is specified, but the default request is not, default request to limit + for key, value := range obj.Default { + if _, exists := obj.DefaultRequest[key]; !exists { + obj.DefaultRequest[key] = *(value.Copy()) + } + } + // If a default request is not specified, but the min is provided, default request to the min + for key, value := range obj.Min { + if _, exists := obj.DefaultRequest[key]; !exists { + obj.DefaultRequest[key] = *(value.Copy()) + } + } + } + }, ) } diff --git a/pkg/api/v1/defaults_test.go b/pkg/api/v1/defaults_test.go index f417176c2de..6b451f5781b 100644 --- a/pkg/api/v1/defaults_test.go +++ b/pkg/api/v1/defaults_test.go @@ -21,6 +21,7 @@ import ( "testing" "k8s.io/kubernetes/pkg/api" + "k8s.io/kubernetes/pkg/api/resource" versioned "k8s.io/kubernetes/pkg/api/v1" "k8s.io/kubernetes/pkg/runtime" "k8s.io/kubernetes/pkg/util" @@ -489,3 +490,45 @@ func TestSetDefaultObjectFieldSelectorAPIVersion(t *testing.T) { t.Errorf("Expected default APIVersion v1, got: %v", apiVersion) } } + +func TestSetDefaultLimitRangeItem(t *testing.T) { + limitRange := &versioned.LimitRange{ + ObjectMeta: versioned.ObjectMeta{ + Name: "test-defaults", + }, + Spec: versioned.LimitRangeSpec{ + Limits: []versioned.LimitRangeItem{{ + Type: versioned.LimitTypeContainer, + Max: versioned.ResourceList{ + versioned.ResourceCPU: resource.MustParse("100m"), + }, + Min: versioned.ResourceList{ + versioned.ResourceMemory: resource.MustParse("100Mi"), + }, + Default: versioned.ResourceList{}, + DefaultRequest: versioned.ResourceList{}, + }}, + }, + } + + output := roundTrip(t, runtime.Object(limitRange)) + limitRange2 := output.(*versioned.LimitRange) + defaultLimit := limitRange2.Spec.Limits[0].Default + defaultRequest := limitRange2.Spec.Limits[0].DefaultRequest + + // verify that default cpu was set to the max + defaultValue := defaultLimit[versioned.ResourceCPU] + if defaultValue.String() != "100m" { + t.Errorf("Expected default cpu: %s, got: %s", "100m", defaultValue.String()) + } + // verify that default request was set to the limit + requestValue := defaultRequest[versioned.ResourceCPU] + if requestValue.String() != "100m" { + t.Errorf("Expected request cpu: %s, got: %s", "100m", requestValue.String()) + } + // verify that if a min is provided, it will be the default if no limit is specified + requestMinValue := defaultRequest[versioned.ResourceMemory] + if requestMinValue.String() != "100Mi" { + t.Errorf("Expected request memory: %s, got: %s", "100Mi", requestMinValue.String()) + } +} diff --git a/pkg/api/v1/types.go b/pkg/api/v1/types.go index 45b100c87dc..6fea5902d77 100644 --- a/pkg/api/v1/types.go +++ b/pkg/api/v1/types.go @@ -2327,9 +2327,12 @@ type LimitRangeItem struct { Max ResourceList `json:"max,omitempty"` // Min usage constraints on this kind by resource name. Min ResourceList `json:"min,omitempty"` - // Default usage constraints on this kind by resource name. - // Default values on this kind by resource name if omitted. + // Default resource requirement limit value by resource name if resource limit is omitted. Default ResourceList `json:"default,omitempty"` + // DefaultRequest is the default resource requirement request value by resource name if resource request is omitted. + DefaultRequest ResourceList `json:"defaultRequest,omitempty"` + // MaxLimitRequestRatio if specified, the named resource must have a request and limit that are both non-zero where limit divided by request is less than or equal to the enumerated value; this represents the max burst for the named resource. + MaxLimitRequestRatio ResourceList `json:"maxLimitRequestRatio,omitempty"` } // LimitRangeSpec defines a min/max usage limit for resources that match on kind. diff --git a/pkg/api/v1/types_swagger_doc_generated.go b/pkg/api/v1/types_swagger_doc_generated.go index 73da4e60ffb..33656b28184 100644 --- a/pkg/api/v1/types_swagger_doc_generated.go +++ b/pkg/api/v1/types_swagger_doc_generated.go @@ -474,11 +474,13 @@ func (LimitRange) SwaggerDoc() map[string]string { } var map_LimitRangeItem = map[string]string{ - "": "LimitRangeItem defines a min/max usage limit for any resource that matches on kind.", - "type": "Type of resource that this limit applies to.", - "max": "Max usage constraints on this kind by resource name.", - "min": "Min usage constraints on this kind by resource name.", - "default": "Default usage constraints on this kind by resource name. Default values on this kind by resource name if omitted.", + "": "LimitRangeItem defines a min/max usage limit for any resource that matches on kind.", + "type": "Type of resource that this limit applies to.", + "max": "Max usage constraints on this kind by resource name.", + "min": "Min usage constraints on this kind by resource name.", + "default": "Default resource requirement limit value by resource name if resource limit is omitted.", + "defaultRequest": "DefaultRequest is the default resource requirement request value by resource name if resource request is omitted.", + "maxLimitRequestRatio": "MaxLimitRequestRatio if specified, the named resource must have a request and limit that are both non-zero where limit divided by request is less than or equal to the enumerated value; this represents the max burst for the named resource.", } func (LimitRangeItem) SwaggerDoc() map[string]string { diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index f87ceff2c54..606844aa7b1 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -1418,6 +1418,7 @@ func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList { min := map[string]int64{} max := map[string]int64{} defaults := map[string]int64{} + defaultRequests := map[string]int64{} for k := range limit.Max { allErrs = append(allErrs, validateResourceName(string(k), fmt.Sprintf("spec.limits[%d].max[%s]", i, k))...) @@ -1437,28 +1438,56 @@ func ValidateLimitRange(limitRange *api.LimitRange) errs.ValidationErrorList { q := limit.Default[k] defaults[string(k)] = q.Value() } + for k := range limit.DefaultRequest { + allErrs = append(allErrs, validateResourceName(string(k), fmt.Sprintf("spec.limits[%d].defaultRequest[%s]", i, k))...) + keys.Insert(string(k)) + q := limit.DefaultRequest[k] + defaultRequests[string(k)] = q.Value() + } + for k := range limit.MaxLimitRequestRatio { + allErrs = append(allErrs, validateResourceName(string(k), fmt.Sprintf("spec.limits[%d].maxLimitRequestRatio[%s]", i, k))...) + } for k := range keys { minValue, minValueFound := min[k] maxValue, maxValueFound := max[k] defaultValue, defaultValueFound := defaults[k] + defaultRequestValue, defaultRequestValueFound := defaultRequests[k] if minValueFound && maxValueFound && minValue > maxValue { minQuantity := limit.Min[api.ResourceName(k)] maxQuantity := limit.Max[api.ResourceName(k)] - allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.limits[%d].max[%s]", i, k), minValue, fmt.Sprintf("min value %s is greater than max value %s", minQuantity.String(), maxQuantity.String()))) + allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.limits[%d].min[%s]", i, k), minValue, fmt.Sprintf("min value %s is greater than max value %s", minQuantity.String(), maxQuantity.String()))) + } + + if defaultRequestValueFound && minValueFound && minValue > defaultRequestValue { + minQuantity := limit.Min[api.ResourceName(k)] + defaultRequestQuantity := limit.DefaultRequest[api.ResourceName(k)] + allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.limits[%d].defaultRequest[%s]", i, k), defaultRequestValue, fmt.Sprintf("min value %s is greater than default request value %s", minQuantity.String(), defaultRequestQuantity.String()))) + } + + if defaultRequestValueFound && maxValueFound && defaultRequestValue > maxValue { + maxQuantity := limit.Max[api.ResourceName(k)] + defaultRequestQuantity := limit.DefaultRequest[api.ResourceName(k)] + allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.limits[%d].defaultRequest[%s]", i, k), defaultRequestValue, fmt.Sprintf("default request value %s is greater than max value %s", defaultRequestQuantity.String(), maxQuantity.String()))) + } + + if defaultRequestValueFound && defaultValueFound && defaultRequestValue > defaultValue { + defaultQuantity := limit.Default[api.ResourceName(k)] + defaultRequestQuantity := limit.DefaultRequest[api.ResourceName(k)] + allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.limits[%d].defaultRequest[%s]", i, k), defaultRequestValue, fmt.Sprintf("default request value %s is greater than default limit value %s", defaultRequestQuantity.String(), defaultQuantity.String()))) } if defaultValueFound && minValueFound && minValue > defaultValue { minQuantity := limit.Min[api.ResourceName(k)] defaultQuantity := limit.Default[api.ResourceName(k)] - allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.limits[%d].max[%s]", i, k), minValue, fmt.Sprintf("min value %s is greater than default value %s", minQuantity.String(), defaultQuantity.String()))) + allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.limits[%d].default[%s]", i, k), minValue, fmt.Sprintf("min value %s is greater than default value %s", minQuantity.String(), defaultQuantity.String()))) } if defaultValueFound && maxValueFound && defaultValue > maxValue { maxQuantity := limit.Max[api.ResourceName(k)] defaultQuantity := limit.Default[api.ResourceName(k)] - allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.limits[%d].max[%s]", i, k), minValue, fmt.Sprintf("default value %s is greater than max value %s", defaultQuantity.String(), maxQuantity.String()))) + allErrs = append(allErrs, errs.NewFieldInvalid(fmt.Sprintf("spec.limits[%d].default[%s]", i, k), maxValue, fmt.Sprintf("default value %s is greater than max value %s", defaultQuantity.String(), maxQuantity.String()))) } } } diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index 75d4c099972..db5bd8636ed 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -3226,13 +3226,20 @@ func TestValidateLimitRange(t *testing.T) { api.ResourceMemory: resource.MustParse("10000"), }, Min: api.ResourceList{ - api.ResourceCPU: resource.MustParse("0"), + api.ResourceCPU: resource.MustParse("5"), api.ResourceMemory: resource.MustParse("100"), }, Default: api.ResourceList{ api.ResourceCPU: resource.MustParse("50"), api.ResourceMemory: resource.MustParse("500"), }, + DefaultRequest: api.ResourceList{ + api.ResourceCPU: resource.MustParse("10"), + api.ResourceMemory: resource.MustParse("200"), + }, + MaxLimitRequestRatio: api.ResourceList{ + api.ResourceCPU: resource.MustParse("20"), + }, }, }, } @@ -3291,6 +3298,43 @@ func TestValidateLimitRange(t *testing.T) { }, } + invalidSpecRangeDefaultRequestOutsideRange := api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("1000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + }, + DefaultRequest: api.ResourceList{ + api.ResourceCPU: resource.MustParse("2000"), + }, + }, + }, + } + + invalidSpecRangeRequestMoreThanDefaultRange := api.LimitRangeSpec{ + Limits: []api.LimitRangeItem{ + { + Type: api.LimitTypePod, + Max: api.ResourceList{ + api.ResourceCPU: resource.MustParse("1000"), + }, + Min: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + }, + Default: api.ResourceList{ + api.ResourceCPU: resource.MustParse("500"), + }, + DefaultRequest: api.ResourceList{ + api.ResourceCPU: resource.MustParse("800"), + }, + }, + }, + } + successCases := []api.LimitRange{ { ObjectMeta: api.ObjectMeta{ @@ -3339,6 +3383,14 @@ func TestValidateLimitRange(t *testing.T) { api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidSpecRangeDefaultOutsideRange}, "default value 2k is greater than max value 1k", }, + "invalid spec defaultrequest outside range": { + api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidSpecRangeDefaultRequestOutsideRange}, + "default request value 2k is greater than max value 1k", + }, + "invalid spec defaultrequest more than default": { + api.LimitRange{ObjectMeta: api.ObjectMeta{Name: "abc", Namespace: "foo"}, Spec: invalidSpecRangeRequestMoreThanDefaultRange}, + "default request value 800 is greater than default limit value 500", + }, } for k, v := range errorCases { errs := ValidateLimitRange(&v.R)