@@ -18,6 +18,8 @@ package planner
1818
1919import (
2020 "fmt"
21+ "k8s.io/autoscaler/cluster-autoscaler/resourcequotas"
22+
2123 "testing"
2224 "time"
2325
@@ -505,7 +507,8 @@ func TestUpdateClusterState(t *testing.T) {
505507 assert .NoError (t , err )
506508 clustersnapshot .InitializeClusterSnapshotOrDie (t , autoscalingCtx .ClusterSnapshot , tc .nodes , tc .pods )
507509 deleteOptions := options.NodeDeleteOptions {}
508- p := New (& autoscalingCtx , processors , deleteOptions , nil )
510+ factory := resourcequotas .NewTrackerFactory (resourcequotas.TrackerOptions {CustomResourcesProcessor : processors .CustomResourcesProcessor , QuotaProvider : resourcequotas .NewCloudMinProvider (provider )})
511+ p := New (& autoscalingCtx , processors , deleteOptions , nil , factory )
509512 p .eligibilityChecker = & fakeEligibilityChecker {eligible : asMap (tc .eligible )}
510513 if tc .isSimulationTimeout {
511514 autoscalingCtx .AutoscalingOptions .ScaleDownSimulationTimeout = 1 * time .Second
@@ -703,7 +706,8 @@ func TestUpdateClusterStatUnneededNodesLimit(t *testing.T) {
703706 assert .NoError (t , err )
704707 clustersnapshot .InitializeClusterSnapshotOrDie (t , autoscalingCtx .ClusterSnapshot , nodes , nil )
705708 deleteOptions := options.NodeDeleteOptions {}
706- p := New (& autoscalingCtx , processors , deleteOptions , nil )
709+ factory := resourcequotas .NewTrackerFactory (resourcequotas.TrackerOptions {CustomResourcesProcessor : processors .CustomResourcesProcessor , QuotaProvider : resourcequotas .NewCloudMinProvider (provider )})
710+ p := New (& autoscalingCtx , processors , deleteOptions , nil , factory )
707711 p .eligibilityChecker = & fakeEligibilityChecker {eligible : asMap (nodeNames (nodes ))}
708712 p .minUpdateInterval = tc .updateInterval
709713 p .unneededNodes .Update (& autoscalingCtx , previouslyUnneeded , time .Now ())
@@ -838,7 +842,8 @@ func TestNewPlannerWithExistingDeletionCandidateNodes(t *testing.T) {
838842 assert .NoError (t , err )
839843
840844 deleteOptions := options.NodeDeleteOptions {}
841- p := New (& autoscalingCtx , processors , deleteOptions , nil )
845+ factory := resourcequotas .NewTrackerFactory (resourcequotas.TrackerOptions {CustomResourcesProcessor : processors .CustomResourcesProcessor , QuotaProvider : resourcequotas .NewCloudMinProvider (provider )})
846+ p := New (& autoscalingCtx , processors , deleteOptions , nil , factory )
842847
843848 p .unneededNodes .AsList ()
844849 })
@@ -847,11 +852,13 @@ func TestNewPlannerWithExistingDeletionCandidateNodes(t *testing.T) {
847852
848853func TestNodesToDelete (t * testing.T ) {
849854 testCases := []struct {
850- name string
851- nodes map [string ][]* apiv1.Node
852- removableNodes map [cloudprovider.NodeGroup ][]simulator.NodeToBeRemoved
853- wantEmpty []* apiv1.Node
854- wantDrain []* apiv1.Node
855+ name string
856+ nodes map [string ][]* apiv1.Node
857+ removableNodes map [cloudprovider.NodeGroup ][]simulator.NodeToBeRemoved
858+ wantEmpty []* apiv1.Node
859+ wantDrain []* apiv1.Node
860+ minLimits map [string ]int64
861+ wantUnremovable map [string ]simulator.UnremovableReason
855862 }{
856863 {
857864 name : "empty" ,
@@ -1016,6 +1023,56 @@ func TestNodesToDelete(t *testing.T) {
10161023 buildRemovableNode ("atomic-partial-ng-partially-registered-1" , 0 ).Node ,
10171024 },
10181025 },
1026+ {
1027+ name : "min limits race condition protection" ,
1028+ nodes : map [string ][]* apiv1.Node {
1029+ "ng1" : {
1030+ BuildTestNode ("node-3" , 1000 , 10 ),
1031+ },
1032+ },
1033+ removableNodes : map [cloudprovider.NodeGroup ][]simulator.NodeToBeRemoved {
1034+ sizedNodeGroup ("ng1" , 3 , false ): {
1035+ buildRemovableNode ("node-1" , 0 ),
1036+ buildRemovableNode ("node-2" , 1 ),
1037+ },
1038+ },
1039+ wantEmpty : []* apiv1.Node {
1040+ buildRemovableNode ("node-1" , 0 ).Node ,
1041+ },
1042+ wantDrain : []* apiv1.Node {},
1043+ minLimits : map [string ]int64 {
1044+ cloudprovider .ResourceNameCores : 2 ,
1045+ cloudprovider .ResourceNameMemory : 20 ,
1046+ "nodes" : 2 ,
1047+ },
1048+ wantUnremovable : map [string ]simulator.UnremovableReason {
1049+ "node-2" : simulator .MinimalResourceLimitExceeded ,
1050+ },
1051+ },
1052+ {
1053+ name : "two nodes removable, quota allows both" ,
1054+ nodes : map [string ][]* apiv1.Node {
1055+ "ng1" : {
1056+ BuildTestNode ("node-3" , 1000 , 10 ),
1057+ },
1058+ },
1059+ removableNodes : map [cloudprovider.NodeGroup ][]simulator.NodeToBeRemoved {
1060+ sizedNodeGroup ("ng1" , 3 , false ): {
1061+ buildRemovableNode ("node-1" , 0 ),
1062+ buildRemovableNode ("node-2" , 0 ),
1063+ },
1064+ },
1065+ wantEmpty : []* apiv1.Node {
1066+ buildRemovableNode ("node-1" , 0 ).Node ,
1067+ buildRemovableNode ("node-2" , 0 ).Node ,
1068+ },
1069+ wantDrain : []* apiv1.Node {},
1070+ minLimits : map [string ]int64 {
1071+ cloudprovider .ResourceNameCores : 1 ,
1072+ cloudprovider .ResourceNameMemory : 10 ,
1073+ "nodes" : 1 ,
1074+ },
1075+ },
10191076 }
10201077 for _ , tc := range testCases {
10211078 tc := tc
@@ -1044,20 +1101,41 @@ func TestNodesToDelete(t *testing.T) {
10441101 ScaleDownUnneededTime : 10 * time .Minute ,
10451102 ScaleDownUnreadyTime : 0 * time .Minute ,
10461103 },
1104+ ScaleDownSimulationTimeout : 5 * time .Minute ,
10471105 }
10481106 processors , templateNodeInfoRegistry := processorstest .NewTestProcessors (autoscalingOpts )
10491107 autoscalingCtx , err := NewScaleTestAutoscalingContext (autoscalingOpts , & fake.Clientset {}, nil , provider , nil , nil , templateNodeInfoRegistry )
10501108 assert .NoError (t , err )
10511109 clustersnapshot .InitializeClusterSnapshotOrDie (t , autoscalingCtx .ClusterSnapshot , allNodes , nil )
10521110 deleteOptions := options.NodeDeleteOptions {}
1053- p := New (& autoscalingCtx , processors , deleteOptions , nil )
1111+ factory := resourcequotas .NewTrackerFactory (resourcequotas.TrackerOptions {CustomResourcesProcessor : processors .CustomResourcesProcessor , QuotaProvider : resourcequotas .NewCloudMinProvider (provider )})
1112+ p := New (& autoscalingCtx , processors , deleteOptions , nil , factory )
10541113 p .latestUpdate = time .Now ()
1114+
1115+ if tc .minLimits != nil {
1116+ resourceLimiter := cloudprovider .NewResourceLimiter (tc .minLimits , nil )
1117+ provider .SetResourceLimiter (resourceLimiter )
1118+ }
1119+
10551120 p .scaleDownContext .ActuationStatus = deletiontracker .NewNodeDeletionTracker (0 * time .Second )
10561121 p .unneededNodes .Update (& autoscalingCtx , allRemovables , time .Now ().Add (- 1 * time .Hour ))
10571122 p .eligibilityChecker = & fakeEligibilityChecker {eligible : asMap (nodeNames (allNodes ))}
10581123 empty , drain := p .NodesToDelete (time .Now ())
10591124 assert .ElementsMatch (t , tc .wantEmpty , empty )
10601125 assert .ElementsMatch (t , tc .wantDrain , drain )
1126+
1127+ if tc .wantUnremovable != nil {
1128+ unremovable := p .unremovableNodes .AsList ()
1129+ unremovableMap := make (map [string ]simulator.UnremovableReason )
1130+ for _ , u := range unremovable {
1131+ unremovableMap [u .Node .Name ] = u .Reason
1132+ }
1133+ for nodeName , expectedReason := range tc .wantUnremovable {
1134+ reason , found := unremovableMap [nodeName ]
1135+ assert .True (t , found , "Node %s not found in unremovable list" , nodeName )
1136+ assert .Equal (t , expectedReason , reason , "Reason mismatch for node %s" , nodeName )
1137+ }
1138+ }
10611139 })
10621140 }
10631141}
0 commit comments