Skip to content

Commit e903e46

Browse files
committed
fix: let the CoderWorkspacePool scale-down skip members that the owner cap already deleted
Change-Id: I92f9b54bbc1453c1ace314193787b700f426cf26 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent e5d7862 commit e903e46

2 files changed

Lines changed: 30 additions & 8 deletions

File tree

‎internal/controller/coderworkspacepool_controller.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,7 +138,8 @@ func (r *CoderWorkspacePoolReconciler) Reconcile(ctx context.Context, req ctrl.R
138138
return ctrl.Result{}, err
139139
}
140140
}
141-
if err := r.deleteUnclaimedMembers(ctx, pool, members, step); err != nil {
141+
// The namespace list, where trimToOwnerCap marked its deletes.
142+
if err := r.deleteUnclaimedMembers(ctx, pool, all.Items, step); err != nil {
142143
return ctrl.Result{}, err
143144
}
144145
r.applyPoolStatus(pool, counts, step)

‎internal/controller/coderworkspacepool_delete_test.go‎

Lines changed: 28 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,6 @@ var usedStatus = memberPhase(coderv1alpha1.CoderWorkspacePoolMemberPhaseReady,
3232
memberCondition(coderv1alpha1.CoderWorkspacePoolMemberConditionReady, metav1.ConditionTrue, "WorkspaceReady"),
3333
memberCondition(coderv1alpha1.CoderWorkspacePoolMemberConditionUsed, metav1.ConditionTrue, "Used"))
3434

35-
// gone reports whether the member no longer exists.
36-
func (e *poolMemberEnv) gone(t *testing.T, key types.NamespacedName) bool {
37-
t.Helper()
38-
return e.reconcileMember(t, key, 0) == nil
39-
}
40-
4135
// TestPoolDeleteOrder: on a scale-down the pool deletes current members
4236
// without a create request first, then members that are not ready (newest
4337
// first), then ready members (oldest first).
@@ -46,6 +40,7 @@ func TestPoolDeleteOrder(t *testing.T) {
4640
e := newPoolMemberEnv(t)
4741
e.editPool(t, func(s *coderv1alpha1.CoderWorkspacePoolSpec) { s.Replicas = new(int32(5)) })
4842
now := e.clock.Now()
43+
pending := e.createMember(t, nil) // The oldest, but it goes first.
4944
readyNew := e.createMember(t, nil)
5045
e.setMemberStatus(t, readyNew, withWorkspace(e.readyAt(now.Add(-time.Minute))))
5146
readyOld := e.createMember(t, nil)
@@ -55,7 +50,6 @@ func TestPoolDeleteOrder(t *testing.T) {
5550
time.Sleep(1100 * time.Millisecond) // creationTimestamp has second precision.
5651
buildingNew := e.createMember(t, nil)
5752
e.setMemberStatus(t, buildingNew, withWorkspace(nil))
58-
pending := e.createMember(t, nil)
5953
e.reconcilePool(t, 1)
6054
require.Len(t, e.poolMembers(t, e.pool.Name), 5)
6155

@@ -190,3 +184,30 @@ func TestPoolConsentLossDeletesUnclaimed(t *testing.T) {
190184
})
191185
}
192186
}
187+
188+
// TestPoolScaleDownAndLoweredCap: replicas and maxWorkspaces go down
189+
// together while members wait with finalizers. The pool deletes each member
190+
// once: the scale-down skips the members that the cap already deleted.
191+
func TestPoolScaleDownAndLoweredCap(t *testing.T) {
192+
t.Parallel()
193+
e := newPoolMemberEnv(t)
194+
e.editPool(t, func(s *coderv1alpha1.CoderWorkspacePoolSpec) {
195+
s.Replicas, s.MaxInFlight = new(int32(3)), new(int32(3))
196+
})
197+
e.reconcilePool(t, 2)
198+
members := e.poolMembers(t, e.pool.Name)
199+
require.Len(t, members, 3)
200+
for _, m := range members {
201+
e.reconcileMember(t, memberKey(m), 1) // The finalizer only.
202+
}
203+
e.setOwnerCap(t, 2)
204+
e.editPool(t, func(s *coderv1alpha1.CoderWorkspacePoolSpec) { s.Replicas = new(int32(1)) })
205+
e.reconcilePool(t, 1) // Fails on a 409 if one member is deleted twice.
206+
deleting := 0
207+
for _, m := range e.poolMembers(t, e.pool.Name) {
208+
if !m.DeletionTimestamp.IsZero() {
209+
deleting++
210+
}
211+
}
212+
require.Equal(t, 2, deleting, "the cap deletes one, the scale-down one more")
213+
}

0 commit comments

Comments
 (0)