@@ -53,6 +53,7 @@ var ctx context.Context
53
53
var env * test.Environment
54
54
var cluster * state.Cluster
55
55
var controller * consolidation.Controller
56
+ var provisioningController * provisioning.Controller
56
57
var provisioner * provisioning.Provisioner
57
58
var cloudProvider * fake.CloudProvider
58
59
var clientSet * kubernetes.Clientset
@@ -82,6 +83,7 @@ var _ = BeforeSuite(func() {
82
83
clientSet = kubernetes .NewForConfigOrDie (e .Config )
83
84
recorder = test .NewEventRecorder ()
84
85
provisioner = provisioning .NewProvisioner (ctx , cfg , env .Client , clientSet .CoreV1 (), recorder , cloudProvider , cluster )
86
+ provisioningController = provisioning .NewController (env .Client , provisioner , recorder )
85
87
})
86
88
Expect (env .Start ()).To (Succeed (), "Failed to start environment" )
87
89
})
@@ -1307,6 +1309,158 @@ var _ = Describe("Empty Nodes", func() {
1307
1309
})
1308
1310
})
1309
1311
1312
+ var _ = Describe ("Parallelization" , func () {
1313
+ It ("should schedule an additional node when receiving pending pods while consolidating" , func () {
1314
+ labels := map [string ]string {
1315
+ "app" : "test" ,
1316
+ }
1317
+ // create our RS so we can link a pod to it
1318
+ rs := test .ReplicaSet ()
1319
+ ExpectApplied (ctx , env .Client , rs )
1320
+ Expect (env .Client .Get (ctx , client .ObjectKeyFromObject (rs ), rs )).To (Succeed ())
1321
+
1322
+ pod := test .Pod (test.PodOptions {
1323
+ ObjectMeta : metav1.ObjectMeta {Labels : labels ,
1324
+ OwnerReferences : []metav1.OwnerReference {
1325
+ {
1326
+ APIVersion : "apps/v1" ,
1327
+ Kind : "ReplicaSet" ,
1328
+ Name : rs .Name ,
1329
+ UID : rs .UID ,
1330
+ Controller : aws .Bool (true ),
1331
+ BlockOwnerDeletion : aws .Bool (true ),
1332
+ },
1333
+ }}})
1334
+
1335
+ prov := test .Provisioner (test.ProvisionerOptions {Consolidation : & v1alpha5.Consolidation {Enabled : aws .Bool (true )}})
1336
+
1337
+ // Add a finalizer to the node so that it sticks around for the scheduling loop
1338
+ node := test .Node (test.NodeOptions {
1339
+ ObjectMeta : metav1.ObjectMeta {
1340
+ Labels : map [string ]string {
1341
+ v1alpha5 .ProvisionerNameLabelKey : prov .Name ,
1342
+ v1 .LabelInstanceTypeStable : mostExpensiveInstance .Name (),
1343
+ v1alpha5 .LabelCapacityType : mostExpensiveOffering .CapacityType ,
1344
+ v1 .LabelTopologyZone : mostExpensiveOffering .Zone ,
1345
+ },
1346
+ Finalizers : []string {"karpenter.sh/test-finalizer" },
1347
+ },
1348
+ Allocatable : map [v1.ResourceName ]resource.Quantity {v1 .ResourceCPU : resource .MustParse ("32" )}})
1349
+
1350
+ ExpectApplied (ctx , env .Client , rs , pod , node , prov )
1351
+ ExpectMakeNodesReady (ctx , env .Client , node )
1352
+ ExpectReconcileSucceeded (ctx , nodeStateController , client .ObjectKeyFromObject (node ))
1353
+ ExpectManualBinding (ctx , env .Client , pod , node )
1354
+ ExpectScheduled (ctx , env .Client , pod )
1355
+ Expect (env .Client .Get (ctx , client .ObjectKeyFromObject (node ), node )).To (Succeed ())
1356
+
1357
+ fakeClock .Step (10 * time .Minute )
1358
+
1359
+ // Run the processing loop in parallel in the background with environment context
1360
+ go func () {
1361
+ _ , err := controller .ProcessCluster (env .Ctx )
1362
+ Expect (err ).ToNot (HaveOccurred ())
1363
+ }()
1364
+
1365
+ Eventually (func (g Gomega ) {
1366
+ // should create a new node as there is a cheaper one that can hold the pod
1367
+ nodes := & v1.NodeList {}
1368
+ g .Expect (env .Client .List (ctx , nodes )).To (Succeed ())
1369
+ g .Expect (len (nodes .Items )).To (Equal (2 ))
1370
+ }).Should (Succeed ())
1371
+
1372
+ // Add a new pending pod that should schedule while node is not yet deleted
1373
+ pods := ExpectProvisionedNoBinding (ctx , env .Client , provisioningController , test .UnschedulablePod ())
1374
+ nodes := & v1.NodeList {}
1375
+ Expect (env .Client .List (ctx , nodes )).To (Succeed ())
1376
+ Expect (len (nodes .Items )).To (Equal (3 ))
1377
+ Expect (pods [0 ].Spec .NodeName ).NotTo (Equal (node .Name ))
1378
+ })
1379
+ It ("should not consolidate a node that is launched for pods on a deleting node" , func () {
1380
+ labels := map [string ]string {
1381
+ "app" : "test" ,
1382
+ }
1383
+ // create our RS so we can link a pod to it
1384
+ rs := test .ReplicaSet ()
1385
+ ExpectApplied (ctx , env .Client , rs )
1386
+ Expect (env .Client .Get (ctx , client .ObjectKeyFromObject (rs ), rs )).To (Succeed ())
1387
+
1388
+ prov := test .Provisioner (test.ProvisionerOptions {Consolidation : & v1alpha5.Consolidation {Enabled : aws .Bool (true )}})
1389
+ podOpts := test.PodOptions {
1390
+ ObjectMeta : metav1.ObjectMeta {
1391
+ Labels : labels ,
1392
+ OwnerReferences : []metav1.OwnerReference {
1393
+ {
1394
+ APIVersion : "apps/v1" ,
1395
+ Kind : "ReplicaSet" ,
1396
+ Name : rs .Name ,
1397
+ UID : rs .UID ,
1398
+ Controller : aws .Bool (true ),
1399
+ BlockOwnerDeletion : aws .Bool (true ),
1400
+ },
1401
+ },
1402
+ },
1403
+ ResourceRequirements : v1.ResourceRequirements {
1404
+ Requests : v1.ResourceList {
1405
+ v1 .ResourceCPU : resource .MustParse ("1" ),
1406
+ },
1407
+ },
1408
+ }
1409
+
1410
+ var pods []* v1.Pod
1411
+ for i := 0 ; i < 5 ; i ++ {
1412
+ pod := test .UnschedulablePod (podOpts )
1413
+ pods = append (pods , pod )
1414
+ }
1415
+ ExpectApplied (ctx , env .Client , rs , prov )
1416
+ ExpectProvisioned (ctx , env .Client , provisioningController , pods ... )
1417
+
1418
+ nodeList := & v1.NodeList {}
1419
+ Expect (env .Client .List (ctx , nodeList )).To (Succeed ())
1420
+ Expect (len (nodeList .Items )).To (Equal (1 ))
1421
+
1422
+ // Update cluster state with new node
1423
+ ExpectReconcileSucceeded (ctx , nodeStateController , client .ObjectKeyFromObject (& nodeList .Items [0 ]))
1424
+
1425
+ // Reset the bindings so we can re-record bindings
1426
+ recorder .ResetBindings ()
1427
+
1428
+ // Mark the node for deletion and re-trigger reconciliation
1429
+ oldNodeName := nodeList .Items [0 ].Name
1430
+ cluster .MarkForDeletion (nodeList .Items [0 ].Name )
1431
+ ExpectProvisionedNoBinding (ctx , env .Client , provisioningController )
1432
+
1433
+ // Make sure that the cluster state is aware of the current node state
1434
+ Expect (env .Client .List (ctx , nodeList )).To (Succeed ())
1435
+ Expect (len (nodeList .Items )).To (Equal (2 ))
1436
+ newNode , _ := lo .Find (nodeList .Items , func (n v1.Node ) bool { return n .Name != oldNodeName })
1437
+
1438
+ for i := range nodeList .Items {
1439
+ node := nodeList .Items [i ]
1440
+ ExpectMakeNodesReady (ctx , env .Client , & node )
1441
+ ExpectReconcileSucceeded (ctx , nodeStateController , client .ObjectKeyFromObject (& node ))
1442
+ }
1443
+
1444
+ // Wait for the nomination cache to expire
1445
+ time .Sleep (time .Second * 11 )
1446
+
1447
+ // Re-create the pods to re-bind them
1448
+ for i := 0 ; i < 2 ; i ++ {
1449
+ ExpectDeleted (ctx , env .Client , pods [i ])
1450
+ pod := test .UnschedulablePod (podOpts )
1451
+ ExpectApplied (ctx , env .Client , pod )
1452
+ ExpectManualBinding (ctx , env .Client , pod , & newNode )
1453
+ }
1454
+
1455
+ // Trigger a reconciliation run which should take into account the deleting node
1456
+ // Consolidation shouldn't trigger additional actions
1457
+ fakeClock .Step (10 * time .Minute )
1458
+ result , err := controller .ProcessCluster (env .Ctx )
1459
+ Expect (err ).ToNot (HaveOccurred ())
1460
+ Expect (result ).To (Equal (consolidation .ProcessResultNothingToDo ))
1461
+ })
1462
+ })
1463
+
1310
1464
func leastExpensiveInstanceWithZone (zone string ) cloudprovider.InstanceType {
1311
1465
for _ , elem := range onDemandInstances {
1312
1466
if hasZone (elem .Offerings (), zone ) {
0 commit comments