diff --git a/coderd/autobuild/lifecycle_executor.go b/coderd/autobuild/lifecycle_executor.go index e0d804328b2d3..4bbbaba667c7e 100644 --- a/coderd/autobuild/lifecycle_executor.go +++ b/coderd/autobuild/lifecycle_executor.go @@ -316,7 +316,7 @@ func getNextTransition( error, ) { switch { - case isEligibleForAutostop(ws, latestBuild, latestJob, currentTick): + case isEligibleForAutostop(user, ws, latestBuild, latestJob, currentTick): return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil case isEligibleForAutostart(user, ws, latestBuild, latestJob, templateSchedule, currentTick): return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil @@ -376,8 +376,8 @@ func isEligibleForAutostart(user database.User, ws database.Workspace, build dat return !currentTick.Before(nextTransition) } -// isEligibleForAutostart returns true if the workspace should be autostopped. -func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, currentTick time.Time) bool { +// isEligibleForAutostop returns true if the workspace should be autostopped. +func isEligibleForAutostop(user database.User, ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, currentTick time.Time) bool { if job.JobStatus == database.ProvisionerJobStatusFailed { return false } @@ -387,6 +387,10 @@ func isEligibleForAutostop(ws database.Workspace, build database.WorkspaceBuild, return false } + if build.Transition == database.WorkspaceTransitionStart && user.Status == database.UserStatusSuspended { + return true + } + // A workspace must be started in order for it to be auto-stopped. return build.Transition == database.WorkspaceTransitionStart && !build.Deadline.IsZero() && diff --git a/coderd/autobuild/lifecycle_executor_test.go b/coderd/autobuild/lifecycle_executor_test.go index 54ceb53254680..bc480b97e4aa2 100644 --- a/coderd/autobuild/lifecycle_executor_test.go +++ b/coderd/autobuild/lifecycle_executor_test.go @@ -563,6 +563,52 @@ func TestExecutorWorkspaceAutostopBeforeDeadline(t *testing.T) { assert.Len(t, stats.Transitions, 0) } +func TestExecuteAutostopSuspendedUser(t *testing.T) { + t.Parallel() + + var ( + ctx = testutil.Context(t, testutil.WaitShort) + tickCh = make(chan time.Time) + statsCh = make(chan autobuild.Stats) + client = coderdtest.New(t, &coderdtest.Options{ + AutobuildTicker: tickCh, + IncludeProvisionerDaemon: true, + AutobuildStats: statsCh, + }) + ) + + admin := coderdtest.CreateFirstUser(t, client) + version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID) + userClient, user := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID) + workspace := coderdtest.CreateWorkspace(t, userClient, admin.OrganizationID, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID) + + // Given: workspace is running, and the user is suspended. + workspace = coderdtest.MustWorkspace(t, userClient, workspace.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, workspace.LatestBuild.Status) + _, err := client.UpdateUserStatus(ctx, user.ID.String(), codersdk.UserStatusSuspended) + require.NoError(t, err, "update user status") + + // When: the autobuild executor ticks after the scheduled time + go func() { + tickCh <- time.Unix(0, 0) // the exact time is not important + close(tickCh) + }() + + // Then: the workspace should be stopped + stats := <-statsCh + assert.Len(t, stats.Errors, 0) + assert.Len(t, stats.Transitions, 1) + assert.Equal(t, stats.Transitions[workspace.ID], database.WorkspaceTransitionStop) + + // Wait for stop to complete + workspace = coderdtest.MustWorkspace(t, client, workspace.ID) + workspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + assert.Equal(t, codersdk.WorkspaceStatusStopped, workspaceBuild.Status) +} + func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) { t.Parallel() diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index d19d218556b8d..ec7becdfd39c9 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -5844,6 +5844,15 @@ func (q *FakeQuerier) GetWorkspacesEligibleForTransition(ctx context.Context, no workspaces = append(workspaces, workspace) continue } + + user, err := q.getUserByIDNoLock(workspace.OwnerID) + if err != nil { + return nil, xerrors.Errorf("get user by ID: %w", err) + } + if user.Status == database.UserStatusSuspended && build.Transition == database.WorkspaceTransitionStart { + workspaces = append(workspaces, workspace) + continue + } } return workspaces, nil diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index ff7b7f6f955bd..cd48412c2ff40 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -13426,6 +13426,8 @@ INNER JOIN provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id INNER JOIN templates ON workspaces.template_id = templates.id +INNER JOIN + users ON workspaces.owner_id = users.id WHERE workspace_builds.build_number = ( SELECT @@ -13477,6 +13479,12 @@ WHERE ( templates.time_til_dormant_autodelete > 0 AND workspaces.dormant_at IS NOT NULL + ) OR + + -- If the user account is suspended, and the workspace is running. + ( + users.status = 'suspended'::user_status AND + workspace_builds.transition = 'start'::workspace_transition ) ) AND workspaces.deleted = 'false' ` diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 616e83a2bae16..ec8767e1f2be5 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -557,6 +557,8 @@ INNER JOIN provisioner_jobs ON workspace_builds.job_id = provisioner_jobs.id INNER JOIN templates ON workspaces.template_id = templates.id +INNER JOIN + users ON workspaces.owner_id = users.id WHERE workspace_builds.build_number = ( SELECT @@ -608,6 +610,12 @@ WHERE ( templates.time_til_dormant_autodelete > 0 AND workspaces.dormant_at IS NOT NULL + ) OR + + -- If the user account is suspended, and the workspace is running. + ( + users.status = 'suspended'::user_status AND + workspace_builds.transition = 'start'::workspace_transition ) ) AND workspaces.deleted = 'false'; 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