Skip to content

Commit 007545e

Browse files
committed
fix: read the owner cap again right before the member's create marker, and check pool consent uncached whatever the demand
Change-Id: I24972f715031adeae241dbf3b92f6e975046cc59 Signed-off-by: Thomas Kosiewski <tk@coder.com>
1 parent ee015d1 commit 007545e

3 files changed

Lines changed: 107 additions & 43 deletions

File tree

‎internal/controller/coderworkspacepool_cap_test.go‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
package controller_test
22

33
import (
4+
"context"
45
"testing"
56

67
"github.com/stretchr/testify/require"
78
"k8s.io/apimachinery/pkg/types"
9+
ctrl "sigs.k8s.io/controller-runtime"
10+
"sigs.k8s.io/controller-runtime/pkg/client"
811

912
coderv1alpha1 "github.com/coder/coder-k8s/api/v1alpha1"
13+
"github.com/coder/coder-k8s/internal/controller"
1014
)
1115

1216
// setOwnerCap sets maxWorkspaces of the control plane's only owner entry.
@@ -89,3 +93,43 @@ func TestPoolLoweredOwnerCapDeletesOnce(t *testing.T) {
8993
require.Equal(t, 2, deleting, "one member stays at maxWorkspaces 1")
9094
require.Len(t, e.poolMembers(t, e.pool.Name), 3, "no new member")
9195
}
96+
97+
// firstReadOldCap answers the first control plane read with maxWorkspaces
98+
// 10, as before an admin lowered it, and later reads as stored.
99+
type firstReadOldCap struct {
100+
client.Reader
101+
reads *int
102+
}
103+
104+
func (c firstReadOldCap) Get(ctx context.Context, key client.ObjectKey, obj client.Object, opts ...client.GetOption) error {
105+
if err := c.Reader.Get(ctx, key, obj, opts...); err != nil {
106+
return err
107+
}
108+
if cp, ok := obj.(*coderv1alpha1.CoderControlPlane); ok && cp.Spec.WorkspacePools != nil {
109+
if *c.reads++; *c.reads == 1 {
110+
for i := range cp.Spec.WorkspacePools.Owners {
111+
cp.Spec.WorkspacePools.Owners[i].MaxWorkspaces = new(int32(10))
112+
}
113+
}
114+
}
115+
return nil
116+
}
117+
118+
// TestPoolMemberRechecksCapLast: the member reads the cap again after its
119+
// Coder reads, right before the marker, so a cap that an admin lowered
120+
// during those reads holds (Codex 4274565367).
121+
func TestPoolMemberRechecksCapLast(t *testing.T) {
122+
t.Parallel()
123+
e := newPoolMemberEnv(t)
124+
e.setOwnerCap(t, 1)
125+
e.reconcileMember(t, e.createMember(t, nil), 2)
126+
require.Equal(t, 1, e.fake.workspaceCount())
127+
second := e.createMember(t, nil)
128+
r := &controller.CoderWorkspacePoolMemberReconciler{Client: k8sClient, APIReader: firstReadOldCap{k8sClient, new(0)}, Scheme: scheme, Clock: e.clock}
129+
for range 2 { // The finalizer, then the checks.
130+
_, err := r.Reconcile(e.ctx, ctrl.Request{NamespacedName: second})
131+
require.NoError(t, err)
132+
}
133+
require.Equal(t, 1, e.fake.workspaceCount(), "no workspace above maxWorkspaces 1")
134+
require.Equal(t, "OwnerWorkspaceLimit", e.reconcileMember(t, second, 0).Status.Reason)
135+
}

‎internal/controller/coderworkspacepool_controller.go‎

Lines changed: 41 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -126,10 +126,18 @@ func (r *CoderWorkspacePoolReconciler) Reconcile(ctx context.Context, req ctrl.R
126126
// its own object shows the new resolved inputs stored.
127127
requeue = time.Second
128128
default:
129-
if err := r.trimToOwnerCap(ctx, pool, all.Items); err != nil {
129+
var maxWorkspaces int32
130+
if maxWorkspaces, step, err = r.freshOwnerCap(ctx, pool); err != nil {
130131
return ctrl.Result{}, err
131132
}
132-
step, requeue, err = r.createMembers(ctx, pool, all.Items, &counts)
133+
if step != nil {
134+
requeue = poolResolvePoll
135+
break
136+
}
137+
if err := r.trimToOwnerCap(ctx, pool, maxWorkspaces, all.Items); err != nil {
138+
return ctrl.Result{}, err
139+
}
140+
step, requeue, err = r.createMembers(ctx, pool, maxWorkspaces, all.Items, &counts)
133141
if err != nil {
134142
return ctrl.Result{}, err
135143
}
@@ -243,13 +251,37 @@ func poolMemberFailedAt(m *coderv1alpha1.CoderWorkspacePoolMember) time.Time {
243251
return m.CreationTimestamp.Time
244252
}
245253

254+
// freshOwnerCap reads the pool's control plane without the cache, whatever
255+
// the pool's demand: a deletion or a consent that an admin just withdrew
256+
// holds even while the cache has the old object. It returns the owner's
257+
// maxWorkspaces from that read, or the wait.
258+
func (r *CoderWorkspacePoolReconciler) freshOwnerCap(ctx context.Context, pool *coderv1alpha1.CoderWorkspacePool) (int32, *coderStep, error) {
259+
controlPlane := &coderv1alpha1.CoderControlPlane{}
260+
if err := r.APIReader.Get(ctx, types.NamespacedName{Namespace: pool.Namespace, Name: pool.Spec.ControlPlaneRef.Name}, controlPlane); err != nil {
261+
return 0, nil, fmt.Errorf("get CoderControlPlane %s/%s: %w", pool.Namespace, pool.Spec.ControlPlaneRef.Name, err)
262+
}
263+
if !controlPlane.DeletionTimestamp.IsZero() {
264+
return 0, coderWait("ControlPlaneDeleting", "CoderControlPlane %s is being deleted.", controlPlane.Name), nil
265+
}
266+
if step := workspacePoolConsent(controlPlane, pool.Status.Resolved.OwnerID, pool.Spec.Template); step != nil {
267+
return 0, step, nil
268+
}
269+
owner := workspacePoolOwner(controlPlane, pool.Status.Resolved.OwnerID)
270+
if owner.MaxWorkspaces == nil {
271+
return 0, nil, fmt.Errorf("assertion failed: CoderControlPlane %s has no defaulted maxWorkspaces for Coder user %s", controlPlane.Name, pool.Status.Resolved.OwnerID)
272+
}
273+
return *owner.MaxWorkspaces, nil, nil
274+
}
275+
246276
// createMembers creates current members while all caps hold (plan 6.3 step
247277
// 7): current unclaimed members fewer than spec.replicas, building members
248278
// fewer than spec.maxInFlight, the owner's live members on this control
249-
// plane fewer than maxWorkspaces, and the failure backoff passed. It returns
250-
// the wait that stops it, if any, and when to read again.
279+
// plane fewer than maxWorkspaces (from an uncached read), and the failure
280+
// backoff passed. It returns the wait that stops it, if any, and when to
281+
// read again.
251282
func (r *CoderWorkspacePoolReconciler) createMembers(
252-
ctx context.Context, pool *coderv1alpha1.CoderWorkspacePool, all []coderv1alpha1.CoderWorkspacePoolMember, counts *poolMemberCounts,
283+
ctx context.Context, pool *coderv1alpha1.CoderWorkspacePool, maxWorkspaces int32,
284+
all []coderv1alpha1.CoderWorkspacePoolMember, counts *poolMemberCounts,
253285
) (*coderStep, time.Duration, error) {
254286
st, now := &pool.Status, r.Clock.Now()
255287
need := *pool.Spec.Replicas - counts.current // Claims add their demand in PR 11.
@@ -265,25 +297,8 @@ func (r *CoderWorkspacePoolReconciler) createMembers(
265297
last.Member, last.Reason, retryAt.UTC().Format(time.RFC3339)), retryAt.Sub(now), nil
266298
}
267299
}
268-
// The cap and the count both come from uncached reads, so that neither
269-
// a cap that an admin just lowered nor two pools on a stale cache pass it.
270-
controlPlane := &coderv1alpha1.CoderControlPlane{}
271-
if err := r.APIReader.Get(ctx, types.NamespacedName{Namespace: pool.Namespace, Name: pool.Spec.ControlPlaneRef.Name}, controlPlane); err != nil {
272-
return nil, 0, fmt.Errorf("get CoderControlPlane %s/%s: %w", pool.Namespace, pool.Spec.ControlPlaneRef.Name, err)
273-
}
274-
if !controlPlane.DeletionTimestamp.IsZero() {
275-
return coderWait("ControlPlaneDeleting", "CoderControlPlane %s is being deleted.", controlPlane.Name), poolResolvePoll, nil
276-
}
277-
// The consent check again, on the uncached control plane: a consent that
278-
// an admin just withdrew holds even while the cache has the old one.
279-
if step := workspacePoolConsent(controlPlane, st.Resolved.OwnerID, pool.Spec.Template); step != nil {
280-
return step, poolResolvePoll, nil
281-
}
282-
owner := workspacePoolOwner(controlPlane, st.Resolved.OwnerID)
283-
if owner.MaxWorkspaces == nil {
284-
return nil, 0, fmt.Errorf("assertion failed: CoderControlPlane %s has no defaulted maxWorkspaces for Coder user %s", controlPlane.Name, st.Resolved.OwnerID)
285-
}
286-
maxWorkspaces := *owner.MaxWorkspaces
300+
// The count comes from the uncached list, so two pools on a stale cache
301+
// never pass the cap together.
287302
live := poolOwnerLiveMembers(all, st.Resolved.OwnerID, pool.Spec.ControlPlaneRef.Name, nil)
288303
n := min(need, *pool.Spec.MaxInFlight-counts.building, maxWorkspaces-live)
289304
for range n {
@@ -329,17 +344,9 @@ func poolOwnerLiveMembers(
329344
// member is marked deleting in all, so the later steps of this reconcile
330345
// skip it.
331346
func (r *CoderWorkspacePoolReconciler) trimToOwnerCap(
332-
ctx context.Context, pool *coderv1alpha1.CoderWorkspacePool, all []coderv1alpha1.CoderWorkspacePoolMember,
347+
ctx context.Context, pool *coderv1alpha1.CoderWorkspacePool, maxWorkspaces int32, all []coderv1alpha1.CoderWorkspacePoolMember,
333348
) error {
334-
controlPlane := &coderv1alpha1.CoderControlPlane{}
335-
if err := r.APIReader.Get(ctx, types.NamespacedName{Namespace: pool.Namespace, Name: pool.Spec.ControlPlaneRef.Name}, controlPlane); err != nil {
336-
return fmt.Errorf("get CoderControlPlane %s/%s: %w", pool.Namespace, pool.Spec.ControlPlaneRef.Name, err)
337-
}
338-
owner := workspacePoolOwner(controlPlane, pool.Status.Resolved.OwnerID)
339-
if !controlPlane.DeletionTimestamp.IsZero() || owner == nil || owner.MaxWorkspaces == nil {
340-
return nil // createMembers reports these.
341-
}
342-
excess := poolOwnerLiveMembers(all, pool.Status.Resolved.OwnerID, pool.Spec.ControlPlaneRef.Name, nil) - *owner.MaxWorkspaces
349+
excess := poolOwnerLiveMembers(all, pool.Status.Resolved.OwnerID, pool.Spec.ControlPlaneRef.Name, nil) - maxWorkspaces
343350
if excess <= 0 {
344351
return nil
345352
}

‎internal/controller/coderworkspacepoolmember_controller.go‎

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -220,9 +220,6 @@ func (r *CoderWorkspacePoolMemberReconciler) checkCreate(
220220
if step := workspacePoolConsent(fresh, spec.OwnerID, pool.Spec.Template); step != nil {
221221
return nil, step, nil
222222
}
223-
if step, err := r.checkOwnerCap(ctx, member, fresh); step != nil || err != nil {
224-
return nil, step, err
225-
}
226223
ownerUser, step, err := checkCoderWorkspaceOwner(ctx, sdk, uuid.MustParse(spec.OwnerID))
227224
if step != nil || err != nil {
228225
return nil, step, err
@@ -275,19 +272,35 @@ func (r *CoderWorkspacePoolMemberReconciler) checkCreate(
275272
case !isCoderNotFound(err):
276273
return nil, coderStepUnavailable("get workspace by name", err), nil
277274
}
275+
// Last, after the Coder reads: the owner cap and the consent again.
276+
if step, err := r.checkOwnerCap(ctx, member, pool.Spec.Template); step != nil || err != nil {
277+
return nil, step, err
278+
}
278279
return sdk, nil, nil
279280
}
280281

281-
// checkOwnerCap counts, without the cache, the owner's other members on the
282-
// control plane that may have a workspace. At maxWorkspaces the member
282+
// checkOwnerCap reads the control plane again without the cache, right
283+
// before the create marker, and counts, also without the cache, the owner's
284+
// other members on it that may have a workspace. At maxWorkspaces the member
283285
// waits, so a member that the pool created under a higher cap creates
284-
// nothing above a cap that an admin lowered since (Codex 4271920249).
286+
// nothing above a cap that an admin lowered since (Codex 4271920249 and
287+
// 4274565367). The consent check runs again on the same read.
285288
func (r *CoderWorkspacePoolMemberReconciler) checkOwnerCap(
286-
ctx context.Context, member *coderv1alpha1.CoderWorkspacePoolMember, controlPlane *coderv1alpha1.CoderControlPlane,
289+
ctx context.Context, member *coderv1alpha1.CoderWorkspacePoolMember, template string,
287290
) (*coderStep, error) {
291+
controlPlane := &coderv1alpha1.CoderControlPlane{}
292+
if err := r.APIReader.Get(ctx, types.NamespacedName{Namespace: member.Namespace, Name: member.Spec.ControlPlaneRef.Name}, controlPlane); err != nil {
293+
return nil, fmt.Errorf("get CoderControlPlane %s/%s: %w", member.Namespace, member.Spec.ControlPlaneRef.Name, err)
294+
}
295+
if !controlPlane.DeletionTimestamp.IsZero() {
296+
return coderWait("ControlPlaneDeleting", "CoderControlPlane %s is being deleted.", controlPlane.Name), nil
297+
}
298+
if step := workspacePoolConsent(controlPlane, member.Spec.OwnerID, template); step != nil {
299+
return step, nil
300+
}
288301
owner := workspacePoolOwner(controlPlane, member.Spec.OwnerID)
289-
if owner == nil || owner.MaxWorkspaces == nil {
290-
return nil, fmt.Errorf("assertion failed: CoderControlPlane %s has no defaulted maxWorkspaces for Coder user %s after the consent check", controlPlane.Name, member.Spec.OwnerID)
302+
if owner.MaxWorkspaces == nil {
303+
return nil, fmt.Errorf("assertion failed: CoderControlPlane %s has no defaulted maxWorkspaces for Coder user %s", controlPlane.Name, member.Spec.OwnerID)
291304
}
292305
all := &coderv1alpha1.CoderWorkspacePoolMemberList{}
293306
if err := r.APIReader.List(ctx, all, client.InNamespace(member.Namespace)); err != nil {

0 commit comments

Comments
 (0)