From a2ddde877c5b0a5bab25c4fefaca02175eac73b7 Mon Sep 17 00:00:00 2001 From: ndixita Date: Sat, 26 Oct 2024 01:19:52 +0000 Subject: [PATCH] Adding the logic to set default pod-level request as following: 1. If pod-level limit is set, pod-level request is unset and container-level request is set: derive pod-level request from container-level requests 2. If pod-level limit is set, pod-level request is unset and container-level request is unset: set pod-level request equal to pod-level limit --- pkg/apis/core/v1/defaults.go | 61 ++ pkg/apis/core/v1/defaults_test.go | 874 ++++++++++++++++++ pkg/apis/core/validation/validation_test.go | 1 + .../component-helpers/resource/helpers.go | 141 ++- .../resource/helpers_test.go | 519 +++++++++++ 5 files changed, 1574 insertions(+), 22 deletions(-) diff --git a/pkg/apis/core/v1/defaults.go b/pkg/apis/core/v1/defaults.go index aa5f448f791..a058c5f7c7f 100644 --- a/pkg/apis/core/v1/defaults.go +++ b/pkg/apis/core/v1/defaults.go @@ -25,6 +25,7 @@ import ( "k8s.io/apimachinery/pkg/runtime" "k8s.io/apimachinery/pkg/util/intstr" utilfeature "k8s.io/apiserver/pkg/util/feature" + resourcehelper "k8s.io/component-helpers/resource" "k8s.io/kubernetes/pkg/api/v1/service" "k8s.io/kubernetes/pkg/features" "k8s.io/kubernetes/pkg/util/parsers" @@ -217,6 +218,13 @@ func SetDefaults_Pod(obj *v1.Pod) { } } } + + // Pod Requests default values must be applied after container-level default values + // have been populated. + if utilfeature.DefaultFeatureGate.Enabled(features.PodLevelResources) { + defaultPodRequests(obj) + } + if obj.Spec.EnableServiceLinks == nil { enableServiceLinks := v1.DefaultEnableServiceLinks obj.Spec.EnableServiceLinks = &enableServiceLinks @@ -438,3 +446,56 @@ func SetDefaults_PodLogOptions(obj *v1.PodLogOptions) { } } } + +// defaultPodRequests applies default values for pod-level requests, only when +// pod-level limits are set, in following scenarios: +// 1. When at least one container (regular, init or sidecar) has requests set: +// The pod-level requests become equal to the effective requests of all containers +// in the pod. +// 2. When no containers have requests set: The pod-level requests become equal to +// pod-level limits. +// This defaulting behavior ensures consistent resource accounting at the pod-level +// while maintaining compatibility with the container-level specifications, as detailed +// in KEP-2837: https://github.com/kubernetes/enhancements/blob/master/keps/sig-node/2837-pod-level-resource-spec/README.md#proposed-validation--defaulting-rules +func defaultPodRequests(obj *v1.Pod) { + // We only populate defaults when the pod-level resources are partly specified already. + if obj.Spec.Resources == nil { + return + } + + if len(obj.Spec.Resources.Limits) == 0 { + return + } + + var podReqs v1.ResourceList + podReqs = obj.Spec.Resources.Requests + if podReqs == nil { + podReqs = make(v1.ResourceList) + } + + aggrCtrReqs := resourcehelper.AggregateContainerRequests(obj, resourcehelper.PodResourcesOptions{}) + + // When containers specify requests for a resource (supported by + // PodLevelResources feature) and pod-level requests are not set, the pod-level requests + // default to the effective requests of all the containers for that resource. + for key, aggrCtrLim := range aggrCtrReqs { + if _, exists := podReqs[key]; !exists && resourcehelper.IsSupportedPodLevelResource(key) { + podReqs[key] = aggrCtrLim.DeepCopy() + } + } + + // When no containers specify requests for a resource, the pod-level requests + // will default to match the pod-level limits, if pod-level + // limits exist for that resource. + for key, podLim := range obj.Spec.Resources.Limits { + if _, exists := podReqs[key]; !exists && resourcehelper.IsSupportedPodLevelResource(key) { + podReqs[key] = podLim.DeepCopy() + } + } + + // Only set pod-level resource requests in the PodSpec if the requirements map + // contains entries after collecting container-level requests and pod-level limits. + if len(podReqs) > 0 { + obj.Spec.Resources.Requests = podReqs + } +} diff --git a/pkg/apis/core/v1/defaults_test.go b/pkg/apis/core/v1/defaults_test.go index d8292d85262..a460a274b07 100644 --- a/pkg/apis/core/v1/defaults_test.go +++ b/pkg/apis/core/v1/defaults_test.go @@ -380,6 +380,880 @@ func testPodDefaults(t *testing.T, featuresEnabled bool) { } } +func TestPodResourcesDefaults(t *testing.T) { + cases := []struct { + name string + podLevelResourcesEnabled bool + containers []v1.Container + podResources *v1.ResourceRequirements + expectedPodSpec v1.PodSpec + }{ + { + name: "pod resources=unset, container resources=unset", + containers: []v1.Container{{Resources: v1.ResourceRequirements{}}, {Resources: v1.ResourceRequirements{}}}, + expectedPodSpec: v1.PodSpec{ + Containers: []v1.Container{{Resources: v1.ResourceRequirements{}}, {Resources: v1.ResourceRequirements{}}}, + }, + }, { + name: "pod resources=unset, container requests=unset limits=set", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod resources=unset, container requests=set limits=unset", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod resources=unset, container requests=set limits=set", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod requests=unset limits=set, container resources=unset", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, + }, { + name: "pod limits=nil, container requests=unset limits=set", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: nil, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod limits=empty map, container requests=unset limits=set", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{}, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod requests=empty map limits=set, container requests=unset limits=set", + podLevelResourcesEnabled: true, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + podResources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{}, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("5m"), + "memory": resource.MustParse("7Mi"), + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("3m"), + "memory": resource.MustParse("6Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("5m"), + "memory": resource.MustParse("7Mi"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod requests=nil limits=set, container requests=unset limits=set", + podLevelResourcesEnabled: true, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + podResources: &v1.ResourceRequirements{ + Requests: nil, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("5m"), + "memory": resource.MustParse("7Mi"), + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("3m"), + "memory": resource.MustParse("6Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("5m"), + "memory": resource.MustParse("7Mi"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod requests=unset limits=set, container requests=unset limits=set", + podLevelResourcesEnabled: true, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("5m"), + "memory": resource.MustParse("7Mi"), + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("3m"), + "memory": resource.MustParse("6Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("5m"), + "memory": resource.MustParse("7Mi"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod requests=unset limits=set, container requests=set limits=unset", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + "memory": resource.MustParse("8Mi"), + }, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("2Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("3Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + "memory": resource.MustParse("8Mi"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("2Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod requests=unset cpu limits=set, container resources=unset", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + }, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{}, + }, { + Resources: v1.ResourceRequirements{}, + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{}, + }, { + Resources: v1.ResourceRequirements{}, + }, + }, + }, + }, { + name: "pod requests=unset limits=set, container requests=set limits=set", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + "memory": resource.MustParse("8Mi"), + }, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("2Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("3Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + "memory": resource.MustParse("8Mi"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("2Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod requests=unset limits=set, container memory requests=set limits=unset", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + "memory": resource.MustParse("8Mi"), + }, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + "memory": resource.MustParse("6Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("4m"), + "memory": resource.MustParse("8Mi"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("1m"), + "memory": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod limits=set, container unsupported requests=set limits=set", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "ephemeral-storage": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "ephemeral-storage": resource.MustParse("5Mi"), + }, + Limits: v1.ResourceList{ + "ephemeral-storage": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod unsupported resources limits=set, container unsupported requests=set limits=set", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "ephemeral-storage": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{}, + Limits: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "ephemeral-storage": resource.MustParse("5Mi"), + }, + Limits: v1.ResourceList{ + "ephemeral-storage": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }, { + name: "pod supported and unsupported resources limits=set, container unsupported requests=set limits=set", + podLevelResourcesEnabled: true, + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + "storage": resource.MustParse("1Mi"), + }, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{ + "ephemeral-storage": resource.MustParse("5Mi"), + }, + }, + }, + }, + expectedPodSpec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "cpu": resource.MustParse("2m"), + "memory": resource.MustParse("1Mi"), + }, + }, + Containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + Limits: v1.ResourceList{ + "storage": resource.MustParse("1Mi"), + }, + }, + }, { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + "ephemeral-storage": resource.MustParse("5Mi"), + }, + Limits: v1.ResourceList{ + "ephemeral-storage": resource.MustParse("5Mi"), + }, + }, + }, + }, + }, + }} + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + if tc.podLevelResourcesEnabled { + featuregatetesting.SetFeatureGateDuringTest(t, utilfeature.DefaultFeatureGate, features.PodLevelResources, true) + } + spec := v1.PodSpec{ + Containers: tc.containers, + Resources: tc.podResources, + } + p := v1.Pod{Spec: *spec.DeepCopy()} + corev1.SetDefaults_Pod(&p) + for i, container := range p.Spec.Containers { + for resource, quantity := range container.Resources.Requests { + if quantity.Cmp(tc.expectedPodSpec.Containers[i].Resources.Requests[resource]) != 0 { + t.Errorf("got: %v, expected: %v", quantity, tc.expectedPodSpec.Containers[i].Resources.Requests[resource]) + } + } + } + + if tc.podResources != nil { + for resource, quantity := range p.Spec.Resources.Requests { + if quantity.Cmp(tc.expectedPodSpec.Resources.Requests[resource]) != 0 { + t.Errorf("got: %v, expected: %v", quantity, tc.expectedPodSpec.Resources.Requests[resource]) + } + } + } + }) + } +} + func TestPodHostNetworkDefaults(t *testing.T) { cases := []struct { name string diff --git a/pkg/apis/core/validation/validation_test.go b/pkg/apis/core/validation/validation_test.go index e22078723a4..23d500e48fe 100644 --- a/pkg/apis/core/validation/validation_test.go +++ b/pkg/apis/core/validation/validation_test.go @@ -20656,6 +20656,7 @@ func TestValidateOSFields(t *testing.T) { "ResourceClaims[*].Name", "ResourceClaims[*].ResourceClaimName", "ResourceClaims[*].ResourceClaimTemplateName", + "Resources", "RestartPolicy", "RuntimeClassName", "SchedulerName", diff --git a/staging/src/k8s.io/component-helpers/resource/helpers.go b/staging/src/k8s.io/component-helpers/resource/helpers.go index a8b252cfdd4..3bdcef6e816 100644 --- a/staging/src/k8s.io/component-helpers/resource/helpers.go +++ b/staging/src/k8s.io/component-helpers/resource/helpers.go @@ -18,6 +18,7 @@ package resource import ( v1 "k8s.io/api/core/v1" + "k8s.io/apimachinery/pkg/util/sets" ) // ContainerType signifies container type @@ -46,15 +47,101 @@ type PodResourcesOptions struct { // NonMissingContainerRequests if provided will replace any missing container level requests for the specified resources // with the given values. If the requests for those resources are explicitly set, even if zero, they will not be modified. NonMissingContainerRequests v1.ResourceList + // SkipPodLevelResources controls whether pod-level resources should be skipped + // from the calculation. If pod-level resources are not set in PodSpec, + // pod-level resources will always be skipped. + SkipPodLevelResources bool } -// PodRequests computes the pod requests per the PodResourcesOptions supplied. If PodResourcesOptions is nil, then -// the requests are returned including pod overhead. The computation is part of the API and must be reviewed -// as an API change. +var supportedPodLevelResources = sets.New(v1.ResourceCPU, v1.ResourceMemory) + +func SupportedPodLevelResources() sets.Set[v1.ResourceName] { + return supportedPodLevelResources +} + +// IsSupportedPodLevelResources checks if a given resource is supported by pod-level +// resource management through the PodLevelResources feature. Returns true if +// the resource is supported. +func IsSupportedPodLevelResource(name v1.ResourceName) bool { + return supportedPodLevelResources.Has(name) +} + +// IsPodLevelResourcesSet check if PodLevelResources pod-level resources are set. +// It returns true if either the Requests or Limits maps are non-empty. +func IsPodLevelResourcesSet(pod *v1.Pod) bool { + if pod.Spec.Resources == nil { + return false + } + + if (len(pod.Spec.Resources.Requests) + len(pod.Spec.Resources.Limits)) == 0 { + return false + } + + for resourceName := range pod.Spec.Resources.Requests { + if IsSupportedPodLevelResource(resourceName) { + return true + } + } + + for resourceName := range pod.Spec.Resources.Limits { + if IsSupportedPodLevelResource(resourceName) { + return true + } + } + + return false +} + +// IsPodLevelRequestsSet checks if pod-level requests are set. It returns true if +// Requests map is non-empty. +func IsPodLevelRequestsSet(pod *v1.Pod) bool { + if pod.Spec.Resources == nil { + return false + } + + if len(pod.Spec.Resources.Requests) == 0 { + return false + } + + for resourceName := range pod.Spec.Resources.Requests { + if IsSupportedPodLevelResource(resourceName) { + return true + } + } + + return false +} + +// PodRequests computes the total pod requests per the PodResourcesOptions supplied. +// If PodResourcesOptions is nil, then the requests are returned including pod overhead. +// If the PodLevelResources feature is enabled AND the pod-level resources are set, +// those pod-level values are used in calculating Pod Requests. +// The computation is part of the API and must be reviewed as an API change. func PodRequests(pod *v1.Pod, opts PodResourcesOptions) v1.ResourceList { + reqs := AggregateContainerRequests(pod, opts) + if !opts.SkipPodLevelResources && IsPodLevelRequestsSet(pod) { + for resourceName, quantity := range pod.Spec.Resources.Requests { + if IsSupportedPodLevelResource(resourceName) { + reqs[resourceName] = quantity + } + } + } + + // Add overhead for running a pod to the sum of requests if requested: + if !opts.ExcludeOverhead && pod.Spec.Overhead != nil { + addResourceList(reqs, pod.Spec.Overhead) + } + + return reqs +} + +// AggregateContainerRequests computes the total resource requests of all the containers +// in a pod. This computation folows the formula defined in the KEP for sidecar +// containers. See https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/753-sidecar-containers#resources-calculation-for-scheduling-and-pod-admission +// for more details. +func AggregateContainerRequests(pod *v1.Pod, opts PodResourcesOptions) v1.ResourceList { // attempt to reuse the maps if passed, or allocate otherwise reqs := reuseOrClearResourceList(opts.Reuse) - var containerStatuses map[string]*v1.ContainerStatus if opts.UseStatusResources { containerStatuses = make(map[string]*v1.ContainerStatus, len(pod.Status.ContainerStatuses)) @@ -124,12 +211,6 @@ func PodRequests(pod *v1.Pod, opts PodResourcesOptions) v1.ResourceList { } maxResourceList(reqs, initContainerReqs) - - // Add overhead for running a pod to the sum of requests if requested: - if !opts.ExcludeOverhead && pod.Spec.Overhead != nil { - addResourceList(reqs, pod.Spec.Overhead) - } - return reqs } @@ -155,8 +236,35 @@ func applyNonMissing(reqs v1.ResourceList, nonMissing v1.ResourceList) v1.Resour // as an API change. func PodLimits(pod *v1.Pod, opts PodResourcesOptions) v1.ResourceList { // attempt to reuse the maps if passed, or allocate otherwise - limits := reuseOrClearResourceList(opts.Reuse) + limits := AggregateContainerLimits(pod, opts) + if !opts.SkipPodLevelResources && IsPodLevelResourcesSet(pod) { + for resourceName, quantity := range pod.Spec.Resources.Limits { + if IsSupportedPodLevelResource(resourceName) { + limits[resourceName] = quantity + } + } + } + // Add overhead to non-zero limits if requested: + if !opts.ExcludeOverhead && pod.Spec.Overhead != nil { + for name, quantity := range pod.Spec.Overhead { + if value, ok := limits[name]; ok && !value.IsZero() { + value.Add(quantity) + limits[name] = value + } + } + } + + return limits +} + +// AggregateContainerLimits computes the aggregated resource limits of all the containers +// in a pod. This computation follows the formula defined in the KEP for sidecar +// containers. See https://github.com/kubernetes/enhancements/tree/master/keps/sig-node/753-sidecar-containers#resources-calculation-for-scheduling-and-pod-admission +// for more details. +func AggregateContainerLimits(pod *v1.Pod, opts PodResourcesOptions) v1.ResourceList { + // attempt to reuse the maps if passed, or allocate otherwise + limits := reuseOrClearResourceList(opts.Reuse) var containerStatuses map[string]*v1.ContainerStatus if opts.UseStatusResources { containerStatuses = make(map[string]*v1.ContainerStatus, len(pod.Status.ContainerStatuses)) @@ -216,17 +324,6 @@ func PodLimits(pod *v1.Pod, opts PodResourcesOptions) v1.ResourceList { } maxResourceList(limits, initContainerLimits) - - // Add overhead to non-zero limits if requested: - if !opts.ExcludeOverhead && pod.Spec.Overhead != nil { - for name, quantity := range pod.Spec.Overhead { - if value, ok := limits[name]; ok && !value.IsZero() { - value.Add(quantity) - limits[name] = value - } - } - } - return limits } diff --git a/staging/src/k8s.io/component-helpers/resource/helpers_test.go b/staging/src/k8s.io/component-helpers/resource/helpers_test.go index f2d984cf65d..e146a311971 100644 --- a/staging/src/k8s.io/component-helpers/resource/helpers_test.go +++ b/staging/src/k8s.io/component-helpers/resource/helpers_test.go @@ -1228,10 +1228,529 @@ func TestPodResourceLimits(t *testing.T) { } } +func TestIsPodLevelResourcesSet(t *testing.T) { + testCases := []struct { + name string + podResources *v1.ResourceRequirements + expected bool + }{ + { + name: "nil resources struct", + expected: false, + }, + { + name: "empty resources struct", + podResources: &v1.ResourceRequirements{}, + expected: false, + }, + { + name: "only unsupported resource requests set", + podResources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceEphemeralStorage: resource.MustParse("1Mi")}, + }, + expected: false, + }, + { + name: "only unsupported resource limits set", + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{v1.ResourceEphemeralStorage: resource.MustParse("1Mi")}, + }, + expected: false, + }, + { + name: "unsupported and suported resources requests set", + podResources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceEphemeralStorage: resource.MustParse("1Mi"), + v1.ResourceCPU: resource.MustParse("1m"), + }, + }, + expected: true, + }, + { + name: "unsupported and suported resources limits set", + podResources: &v1.ResourceRequirements{ + Limits: v1.ResourceList{ + v1.ResourceEphemeralStorage: resource.MustParse("1Mi"), + v1.ResourceCPU: resource.MustParse("1m"), + }, + }, + expected: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + testPod := &v1.Pod{Spec: v1.PodSpec{Resources: tc.podResources}} + if got := IsPodLevelResourcesSet(testPod); got != tc.expected { + t.Errorf("got=%t, want=%t", got, tc.expected) + } + }) + } + +} + +func TestPodLevelResourceRequests(t *testing.T) { + restartAlways := v1.ContainerRestartPolicyAlways + testCases := []struct { + name string + opts PodResourcesOptions + podResources v1.ResourceRequirements + overhead v1.ResourceList + initContainers []v1.Container + containers []v1.Container + expectedRequests v1.ResourceList + }{ + { + name: "nil", + expectedRequests: v1.ResourceList{}, + }, + { + name: "pod level memory resource with SkipPodLevelResources true", + podResources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("2Mi")}}, + opts: PodResourcesOptions{SkipPodLevelResources: true}, + expectedRequests: v1.ResourceList{}, + }, + { + name: "pod level memory resource with SkipPodLevelResources false", + podResources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("2Mi")}}, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("2Mi")}, + }, + { + name: "pod level memory and container level cpu resources with SkipPodLevelResources false", + podResources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("2Mi")}}, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("2m")}}, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("2Mi"), v1.ResourceCPU: resource.MustParse("2m")}, + }, + { + name: "pod level unsupported resources set at both pod-level and container-level with SkipPodLevelResources false", + podResources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceStorage: resource.MustParse("2Mi")}}, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceStorage: resource.MustParse("3Mi")}}, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceStorage: resource.MustParse("3Mi")}, + }, + { + name: "pod level unsupported resources set at pod-level with SkipPodLevelResources false", + podResources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceStorage: resource.MustParse("2Mi")}}, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{Requests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("3Mi")}}, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("3Mi")}, + }, + { + name: "only container level resources set with SkipPodLevelResources false", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("3Mi"), + v1.ResourceCPU: resource.MustParse("2m"), + }, + }, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("3Mi"), v1.ResourceCPU: resource.MustParse("2m")}, + }, + { + name: "both container-level and pod-level resources set with SkipPodLevelResources false", + podResources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("6Mi"), + v1.ResourceCPU: resource.MustParse("8m"), + }, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("3Mi"), + v1.ResourceCPU: resource.MustParse("2m"), + }, + }, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("6Mi"), v1.ResourceCPU: resource.MustParse("8m")}, + }, + { + name: "container-level resources and init container set with SkipPodLevelResources false", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("3Mi"), + v1.ResourceCPU: resource.MustParse("2m"), + }, + }, + }, + }, + initContainers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("5Mi"), + v1.ResourceCPU: resource.MustParse("4m"), + }, + }, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("5Mi"), v1.ResourceCPU: resource.MustParse("4m")}, + }, + { + name: "container-level resources and init container set with SkipPodLevelResources false", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("3Mi"), + v1.ResourceCPU: resource.MustParse("2m"), + }, + }, + }, + }, + initContainers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("5Mi"), + v1.ResourceCPU: resource.MustParse("4m"), + }, + }, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: true}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("5Mi"), v1.ResourceCPU: resource.MustParse("4m")}, + }, + { + name: "container-level resources and sidecar container set with SkipPodLevelResources false", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("3Mi"), + v1.ResourceCPU: resource.MustParse("2m"), + }, + }, + }, + }, + initContainers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("5Mi"), + v1.ResourceCPU: resource.MustParse("4m"), + }, + }, + RestartPolicy: &restartAlways, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("8Mi"), v1.ResourceCPU: resource.MustParse("6m")}, + }, + { + name: "container-level resources, init and sidecar container set with SkipPodLevelResources false", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("3Mi"), + v1.ResourceCPU: resource.MustParse("2m"), + }, + }, + }, + }, + initContainers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("5Mi"), + v1.ResourceCPU: resource.MustParse("4m"), + }, + }, + RestartPolicy: &restartAlways, + }, + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("6Mi"), + v1.ResourceCPU: resource.MustParse("8m"), + }, + }, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("11Mi"), v1.ResourceCPU: resource.MustParse("12m")}, + }, + { + name: "pod-level resources, container-level resources, init and sidecar container set with SkipPodLevelResources false", + podResources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("15Mi"), + v1.ResourceCPU: resource.MustParse("18m"), + }, + }, + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("3Mi"), + v1.ResourceCPU: resource.MustParse("2m"), + }, + }, + }, + }, + initContainers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("5Mi"), + v1.ResourceCPU: resource.MustParse("4m"), + }, + }, + RestartPolicy: &restartAlways, + }, + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceMemory: resource.MustParse("6Mi"), + v1.ResourceCPU: resource.MustParse("8m"), + }, + }, + }, + }, + opts: PodResourcesOptions{SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{v1.ResourceMemory: resource.MustParse("15Mi"), v1.ResourceCPU: resource.MustParse("18m")}, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + podReqs := PodRequests(getPodLevelResourcesPod(tc.podResources, tc.overhead, tc.containers, tc.initContainers), tc.opts) + if diff := cmp.Diff(podReqs, tc.expectedRequests); diff != "" { + t.Errorf("got=%v, want=%v, diff=%s", podReqs, tc.expectedRequests, diff) + } + }) + } +} + +func TestAggregateContainerRequestsAndLimits(t *testing.T) { + restartAlways := v1.ContainerRestartPolicyAlways + cases := []struct { + containers []v1.Container + initContainers []v1.Container + name string + expectedRequests v1.ResourceList + expectedLimits v1.ResourceList + }{ + { + name: "one container with limits", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + expectedRequests: v1.ResourceList{}, + expectedLimits: v1.ResourceList{ + v1.ResourceName(v1.ResourceCPU): resource.MustParse("9"), + }, + }, + { + name: "two containers with limits", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + expectedRequests: v1.ResourceList{}, + expectedLimits: v1.ResourceList{ + v1.ResourceName(v1.ResourceCPU): resource.MustParse("18"), + }, + }, + { + name: "one container with requests", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + expectedRequests: v1.ResourceList{ + v1.ResourceName(v1.ResourceCPU): resource.MustParse("9"), + }, + expectedLimits: v1.ResourceList{}, + }, + { + name: "two containers with requests", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + expectedRequests: v1.ResourceList{ + v1.ResourceName(v1.ResourceCPU): resource.MustParse("18"), + }, + expectedLimits: v1.ResourceList{}, + }, + { + name: "regular and init containers with requests", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + initContainers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + expectedRequests: v1.ResourceList{ + v1.ResourceName(v1.ResourceCPU): resource.MustParse("9"), + }, + expectedLimits: v1.ResourceList{}, + }, + { + name: "regular, init and sidecar containers with requests", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + initContainers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("8")}, + }, + RestartPolicy: &restartAlways, + }, + { + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("6")}, + }, + }, + }, + expectedRequests: v1.ResourceList{ + v1.ResourceName(v1.ResourceCPU): resource.MustParse("17"), + }, + expectedLimits: v1.ResourceList{}, + }, + { + name: "regular and init containers with limits", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + initContainers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + expectedRequests: v1.ResourceList{}, + expectedLimits: v1.ResourceList{ + v1.ResourceName(v1.ResourceCPU): resource.MustParse("9"), + }, + }, + { + name: "regular, init and sidecar containers with limits", + containers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("9")}, + }, + }, + }, + initContainers: []v1.Container{ + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("8")}, + }, + RestartPolicy: &restartAlways, + }, + { + Resources: v1.ResourceRequirements{ + Limits: v1.ResourceList{v1.ResourceName(v1.ResourceCPU): resource.MustParse("6")}, + }, + }, + }, + expectedRequests: v1.ResourceList{}, + expectedLimits: v1.ResourceList{ + v1.ResourceName(v1.ResourceCPU): resource.MustParse("17"), + }, + }, + } + + for idx, tc := range cases { + testPod := &v1.Pod{Spec: v1.PodSpec{Containers: tc.containers, InitContainers: tc.initContainers}} + resRequests := AggregateContainerRequests(testPod, PodResourcesOptions{}) + resLimits := AggregateContainerLimits(testPod, PodResourcesOptions{}) + + if !equality.Semantic.DeepEqual(tc.expectedRequests, resRequests) { + t.Errorf("test case failure[%d]: %v, requests:\n expected:\t%v\ngot\t\t%v", idx, tc.name, tc.expectedRequests, resRequests) + } + + if !equality.Semantic.DeepEqual(tc.expectedLimits, resLimits) { + t.Errorf("test case failure[%d]: %v, limits:\n expected:\t%v\ngot\t\t%v", idx, tc.name, tc.expectedLimits, resLimits) + } + } +} + type podResources struct { cpuRequest, cpuLimit, memoryRequest, memoryLimit, cpuOverhead, memoryOverhead string } +func getPodLevelResourcesPod(podResources v1.ResourceRequirements, overhead v1.ResourceList, containers, initContainers []v1.Container) *v1.Pod { + return &v1.Pod{ + Spec: v1.PodSpec{ + Resources: &podResources, + Containers: containers, + InitContainers: initContainers, + Overhead: overhead, + }, + } +} + +// TODO(ndixita): refactor to re-use getPodResourcesPod() func getPod(cname string, resources podResources) *v1.Pod { r := v1.ResourceRequirements{ Limits: make(v1.ResourceList),