Skip to content

Commit 7c41f95

Browse files
authored
feat: autostop workspaces owned by suspended users (#13790)
1 parent c2d44d1 commit 7c41f95

File tree

5 files changed

+78
-3
lines changed

5 files changed

+78
-3
lines changed

coderd/autobuild/lifecycle_executor.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -316,7 +316,7 @@ func getNextTransition(
316316
error,
317317
) {
318318
switch {
319-
case isEligibleForAutostop(ws, latestBuild, latestJob, currentTick):
319+
case isEligibleForAutostop(user, ws, latestBuild, latestJob, currentTick):
320320
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
321321
case isEligibleForAutostart(user, ws, latestBuild, latestJob, templateSchedule, currentTick):
322322
return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil
@@ -376,8 +376,8 @@ func isEligibleForAutostart(user database.User, ws database.Workspace, build dat
376376
return !currentTick.Before(nextTransition)
377377
}
378378

379-
// isEligibleForAutostart returns true if the workspace should be autostopped.
380-
func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, currentTick time.Time) bool {
379+
// isEligibleForAutostop returns true if the workspace should be autostopped.
380+
func isEligibleForAutostop(user database.User, ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, currentTick time.Time) bool {
381381
if job.JobStatus == database.ProvisionerJobStatusFailed {
382382
return false
383383
}
@@ -387,6 +387,10 @@ func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild,
387387
return false
388388
}
389389

390+
if build.Transition == database.WorkspaceTransitionStart && user.Status == database.UserStatusSuspended {
391+
return true
392+
}
393+
390394
// A workspace must be started in order for it to be auto-stopped.
391395
return build.Transition == database.WorkspaceTransitionStart &&
392396
!build.Deadline.IsZero() &&

coderd/autobuild/lifecycle_executor_test.go

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -563,6 +563,52 @@ func TestExecutorWorkspaceAutostopBeforeDeadline(t *testing.T) {
563563
assert.Len(t, stats.Transitions, 0)
564564
}
565565

566+
func TestExecuteAutostopSuspendedUser(t *testing.T) {
567+
t.Parallel()
568+
569+
var (
570+
ctx = testutil.Context(t, testutil.WaitShort)
571+
tickCh = make(chan time.Time)
572+
statsCh = make(chan autobuild.Stats)
573+
client = coderdtest.New(t, &coderdtest.Options{
574+
AutobuildTicker: tickCh,
575+
IncludeProvisionerDaemon: true,
576+
AutobuildStats: statsCh,
577+
})
578+
)
579+
580+
admin := coderdtest.CreateFirstUser(t, client)
581+
version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil)
582+
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
583+
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
584+
userClient, user := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
585+
workspace := coderdtest.CreateWorkspace(t, userClient, admin.OrganizationID, template.ID)
586+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
587+
588+
// Given: workspace is running, and the user is suspended.
589+
workspace = coderdtest.MustWorkspace(t, userClient, workspace.ID)
590+
require.Equal(t, codersdk.WorkspaceStatusRunning, workspace.LatestBuild.Status)
591+
_, err := client.UpdateUserStatus(ctx, user.ID.String(), codersdk.UserStatusSuspended)
592+
require.NoError(t, err, "update user status")
593+
594+
// When: the autobuild executor ticks after the scheduled time
595+
go func() {
596+
tickCh <- time.Unix(0, 0) // the exact time is not important
597+
close(tickCh)
598+
}()
599+
600+
// Then: the workspace should be stopped
601+
stats := <-statsCh
602+
assert.Len(t, stats.Errors, 0)
603+
assert.Len(t, stats.Transitions, 1)
604+
assert.Equal(t, stats.Transitions[workspace.ID], database.WorkspaceTransitionStop)
605+
606+
// Wait for stop to complete
607+
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
608+
workspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
609+
assert.Equal(t, codersdk.WorkspaceStatusStopped, workspaceBuild.Status)
610+
}
611+
566612
func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
567613
t.Parallel()
568614

coderd/database/dbmem/dbmem.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5844,6 +5844,15 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no
58445844
workspaces = append(workspaces, workspace)
58455845
continue
58465846
}
5847+
5848+
user, err := q.getUserByIDNoLock(workspace.OwnerID)
5849+
if err != nil {
5850+
return nil, xerrors.Errorf("get user by ID: %w", err)
5851+
}
5852+
if user.Status == database.UserStatusSuspended && build.Transition == database.WorkspaceTransitionStart {
5853+
workspaces = append(workspaces, workspace)
5854+
continue
5855+
}
58475856
}
58485857

58495858
return workspaces, nil

coderd/database/queries.sql.go

Lines changed: 8 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

coderd/database/queries/workspaces.sql

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,8 @@ INNER JOIN
557557
provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id
558558
INNER JOIN
559559
templates ON workspaces.template_id = templates.id
560+
INNER JOIN
561+
users ON workspaces.owner_id = users.id
560562
WHERE
561563
workspace_builds.build_number = (
562564
SELECT
@@ -608,6 +610,12 @@ WHERE
608610
(
609611
templates.time_til_dormant_autodelete > 0 AND
610612
workspaces.dormant_at IS NOT NULL
613+
) OR
614+
615+
-- If the user account is suspended, and the workspace is running.
616+
(
617+
users.status = 'suspended'::user_status AND
618+
workspace_builds.transition = 'start'::workspace_transition
611619
)
612620
) AND workspaces.deleted = 'false';
613621

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy