Skip to content

Commit 7595277

Browse files
committed
fix: skip adding spot requirement during consolidation when no spot offerings are available
1 parent 0bc0053 commit 7595277

2 files changed

Lines changed: 101 additions & 1 deletion

File tree

pkg/controllers/disruption/consolidation.go

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -238,7 +238,7 @@ func (c *consolidation) computeConsolidation(ctx context.Context, candidates ...
238238
// should fail and we'll just leave the node alone. We don't need to do the same for reserved since the requirements
239239
// are injected on by the scheduler.
240240
ctReq := results.NewNodeClaims[0].Requirements.Get(v1.CapacityTypeLabelKey)
241-
if ctReq.Has(v1.CapacityTypeSpot) && ctReq.Has(v1.CapacityTypeOnDemand) {
241+
if ctReq.Has(v1.CapacityTypeSpot) && ctReq.Has(v1.CapacityTypeOnDemand) && hasSpotOffering(results.NewNodeClaims[0]) {
242242
results.NewNodeClaims[0].Requirements.Add(scheduling.NewRequirement(v1.CapacityTypeLabelKey, corev1.NodeSelectorOpIn, v1.CapacityTypeSpot))
243243
}
244244

@@ -253,6 +253,13 @@ func (c *consolidation) computeConsolidation(ctx context.Context, candidates ...
253253
return cmd, nil
254254
}
255255

256+
// hasSpotOffering returns true if the replacement has an available Spot offering.
257+
func hasSpotOffering(nodeClaim *pscheduling.NodeClaim) bool {
258+
return lo.ContainsBy(nodeClaim.InstanceTypeOptions, func(it *cloudprovider.InstanceType) bool {
259+
return it.Offerings.Available().Compatible(nodeClaim.Requirements).HasCompatible(cloudprovider.SpotRequirement)
260+
})
261+
}
262+
256263
// Compute command to execute spot-to-spot consolidation if:
257264
// 1. The SpotToSpotConsolidation feature flag is set to true.
258265
// 2. For single-node consolidation:

pkg/controllers/disruption/consolidation_test.go

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)