diff --git a/staging/src/k8s.io/component-helpers/resource/helpers.go b/staging/src/k8s.io/component-helpers/resource/helpers.go index c8b3c7ece6f..23f57a7fca1 100644 --- a/staging/src/k8s.io/component-helpers/resource/helpers.go +++ b/staging/src/k8s.io/component-helpers/resource/helpers.go @@ -60,6 +60,8 @@ type PodResourcesOptions struct { SkipPodLevelResources bool // SkipContainerLevelResources SkipContainerLevelResources bool + // Use node allocatable resource claim information from pod status to compute the effective pod resource request. + UseDRANodeAllocatableResourceClaimStatus bool } var supportedPodLevelResources = sets.New(v1.ResourceCPU, v1.ResourceMemory) @@ -275,6 +277,16 @@ func AggregateContainerRequests(pod *v1.Pod, opts PodResourcesOptions) v1.Resour } maxResourceList(reqs, initContainerReqs) + + // Add resources from node allocatable ResourceClaims + if opts.UseDRANodeAllocatableResourceClaimStatus && len(pod.Status.NodeAllocatableResourceClaimStatuses) > 0 { + for _, claimStatus := range pod.Status.NodeAllocatableResourceClaimStatuses { + for resName, resQty := range claimStatus.Resources { + addResourceList(reqs, v1.ResourceList{resName: resQty}) + } + } + } + return reqs } 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 41c2bc81828..44201a73cab 100644 --- a/staging/src/k8s.io/component-helpers/resource/helpers_test.go +++ b/staging/src/k8s.io/component-helpers/resource/helpers_test.go @@ -2017,6 +2017,398 @@ func TestIsSupportedPodLevelResource(t *testing.T) { } } +var hugePageResource1Gi = v1.ResourceName(v1.ResourceHugePagesPrefix + "1Gi") + +func TestPodResourceRequestsWithDRANodeAllocatableClaims(t *testing.T) { + testCases := []struct { + description string + pod *v1.Pod + options PodResourcesOptions + expectedRequests v1.ResourceList + }{ + { + description: "No claims", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "c1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + }, + }, + }, + options: PodResourcesOptions{UseDRANodeAllocatableResourceClaimStatus: true}, + expectedRequests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + { + description: "Single claim on one container", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "c1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("1Gi"), + hugePageResource1Gi: resource.MustParse("2"), + }, + Claims: []v1.ResourceClaim{ + {Name: "claim-1"}, + }, + }, + }, + }, + ResourceClaims: []v1.PodResourceClaim{ + { + Name: "claim-1", + ResourceClaimName: ptr.To("node-allocatable-claim-1"), + }, + }, + }, + Status: v1.PodStatus{ + NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{ + { + ResourceClaimName: "node-allocatable-claim-1", + Containers: []string{"c1"}, + Resources: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("50m"), + hugePageResource1Gi: resource.MustParse("1"), + }, + }, + }, + }, + }, + options: PodResourcesOptions{UseDRANodeAllocatableResourceClaimStatus: true}, + expectedRequests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("150m"), // 100m + 50m + v1.ResourceMemory: resource.MustParse("1Gi"), + hugePageResource1Gi: resource.MustParse("3"), // 2 + 1 + }, + }, + { + description: "Multiple claims on one container", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "c1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + Claims: []v1.ResourceClaim{ + {Name: "claim-1"}, + {Name: "claim-2"}, + }, + }, + }, + }, + ResourceClaims: []v1.PodResourceClaim{ + { + Name: "claim-1", + ResourceClaimName: ptr.To("node-allocatable-claim-1"), + }, + { + Name: "claim-2", + ResourceClaimName: ptr.To("node-allocatable-claim-2"), + }, + }, + }, + Status: v1.PodStatus{ + NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{ + { + ResourceClaimName: "node-allocatable-claim-1", + Containers: []string{"c1"}, + Resources: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("50m"), + }, + }, + { + ResourceClaimName: "node-allocatable-claim-2", + Containers: []string{"c1"}, + Resources: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("25m"), + v1.ResourceMemory: resource.MustParse("512Mi"), + }, + }, + }, + }, + }, + options: PodResourcesOptions{UseDRANodeAllocatableResourceClaimStatus: true}, + expectedRequests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("175m"), // 100m + 50m + 25m + v1.ResourceMemory: resource.MustParse("1.5Gi"), // 1Gi + 512Mi + }, + }, + { + description: "Same claim referenced in multiple containers", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "c1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")}, + Claims: []v1.ResourceClaim{ + {Name: "claim-1"}, + }, + }, + }, + { + Name: "c2", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("200m")}, + Claims: []v1.ResourceClaim{ + {Name: "claim-1"}, + }, + }, + }, + }, + ResourceClaims: []v1.PodResourceClaim{ + { + Name: "claim-1", + ResourceClaimName: ptr.To("node-allocatable-claim-1"), + }, + }, + }, + Status: v1.PodStatus{ + NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{ + { + ResourceClaimName: "node-allocatable-claim-1", + Containers: []string{"c1", "c2"}, + Resources: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("50m"), + }, + }, + }, + }, + }, + options: PodResourcesOptions{UseDRANodeAllocatableResourceClaimStatus: true}, + expectedRequests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("350m"), // 100m + 200m + 50m (claim counted once) + }, + }, + { + description: "Different claims on multiple containers", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "c1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + hugePageResource1Gi: resource.MustParse("1"), + }, + Claims: []v1.ResourceClaim{ + {Name: "claim-1"}, + }, + }, + }, + { + Name: "c2", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("200m"), + v1.ResourceMemory: resource.MustParse("1Gi"), + hugePageResource1Gi: resource.MustParse("2"), + }, + Claims: []v1.ResourceClaim{ + {Name: "claim-2"}, + }, + }, + }, + }, + ResourceClaims: []v1.PodResourceClaim{ + { + Name: "claim-1", + ResourceClaimName: ptr.To("node-allocatable-claim-1"), + }, + { + Name: "claim-2", + ResourceClaimName: ptr.To("node-allocatable-claim-2"), + }, + }, + }, + Status: v1.PodStatus{ + NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{ + { + ResourceClaimName: "node-allocatable-claim-1", + Containers: []string{"c1"}, + Resources: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("50m"), + hugePageResource1Gi: resource.MustParse("1"), + }, + }, + { + ResourceClaimName: "node-allocatable-claim-2", + Containers: []string{"c2"}, + Resources: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("25m"), + v1.ResourceMemory: resource.MustParse("512Mi"), + hugePageResource1Gi: resource.MustParse("2"), + }, + }, + }, + }, + }, + options: PodResourcesOptions{UseDRANodeAllocatableResourceClaimStatus: true}, + expectedRequests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("375m"), // 100m + 200m + 50m + 25m + v1.ResourceMemory: resource.MustParse("1.5Gi"), // 1Gi + 1Gi + 1Gi + 512Mi + hugePageResource1Gi: resource.MustParse("6"), // 1 + 2 + 1 + 2 + }, + }, + { + description: "Claim on init container", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + InitContainers: []v1.Container{ + { + Name: "ic1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")}, + Claims: []v1.ResourceClaim{ + {Name: "claim-1"}, + }, + }, + }, + }, + Containers: []v1.Container{ + { + Name: "c1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("50m")}, + }, + }, + }, + ResourceClaims: []v1.PodResourceClaim{ + { + Name: "claim-1", + ResourceClaimName: ptr.To("node-allocatable-claim"), + }, + }, + }, + Status: v1.PodStatus{ + NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{ + { + ResourceClaimName: "node-allocatable-claim", + Containers: []string{"ic1"}, + Resources: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("25m"), + }, + }, + }, + }, + }, + options: PodResourcesOptions{UseDRANodeAllocatableResourceClaimStatus: true}, + expectedRequests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("125m"), // max(100m, 50m) + 25m + }, + }, + { + description: "UseDRANodeAllocatableResourceClaimStatus flag disabled", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Containers: []v1.Container{ + { + Name: "c1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + }, + Claims: []v1.ResourceClaim{ + {Name: "claim-1"}, + }, + }, + }, + }, + ResourceClaims: []v1.PodResourceClaim{ + { + Name: "claim-1", + ResourceClaimName: ptr.To("node-allocatable-claim"), + }, + }, + }, + Status: v1.PodStatus{ + NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{ + { + ResourceClaimName: "node-allocatable-claim", + Containers: []string{"c1"}, + Resources: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("50m"), + }, + }, + }, + }, + }, + options: PodResourcesOptions{UseDRANodeAllocatableResourceClaimStatus: false}, + expectedRequests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("100m"), + }, + }, + { + description: "Pod Level Resources overrides the claim request", + pod: &v1.Pod{ + Spec: v1.PodSpec{ + Resources: &v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("2"), + }, + }, + Containers: []v1.Container{ + { + Name: "c1", + Resources: v1.ResourceRequirements{ + Requests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("1"), + v1.ResourceMemory: resource.MustParse("1Gi"), + }, + }, + }, + }, + }, + Status: v1.PodStatus{ + NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{ + { + ResourceClaimName: "node-allocatable-claim", + Containers: []string{"c1"}, + Resources: map[v1.ResourceName]resource.Quantity{ + v1.ResourceCPU: resource.MustParse("3"), + }, + }, + }, + }, + }, + options: PodResourcesOptions{UseDRANodeAllocatableResourceClaimStatus: true, SkipPodLevelResources: false}, + expectedRequests: v1.ResourceList{ + v1.ResourceCPU: resource.MustParse("2"), // Pod level resource should not be overridden by claim request (3) + v1.ResourceMemory: resource.MustParse("1Gi"), // Container level resource should be included + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.description, func(t *testing.T) { + request := PodRequests(tc.pod, tc.options) + if diff := diff.Diff(tc.expectedRequests, request); diff != "" { + t.Errorf("PodRequests() mismatch (-want +got):\n%s", diff) + } + }) + } +} + func TestAggregateContainerRequestsAndLimits(t *testing.T) { restartAlways := v1.ContainerRestartPolicyAlways cases := []struct {