mirror of
https://github.com/coder/coder.git
synced 2025-07-09 11:45:56 +00:00
fix: skip autostart for suspended/dormant users (#10771)
This commit is contained in:
@ -149,6 +149,11 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
|||||||
return xerrors.Errorf("get workspace by id: %w", err)
|
return xerrors.Errorf("get workspace by id: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
user, err := tx.GetUserByID(e.ctx, ws.OwnerID)
|
||||||
|
if err != nil {
|
||||||
|
return xerrors.Errorf("get user by id: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Determine the workspace state based on its latest build.
|
// Determine the workspace state based on its latest build.
|
||||||
latestBuild, err := tx.GetLatestWorkspaceBuildByWorkspaceID(e.ctx, ws.ID)
|
latestBuild, err := tx.GetLatestWorkspaceBuildByWorkspaceID(e.ctx, ws.ID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -172,7 +177,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
|||||||
|
|
||||||
accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(template)
|
accessControl := (*(e.accessControlStore.Load())).GetTemplateAccessControl(template)
|
||||||
|
|
||||||
nextTransition, reason, err := getNextTransition(ws, latestBuild, latestJob, templateSchedule, currentTick)
|
nextTransition, reason, err := getNextTransition(user, ws, latestBuild, latestJob, templateSchedule, currentTick)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debug(e.ctx, "skipping workspace", slog.Error(err))
|
log.Debug(e.ctx, "skipping workspace", slog.Error(err))
|
||||||
// err is used to indicate that a workspace is not eligible
|
// err is used to indicate that a workspace is not eligible
|
||||||
@ -300,6 +305,7 @@ func (e *Executor) runOnce(t time.Time) Stats {
|
|||||||
// may be "transitioning" to a new state (such as an inactive, stopped
|
// may be "transitioning" to a new state (such as an inactive, stopped
|
||||||
// workspace transitioning to the dormant state).
|
// workspace transitioning to the dormant state).
|
||||||
func getNextTransition(
|
func getNextTransition(
|
||||||
|
user database.User,
|
||||||
ws database.Workspace,
|
ws database.Workspace,
|
||||||
latestBuild database.WorkspaceBuild,
|
latestBuild database.WorkspaceBuild,
|
||||||
latestJob database.ProvisionerJob,
|
latestJob database.ProvisionerJob,
|
||||||
@ -313,7 +319,7 @@ func getNextTransition(
|
|||||||
switch {
|
switch {
|
||||||
case isEligibleForAutostop(ws, latestBuild, latestJob, currentTick):
|
case isEligibleForAutostop(ws, latestBuild, latestJob, currentTick):
|
||||||
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
|
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
|
||||||
case isEligibleForAutostart(ws, latestBuild, latestJob, templateSchedule, currentTick):
|
case isEligibleForAutostart(user, ws, latestBuild, latestJob, templateSchedule, currentTick):
|
||||||
return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil
|
return database.WorkspaceTransitionStart, database.BuildReasonAutostart, nil
|
||||||
case isEligibleForFailedStop(latestBuild, latestJob, templateSchedule, currentTick):
|
case isEligibleForFailedStop(latestBuild, latestJob, templateSchedule, currentTick):
|
||||||
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
|
return database.WorkspaceTransitionStop, database.BuildReasonAutostop, nil
|
||||||
@ -334,7 +340,12 @@ func getNextTransition(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// isEligibleForAutostart returns true if the workspace should be autostarted.
|
// isEligibleForAutostart returns true if the workspace should be autostarted.
|
||||||
func isEligibleForAutostart(ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool {
|
func isEligibleForAutostart(user database.User, ws database.Workspace, build database.WorkspaceBuild, job database.ProvisionerJob, templateSchedule schedule.TemplateScheduleOptions, currentTick time.Time) bool {
|
||||||
|
// Don't attempt to autostart workspaces for suspended users.
|
||||||
|
if user.Status != database.UserStatusActive {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
// Don't attempt to autostart failed workspaces.
|
// Don't attempt to autostart failed workspaces.
|
||||||
if job.JobStatus == database.ProvisionerJobStatusFailed {
|
if job.JobStatus == database.ProvisionerJobStatusFailed {
|
||||||
return false
|
return false
|
||||||
|
@ -25,6 +25,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
|
|||||||
|
|
||||||
// 5s after the autostart in UTC.
|
// 5s after the autostart in UTC.
|
||||||
okTick := time.Date(2021, 1, 1, 20, 0, 5, 0, localLocation).UTC()
|
okTick := time.Date(2021, 1, 1, 20, 0, 5, 0, localLocation).UTC()
|
||||||
|
okUser := database.User{Status: database.UserStatusActive}
|
||||||
okWorkspace := database.Workspace{
|
okWorkspace := database.Workspace{
|
||||||
DormantAt: sql.NullTime{Valid: false},
|
DormantAt: sql.NullTime{Valid: false},
|
||||||
AutostartSchedule: sql.NullString{
|
AutostartSchedule: sql.NullString{
|
||||||
@ -57,6 +58,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
|
|||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
Name string
|
Name string
|
||||||
|
User database.User
|
||||||
Workspace database.Workspace
|
Workspace database.Workspace
|
||||||
Build database.WorkspaceBuild
|
Build database.WorkspaceBuild
|
||||||
Job database.ProvisionerJob
|
Job database.ProvisionerJob
|
||||||
@ -67,6 +69,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
|
|||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
Name: "Ok",
|
Name: "Ok",
|
||||||
|
User: okUser,
|
||||||
Workspace: okWorkspace,
|
Workspace: okWorkspace,
|
||||||
Build: okBuild,
|
Build: okBuild,
|
||||||
Job: okJob,
|
Job: okJob,
|
||||||
@ -74,8 +77,19 @@ func Test_isEligibleForAutostart(t *testing.T) {
|
|||||||
Tick: okTick,
|
Tick: okTick,
|
||||||
ExpectedResponse: true,
|
ExpectedResponse: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "SuspendedUser",
|
||||||
|
User: database.User{Status: database.UserStatusSuspended},
|
||||||
|
Workspace: okWorkspace,
|
||||||
|
Build: okBuild,
|
||||||
|
Job: okJob,
|
||||||
|
TemplateSchedule: okTemplateSchedule,
|
||||||
|
Tick: okTick,
|
||||||
|
ExpectedResponse: false,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "AutostartOnlyDayEnabled",
|
Name: "AutostartOnlyDayEnabled",
|
||||||
|
User: okUser,
|
||||||
Workspace: okWorkspace,
|
Workspace: okWorkspace,
|
||||||
Build: okBuild,
|
Build: okBuild,
|
||||||
Job: okJob,
|
Job: okJob,
|
||||||
@ -91,6 +105,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "AutostartOnlyDayDisabled",
|
Name: "AutostartOnlyDayDisabled",
|
||||||
|
User: okUser,
|
||||||
Workspace: okWorkspace,
|
Workspace: okWorkspace,
|
||||||
Build: okBuild,
|
Build: okBuild,
|
||||||
Job: okJob,
|
Job: okJob,
|
||||||
@ -106,6 +121,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "AutostartAllDaysDisabled",
|
Name: "AutostartAllDaysDisabled",
|
||||||
|
User: okUser,
|
||||||
Workspace: okWorkspace,
|
Workspace: okWorkspace,
|
||||||
Build: okBuild,
|
Build: okBuild,
|
||||||
Job: okJob,
|
Job: okJob,
|
||||||
@ -121,6 +137,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
Name: "BuildTransitionNotStop",
|
Name: "BuildTransitionNotStop",
|
||||||
|
User: okUser,
|
||||||
Workspace: okWorkspace,
|
Workspace: okWorkspace,
|
||||||
Build: func(b database.WorkspaceBuild) database.WorkspaceBuild {
|
Build: func(b database.WorkspaceBuild) database.WorkspaceBuild {
|
||||||
cpy := b
|
cpy := b
|
||||||
@ -139,7 +156,7 @@ func Test_isEligibleForAutostart(t *testing.T) {
|
|||||||
t.Run(c.Name, func(t *testing.T) {
|
t.Run(c.Name, func(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
autostart := isEligibleForAutostart(c.Workspace, c.Build, c.Job, c.TemplateSchedule, c.Tick)
|
autostart := isEligibleForAutostart(c.User, c.Workspace, c.Build, c.Job, c.TemplateSchedule, c.Tick)
|
||||||
require.Equal(t, c.ExpectedResponse, autostart, "autostart not expected")
|
require.Equal(t, c.ExpectedResponse, autostart, "autostart not expected")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -265,6 +265,50 @@ func TestExecutorAutostartNotEnabled(t *testing.T) {
|
|||||||
require.Len(t, stats.Transitions, 0)
|
require.Len(t, stats.Transitions, 0)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExecutorAutostartUserSuspended(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
var (
|
||||||
|
ctx = testutil.Context(t, testutil.WaitShort)
|
||||||
|
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||||||
|
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)
|
||||||
|
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
|
||||||
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||||
|
userClient, user := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||||||
|
workspace := coderdtest.CreateWorkspace(t, userClient, admin.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||||
|
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||||
|
})
|
||||||
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||||||
|
workspace = coderdtest.MustWorkspace(t, userClient, workspace.ID)
|
||||||
|
|
||||||
|
// Given: workspace is stopped, and the user is suspended.
|
||||||
|
workspace = coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||||
|
|
||||||
|
_, 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 <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||||||
|
close(tickCh)
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Then: nothing should happen
|
||||||
|
stats := testutil.RequireRecvCtx(ctx, t, statsCh)
|
||||||
|
assert.NoError(t, stats.Error)
|
||||||
|
assert.Len(t, stats.Transitions, 0)
|
||||||
|
}
|
||||||
|
|
||||||
func TestExecutorAutostopOK(t *testing.T) {
|
func TestExecutorAutostopOK(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user