@@ -4027,6 +4027,99 @@ var _ = Describe("Consolidation", func() {
40274027 Entry ("if the candidate is on-demand node" , false ),
40284028 Entry ("if the candidate is spot node" , true ),
40294029 )
4030+ DescribeTable ("can merge 3 nodes into 1 without available Spot offerings" , func (requirements []v1.NodeSelectorRequirementWithMinValues , includeUnavailableSpot bool ) {
4031+ if requirements != nil {
4032+ nodePool .Spec .Template .Spec .Requirements = requirements
4033+ }
4034+ offerings := func (price float64 ) []cloudprovider.Offering {
4035+ result := []cloudprovider.Offering {{
4036+ Available : true ,
4037+ Requirements : scheduling .NewLabelRequirements (map [string ]string {v1 .CapacityTypeLabelKey : v1 .CapacityTypeOnDemand , corev1 .LabelTopologyZone : "test-zone-1" }),
4038+ Price : price ,
4039+ }}
4040+ if includeUnavailableSpot {
4041+ result = append (result , cloudprovider.Offering {
4042+ Available : false ,
4043+ Requirements : scheduling .NewLabelRequirements (map [string ]string {v1 .CapacityTypeLabelKey : v1 .CapacityTypeSpot , corev1 .LabelTopologyZone : "test-zone-1" }),
4044+ Price : price / 2 ,
4045+ })
4046+ }
4047+ return result
4048+ }
4049+ currentInstance := fake .NewInstanceType ("current-on-demand-only" ,
4050+ fake .WithOfferings (offerings (1.5 )... ),
4051+ )
4052+ replacementInstance := fake .NewInstanceType ("replacement-on-demand-only" ,
4053+ fake .WithOfferings (offerings (1.0 )... ),
4054+ )
4055+ cloudProvider .InstanceTypes = []* cloudprovider.InstanceType {currentInstance , replacementInstance }
4056+ ExpectSingletonReconciled (ctx , pricingController )
4057+
4058+ nodeClaims , nodes = test .NodeClaimsAndNodes (3 , v1.NodeClaim {
4059+ ObjectMeta : metav1.ObjectMeta {
4060+ Labels : map [string ]string {
4061+ v1 .NodePoolLabelKey : nodePool .Name ,
4062+ corev1 .LabelInstanceTypeStable : currentInstance .Name ,
4063+ v1 .CapacityTypeLabelKey : v1 .CapacityTypeOnDemand ,
4064+ corev1 .LabelTopologyZone : "test-zone-1" ,
4065+ },
4066+ },
4067+ Status : v1.NodeClaimStatus {
4068+ Allocatable : map [corev1.ResourceName ]resource.Quantity {
4069+ corev1 .ResourceCPU : resource .MustParse ("32" ),
4070+ corev1 .ResourcePods : resource .MustParse ("100" ),
4071+ },
4072+ },
4073+ })
4074+ for i := range nodeClaims {
4075+ nodeClaims [i ].StatusConditions ().SetTrue (v1 .ConditionTypeConsolidatable )
4076+ }
4077+
4078+ rs := test .ReplicaSet ()
4079+ ExpectApplied (ctx , env .Client , rs )
4080+ pods := test .Pods (3 , test.PodOptions {
4081+ ObjectMeta : metav1.ObjectMeta {Labels : labels ,
4082+ OwnerReferences : []metav1.OwnerReference {
4083+ {
4084+ APIVersion : "apps/v1" ,
4085+ Kind : "ReplicaSet" ,
4086+ Name : rs .Name ,
4087+ UID : rs .UID ,
4088+ Controller : new (true ),
4089+ BlockOwnerDeletion : new (true ),
4090+ },
4091+ }}})
4092+
4093+ ExpectApplied (ctx , env .Client , rs , pods [0 ], pods [1 ], pods [2 ], nodeClaims [0 ], nodes [0 ], nodeClaims [1 ], nodes [1 ], nodeClaims [2 ], nodes [2 ], nodePool )
4094+ ExpectMakeNodesInitialized (ctx , env .Client , env .Clock , nodes [0 ], nodes [1 ], nodes [2 ])
4095+
4096+ ExpectManualBinding (ctx , env .Client , pods [0 ], nodes [0 ])
4097+ ExpectManualBinding (ctx , env .Client , pods [1 ], nodes [1 ])
4098+ ExpectManualBinding (ctx , env .Client , pods [2 ], nodes [2 ])
4099+
4100+ ExpectMakeNodesAndNodeClaimsInitializedAndStateUpdated (ctx , env .Client , env .Clock , nodeStateController , nodeClaimStateController , []* corev1.Node {nodes [0 ], nodes [1 ], nodes [2 ]}, []* v1.NodeClaim {nodeClaims [0 ], nodeClaims [1 ], nodeClaims [2 ]})
4101+ ExpectSingletonReconciled (ctx , disruptionController )
4102+
4103+ cmds := queue .GetCommands ()
4104+ Expect (cmds ).To (HaveLen (1 ))
4105+ Expect (cmds [0 ].Replacements [0 ].Requirements .Get (v1 .CapacityTypeLabelKey ).Has (v1 .CapacityTypeOnDemand )).To (BeTrue ())
4106+ Expect (cmds [0 ].Replacements [0 ].Requirements .Get (v1 .CapacityTypeLabelKey ).Has (v1 .CapacityTypeSpot )).To (BeFalse ())
4107+
4108+ ExpectMakeNewNodeClaimsReady (ctx , env .Client , env .Clock , cluster , cloudProvider , cmds [0 ])
4109+ ExpectObjectReconciled (ctx , env .Client , queue , cmds [0 ].Candidates [0 ].NodeClaim )
4110+ ExpectNodeClaimsCascadeDeletion (ctx , env .Client , nodeClaims [0 ], nodeClaims [1 ], nodeClaims [2 ])
4111+
4112+ Expect (ExpectNodeClaims (ctx , env .Client )).To (HaveLen (1 ))
4113+ Expect (ExpectNodes (ctx , env .Client )).To (HaveLen (1 ))
4114+ ExpectNotFound (ctx , env .Client , nodeClaims [0 ], nodes [0 ], nodeClaims [1 ], nodes [1 ], nodeClaims [2 ], nodes [2 ])
4115+ },
4116+ Entry ("when the NodePool does not constrain capacity type" , nil , false ),
4117+ Entry ("when the NodePool allows Spot and on-demand" , []v1.NodeSelectorRequirementWithMinValues {{
4118+ Key : v1 .CapacityTypeLabelKey ,
4119+ Operator : corev1 .NodeSelectorOpIn ,
4120+ Values : []string {v1 .CapacityTypeSpot , v1 .CapacityTypeOnDemand },
4121+ }}, true ),
4122+ )
40304123 It ("can merge 3 nodes into 1 if the candidates have both spot and on-demand" , func () {
40314124 // By default all the 3 nodeClaims are OD.
40324125 nodeClaims = lo .Ternary (false , spotNodeClaims , nodeClaims )
0 commit comments