From 02b26d18de9474f4d33030ba4ca6b2b25d4ab98b Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 25 Nov 2025 00:10:56 +0000 Subject: [PATCH] fix: allow agents to be created on dormant workspaces Closes https://github.com/coder/coder/issues/20711 We now allow agents to be created on dormant workspaces. --- coderd/database/dbauthz/dbauthz.go | 2 +- enterprise/coderd/workspaces_test.go | 67 ++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/coderd/database/dbauthz/dbauthz.go b/coderd/database/dbauthz/dbauthz.go index c3cc39116f656..6e981ac3c2fd5 100644 --- a/coderd/database/dbauthz/dbauthz.go +++ b/coderd/database/dbauthz/dbauthz.go @@ -217,7 +217,7 @@ var ( rbac.ResourceTemplate.Type: {policy.ActionRead, policy.ActionUpdate}, // Unsure why provisionerd needs update and read personal rbac.ResourceUser.Type: {policy.ActionRead, policy.ActionReadPersonal, policy.ActionUpdatePersonal}, - rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop}, + rbac.ResourceWorkspaceDormant.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStop, policy.ActionCreateAgent}, rbac.ResourceWorkspace.Type: {policy.ActionDelete, policy.ActionRead, policy.ActionUpdate, policy.ActionWorkspaceStart, policy.ActionWorkspaceStop, policy.ActionCreateAgent}, // Provisionerd needs to read, update, and delete tasks associated with workspaces. rbac.ResourceTask.Type: {policy.ActionRead, policy.ActionUpdate, policy.ActionDelete}, diff --git a/enterprise/coderd/workspaces_test.go b/enterprise/coderd/workspaces_test.go index 9d1377610cf69..7cf9cd890b6df 100644 --- a/enterprise/coderd/workspaces_test.go +++ b/enterprise/coderd/workspaces_test.go @@ -831,6 +831,73 @@ func TestWorkspaceAutobuild(t *testing.T) { require.True(t, ws.LastUsedAt.After(dormantLastUsedAt)) }) + // This test has been added to ensure we don't introduce a regression + // to this issue https://github.com/coder/coder/issues/20711. + t.Run("DormantAutostop", func(t *testing.T) { + t.Parallel() + + var ( + ticker = make(chan time.Time) + statCh = make(chan autobuild.Stats) + inactiveTTL = time.Minute + logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug) + ) + + client, db, user := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{ + Options: &coderdtest.Options{ + AutobuildTicker: ticker, + AutobuildStats: statCh, + IncludeProvisionerDaemon: true, + TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil), + }, + LicenseOptions: &coderdenttest.LicenseOptions{ + Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1}, + }, + }) + + // Create a template version that includes agents on both start AND stop builds. + // This simulates a template without `count = data.coder_workspace.me.start_count`. + authToken := uuid.NewString() + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionPlan: echo.PlanComplete, + ProvisionApply: echo.ProvisionApplyWithAgent(authToken), + }) + + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) { + ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds()) + }) + + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + ws := coderdtest.CreateWorkspace(t, client, template.ID) + build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status) + + // Simulate the workspace becoming inactive and transitioning to dormant. + tickTime := ws.LastUsedAt.Add(inactiveTTL * 2) + + p, err := coderdtest.GetProvisionerForTags(db, time.Now(), ws.OrganizationID, nil) + require.NoError(t, err) + coderdtest.UpdateProvisionerLastSeenAt(t, db, p.ID, tickTime) + ticker <- tickTime + stats := <-statCh + + // Expect workspace to transition to stopped state. + require.Len(t, stats.Transitions, 1) + require.Equal(t, stats.Transitions[ws.ID], database.WorkspaceTransitionStop) + + // The autostop build should succeed even though the template includes + // agents without `count = data.coder_workspace.me.start_count`. + // This verifies that provisionerd has permission to create agents on + // dormant workspaces during stop builds. + ws = coderdtest.MustWorkspace(t, client, ws.ID) + require.NotNil(t, ws.DormantAt, "workspace should be marked as dormant") + require.Equal(t, codersdk.WorkspaceTransitionStop, ws.LatestBuild.Transition) + + latestBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) + require.Equal(t, codersdk.WorkspaceStatusStopped, latestBuild.Status) + }) + // This test serves as a regression prevention for generating // audit logs in the same transaction the transition workspaces to // the dormant state. The auditor that is passed to autobuild does