diff --git a/pkg/api/helper/helpers.go b/pkg/api/helper/helpers.go index e8b7ac923c..e181d4a249 100644 --- a/pkg/api/helper/helpers.go +++ b/pkg/api/helper/helpers.go @@ -170,11 +170,14 @@ func IsStandardLimitRangeType(str string) bool { var standardQuotaResources = sets.NewString( string(api.ResourceCPU), string(api.ResourceMemory), + string(api.ResourceEphemeralStorage), string(api.ResourceRequestsCPU), string(api.ResourceRequestsMemory), string(api.ResourceRequestsStorage), + string(api.ResourceRequestsEphemeralStorage), string(api.ResourceLimitsCPU), string(api.ResourceLimitsMemory), + string(api.ResourceLimitsEphemeralStorage), string(api.ResourcePods), string(api.ResourceQuotas), string(api.ResourceServices), diff --git a/pkg/api/validation/validation.go b/pkg/api/validation/validation.go index 48084abd33..6467f60d18 100644 --- a/pkg/api/validation/validation.go +++ b/pkg/api/validation/validation.go @@ -3493,10 +3493,23 @@ func validateContainerResourceName(value string, fldPath *field.Path) field.Erro return allErrs } +// isLocalStorageResource checks whether the resource is local ephemeral storage +func isLocalStorageResource(name string) bool { + if name == string(api.ResourceEphemeralStorage) || name == string(api.ResourceRequestsEphemeralStorage) || + name == string(api.ResourceLimitsEphemeralStorage) { + return true + } else { + return false + } +} + // Validate resource names that can go in a resource quota // Refer to docs/design/resources.md for more details. func ValidateResourceQuotaResourceName(value string, fldPath *field.Path) field.ErrorList { allErrs := validateResourceName(value, fldPath) + if isLocalStorageResource(value) && !utilfeature.DefaultFeatureGate.Enabled(features.LocalStorageCapacityIsolation) { + return append(allErrs, field.Forbidden(fldPath, "ResourceEphemeralStorage field disabled by feature-gate for ResourceQuota")) + } if len(strings.Split(value, "/")) == 1 { if !helper.IsStandardQuotaResourceName(value) { return append(allErrs, field.Invalid(fldPath, value, isInvalidQuotaResource)) diff --git a/pkg/api/validation/validation_test.go b/pkg/api/validation/validation_test.go index b6f99b2a94..abf57b9387 100644 --- a/pkg/api/validation/validation_test.go +++ b/pkg/api/validation/validation_test.go @@ -2709,6 +2709,62 @@ func TestAlphaLocalStorageCapacityIsolation(t *testing.T) { } +func TestValidateResourceQuotaWithAlphaLocalStorageCapacityIsolation(t *testing.T) { + spec := api.ResourceQuotaSpec{ + Hard: api.ResourceList{ + api.ResourceCPU: resource.MustParse("100"), + api.ResourceMemory: resource.MustParse("10000"), + api.ResourceRequestsCPU: resource.MustParse("100"), + api.ResourceRequestsMemory: resource.MustParse("10000"), + api.ResourceLimitsCPU: resource.MustParse("100"), + api.ResourceLimitsMemory: resource.MustParse("10000"), + api.ResourcePods: resource.MustParse("10"), + api.ResourceServices: resource.MustParse("0"), + api.ResourceReplicationControllers: resource.MustParse("10"), + api.ResourceQuotas: resource.MustParse("10"), + api.ResourceConfigMaps: resource.MustParse("10"), + api.ResourceSecrets: resource.MustParse("10"), + api.ResourceEphemeralStorage: resource.MustParse("10000"), + api.ResourceRequestsEphemeralStorage: resource.MustParse("10000"), + api.ResourceLimitsEphemeralStorage: resource.MustParse("10000"), + }, + } + resourceQuota := &api.ResourceQuota{ + ObjectMeta: metav1.ObjectMeta{ + Name: "abc", + Namespace: "foo", + }, + Spec: spec, + } + + // Enable alpha feature LocalStorageCapacityIsolation + err := utilfeature.DefaultFeatureGate.Set("LocalStorageCapacityIsolation=true") + if err != nil { + t.Errorf("Failed to enable feature gate for LocalStorageCapacityIsolation: %v", err) + return + } + if errs := ValidateResourceQuota(resourceQuota); len(errs) != 0 { + t.Errorf("expected success: %v", errs) + } + + // Disable alpha feature LocalStorageCapacityIsolation + err = utilfeature.DefaultFeatureGate.Set("LocalStorageCapacityIsolation=false") + if err != nil { + t.Errorf("Failed to disable feature gate for LocalStorageCapacityIsolation: %v", err) + return + } + errs := ValidateResourceQuota(resourceQuota) + if len(errs) == 0 { + t.Errorf("expected failure for %s", resourceQuota.Name) + } + expectedErrMes := "ResourceEphemeralStorage field disabled by feature-gate for ResourceQuota" + for i := range errs { + if !strings.Contains(errs[i].Detail, expectedErrMes) { + t.Errorf("[%s]: expected error detail either empty or %s, got %s", resourceQuota.Name, expectedErrMes, errs[i].Detail) + } + } +} + func TestValidatePorts(t *testing.T) { successCase := []api.ContainerPort{ {Name: "abc", ContainerPort: 80, HostPort: 80, Protocol: "TCP"}, diff --git a/pkg/quota/evaluator/core/pods.go b/pkg/quota/evaluator/core/pods.go index da398860df..0417bb8ecc 100644 --- a/pkg/quota/evaluator/core/pods.go +++ b/pkg/quota/evaluator/core/pods.go @@ -43,10 +43,13 @@ import ( var podResources = []api.ResourceName{ api.ResourceCPU, api.ResourceMemory, + api.ResourceEphemeralStorage, api.ResourceRequestsCPU, api.ResourceRequestsMemory, + api.ResourceRequestsEphemeralStorage, api.ResourceLimitsCPU, api.ResourceLimitsMemory, + api.ResourceLimitsEphemeralStorage, api.ResourcePods, } @@ -201,6 +204,13 @@ func podUsageHelper(requests api.ResourceList, limits api.ResourceList) api.Reso if limit, found := limits[api.ResourceMemory]; found { result[api.ResourceLimitsMemory] = limit } + if request, found := requests[api.ResourceEphemeralStorage]; found { + result[api.ResourceEphemeralStorage] = request + result[api.ResourceRequestsEphemeralStorage] = request + } + if limit, found := limits[api.ResourceEphemeralStorage]; found { + result[api.ResourceLimitsEphemeralStorage] = limit + } return result } diff --git a/pkg/quota/evaluator/core/pods_test.go b/pkg/quota/evaluator/core/pods_test.go index 061aa32583..3a93165b81 100644 --- a/pkg/quota/evaluator/core/pods_test.go +++ b/pkg/quota/evaluator/core/pods_test.go @@ -142,6 +142,24 @@ func TestPodEvaluatorUsage(t *testing.T) { api.ResourceMemory: resource.MustParse("1m"), }, }, + "init container local ephemeral storage": { + pod: &api.Pod{ + Spec: api.PodSpec{ + InitContainers: []api.Container{{ + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{api.ResourceEphemeralStorage: resource.MustParse("32Mi")}, + Limits: api.ResourceList{api.ResourceEphemeralStorage: resource.MustParse("64Mi")}, + }, + }}, + }, + }, + usage: api.ResourceList{ + api.ResourceEphemeralStorage: resource.MustParse("32Mi"), + api.ResourceRequestsEphemeralStorage: resource.MustParse("32Mi"), + api.ResourceLimitsEphemeralStorage: resource.MustParse("64Mi"), + api.ResourcePods: resource.MustParse("1"), + }, + }, "container CPU": { pod: &api.Pod{ Spec: api.PodSpec{ @@ -178,6 +196,24 @@ func TestPodEvaluatorUsage(t *testing.T) { api.ResourceMemory: resource.MustParse("1m"), }, }, + "container local ephemeral storage": { + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{{ + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{api.ResourceEphemeralStorage: resource.MustParse("32Mi")}, + Limits: api.ResourceList{api.ResourceEphemeralStorage: resource.MustParse("64Mi")}, + }, + }}, + }, + }, + usage: api.ResourceList{ + api.ResourceEphemeralStorage: resource.MustParse("32Mi"), + api.ResourceRequestsEphemeralStorage: resource.MustParse("32Mi"), + api.ResourceLimitsEphemeralStorage: resource.MustParse("64Mi"), + api.ResourcePods: resource.MustParse("1"), + }, + }, "init container maximums override sum of containers": { pod: &api.Pod{ Spec: api.PodSpec{