Files
kubernetes/pkg/scheduler/framework/plugins/dynamicresources/nodeallocatabledynamicresources_test.go
T
Karthikeyan ValliyurnattandKrishna Parihar 004a48462d DRA: Add non-negative, size and name validations to consumable capacity
Validate capacity request and allocation-result maps, rejecting negative
quantities, invalid capacity names, and more than 32 entries. Use
declarative validation for map sizes and quantities while preserving
updates to unchanged fields accepted under earlier validation.

Reject raw negative requests before request-policy rounding in both
allocator variants. Reject negative consumed quantities and computed
node-allocatable resource footprints.

Return errors when persisted or in-flight allocations contain negative
consumed capacity, preventing positive allocations from masking invalid
values in the aggregate. Clear recorded errors when invalid claims are
removed.

Co-authored-by: Krishna Parihar <krishnasparihar88@gmail.com>
2026-10-26 10:19:55 -04:00

2370 lines
79 KiB
Go

/*
Copyright 2025 The Kubernetes Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package dynamicresources
import (
"context"
"errors"
"fmt"
"sort"
"strings"
"testing"
"github.com/google/go-cmp/cmp"
v1 "k8s.io/api/core/v1"
resourceapi "k8s.io/api/resource/v1"
"k8s.io/apimachinery/pkg/api/resource"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/types"
"k8s.io/apimachinery/pkg/util/sets"
utilfeature "k8s.io/apiserver/pkg/util/feature"
"k8s.io/client-go/kubernetes/fake"
clienttesting "k8s.io/client-go/testing"
"k8s.io/dynamic-resource-allocation/structured"
"k8s.io/klog/v2"
"k8s.io/kubernetes/pkg/scheduler/framework"
"k8s.io/kubernetes/pkg/scheduler/framework/plugins/feature"
"k8s.io/utils/ptr"
fwk "k8s.io/kube-scheduler/framework"
st "k8s.io/kubernetes/pkg/scheduler/testing"
)
var (
hugePages2Mi = v1.ResourceName(v1.ResourceHugePagesPrefix + "2Mi")
hugePages1Gi = v1.ResourceName(v1.ResourceHugePagesPrefix + "1Gi")
)
// mockDRAManager is a mock implementation of fwk.SharedDRAManager.
type mockDRAManager struct {
fwk.SharedDRAManager
claims []*resourceapi.ResourceClaim
resourceSlices []*resourceapi.ResourceSlice
}
func (m *mockDRAManager) ResourceClaims() fwk.ResourceClaimTracker {
return m
}
func (m *mockDRAManager) Get(namespace, name string) (*resourceapi.ResourceClaim, error) {
for _, claim := range m.claims {
if claim.Namespace == namespace && claim.Name == name {
return claim, nil
}
}
return nil, fmt.Errorf("claim %s/%s not found", namespace, name)
}
func (m *mockDRAManager) AssumeClaimAfterAPICall(claim *resourceapi.ResourceClaim) error {
return nil
}
func (m *mockDRAManager) AssumedClaimRestore(namespace, name string) {}
func (m *mockDRAManager) GetPendingAllocation(uid types.UID) *resourceapi.AllocationResult {
return nil
}
func (m *mockDRAManager) SignalClaimPendingAllocation(uid types.UID, claim *resourceapi.ResourceClaim) error {
return nil
}
func (m *mockDRAManager) MaybeRemoveClaimPendingAllocation(_ types.UID, _ bool) (deleted bool) {
return false
}
func (m *mockDRAManager) List() ([]*resourceapi.ResourceClaim, error) {
return nil, nil
}
func (m *mockDRAManager) GatherAllocatedState() (*structured.AllocatedState, error) {
return nil, nil
}
func (m *mockDRAManager) ListAllAllocatedDevices() (sets.Set[structured.DeviceID], error) {
return nil, nil
}
func (m *mockDRAManager) ResourceSlices() fwk.ResourceSliceLister {
return m
}
func (m *mockDRAManager) ListWithDeviceTaintRules() ([]*resourceapi.ResourceSlice, error) {
return m.resourceSlices, nil
}
func TestValidateNodeAllocatableDRAClaimSharing(t *testing.T) {
claimName := "node-allocatable-claim"
claimNameSpace := "test-ns"
mappedMultiplier := resource.MustParse("1")
cpuDevice := resourceapi.Device{
Name: "cpu0",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
DeviceMultiplier: &mappedMultiplier,
},
},
},
}
gpuOverheadDevice := resourceapi.Device{
Name: "gpu0",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceMemory: {
Overhead: &resourceapi.NodeAllocatableOverhead{
PerContainer: resource.NewQuantity(100, resource.DecimalSI),
},
},
},
}
sliceMapped := &resourceapi.ResourceSlice{
Spec: resourceapi.ResourceSliceSpec{
Pool: resourceapi.ResourcePool{
Name: "pool-1",
},
Devices: []resourceapi.Device{cpuDevice},
},
}
sliceOverhead := &resourceapi.ResourceSlice{
Spec: resourceapi.ResourceSliceSpec{
Pool: resourceapi.ResourcePool{
Name: "pool-2",
},
Devices: []resourceapi.Device{gpuOverheadDevice},
},
}
claimMapped := &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: claimName,
Namespace: claimNameSpace,
UID: "claim-uid",
},
Status: resourceapi.ResourceClaimStatus{
Allocation: &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: []resourceapi.DeviceRequestAllocationResult{
{
Pool: "pool-1",
Device: "cpu0",
},
},
},
},
},
}
claimOverhead := &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: claimName,
Namespace: claimNameSpace,
UID: "claim-uid",
},
Status: resourceapi.ResourceClaimStatus{
Allocation: &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: []resourceapi.DeviceRequestAllocationResult{
{
Pool: "pool-2",
Device: "gpu0",
},
},
},
},
},
}
combinedMappingAndOverheadDevice := resourceapi.Device{
Name: "combined",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
DeviceMultiplier: &mappedMultiplier,
},
Overhead: &resourceapi.NodeAllocatableOverhead{
PerContainer: resource.NewQuantity(100, resource.DecimalSI),
},
},
},
}
sliceCombinedOverheadAndMapped := &resourceapi.ResourceSlice{
Spec: resourceapi.ResourceSliceSpec{
Pool: resourceapi.ResourcePool{
Name: "pool-combined",
},
Devices: []resourceapi.Device{combinedMappingAndOverheadDevice},
},
}
claimCombinedOverheadAndMappedDevice := &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: claimName,
Namespace: claimNameSpace,
UID: "claim-uid",
},
Status: resourceapi.ResourceClaimStatus{
Allocation: &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: []resourceapi.DeviceRequestAllocationResult{
{
Pool: "pool-combined",
Device: "combined",
},
},
},
},
},
}
claimMappedReserved := claimMapped.DeepCopy()
claimMappedReserved.Status.ReservedFor = []resourceapi.ResourceClaimConsumerReference{
{UID: "other-pod-uid"},
}
claimCombinedReserved := claimCombinedOverheadAndMappedDevice.DeepCopy()
claimCombinedReserved.Status.ReservedFor = []resourceapi.ResourceClaimConsumerReference{
{UID: "other-pod-uid"},
}
tests := []struct {
name string
pod *v1.Pod
claim *resourceapi.ResourceClaim
nodeInfo *framework.NodeInfo
draManager fwk.SharedDRAManager
podGroupState *podGroupStateData
features feature.Features
wantStatus *fwk.Status
}{
{
name: "empty claim",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").Obj(),
claim: nil,
nodeInfo: func() *framework.NodeInfo {
return framework.NewNodeInfo()
}(),
draManager: &mockDRAManager{},
wantStatus: fwk.NewStatus(fwk.Success),
},
{
name: "claim not allocated yet",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: claimName,
Namespace: claimNameSpace,
},
},
nodeInfo: func() *framework.NodeInfo {
return framework.NewNodeInfo()
}(),
draManager: &mockDRAManager{},
wantStatus: fwk.NewStatus(fwk.Success),
},
{
name: "mapped claim, not shared (no other pods on node)",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: claimMapped,
nodeInfo: func() *framework.NodeInfo {
return framework.NewNodeInfo()
}(),
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceMapped},
},
wantStatus: fwk.NewStatus(fwk.Success),
},
{
name: "mapped claim, shared (other pod using it)",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: claimMappedReserved,
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceMapped},
},
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "node allocatable resource claim node-allocatable-claim has a mapped device and cannot be shared across pods"),
},
{
name: "overhead-only mapped claim, shared (other pod using it)",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: claimOverhead,
nodeInfo: func() *framework.NodeInfo {
ni := framework.NewNodeInfo()
otherPod := st.MakePod().Name("other-pod").Namespace(claimNameSpace).UID("other-pod-uid").Obj()
otherPod.Spec.ResourceClaims = []v1.PodResourceClaim{
{
Name: "my-claim-ref",
ResourceClaimName: new(claimName),
},
}
otherPod.Status = v1.PodStatus{
NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: claimName,
},
},
}
ni.AddPod(otherPod)
return ni
}(),
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceOverhead},
},
wantStatus: fwk.NewStatus(fwk.Success),
},
{
name: "mapped claim, same name but different namespace (sharing allowed)",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: claimMapped,
nodeInfo: func() *framework.NodeInfo {
ni := framework.NewNodeInfo()
otherPod := st.MakePod().Name("other-pod").Namespace("other-namespace").UID("other-pod-uid").Obj()
otherPod.Spec.ResourceClaims = []v1.PodResourceClaim{
{
Name: "my-claim-ref",
ResourceClaimName: new(claimName),
},
}
ni.AddPod(otherPod)
return ni
}(),
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceMapped},
},
wantStatus: fwk.NewStatus(fwk.Success),
},
{
name: "mapped claim, shared (other pod using it via template)",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: claimMappedReserved,
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceMapped},
},
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "node allocatable resource claim node-allocatable-claim has a mapped device and cannot be shared across pods"),
},
{
name: "mapped claim, already reserved for another pod - early exit",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: claimName,
Namespace: claimNameSpace,
UID: "claim-uid",
},
Status: resourceapi.ResourceClaimStatus{
Allocation: &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: []resourceapi.DeviceRequestAllocationResult{{Pool: "pool-1", Device: "cpu0"}},
},
},
ReservedFor: []resourceapi.ResourceClaimConsumerReference{
{UID: "other-pod-uid"},
},
},
},
nodeInfo: func() *framework.NodeInfo {
return framework.NewNodeInfo() // no pods on node, early exit triggers purely on reservation
}(),
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceMapped},
},
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "node allocatable resource claim node-allocatable-claim has a mapped device and cannot be shared across pods"),
},
{
name: "mapped claim, reserved for same pod group, workload claims enabled - allowed since no pods on node",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").
PodGroupName("my-pod-group").Obj(),
claim: &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: claimName,
Namespace: claimNameSpace,
UID: "claim-uid",
},
Status: resourceapi.ResourceClaimStatus{
Allocation: &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: []resourceapi.DeviceRequestAllocationResult{{Pool: "pool-1", Device: "cpu0"}},
},
},
ReservedFor: []resourceapi.ResourceClaimConsumerReference{
{
APIGroup: "scheduling.k8s.io",
Resource: "podgroups",
Name: "my-pod-group",
},
},
},
},
nodeInfo: func() *framework.NodeInfo {
return framework.NewNodeInfo()
}(),
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceMapped},
},
features: feature.Features{EnableDRAWorkloadResourceClaims: true},
wantStatus: fwk.NewStatus(fwk.Success),
},
{
name: "mapped claim, reserved for different pod group, workload claims enabled - exit early",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").
PodGroupName("my-pod-group").Obj(),
claim: &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{
Name: claimName,
Namespace: claimNameSpace,
UID: "claim-uid",
},
Status: resourceapi.ResourceClaimStatus{
Allocation: &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: []resourceapi.DeviceRequestAllocationResult{{Pool: "pool-1", Device: "cpu0"}},
},
},
ReservedFor: []resourceapi.ResourceClaimConsumerReference{
{
APIGroup: "scheduling.k8s.io",
Resource: "podgroups",
Name: "other-pod-group", // reserved for a different group
},
},
},
},
nodeInfo: func() *framework.NodeInfo {
return framework.NewNodeInfo() // no pods on node, early exit triggers purely on reservation mismatch
}(),
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceMapped},
},
features: feature.Features{EnableDRAWorkloadResourceClaims: true},
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "node allocatable resource claim node-allocatable-claim has a mapped device and cannot be shared across pods"),
},
{
name: "mapping and overhead in same device, shared (other pod using it)",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: claimCombinedReserved,
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceCombinedOverheadAndMapped},
},
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "node allocatable resource claim node-allocatable-claim has a mapped device and cannot be shared across pods"),
},
{
name: "mapped claim, pending allocation by another pod in same group",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: claimMapped,
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceMapped},
},
podGroupState: &podGroupStateData{
pendingAllocations: map[types.UID]sets.Set[types.UID]{
"claim-uid": sets.New[types.UID]("other-pod-uid"),
},
},
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, "node allocatable resource claim node-allocatable-claim has a mapped device and cannot be shared across pods"),
},
{
name: "mapped claim, pending allocation by same pod in same group",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: claimMapped,
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceMapped},
},
podGroupState: &podGroupStateData{
pendingAllocations: map[types.UID]sets.Set[types.UID]{
"claim-uid": sets.New[types.UID]("pod-uid"),
},
},
wantStatus: fwk.NewStatus(fwk.Success),
},
{
name: "overhead-only claim, pending allocation by another pod in same group",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("pod-uid").Obj(),
claim: claimOverhead,
draManager: &mockDRAManager{
resourceSlices: []*resourceapi.ResourceSlice{sliceOverhead},
},
podGroupState: &podGroupStateData{
pendingAllocations: map[types.UID]sets.Set[types.UID]{
"claim-uid": sets.New[types.UID]("other-pod-uid"),
},
},
wantStatus: fwk.NewStatus(fwk.Success),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pl := &DynamicResources{
draManager: tt.draManager,
fts: tt.features,
}
state := &stateData{
claimHasNodeAllocatableMappedDevice: make(map[types.UID]bool),
}
if tt.claim != nil {
if tt.claim.Status.Allocation != nil {
isMapped := false
for _, result := range tt.claim.Status.Allocation.Devices.Results {
device, err := getDeviceFromManager(tt.draManager, &result)
if err == nil && device != nil && device.NodeAllocatableResources != nil {
for _, mapping := range device.NodeAllocatableResources {
if mapping.Mapping != nil {
isMapped = true
break
}
}
}
if isMapped {
break
}
}
state.claimHasNodeAllocatableMappedDevice[tt.claim.UID] = isMapped
}
}
gotStatus := pl.validateNodeAllocatableDRAClaimSharing(tt.pod, tt.claim, state, tt.podGroupState)
if diff := cmp.Diff(tt.wantStatus, gotStatus); diff != "" {
t.Errorf("validateDRAClaimShareState() returned diff (-want +got):\n%s", diff)
}
})
}
}
func TestBuildNodeAllocatableDRAInfo(t *testing.T) {
cpuDevicePerInstance := resourceapi.Device{
Name: "cpu0",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
DeviceMultiplier: new(resource.MustParse("1")),
},
},
},
}
cpuDeviceCapacity := resourceapi.Device{
Name: "cpu0",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
CapacityKey: ptr.To(resourceapi.QualifiedName("dra.example.com/cpu")),
},
},
},
}
cpuMemDeviceCapacity := resourceapi.Device{
Name: "device1",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
CapacityKey: ptr.To(resourceapi.QualifiedName("dra.example.com/cpu")),
},
},
v1.ResourceMemory: {
Mapping: &resourceapi.NodeAllocatableMapping{
CapacityKey: ptr.To(resourceapi.QualifiedName("dra.example.com/memory")),
},
},
},
}
gpuDeviceAux := resourceapi.Device{
Name: "gpu0",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
DeviceMultiplier: new(resource.MustParse("2")),
},
},
v1.ResourceMemory: {
Mapping: &resourceapi.NodeAllocatableMapping{
DeviceMultiplier: new(resource.MustParse("4Gi")),
},
},
},
}
combinedDevice := resourceapi.Device{
Name: "combined-device",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
DeviceMultiplier: new(resource.MustParse("2")),
},
Overhead: &resourceapi.NodeAllocatableOverhead{
PerPod: new(resource.MustParse("1")),
PerContainer: new(resource.MustParse("500m")),
},
},
},
}
makeSlice := func(name string, devices ...resourceapi.Device) *resourceapi.ResourceSlice {
return &resourceapi.ResourceSlice{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test-ns"},
Spec: resourceapi.ResourceSliceSpec{
Pool: resourceapi.ResourcePool{Name: "pool1"},
NodeName: ptr.To("test-node"),
Driver: "dra.example.com",
Devices: devices,
},
}
}
makeClaim := func(name, uid string) *resourceapi.ResourceClaim {
return &resourceapi.ResourceClaim{
ObjectMeta: metav1.ObjectMeta{Name: name, Namespace: "test-ns", UID: types.UID(uid)},
}
}
allocResult := func(pool, device string, consumed ...map[resourceapi.QualifiedName]resource.Quantity) *resourceapi.AllocationResult {
res := resourceapi.DeviceRequestAllocationResult{
Driver: "dra.example.com",
Pool: pool,
Device: device,
}
if len(consumed) > 0 {
res.ConsumedCapacity = consumed[0]
}
return &resourceapi.AllocationResult{
Devices: resourceapi.DeviceAllocationResult{
Results: []resourceapi.DeviceRequestAllocationResult{res},
},
}
}
claimNameSpace := "test-ns"
tests := []struct {
name string
pod *v1.Pod
claims []*resourceapi.ResourceClaim
resourceSlices []*resourceapi.ResourceSlice
nodeAllocatableClaimAllocations map[v1.ObjectReference]*resourceapi.AllocationResult
want []v1.NodeAllocatableResourceClaimStatus
wantErr bool
}{
{
name: "empty",
pod: &v1.Pod{},
want: []v1.NodeAllocatableResourceClaimStatus{},
},
{
name: "one container, one claim, per instance quantity",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Claims: []v1.ResourceClaim{{Name: "node-allocatable-claim"}},
},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("node-allocatable-claim", "claim-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("slice1", cpuDevicePerInstance)},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "node-allocatable-claim", UID: "claim-uid"}: allocResult("pool1", "cpu0"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "node-allocatable-claim",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}},
}},
},
{
name: "Pod with Standard and DRA CPU and Memory Request",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("100m"),
v1.ResourceMemory: resource.MustParse("100Mi"),
},
Claims: []v1.ResourceClaim{{Name: "node-allocatable-claim"}},
},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("node-allocatable-claim", "claim-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("slice1", cpuMemDeviceCapacity)},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "node-allocatable-claim", UID: "claim-uid"}: allocResult("pool1", "device1", map[resourceapi.QualifiedName]resource.Quantity{
"dra.example.com/cpu": resource.MustParse("4"),
"dra.example.com/memory": resource.MustParse("8Gi"),
}),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "node-allocatable-claim",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("4")),
}, {
Name: v1.ResourceMemory,
Quantity: new(resource.MustParse("8Gi")),
}},
}},
},
{
name: "Fungible GPU/CPU claim - GPU selected",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")},
Claims: []v1.ResourceClaim{{Name: "fungible-claim"}},
},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("fungible-claim", "claim-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("gpu-slice", resourceapi.Device{Name: "gpu0"})},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "fungible-claim", UID: "claim-uid"}: allocResult("pool1", "gpu0"),
},
want: []v1.NodeAllocatableResourceClaimStatus{},
},
{
name: "Fungible GPU/CPU claim - CPU selected",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("1")},
Claims: []v1.ResourceClaim{{Name: "fungible-claim"}},
},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("fungible-claim", "claim-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("cpu-slice", cpuDeviceCapacity)},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "fungible-claim", UID: "claim-uid"}: allocResult("pool1", "cpu0", map[resourceapi.QualifiedName]resource.Quantity{
"dra.example.com/cpu": resource.MustParse("30"),
}),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "fungible-claim",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("30")),
}},
}},
},
{
name: "Combined CPU request and Auxiliary Request",
pod: st.MakePod().Name("test-pod").Namespace("test-ns").UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")},
Claims: []v1.ResourceClaim{{Name: "gpu-claim"}, {Name: "cpu-claim"}},
},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{
makeClaim("cpu-claim", "cpu-claim-uid"),
makeClaim("gpu-claim", "gpu-claim-uid"),
},
resourceSlices: []*resourceapi.ResourceSlice{
makeSlice("cpu-slice", cpuDeviceCapacity),
makeSlice("gpu-slice", gpuDeviceAux),
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "cpu-claim", UID: "cpu-claim-uid"}: allocResult("pool1", "cpu0", map[resourceapi.QualifiedName]resource.Quantity{
"dra.example.com/cpu": resource.MustParse("10"),
}),
{Name: "gpu-claim", UID: "gpu-claim-uid"}: allocResult("pool1", "gpu0"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "cpu-claim",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("10")),
}},
}, {
ResourceClaimName: "gpu-claim",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("2")),
}, {
Name: v1.ResourceMemory,
Quantity: new(resource.MustParse("4Gi")),
}},
}},
},
{
name: "Pod Level Resources with shared CPU claim and sidecar containers",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Resources(v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("11"),
v1.ResourceMemory: resource.MustParse("10Gi"),
},
}).
Containers([]v1.Container{
{Name: "c1", Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "cpu-claim"}}}},
{Name: "c2", Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "cpu-claim"}}}},
{Name: "sidecar1"},
{Name: "sidecar2"},
}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("cpu-claim", "cpu-claim-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("cpu-slice", cpuDeviceCapacity)},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "cpu-claim", UID: "cpu-claim-uid"}: allocResult("pool1", "cpu0", map[resourceapi.QualifiedName]resource.Quantity{
"dra.example.com/cpu": resource.MustParse("10"),
}),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "cpu-claim",
Containers: []string{"c1", "c2"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("10")),
}},
}},
},
{
name: "Multiple Claims per Container",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{v1.ResourceCPU: resource.MustParse("100m")},
Claims: []v1.ResourceClaim{{Name: "claim1"}, {Name: "claim2"}},
},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{
makeClaim("claim1", "claim1-uid"),
makeClaim("claim2", "claim2-uid"),
},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("slice1", cpuDeviceCapacity), makeSlice("slice2", cpuMemDeviceCapacity)},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "cpu0", map[resourceapi.QualifiedName]resource.Quantity{
"dra.example.com/cpu": resource.MustParse("4"),
}),
{Name: "claim2", UID: "claim2-uid"}: allocResult("pool1", "device1", map[resourceapi.QualifiedName]resource.Quantity{
"dra.example.com/memory": resource.MustParse("8Gi"),
}),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("4")),
}},
}, {
ResourceClaimName: "claim2",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceMemory,
Quantity: new(resource.MustParse("8Gi")),
}},
}},
},
{
name: "Unreferenced Claims",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{Name: "c1"}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("unref-claim", "unref-claim-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("slice1", cpuDeviceCapacity)},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "unref-claim", UID: "unref-claim-uid"}: allocResult("pool1", "cpu0", map[resourceapi.QualifiedName]resource.Quantity{
"dra.example.com/cpu": resource.MustParse("4"),
}),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "unref-claim",
Containers: []string{},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("4")),
}},
}},
},
{
name: "Unreferenced Claims with Overhead",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{Name: "c1"}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("unref-claim", "unref-claim-uid")},
resourceSlices: []*resourceapi.ResourceSlice{
makeSlice("slice1", resourceapi.Device{
Name: "device1",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceMemory: {
Overhead: &resourceapi.NodeAllocatableOverhead{
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("500Mi")),
},
},
},
}),
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "unref-claim", UID: "unref-claim-uid"}: allocResult("pool1", "device1"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "unref-claim",
Containers: []string{},
Mapping: []v1.NodeAllocatableMappedResources{},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("500Mi")),
}},
}},
},
{
name: "Unreferenced Claims with Overhead Per Container Only",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{Name: "c1"}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("unref-claim", "unref-claim-uid")},
resourceSlices: []*resourceapi.ResourceSlice{
makeSlice("slice1", resourceapi.Device{
Name: "device1",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceMemory: {
Overhead: &resourceapi.NodeAllocatableOverhead{
PerContainer: new(resource.MustParse("500Mi")),
},
},
},
}),
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "unref-claim", UID: "unref-claim-uid"}: allocResult("pool1", "device1"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "unref-claim",
Containers: []string{},
Mapping: []v1.NodeAllocatableMappedResources{},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerContainer: new(resource.MustParse("500Mi")),
}},
}},
},
{
name: "Combined Capacity and PerAllocatedUnitQuantity",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{
makeSlice("slice1", resourceapi.Device{
Name: "device1",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
CapacityKey: ptr.To(resourceapi.QualifiedName("dra.example.com/cores")),
CapacityMultiplier: new(resource.MustParse("2")),
},
},
},
}),
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "device1", map[resourceapi.QualifiedName]resource.Quantity{"dra.example.com/cores": resource.MustParse("4")}),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("8")),
}},
}},
},
{
name: "Capacity Key Missing in Allocation - Should be Ignored",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").Containers([]v1.Container{{Name: "c1", Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}}}}).Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("slice1", cpuDeviceCapacity)},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "cpu0", map[resourceapi.QualifiedName]resource.Quantity{"dra.example.com/wrong": resource.MustParse("4")}),
},
want: []v1.NodeAllocatableResourceClaimStatus{},
},
{
name: "Overhead Mappings",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{
makeSlice("slice1", resourceapi.Device{
Name: "device1",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceMemory: {
Overhead: &resourceapi.NodeAllocatableOverhead{
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("500Mi")),
},
},
},
}),
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "device1"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("500Mi")),
}},
}},
},
{
name: "Overhead Mappings - Only PerPod",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{
makeSlice("slice1", resourceapi.Device{
Name: "device1",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceMemory: {
Overhead: &resourceapi.NodeAllocatableOverhead{
PerPod: new(resource.MustParse("1Gi")),
},
},
},
}),
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "device1"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerPod: new(resource.MustParse("1Gi")),
}},
}},
},
{
name: "Overhead Mappings - Only PerContainer",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{
makeSlice("slice1", resourceapi.Device{
Name: "device1",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceMemory: {
Overhead: &resourceapi.NodeAllocatableOverhead{
PerContainer: new(resource.MustParse("500Mi")),
},
},
},
}),
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "device1"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerContainer: new(resource.MustParse("500Mi")),
}},
}},
},
{
name: "Overhead Mappings - Multiple Containers",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{
{
Name: "c1",
Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}},
},
{
Name: "c2",
Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}},
},
}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{
makeSlice("slice1", resourceapi.Device{
Name: "device1",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceMemory: {
Overhead: &resourceapi.NodeAllocatableOverhead{
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("500Mi")),
},
},
},
}),
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "device1"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "claim1",
Containers: []string{"c1", "c2"},
Mapping: []v1.NodeAllocatableMappedResources{},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("500Mi")),
}},
}},
},
{
name: "Combined direct mapped and overhead mappings for the same resource",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{
makeSlice("slice1", resourceapi.Device{
Name: "device1",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceMemory: {
Mapping: &resourceapi.NodeAllocatableMapping{
DeviceMultiplier: new(resource.MustParse("2Gi")),
},
Overhead: &resourceapi.NodeAllocatableOverhead{
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("500Mi")),
},
},
},
}),
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "device1"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceMemory,
Quantity: new(resource.MustParse("2Gi")),
}},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("500Mi")),
}},
}},
},
{
name: "device with both mapped and overhead",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("slice1", combinedDevice)},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "combined-device"),
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("2")),
}},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceCPU,
PerPod: new(resource.MustParse("1")),
PerContainer: new(resource.MustParse("500m")),
}},
}},
},
{
name: "Driver name collision with same pool and device name",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Claims: []v1.ResourceClaim{{Name: "claim1"}},
},
}}).
Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{
{
ObjectMeta: metav1.ObjectMeta{Name: "slice-a", Namespace: "test-ns"},
Spec: resourceapi.ResourceSliceSpec{
Pool: resourceapi.ResourcePool{Name: "pool1"},
NodeName: new("test-node"),
Driver: "driver-a.example.com",
Devices: []resourceapi.Device{{
Name: "cpu0",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
DeviceMultiplier: new(resource.MustParse("1")),
},
},
},
}},
},
},
{
ObjectMeta: metav1.ObjectMeta{Name: "slice-b", Namespace: "test-ns"},
Spec: resourceapi.ResourceSliceSpec{
Pool: resourceapi.ResourcePool{Name: "pool1"},
NodeName: new("test-node"),
Driver: "driver-b.example.com",
Devices: []resourceapi.Device{{
Name: "cpu0",
NodeAllocatableResources: map[v1.ResourceName]resourceapi.NodeAllocatableResource{
v1.ResourceCPU: {
Mapping: &resourceapi.NodeAllocatableMapping{
DeviceMultiplier: new(resource.MustParse("8")),
},
},
},
}},
},
},
},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: {
Devices: resourceapi.DeviceAllocationResult{
Results: []resourceapi.DeviceRequestAllocationResult{{
Driver: "driver-b.example.com",
Pool: "pool1",
Device: "cpu0",
}},
},
},
},
want: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
// Extract quantity correctly from "driver-b.example.com",
Quantity: new(resource.MustParse("8")),
}},
}},
},
{
name: "Invalid - Device Not Found",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").Containers([]v1.Container{{Name: "c1", Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}}}}).Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("slice1", resourceapi.Device{Name: "device1"})},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "nonexistent-device"),
},
wantErr: true,
},
{
name: "Invalid - Negative ConsumedCapacity",
pod: st.MakePod().Name("test-pod").Namespace(claimNameSpace).UID("test-uid").Containers([]v1.Container{{Name: "c1", Resources: v1.ResourceRequirements{Claims: []v1.ResourceClaim{{Name: "claim1"}}}}}).Obj(),
claims: []*resourceapi.ResourceClaim{makeClaim("claim1", "claim1-uid")},
resourceSlices: []*resourceapi.ResourceSlice{makeSlice("slice1", cpuDeviceCapacity)},
nodeAllocatableClaimAllocations: map[v1.ObjectReference]*resourceapi.AllocationResult{
{Name: "claim1", UID: "claim1-uid"}: allocResult("pool1", "cpu0", map[resourceapi.QualifiedName]resource.Quantity{"dra.example.com/cpu": resource.MustParse("-8")}),
},
wantErr: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
draManager := &mockDRAManager{
claims: tt.claims,
resourceSlices: tt.resourceSlices,
}
pl := &DynamicResources{
draManager: draManager,
}
claimNametoUID := make(map[string]types.UID)
for _, claim := range tt.claims {
claimNametoUID[claim.Name] = claim.UID
}
slices, _ := draManager.ResourceSlices().ListWithDeviceTaintRules()
got, err := pl.buildNodeAllocatableDRAInfo(tt.pod, tt.nodeAllocatableClaimAllocations, claimNametoUID, slices, nil)
if (err != nil) != tt.wantErr {
t.Errorf("buildNodeAllocatableDRAInfo() error = %v, wantErr %v", err, tt.wantErr)
return
}
if err != nil {
return
}
normalizeSlices := func(claimStatuses []v1.NodeAllocatableResourceClaimStatus) {
for i := range claimStatuses {
if claimStatuses[i].Mapping == nil {
claimStatuses[i].Mapping = []v1.NodeAllocatableMappedResources{}
}
if claimStatuses[i].Overhead == nil {
claimStatuses[i].Overhead = []v1.NodeAllocatableOverheadResources{}
}
}
}
normalizeSlices(got)
normalizeSlices(tt.want)
if diff := cmp.Diff(tt.want, got); diff != "" {
t.Errorf(`buildNodeAllocatableDRAInfo() diff (-want +got):
%s`, diff)
}
})
}
}
func TestExtractPodNodeAllocatableResourceClaimStatus(t *testing.T) {
logger := klog.TODO()
nodeName := "node-a"
original := []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: specialClaimInMemName,
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}},
},
}
t.Run("no cycle state", func(t *testing.T) {
got := ExtractPodNodeAllocatableResourceClaimStatus(logger, framework.NewCycleState(), nodeName)
if got != nil {
t.Errorf("ExtractPodNodeAllocatableResourceClaimStatus() = %v, want nil", got)
}
})
t.Run("unknown node", func(t *testing.T) {
state := framework.NewCycleState()
state.Write(stateKey, &stateData{
nodeAllocations: map[string]nodeAllocation{
nodeName: {nodeAllocatableResourceClaimStatuses: cloneNodeAllocatableResourceClaimStatuses(original)},
},
})
got := ExtractPodNodeAllocatableResourceClaimStatus(logger, state, "other-node")
if got != nil {
t.Errorf("ExtractPodNodeAllocatableResourceClaimStatus() = %v, want nil", got)
}
})
t.Run("returns a copy not the cycle-state slice", func(t *testing.T) {
state := framework.NewCycleState()
draState := &stateData{
nodeAllocations: map[string]nodeAllocation{
nodeName: {nodeAllocatableResourceClaimStatuses: cloneNodeAllocatableResourceClaimStatuses(original)},
},
}
state.Write(stateKey, draState)
got := ExtractPodNodeAllocatableResourceClaimStatus(logger, state, nodeName)
if diff := cmp.Diff(original, got); diff != "" {
t.Errorf("ExtractPodNodeAllocatableResourceClaimStatus() diff (-want +got):\n%s", diff)
}
if len(got) == 0 {
t.Fatal("ExtractPodNodeAllocatableResourceClaimStatus() returned empty status")
}
got[0].ResourceClaimName = "mutated-by-caller"
got[0].Mapping[0].Quantity.Add(resource.MustParse("1"))
inState := draState.nodeAllocations[nodeName].nodeAllocatableResourceClaimStatuses
if diff := cmp.Diff(original, inState); diff != "" {
t.Errorf("mutating extracted status changed cycle state (-want +got):\n%s", diff)
}
})
}
func TestPatchNodeAllocatableResourceClaimStatus(t *testing.T) {
pod := st.MakePod().Name("test-pod").Namespace("test-ns").UID("pod-uid").Obj()
placeholderStatus := []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: specialClaimInMemName,
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}},
},
}
realClaim := &resourceapi.ResourceClaim{ObjectMeta: metav1.ObjectMeta{Name: "real-claim-name"}}
tests := []struct {
name string
assumedPodStatus v1.PodStatus
finalPodNodeAllocatableClaimStatus []v1.NodeAllocatableResourceClaimStatus
extendedClaim *resourceapi.ResourceClaim
wantPatch bool
wantClaimNameInPatch string
wantClaimNameNotInPatch string
wantAssumedClaimName string
wantComputedClaimName string
setPatchError error
wantStatus *fwk.Status
}{
{
name: "no node allocatable resource claims for this pod",
wantPatch: false,
wantStatus: nil,
},
{
name: "assumed pod status same as new status",
assumedPodStatus: v1.PodStatus{
NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}},
},
},
},
finalPodNodeAllocatableClaimStatus: []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}},
},
},
wantPatch: true,
wantStatus: nil,
},
{
name: "assumed pod status different from new status",
assumedPodStatus: v1.PodStatus{
NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}},
},
},
},
finalPodNodeAllocatableClaimStatus: []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("2")),
}},
},
},
wantPatch: false,
wantStatus: statusError(klog.TODO(), errors.New("assumed pod status does not match calculated status to be patched")),
},
{
name: "pod status patch error",
assumedPodStatus: v1.PodStatus{
NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}},
},
},
},
finalPodNodeAllocatableClaimStatus: []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}},
},
},
wantPatch: true,
setPatchError: errors.New("inject patch error"),
wantStatus: statusError(klog.TODO(), fmt.Errorf("updating pod test-ns/test-pod NodeAllocatableResourceClaimStatuses: %w", errors.New("inject patch error"))),
},
{
name: "placeholder rewritten after DeepEqual, assumed pod unchanged",
assumedPodStatus: v1.PodStatus{NodeAllocatableResourceClaimStatuses: placeholderStatus},
finalPodNodeAllocatableClaimStatus: cloneNodeAllocatableResourceClaimStatuses(placeholderStatus),
extendedClaim: realClaim,
wantPatch: true,
wantClaimNameInPatch: "real-claim-name",
wantClaimNameNotInPatch: specialClaimInMemName,
wantAssumedClaimName: specialClaimInMemName,
wantComputedClaimName: "real-claim-name",
wantStatus: nil,
},
{
name: "mismatch still fails when a real extended claim exists",
assumedPodStatus: v1.PodStatus{NodeAllocatableResourceClaimStatuses: placeholderStatus},
finalPodNodeAllocatableClaimStatus: []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: specialClaimInMemName,
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("2")),
}},
},
},
extendedClaim: realClaim,
wantPatch: false,
wantAssumedClaimName: specialClaimInMemName,
wantComputedClaimName: specialClaimInMemName,
wantStatus: statusError(klog.TODO(), errors.New("assumed pod status does not match calculated status to be patched")),
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
podToUpdate := pod.DeepCopy()
podToUpdate.Status = *tt.assumedPodStatus.DeepCopy()
fakeClient := fake.NewSimpleClientset(podToUpdate)
pl := &DynamicResources{
clientset: fakeClient,
fts: feature.NewSchedulerFeaturesFromGates(utilfeature.DefaultFeatureGate),
}
if tt.setPatchError != nil {
fakeClient.PrependReactor("patch", "pods", func(action clienttesting.Action) (handled bool, ret runtime.Object, err error) {
return true, nil, tt.setPatchError
})
}
status := pl.patchNodeAllocatableResourceClaimStatus(ctx, podToUpdate, tt.finalPodNodeAllocatableClaimStatus, tt.extendedClaim)
if tt.wantStatus != nil && status != nil {
if tt.wantStatus.Code() != status.Code() {
t.Errorf("patchNodeAllocatableResourceClaimStatus() status code = %v, want %v", status.Code(), tt.wantStatus.Code())
}
if tt.wantStatus.AsError().Error() != status.AsError().Error() {
t.Errorf("patchNodeAllocatableResourceClaimStatus() status error = %v, want %v", status.AsError().Error(), tt.wantStatus.AsError().Error())
}
} else if tt.wantStatus != status {
t.Errorf("patchNodeAllocatableResourceClaimStatus() status = %v, want %v", status, tt.wantStatus)
}
actions := fakeClient.Actions()
gotPatch := false
var patchBody string
for _, action := range actions {
if action.Matches("patch", "pods") && action.GetSubresource() == "status" {
gotPatch = true
patchBody = string(action.(clienttesting.PatchAction).GetPatch())
break
}
}
if gotPatch != tt.wantPatch {
t.Errorf("patchNodeAllocatableResourceClaimStatus() gotPatch = %v, want %v", gotPatch, tt.wantPatch)
}
if tt.wantClaimNameInPatch != "" && !strings.Contains(patchBody, tt.wantClaimNameInPatch) {
t.Errorf("patch %s does not contain claim name %q", patchBody, tt.wantClaimNameInPatch)
}
if tt.wantClaimNameNotInPatch != "" && strings.Contains(patchBody, tt.wantClaimNameNotInPatch) {
t.Errorf("patch %s still contains claim name %q", patchBody, tt.wantClaimNameNotInPatch)
}
if tt.wantAssumedClaimName != "" {
got := podToUpdate.Status.NodeAllocatableResourceClaimStatuses[0].ResourceClaimName
if got != tt.wantAssumedClaimName {
t.Errorf("assumed pod ResourceClaimName = %q, want %q", got, tt.wantAssumedClaimName)
}
}
if tt.wantComputedClaimName != "" {
got := tt.finalPodNodeAllocatableClaimStatus[0].ResourceClaimName
if got != tt.wantComputedClaimName {
t.Errorf("computed status ResourceClaimName = %q, want %q", got, tt.wantComputedClaimName)
}
}
})
}
}
func TestClearNodeAllocatableResourceClaimStatus(t *testing.T) {
pod := st.MakePod().Name("test-pod").Namespace("test-ns").UID("pod-uid").Obj()
tests := []struct {
name string
initialPodStatus v1.PodStatus
wantPatch bool
}{
{
name: "no status to clear",
initialPodStatus: v1.PodStatus{},
wantPatch: false,
},
{
name: "status cleared",
initialPodStatus: v1.PodStatus{
NodeAllocatableResourceClaimStatuses: []v1.NodeAllocatableResourceClaimStatus{
{
ResourceClaimName: "claim1",
Containers: []string{"c1"},
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}},
},
},
},
wantPatch: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
podToUpdate := pod.DeepCopy()
podToUpdate.Status = *tt.initialPodStatus.DeepCopy()
fakeClient := fake.NewSimpleClientset(podToUpdate)
pl := &DynamicResources{
clientset: fakeClient,
fts: feature.NewSchedulerFeaturesFromGates(utilfeature.DefaultFeatureGate),
}
pl.clearNodeAllocatableResourceClaimStatus(ctx, podToUpdate)
actions := fakeClient.Actions()
gotPatch := false
for _, action := range actions {
if action.Matches("patch", "pods") && action.GetSubresource() == "status" {
gotPatch = true
patchAction := action.(clienttesting.PatchAction)
if patchAction.GetPatchType() != types.MergePatchType {
t.Errorf("patch type = %q, want %q", patchAction.GetPatchType(), types.MergePatchType)
}
wantPatch := `{"metadata":{"uid":"pod-uid"},"status":{"nodeAllocatableResourceClaimStatuses":[]}}`
if diff := cmp.Diff(wantPatch, string(patchAction.GetPatch())); diff != "" {
t.Errorf("clear patch mismatch (-want +got):\n%s", diff)
}
break
}
}
if gotPatch != tt.wantPatch {
t.Errorf("clearNodeAllocatableResourceClaimStatus() gotPatch = %v, want %v", gotPatch, tt.wantPatch)
}
})
}
}
func TestNodeFitsNativeResources(t *testing.T) {
tests := []struct {
name string
podRequest *framework.Resource
nodeInfo *framework.NodeInfo
wantStatus *fwk.Status
}{
{
name: "empty pod request",
podRequest: &framework.Resource{},
nodeInfo: framework.NewNodeInfo(),
wantStatus: nil,
},
{
name: "sufficient cpu and memory",
podRequest: &framework.Resource{
MilliCPU: 1000,
Memory: 1024 * 1024 * 1024,
},
nodeInfo: func() *framework.NodeInfo {
ni := framework.NewNodeInfo()
ni.SetNode(st.MakeNode().Name("test-node").Capacity(map[v1.ResourceName]string{
v1.ResourceCPU: "2000m",
v1.ResourceMemory: "2Gi",
}).Obj())
return ni
}(),
wantStatus: nil,
},
{
name: "insufficient cpu",
podRequest: &framework.Resource{
MilliCPU: 3000,
},
nodeInfo: func() *framework.NodeInfo {
ni := framework.NewNodeInfo()
ni.SetNode(st.MakeNode().Name("test-node").Capacity(map[v1.ResourceName]string{
v1.ResourceCPU: "2000m",
}).Obj())
return ni
}(),
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, `Insufficient cpu`),
},
{
name: "insufficient memory",
podRequest: &framework.Resource{
Memory: 3 * 1024 * 1024 * 1024,
},
nodeInfo: func() *framework.NodeInfo {
existigPod := v1.Pod{
Spec: v1.PodSpec{
Containers: []v1.Container{
{
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("4Gi"),
},
},
},
},
},
}
ni := framework.NewNodeInfo(&existigPod)
ni.SetNode(st.MakeNode().Name("test-node").Capacity(map[v1.ResourceName]string{
v1.ResourceMemory: "5Gi",
}).Obj())
return ni
}(),
wantStatus: fwk.NewStatus(fwk.Unschedulable, `Insufficient memory`),
},
{
name: "sufficient hugepages",
podRequest: &framework.Resource{
ScalarResources: map[v1.ResourceName]int64{
hugePages2Mi: 1,
hugePages1Gi: 2,
},
},
nodeInfo: func() *framework.NodeInfo {
ni := framework.NewNodeInfo()
ni.SetNode(st.MakeNode().Name("test-node").Capacity(map[v1.ResourceName]string{
hugePages2Mi: "1",
hugePages1Gi: "2",
}).Obj())
return ni
}(),
wantStatus: nil,
},
{
name: "insufficient hugepages 2Mi",
podRequest: &framework.Resource{
ScalarResources: map[v1.ResourceName]int64{
hugePages2Mi: 2,
},
},
nodeInfo: func() *framework.NodeInfo {
ni := framework.NewNodeInfo()
ni.SetNode(st.MakeNode().Name("test-node").Capacity(map[v1.ResourceName]string{
hugePages2Mi: "1",
}).Obj())
return ni
}(),
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, `Insufficient hugepages-2Mi`),
},
{
name: "insufficient hugepages 1Gi",
podRequest: &framework.Resource{
ScalarResources: map[v1.ResourceName]int64{
hugePages1Gi: 3,
},
},
nodeInfo: func() *framework.NodeInfo {
ni := framework.NewNodeInfo()
ni.SetNode(st.MakeNode().Name("test-node").Capacity(map[v1.ResourceName]string{
hugePages1Gi: "2",
}).Obj())
return ni
}(),
wantStatus: fwk.NewStatus(fwk.UnschedulableAndUnresolvable, `Insufficient hugepages-1Gi`),
},
{
name: "all resources sufficient",
podRequest: &framework.Resource{
MilliCPU: 1000,
Memory: 1024 * 1024 * 1024,
ScalarResources: map[v1.ResourceName]int64{
hugePages2Mi: 1,
},
},
nodeInfo: func() *framework.NodeInfo {
ni := framework.NewNodeInfo()
ni.SetNode(st.MakeNode().Name("test-node").Capacity(map[v1.ResourceName]string{
v1.ResourceCPU: "2000m",
v1.ResourceMemory: "2Gi",
hugePages2Mi: "1",
}).Obj())
return ni
}(),
wantStatus: nil,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pl := &DynamicResources{}
status := pl.nodeFitsResources(tt.nodeInfo, tt.podRequest)
if diff := cmp.Diff(tt.wantStatus, status, cmp.Comparer(func(a, b *fwk.Status) bool {
if a == nil && b == nil {
return true
}
if a == nil || b == nil {
return false
}
if a.Code() != b.Code() {
return false
}
// Only compare reasons if the code is Unschedulable
if a.Code() == fwk.Unschedulable {
return cmp.Diff(a.Reasons(), b.Reasons()) == ""
}
return true
})); diff != "" {
t.Errorf(`nodeFitsNativeResources() diff (-want +got):
%s`, diff)
}
})
}
}
func TestValidatePodLevelResourcesCoverDRA(t *testing.T) {
tests := []struct {
name string
pod *v1.Pod
nodeAllocatableStatus []v1.NodeAllocatableResourceClaimStatus
wantStatusCode fwk.Code
wantErrorMessage string
}{
{
name: "PodLevelResources enabled, no pod resources set",
pod: st.MakePod().Name("test-pod").
Containers([]v1.Container{{
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("1"),
v1.ResourceMemory: resource.MustParse("1Gi"),
},
Claims: []v1.ResourceClaim{{Name: "dra-claim"}},
},
}}).
Obj(),
nodeAllocatableStatus: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "dra-claim",
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("500m")),
}, {
Name: v1.ResourceMemory,
Quantity: new(resource.MustParse("512Mi")),
}},
}},
wantStatusCode: fwk.Success,
},
{
name: "PodLevel resources sufficient",
pod: st.MakePod().Name("test-pod").
Resources(v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("2"),
v1.ResourceMemory: resource.MustParse("2Gi"),
},
}).
Containers([]v1.Container{{
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("1"),
v1.ResourceMemory: resource.MustParse("1Gi"),
},
Claims: []v1.ResourceClaim{{Name: "dra-claim"}},
},
}}).
Obj(),
nodeAllocatableStatus: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "dra-claim",
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("500m")),
}, {
Name: v1.ResourceMemory,
Quantity: new(resource.MustParse("512Mi")),
}},
}},
wantStatusCode: fwk.Success,
},
{
name: "PodLevel insufficient for one of the resource",
pod: st.MakePod().Name("test-pod").
Resources(v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("1"),
v1.ResourceMemory: resource.MustParse("2Gi"),
},
}).
Containers([]v1.Container{{
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("800m"),
v1.ResourceMemory: resource.MustParse("1Gi"),
},
Claims: []v1.ResourceClaim{{Name: "dra-claim"}},
},
}}).
Obj(),
nodeAllocatableStatus: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "dra-claim",
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("300m")),
}, {
Name: v1.ResourceMemory,
Quantity: new(resource.MustParse("512Mi")),
}},
}},
wantStatusCode: fwk.UnschedulableAndUnresolvable,
wantErrorMessage: "pod level request for cpu is insufficient to cover the aggregated container and node-allocatable DRA requests",
},
{
name: "PodLevel insufficient for multiple resources",
pod: st.MakePod().Name("test-pod").
Resources(v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("1"),
v1.ResourceMemory: resource.MustParse("1Gi"),
},
}).
Containers([]v1.Container{{
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("1100m"),
v1.ResourceMemory: resource.MustParse("1100Mi"),
},
Claims: []v1.ResourceClaim{{Name: "dra-claim"}},
},
}}).
Obj(),
nodeAllocatableStatus: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "dra-claim",
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("100m")),
}, {
Name: v1.ResourceMemory,
Quantity: new(resource.MustParse("100Mi")),
}},
}},
wantStatusCode: fwk.UnschedulableAndUnresolvable,
wantErrorMessage: "insufficient to cover the aggregated container and node-allocatable DRA requests",
},
{
name: "PodLevel resources sufficient with overheads",
pod: &v1.Pod{
Spec: v1.PodSpec{
Resources: &v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("4"),
v1.ResourceMemory: resource.MustParse("3Gi"),
},
},
Containers: []v1.Container{
{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("1"),
v1.ResourceMemory: resource.MustParse("1Gi"),
},
Claims: []v1.ResourceClaim{{Name: "dra-claim"}},
},
},
},
Overhead: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("500m"),
v1.ResourceMemory: resource.MustParse("512Mi"),
},
},
},
nodeAllocatableStatus: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "dra-claim",
Mapping: []v1.NodeAllocatableMappedResources{{
Name: v1.ResourceCPU,
Quantity: new(resource.MustParse("1")),
}, {
Name: v1.ResourceMemory,
Quantity: new(resource.MustParse("1Gi")),
}},
}},
wantStatusCode: fwk.Success,
},
{
name: "PodLevel resources sufficient with DRA Overhead",
pod: st.MakePod().Name("test-pod").
Resources(v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("2"),
v1.ResourceMemory: resource.MustParse("3Gi"), // Container (1Gi) + DRA flat pod overhead (1Gi) + DRA container overhead (1Gi) = 3Gi
},
}).
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("1"),
v1.ResourceMemory: resource.MustParse("1Gi"),
},
Claims: []v1.ResourceClaim{{Name: "dra-claim"}},
},
}}).
Obj(),
nodeAllocatableStatus: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "dra-claim",
Containers: []string{"c1"},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("1Gi")),
}},
}},
wantStatusCode: fwk.Success,
},
{
name: "PodLevel resources insufficient with DRA Overhead",
pod: st.MakePod().Name("test-pod").
Resources(v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("2"),
v1.ResourceMemory: resource.MustParse("2Gi"), // Container (1Gi) + DRA flat pod overhead (1Gi) + DRA container overhead (1Gi) = 3Gi > 2Gi
},
}).
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Requests: v1.ResourceList{
v1.ResourceCPU: resource.MustParse("1"),
v1.ResourceMemory: resource.MustParse("1Gi"),
},
Claims: []v1.ResourceClaim{{Name: "dra-claim"}},
},
}}).
Obj(),
nodeAllocatableStatus: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "dra-claim",
Containers: []string{"c1"},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerPod: new(resource.MustParse("1Gi")),
PerContainer: new(resource.MustParse("1Gi")),
}},
}},
wantStatusCode: fwk.UnschedulableAndUnresolvable,
wantErrorMessage: "pod level request for memory is insufficient to cover the aggregated container and node-allocatable DRA requests",
},
{
name: "PodLevel limits cover DRA",
pod: st.MakePod().Name("test-pod").
Resources(v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("2Gi"),
},
}).
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("1Gi"),
},
Claims: []v1.ResourceClaim{{Name: "dra-claim"}},
},
}}).
Obj(),
nodeAllocatableStatus: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "dra-claim",
Containers: []string{"c1"},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerContainer: new(resource.MustParse("512Mi")),
}},
}},
wantStatusCode: fwk.Success,
},
{
name: "PodLevel limits does not cover DRA",
pod: st.MakePod().Name("test-pod").
Resources(v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("2Gi"),
},
}).
Containers([]v1.Container{{
Name: "c1",
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("1.5Gi"),
},
Claims: []v1.ResourceClaim{{Name: "dra-claim"}},
},
}}).
Obj(),
nodeAllocatableStatus: []v1.NodeAllocatableResourceClaimStatus{{
ResourceClaimName: "dra-claim",
Containers: []string{"c1"},
Overhead: []v1.NodeAllocatableOverheadResources{{
Name: v1.ResourceMemory,
PerContainer: new(resource.MustParse("1Gi")),
}},
}},
wantStatusCode: fwk.UnschedulableAndUnresolvable,
wantErrorMessage: "pod level limit for memory is insufficient to cover the limit and DRA overhead for container c1",
},
{
name: "PodLevel limits overcommitted but individual container limits with DRA covered by pod level limits",
pod: st.MakePod().Name("test-pod").
Resources(v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("4Gi"),
},
}).
Containers([]v1.Container{
{
Name: "c1",
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("3Gi"),
},
},
},
{
Name: "c2",
Resources: v1.ResourceRequirements{
Limits: v1.ResourceList{
v1.ResourceMemory: resource.MustParse("3Gi"),
},
},
},
}).
Obj(),
wantStatusCode: fwk.Success,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
pl := &DynamicResources{
fts: feature.Features{EnablePodLevelResources: true},
}
tt.pod.Status.NodeAllocatableResourceClaimStatuses = tt.nodeAllocatableStatus
gotStatus := pl.validatePodLevelResourcesCoverDRA(tt.pod)
if diff := cmp.Diff(tt.wantStatusCode, gotStatus.Code()); diff != "" {
t.Errorf("validatePodLevelResourcesCoverDRA() returned diff (-want +got):\n%s", diff)
}
if tt.wantStatusCode != fwk.Success && tt.wantErrorMessage != "" {
if !strings.Contains(gotStatus.Message(), tt.wantErrorMessage) {
t.Errorf("validatePodLevelResourcesCoverDRA() returned error %q, want it to contain %q", gotStatus.Message(), tt.wantErrorMessage)
}
}
})
}
}
func TestFilterSlicesForNode(t *testing.T) {
nodeName := "node1"
otherNodeName := "node2"
allNodes := true
tests := []struct {
name string
node *v1.Node
slices []*resourceapi.ResourceSlice
wantNames []string
}{
{
name: "matches NodeName exactly",
node: st.MakeNode().Name(nodeName).Label("kubernetes.io/hostname", nodeName).Obj(),
slices: []*resourceapi.ResourceSlice{
{
ObjectMeta: metav1.ObjectMeta{Name: "nodename"},
Spec: resourceapi.ResourceSliceSpec{
NodeName: &nodeName,
},
},
{
ObjectMeta: metav1.ObjectMeta{Name: "othernode"},
Spec: resourceapi.ResourceSliceSpec{
NodeName: &otherNodeName,
},
},
},
wantNames: []string{"nodename"},
},
{
name: "matches AllNodes",
node: st.MakeNode().Name(nodeName).Label("kubernetes.io/hostname", nodeName).Obj(),
slices: []*resourceapi.ResourceSlice{
{
ObjectMeta: metav1.ObjectMeta{Name: "allnodes"},
Spec: resourceapi.ResourceSliceSpec{
AllNodes: &allNodes,
},
},
},
wantNames: []string{"allnodes"},
},
{
name: "matches NodeSelector",
node: st.MakeNode().Name(nodeName).Label("kubernetes.io/hostname", nodeName).Obj(),
slices: []*resourceapi.ResourceSlice{
{
ObjectMeta: metav1.ObjectMeta{Name: "nodeselector"},
Spec: resourceapi.ResourceSliceSpec{
NodeSelector: st.MakeNodeSelector().In("kubernetes.io/hostname", []string{nodeName}, st.NodeSelectorTypeMatchExpressions).Obj(),
},
},
{
ObjectMeta: metav1.ObjectMeta{Name: "othernodeselector"},
Spec: resourceapi.ResourceSliceSpec{
NodeSelector: st.MakeNodeSelector().In("kubernetes.io/hostname", []string{otherNodeName}, st.NodeSelectorTypeMatchExpressions).Obj(),
},
},
},
wantNames: []string{"nodeselector"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
draManager := &mockDRAManager{resourceSlices: tt.slices}
got, _ := filterSlicesForNode(draManager, tt.node)
var gotNames []string
for _, slice := range got {
gotNames = append(gotNames, slice.Name)
}
// Sort slices to make comparison stable.
sort.Strings(gotNames)
sort.Strings(tt.wantNames)
if diff := cmp.Diff(tt.wantNames, gotNames); diff != "" {
t.Errorf("filterSlicesForNode() diff (-want +got):\n%s", diff)
}
})
}
}
func TestReplaceSpecialClaimNameInStatus(t *testing.T) {
claim := func(name string) *resourceapi.ResourceClaim {
return &resourceapi.ResourceClaim{ObjectMeta: metav1.ObjectMeta{Name: name}}
}
tests := []struct {
name string
extendedClaim *resourceapi.ResourceClaim
statuses []v1.NodeAllocatableResourceClaimStatus
want []v1.NodeAllocatableResourceClaimStatus
}{
{
name: "placeholder replaced with real claim name",
extendedClaim: claim("real-claim-name"),
statuses: []v1.NodeAllocatableResourceClaimStatus{
{ResourceClaimName: specialClaimInMemName},
},
want: []v1.NodeAllocatableResourceClaimStatus{
{ResourceClaimName: "real-claim-name"},
},
},
{
name: "only placeholder entries are rewritten",
extendedClaim: claim("real-claim-name"),
statuses: []v1.NodeAllocatableResourceClaimStatus{
{ResourceClaimName: "user-claim"},
{ResourceClaimName: specialClaimInMemName},
},
want: []v1.NodeAllocatableResourceClaimStatus{
{ResourceClaimName: "user-claim"},
{ResourceClaimName: "real-claim-name"},
},
},
{
name: "nil extended claim is a no-op",
extendedClaim: nil,
statuses: []v1.NodeAllocatableResourceClaimStatus{
{ResourceClaimName: specialClaimInMemName},
},
want: []v1.NodeAllocatableResourceClaimStatus{
{ResourceClaimName: specialClaimInMemName},
},
},
{
name: "claim not yet created in API server is a no-op",
extendedClaim: claim(specialClaimInMemName),
statuses: []v1.NodeAllocatableResourceClaimStatus{
{ResourceClaimName: specialClaimInMemName},
},
want: []v1.NodeAllocatableResourceClaimStatus{
{ResourceClaimName: specialClaimInMemName},
},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
replaceSpecialClaimNameInStatus(tt.extendedClaim, tt.statuses)
if diff := cmp.Diff(tt.want, tt.statuses); diff != "" {
t.Errorf("replaceSpecialClaimNameInStatus() diff (-want +got):\n%s", diff)
}
})
}
}