Skip to content

Commit 7f39ff8

Browse files
authored
fix: skip autostart for suspended/dormant users (#10771)
1 parent 614c179 commit 7f39ff8

File tree

3 files changed

+76
-4
lines changed

3 files changed

+76
-4
lines changed

coderd/autobuild/lifecycle_executor.go

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -149,6 +149,11 @@ func (e *Executor) runOnce(t time.Time) Stats {
149149
return xerrors.Errorf("get workspace by id: %w", err)
150150
}
151151

152+
user, err := tx.GetUserByID(e.ctx, ws.OwnerID)
153+
if err != nil {
154+
return xerrors.Errorf("get user by id: %w", err)
155+
}
156+
152157
// Determine the workspace state based on its latest build.
153158
latestBuild, err := tx.GetLatestWorkspaceBuildByWorkspaceID(e.ctx, ws.ID)
154159
if err != nil {
@@ -172,7 +177,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
172177

173178
accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(template)
174179

175-
nextTransition, reason, err := getNextTransition(ws, latestBuild, latestJob, templateSchedule, currentTick)
180+
nextTransition, reason, err := getNextTransition(user, ws, latestBuild, latestJob, templateSchedule, currentTick)
176181
if err != nil {
177182
log.Debug(e.ctx, "skipping workspace", slog.Error(err))
178183
// err is used to indicate that a workspace is not eligible
@@ -300,6 +305,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
300305
// may be "transitioning" to a new state (such as an inactive, stopped
301306
// workspace transitioning to the dormant state).
302307
func getNextTransition(
308+
user database.User,
303309
ws database.Workspace,
304310
latestBuild database.WorkspaceBuild,
305311
latestJob database.ProvisionerJob,
@@ -313,7 +319,7 @@ func getNextTransition(
313319
switch {
314320
case isEligibleForAutostop(ws, latestBuild, latestJob, currentTick):
315321
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
316-
case isEligibleForAutostart(ws, latestBuild, latestJob, templateSchedule, currentTick):
322+
case isEligibleForAutostart(user, ws, latestBuild, latestJob, templateSchedule, currentTick):
317323
return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil
318324
case isEligibleForFailedStop(latestBuild, latestJob, templateSchedule, currentTick):
319325
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
@@ -334,7 +340,12 @@ func getNextTransition(
334340
}
335341

336342
// isEligibleForAutostart returns true if the workspace should be autostarted.
337-
func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool {
343+
func isEligibleForAutostart(user database.User, ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool {
344+
// Don't attempt to autostart workspaces for suspended users.
345+
if user.Status != database.UserStatusActive {
346+
return false
347+
}
348+
338349
// Don't attempt to autostart failed workspaces.
339350
if job.JobStatus == database.ProvisionerJobStatusFailed {
340351
return false

coderd/autobuild/lifecycle_executor_internal_test.go

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
2525

2626
// 5s after the autostart in UTC.
2727
okTick := time.Date(2021, 1, 1, 20, 0, 5, 0, localLocation).UTC()
28+
okUser := database.User{Status: database.UserStatusActive}
2829
okWorkspace := database.Workspace{
2930
DormantAt: sql.NullTime{Valid: false},
3031
AutostartSchedule: sql.NullString{
@@ -57,6 +58,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
5758

5859
testCases := []struct {
5960
Name string
61+
User database.User
6062
Workspace database.Workspace
6163
Build database.WorkspaceBuild
6264
Job database.ProvisionerJob
@@ -67,15 +69,27 @@ func Test_isEligibleForAutostart(t *testing.T) {
6769
}{
6870
{
6971
Name: "Ok",
72+
User: okUser,
7073
Workspace: okWorkspace,
7174
Build: okBuild,
7275
Job: okJob,
7376
TemplateSchedule: okTemplateSchedule,
7477
Tick: okTick,
7578
ExpectedResponse: true,
7679
},
80+
{
81+
Name: "SuspendedUser",
82+
User: database.User{Status: database.UserStatusSuspended},
83+
Workspace: okWorkspace,
84+
Build: okBuild,
85+
Job: okJob,
86+
TemplateSchedule: okTemplateSchedule,
87+
Tick: okTick,
88+
ExpectedResponse: false,
89+
},
7790
{
7891
Name: "AutostartOnlyDayEnabled",
92+
User: okUser,
7993
Workspace: okWorkspace,
8094
Build: okBuild,
8195
Job: okJob,
@@ -91,6 +105,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
91105
},
92106
{
93107
Name: "AutostartOnlyDayDisabled",
108+
User: okUser,
94109
Workspace: okWorkspace,
95110
Build: okBuild,
96111
Job: okJob,
@@ -106,6 +121,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
106121
},
107122
{
108123
Name: "AutostartAllDaysDisabled",
124+
User: okUser,
109125
Workspace: okWorkspace,
110126
Build: okBuild,
111127
Job: okJob,
@@ -121,6 +137,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
121137
},
122138
{
123139
Name: "BuildTransitionNotStop",
140+
User: okUser,
124141
Workspace: okWorkspace,
125142
Build: func(b database.WorkspaceBuild) database.WorkspaceBuild {
126143
cpy := b
@@ -139,7 +156,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
139156
t.Run(c.Name, func(t *testing.T) {
140157
t.Parallel()
141158

142-
autostart := isEligibleForAutostart(c.Workspace, c.Build, c.Job, c.TemplateSchedule, c.Tick)
159+
autostart := isEligibleForAutostart(c.User, c.Workspace, c.Build, c.Job, c.TemplateSchedule, c.Tick)
143160
require.Equal(t, c.ExpectedResponse, autostart, "autostart not expected")
144161
})
145162
}

coderd/autobuild/lifecycle_executor_test.go

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -265,6 +265,50 @@ func TestExecutorAutostartNotEnabled(t *testing.T) {
265265
require.Len(t, stats.Transitions, 0)
266266
}
267267

268+
func TestExecutorAutostartUserSuspended(t *testing.T) {
269+
t.Parallel()
270+
271+
var (
272+
ctx = testutil.Context(t, testutil.WaitShort)
273+
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
274+
tickCh = make(chan time.Time)
275+
statsCh = make(chan autobuild.Stats)
276+
client = coderdtest.New(t, &coderdtest.Options{
277+
AutobuildTicker: tickCh,
278+
IncludeProvisionerDaemon: true,
279+
AutobuildStats: statsCh,
280+
})
281+
)
282+
283+
admin := coderdtest.CreateFirstUser(t, client)
284+
version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil)
285+
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
286+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
287+
userClient, user := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
288+
workspace := coderdtest.CreateWorkspace(t, userClient, admin.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
289+
cwr.AutostartSchedule = ptr.Ref(sched.String())
290+
})
291+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
292+
workspace = coderdtest.MustWorkspace(t, userClient, workspace.ID)
293+
294+
// Given: workspace is stopped, and the user is suspended.
295+
workspace = coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
296+
297+
_, err := client.UpdateUserStatus(ctx, user.ID.String(), codersdk.UserStatusSuspended)
298+
require.NoError(t, err, "update user status")
299+
300+
// When: the autobuild executor ticks after the scheduled time
301+
go func() {
302+
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
303+
close(tickCh)
304+
}()
305+
306+
// Then: nothing should happen
307+
stats := testutil.RequireRecvCtx(ctx, t, statsCh)
308+
assert.NoError(t, stats.Error)
309+
assert.Len(t, stats.Transitions, 0)
310+
}
311+
268312
func TestExecutorAutostopOK(t *testing.T) {
269313
t.Parallel()
270314

0 commit comments

Comments
 (0)