diff --git a/pkg/apis/core/helper/helpers.go b/pkg/apis/core/helper/helpers.go index 86800d03877..b30b050b767 100644 --- a/pkg/apis/core/helper/helpers.go +++ b/pkg/apis/core/helper/helpers.go @@ -37,6 +37,12 @@ func IsHugePageResourceName(name core.ResourceName) bool { return strings.HasPrefix(string(name), core.ResourceHugePagesPrefix) } +// IsQuotaHugePageResourceName returns true if the resource name has the quota +// related huge page resource prefix. +func IsQuotaHugePageResourceName(name core.ResourceName) bool { + return strings.HasPrefix(string(name), core.ResourceHugePagesPrefix) || strings.HasPrefix(string(name), core.ResourceRequestsHugePagesPrefix) +} + // HugePageResourceName returns a ResourceName with the canonical hugepage // prefix prepended for the specified page size. The page size is converted // to its canonical representation. @@ -217,7 +223,7 @@ var standardQuotaResources = sets.NewString( // IsStandardQuotaResourceName returns true if the resource is known to // the quota tracking system func IsStandardQuotaResourceName(str string) bool { - return standardQuotaResources.Has(str) + return standardQuotaResources.Has(str) || IsQuotaHugePageResourceName(core.ResourceName(str)) } var standardResources = sets.NewString( @@ -245,7 +251,7 @@ var standardResources = sets.NewString( // IsStandardResourceName returns true if the resource is known to the system func IsStandardResourceName(str string) bool { - return standardResources.Has(str) || IsHugePageResourceName(core.ResourceName(str)) + return standardResources.Has(str) || IsQuotaHugePageResourceName(core.ResourceName(str)) } var integerResources = sets.NewString( diff --git a/pkg/apis/core/helper/helpers_test.go b/pkg/apis/core/helper/helpers_test.go index dad7f2d4a6a..03e01f8083e 100644 --- a/pkg/apis/core/helper/helpers_test.go +++ b/pkg/apis/core/helper/helpers_test.go @@ -59,6 +59,7 @@ func TestIsStandardResource(t *testing.T) { {"blah", false}, {"x.y.z", false}, {"hugepages-2Mi", true}, + {"requests.hugepages-2Mi", true}, } for i, tc := range testCases { if IsStandardResourceName(tc.input) != tc.output { diff --git a/pkg/apis/core/types.go b/pkg/apis/core/types.go index 5dab3b25934..76b10ad9119 100644 --- a/pkg/apis/core/types.go +++ b/pkg/apis/core/types.go @@ -4071,6 +4071,13 @@ const ( ResourceLimitsEphemeralStorage ResourceName = "limits.ephemeral-storage" ) +// The following identify resource prefix for Kubernetes object types +const ( + // HugePages request, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) + // As burst is not supported for HugePages, we would only quota its request, and ignore the limit. + ResourceRequestsHugePagesPrefix = "requests.hugepages-" +) + // A ResourceQuotaScope defines a filter that must match each object tracked by a quota type ResourceQuotaScope string diff --git a/pkg/quota/evaluator/core/pods.go b/pkg/quota/evaluator/core/pods.go index c55fb7f721b..ba935eab5af 100644 --- a/pkg/quota/evaluator/core/pods.go +++ b/pkg/quota/evaluator/core/pods.go @@ -58,6 +58,30 @@ var podResources = []api.ResourceName{ api.ResourcePods, } +// podResourcePrefixes are the set of prefixes for resources (Hugepages, and other +// potential extended reources with specific prefix) managed by quota associated with pods. +var podResourcePrefixes = []string{ + api.ResourceHugePagesPrefix, + api.ResourceRequestsHugePagesPrefix, +} + +// requestedResourcePrefixes are the set of prefixes for resources +// that might be declared in pod's Resources.Requests/Limits +var requestedResourcePrefixes = []string{ + api.ResourceHugePagesPrefix, +} + +const ( + requestsPrefix = "requests." + limitsPrefix = "limits." +) + +// maskResourceWithPrefix mask resource with certain prefix +// e.g. hugepages-XXX -> requests.hugepages-XXX +func maskResourceWithPrefix(resource api.ResourceName, prefix string) api.ResourceName { + return api.ResourceName(fmt.Sprintf("%s%s", prefix, string(resource))) +} + // NOTE: it was a mistake, but if a quota tracks cpu or memory related resources, // the incoming pod is required to have those values set. we should not repeat // this mistake for other future resources (gpus, ephemeral-storage,etc). @@ -157,7 +181,14 @@ func (p *podEvaluator) Matches(resourceQuota *api.ResourceQuota, item runtime.Ob // MatchingResources takes the input specified list of resources and returns the set of resources it matches. func (p *podEvaluator) MatchingResources(input []api.ResourceName) []api.ResourceName { - return quota.Intersection(input, podResources) + result := quota.Intersection(input, podResources) + for _, resource := range input { + if quota.ContainsPrefix(podResourcePrefixes, resource) { + result = append(result, resource) + } + } + + return result } // Usage knows how to measure usage associated with pods @@ -212,6 +243,18 @@ func podComputeUsageHelper(requests api.ResourceList, limits api.ResourceList) a if limit, found := limits[api.ResourceEphemeralStorage]; found { result[api.ResourceLimitsEphemeralStorage] = limit } + for resource, request := range requests { + if quota.ContainsPrefix(requestedResourcePrefixes, resource) { + result[resource] = request + result[maskResourceWithPrefix(resource, requestsPrefix)] = request + } + } + for resource, limit := range limits { + if quota.ContainsPrefix(requestedResourcePrefixes, resource) { + result[maskResourceWithPrefix(resource, limitsPrefix)] = limit + } + } + return result } diff --git a/pkg/quota/evaluator/core/pods_test.go b/pkg/quota/evaluator/core/pods_test.go index f74e4603fa6..2c06bdcb4b2 100644 --- a/pkg/quota/evaluator/core/pods_test.go +++ b/pkg/quota/evaluator/core/pods_test.go @@ -175,6 +175,23 @@ func TestPodEvaluatorUsage(t *testing.T) { generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"), }, }, + "init container hugepages": { + pod: &api.Pod{ + Spec: api.PodSpec{ + InitContainers: []api.Container{{ + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{api.ResourceName(api.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("100Mi")}, + }, + }}, + }, + }, + usage: api.ResourceList{ + api.ResourceName(api.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("100Mi"), + api.ResourceName(api.ResourceRequestsHugePagesPrefix + "2Mi"): resource.MustParse("100Mi"), + api.ResourcePods: resource.MustParse("1"), + generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"), + }, + }, "container CPU": { pod: &api.Pod{ Spec: api.PodSpec{ @@ -232,6 +249,23 @@ func TestPodEvaluatorUsage(t *testing.T) { generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"), }, }, + "container hugepages": { + pod: &api.Pod{ + Spec: api.PodSpec{ + Containers: []api.Container{{ + Resources: api.ResourceRequirements{ + Requests: api.ResourceList{api.ResourceName(api.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("100Mi")}, + }, + }}, + }, + }, + usage: api.ResourceList{ + api.ResourceName(api.ResourceHugePagesPrefix + "2Mi"): resource.MustParse("100Mi"), + api.ResourceName(api.ResourceRequestsHugePagesPrefix + "2Mi"): resource.MustParse("100Mi"), + api.ResourcePods: resource.MustParse("1"), + generic.ObjectCountQuotaResourceNameFor(schema.GroupResource{Resource: "pods"}): resource.MustParse("1"), + }, + }, "init container maximums override sum of containers": { pod: &api.Pod{ Spec: api.PodSpec{ diff --git a/pkg/quota/resources.go b/pkg/quota/resources.go index 4d0ccd5fa9c..924ee41b946 100644 --- a/pkg/quota/resources.go +++ b/pkg/quota/resources.go @@ -17,6 +17,8 @@ limitations under the License. package quota import ( + "strings" + "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/api/resource" "k8s.io/apimachinery/pkg/util/sets" @@ -196,6 +198,16 @@ func Contains(items []api.ResourceName, item api.ResourceName) bool { return ToSet(items).Has(string(item)) } +// ContainsPrefix returns true if the specified item has a prefix that contained in given prefix Set +func ContainsPrefix(prefixSet []string, item api.ResourceName) bool { + for _, prefix := range prefixSet { + if strings.HasPrefix(string(item), prefix) { + return true + } + } + return false +} + // Intersection returns the intersection of both list of resources func Intersection(a []api.ResourceName, b []api.ResourceName) []api.ResourceName { setA := ToSet(a) diff --git a/pkg/quota/resources_test.go b/pkg/quota/resources_test.go index d76abbad473..8cf26a767c5 100644 --- a/pkg/quota/resources_test.go +++ b/pkg/quota/resources_test.go @@ -226,6 +226,30 @@ func TestContains(t *testing.T) { } } +func TestContainsPrefix(t *testing.T) { + testCases := map[string]struct { + a []string + b api.ResourceName + expected bool + }{ + "does-not-contain": { + a: []string{api.ResourceHugePagesPrefix}, + b: api.ResourceCPU, + expected: false, + }, + "does-contain": { + a: []string{api.ResourceHugePagesPrefix}, + b: api.ResourceName(api.ResourceHugePagesPrefix + "2Mi"), + expected: true, + }, + } + for testName, testCase := range testCases { + if actual := ContainsPrefix(testCase.a, testCase.b); actual != testCase.expected { + t.Errorf("%s expected: %v, actual: %v", testName, testCase.expected, actual) + } + } +} + func TestIsZero(t *testing.T) { testCases := map[string]struct { a api.ResourceList diff --git a/staging/src/k8s.io/api/core/v1/types.go b/staging/src/k8s.io/api/core/v1/types.go index 53d9ec68b76..be414e842eb 100644 --- a/staging/src/k8s.io/api/core/v1/types.go +++ b/staging/src/k8s.io/api/core/v1/types.go @@ -4598,6 +4598,13 @@ const ( ResourceLimitsEphemeralStorage ResourceName = "limits.ephemeral-storage" ) +// The following identify resource prefix for Kubernetes object types +const ( + // HugePages request, in bytes. (500Gi = 500GiB = 500 * 1024 * 1024 * 1024) + // As burst is not supported for HugePages, we would only quota its request, and ignore the limit. + ResourceRequestsHugePagesPrefix = "requests.hugepages-" +) + // A ResourceQuotaScope defines a filter that must match each object tracked by a quota type ResourceQuotaScope string