Skip to content

Commit e2d90d5

Browse files
authored
fix: prevent static capacity controllers from modifying NodeClaims during NodePool deletion (kubernetes-sigs#2840)
1 parent 8026fab commit e2d90d5

File tree

5 files changed

+71
-22
lines changed

5 files changed

+71
-22
lines changed

pkg/controllers/static/deprovisioning/controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ func (c *Controller) Name() string {
8181
func (c *Controller) Reconcile(ctx context.Context, np *v1.NodePool) (reconcile.Result, error) {
8282
ctx = injection.WithControllerName(ctx, c.Name())
8383

84-
if !nodepoolutils.IsManaged(np, c.cloudProvider) || np.Spec.Replicas == nil {
84+
if !nodepoolutils.IsManaged(np, c.cloudProvider) || np.Spec.Replicas == nil || !np.DeletionTimestamp.IsZero() {
8585
return reconcile.Result{}, nil
8686
}
8787

pkg/controllers/static/deprovisioning/suite_test.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -154,6 +154,43 @@ var _ = Describe("Static Deprovisioning Controller", func() {
154154

155155
Expect(result.RequeueAfter).To(BeZero())
156156
})
157+
It("should return early if nodepool is being deleted", func() {
158+
nodePool := test.StaticNodePool()
159+
nodePool.Spec.Replicas = lo.ToPtr(int64(1))
160+
161+
// Create 2 nodeclaims (more than desired replicas of 1)
162+
nodeClaims, nodes := test.NodeClaimsAndNodes(2, v1.NodeClaim{
163+
ObjectMeta: metav1.ObjectMeta{
164+
Labels: map[string]string{
165+
v1.NodePoolLabelKey: nodePool.Name,
166+
v1.NodeInitializedLabelKey: "true",
167+
corev1.LabelInstanceTypeStable: "stable.instance",
168+
},
169+
},
170+
Status: v1.NodeClaimStatus{
171+
Capacity: corev1.ResourceList{
172+
corev1.ResourceCPU: resource.MustParse("10"),
173+
corev1.ResourceMemory: resource.MustParse("1000Mi"),
174+
},
175+
},
176+
})
177+
178+
ExpectApplied(ctx, env.Client, nodePool, nodeClaims[0], nodeClaims[1], nodes[0], nodes[1])
179+
ExpectDeletionTimestampSet(ctx, env.Client, nodePool)
180+
181+
// Update cluster state to track the nodes
182+
ExpectMakeNodesAndNodeClaimsInitializedAndStateUpdated(ctx, env.Client, nodeController, nodeClaimStateController, []*corev1.Node{nodes[0], nodes[1]}, []*v1.NodeClaim{nodeClaims[0], nodeClaims[1]})
183+
Expect(cluster.Nodes()).To(HaveLen(2))
184+
ExpectStateNodePoolCount(cluster, nodePool.Name, 2, 0, 0)
185+
186+
result := ExpectObjectReconciled(ctx, env.Client, controller, nodePool)
187+
Expect(result.RequeueAfter).To(BeZero())
188+
189+
// Should not delete any NodeClaims since nodepool is being deleted
190+
existingNodeClaims := &v1.NodeClaimList{}
191+
Expect(env.Client.List(ctx, existingNodeClaims)).To(Succeed())
192+
Expect(existingNodeClaims.Items).To(HaveLen(2))
193+
})
157194
It("should return early if current node count is less than or equal to desired replicas", func() {
158195
nodePool := test.StaticNodePool()
159196
nodePool.Spec.Replicas = lo.ToPtr(int64(3))

pkg/controllers/static/provisioning/controller.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ func (c *Controller) Name() string {
7373
func (c *Controller) Reconcile(ctx context.Context, np *v1.NodePool) (reconcile.Result, error) {
7474
ctx = injection.WithControllerName(ctx, c.Name())
7575

76-
if !nodepoolutils.IsManaged(np, c.cloudProvider) || !np.StatusConditions().Root().IsTrue() || np.Spec.Replicas == nil {
76+
if !nodepoolutils.IsManaged(np, c.cloudProvider) || !np.StatusConditions().Root().IsTrue() || np.Spec.Replicas == nil || !np.DeletionTimestamp.IsZero() {
7777
return reconcile.Result{}, nil
7878
}
7979

pkg/controllers/static/provisioning/suite_test.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,20 @@ var _ = Describe("Static Provisioning Controller", func() {
167167
Expect(env.Client.List(ctx, nodeClaims)).To(Succeed())
168168
Expect(nodeClaims.Items).To(HaveLen(0))
169169
})
170+
It("should return early if nodepool is being deleted", func() {
171+
nodePool := test.StaticNodePool()
172+
nodePool.Spec.Replicas = lo.ToPtr(int64(1))
173+
ExpectApplied(ctx, env.Client, nodePool)
174+
ExpectDeletionTimestampSet(ctx, env.Client, nodePool)
175+
176+
result := ExpectObjectReconciled(ctx, env.Client, controller, nodePool)
177+
Expect(result.RequeueAfter).To(BeZero())
178+
179+
// Should not create any NodeClaims
180+
nodeClaims := &v1.NodeClaimList{}
181+
Expect(env.Client.List(ctx, nodeClaims)).To(Succeed())
182+
Expect(nodeClaims.Items).To(HaveLen(0))
183+
})
170184
It("should return early if nodepool replicas is nil", func() {
171185
nodePool := test.StaticNodePool()
172186
nodePool.Spec.Replicas = nil

test/suites/regression/staticcapacity_test.go

Lines changed: 18 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -332,8 +332,6 @@ var _ = Describe("StaticCapacity", func() {
332332
var dynamicNodePool *v1.NodePool
333333
var label map[string]string
334334
BeforeEach(func() {
335-
// Create a static NodePool
336-
nodePool.Spec.Replicas = lo.ToPtr(int64(2))
337335
if env.IsDefaultNodeClassKWOK() {
338336
nodePool.Spec.Template.Spec.Requirements = append(nodePool.Spec.Template.Spec.Requirements, v1.NodeSelectorRequirementWithMinValues{
339337
Key: corev1.LabelInstanceTypeStable,
@@ -344,26 +342,21 @@ var _ = Describe("StaticCapacity", func() {
344342
},
345343
})
346344
}
345+
346+
// Copy nodePool before applying static-specific modifications
347+
dynamicNodePool = test.NodePool(
348+
lo.FromPtr(nodePool),
349+
v1.NodePool{ObjectMeta: metav1.ObjectMeta{Name: "dynamic-nodepool"}},
350+
)
351+
352+
// Apply static-specific modifications
353+
nodePool.Spec.Replicas = lo.ToPtr(int64(2))
347354
nodePool.Spec.Template.Spec.Taints = []corev1.Taint{
348355
{
349356
Key: "static",
350357
Effect: corev1.TaintEffectNoExecute,
351358
},
352359
}
353-
// Create a dynamic NodePool
354-
dynamicNodePool = test.NodePool(v1.NodePool{
355-
ObjectMeta: metav1.ObjectMeta{
356-
Name: "dynamic-nodepool",
357-
},
358-
Spec: v1.NodePoolSpec{
359-
Template: v1.NodeClaimTemplate{
360-
Spec: v1.NodeClaimTemplateSpec{
361-
Requirements: nodePool.Spec.Template.Spec.Requirements,
362-
NodeClassRef: nodePool.Spec.Template.Spec.NodeClassRef,
363-
},
364-
},
365-
},
366-
})
367360
label = map[string]string{"app": "large-app"}
368361
})
369362

@@ -475,10 +468,12 @@ var _ = Describe("StaticCapacity", func() {
475468
env.EventuallyExpectNodeClaimsReady(finalNodeClaims...)
476469

477470
// All final nodes should have the new annotation (either newly created or replaced due to drift)
478-
nodes := env.EventuallyExpectInitializedNodeCount("==", 4)
479-
for _, node := range nodes {
480-
Expect(node.Annotations).To(HaveKeyWithValue("drift-trigger", "test-value"))
481-
}
471+
Eventually(func(g Gomega) {
472+
nodes := env.EventuallyExpectInitializedNodeCount("==", 4)
473+
for _, node := range nodes {
474+
g.Expect(node.Annotations).To(HaveKeyWithValue("drift-trigger", "test-value"))
475+
}
476+
}).WithTimeout(10 * time.Minute).Should(Succeed())
482477
})
483478

484479
It("should handle NodePool deletion gracefully", func() {
@@ -492,6 +487,9 @@ var _ = Describe("StaticCapacity", func() {
492487
// Delete the NodePool
493488
env.ExpectDeleted(nodePool)
494489

490+
// During deletion, no new NodeClaims should be created - count should only decrease
491+
env.ConsistentlyExpectNodeClaimCountNotExceed(30*time.Second, 3)
492+
495493
// All nodes should eventually be cleaned up
496494
env.EventuallyExpectInitializedNodeCount("==", 0)
497495
env.EventuallyExpectCreatedNodeClaimCount("==", 0)

0 commit comments

Comments
 (0)