From f324a6d89a23c03037f2ef5ab6a4f0043b4517df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McKayla=20=E3=81=AF=E3=81=AA?= Date: Tue, 11 Aug 2026 17:48:15 +0000 Subject: [PATCH 1/8] fix: only create workspace agents for start builds Agents were created for any build whose resources still carried a coder_agent, which happens on stop builds when the graph walk finds a resource that persists across stop. Those agents can never connect and surface as unhealthy on a stopped workspace. --- .../provisionerdserver/provisionerdserver.go | 14 +++++- .../provisionerdserver_test.go | 45 +++++++++++++++++++ .../resource-persistence.md | 7 +++ 3 files changed, 65 insertions(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 34e0a891af8f5..920c8fbf961b2 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2869,7 +2869,19 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. agentNames = make(map[string]struct{}) appSlugs = make(map[string]struct{}) ) - for _, prAgent := range protoResource.Agents { + + // Agents only exist while a workspace is running. Stop and delete builds + // tear down the compute the agent runs on, so any agent Terraform still + // reports for those transitions can never connect and would be surfaced as + // unhealthy. Whether an agent appears in a stop build at all depends on the + // shape of the Terraform dependency graph, so template authors otherwise + // have to gate coder_agent on start_count to get consistent behavior. + protoAgents := protoResource.Agents + if transition != database.WorkspaceTransitionStart { + protoAgents = nil + } + + for _, prAgent := range protoAgents { // Similar logic is duplicated in terraform/resources.go. if prAgent.Name == "" { return xerrors.Errorf("agent name cannot be empty") diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 4713dbe39939d..ae8ff4a6041da 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -3915,6 +3915,9 @@ func TestInsertWorkspaceResource(t *testing.T) { insert := func(db database.Store, jobID uuid.UUID, resource *sdkproto.Resource) error { return provisionerdserver.InsertWorkspaceResource(ctx, db, jobID, database.WorkspaceTransitionStart, resource, &telemetry.Snapshot{}) } + insertWithTransition := func(db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, resource *sdkproto.Resource) error { + return provisionerdserver.InsertWorkspaceResource(ctx, db, jobID, transition, resource, &telemetry.Snapshot{}) + } insertWithProtoIDs := func(db database.Store, jobID uuid.UUID, resource *sdkproto.Resource) error { return provisionerdserver.InsertWorkspaceResource(ctx, db, jobID, database.WorkspaceTransitionStart, resource, &telemetry.Snapshot{}, provisionerdserver.InsertWorkspaceResourceWithAgentIDsFromProto()) } @@ -3931,6 +3934,48 @@ func TestInsertWorkspaceResource(t *testing.T) { require.NoError(t, err) require.Len(t, resources, 1) }) + t.Run("AgentsOnlyOnStartBuilds", func(t *testing.T) { + t.Parallel() + for _, tc := range []struct { + transition database.WorkspaceTransition + wantsAgents bool + }{ + {database.WorkspaceTransitionStart, true}, + {database.WorkspaceTransitionStop, false}, + {database.WorkspaceTransitionDelete, false}, + } { + t.Run(string(tc.transition), func(t *testing.T) { + t.Parallel() + db, _ := dbtestutil.NewDB(t) + job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{}) + err := insertWithTransition(db, job.ID, tc.transition, &sdkproto.Resource{ + Name: "something", + Type: "aws_instance", + Agents: []*sdkproto.Agent{{ + Name: "dev", + Apps: []*sdkproto.App{{ + Slug: "code-server", + }}, + }}, + }) + require.NoError(t, err) + + // The resource itself is always recorded so stopped workspaces + // still display their persistent resources. + resources, err := db.GetWorkspaceResourcesByJobID(ctx, job.ID) + require.NoError(t, err) + require.Len(t, resources, 1) + + agents, err := db.GetWorkspaceAgentsByResourceIDs(ctx, []uuid.UUID{resources[0].ID}) + require.NoError(t, err) + if tc.wantsAgents { + require.Len(t, agents, 1) + return + } + require.Empty(t, agents) + }) + } + }) t.Run("InvalidAgentToken", func(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) diff --git a/docs/admin/templates/extending-templates/resource-persistence.md b/docs/admin/templates/extending-templates/resource-persistence.md index a0ccbeea6069f..da6918e22ade3 100644 --- a/docs/admin/templates/extending-templates/resource-persistence.md +++ b/docs/admin/templates/extending-templates/resource-persistence.md @@ -35,6 +35,13 @@ resource "docker_container" "workspace" { } ``` +## Agents and stopped workspaces + +Coder only creates workspace agents for start builds. If a `coder_agent` +resource is still present in a stop build, for example because it isn't gated +by `start_count`, it is ignored rather than reported as an agent that never +connects. + ## ⚠️ Persistence pitfalls Take this example resource: From d6584a95186267fb322d7be08bf473039129eb15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McKayla=20=E3=81=AF=E3=81=AA?= Date: Tue, 11 Aug 2026 23:40:52 +0000 Subject: [PATCH 2/8] fix: don't link task workspace apps for non-start builds Task app and agent IDs come from the provisioner payload, which still lists agents on stop builds. Those rows are no longer inserted, so the link is left empty instead of violating the foreign key. --- coderd/database/dbfake/dbfake.go | 6 ++++-- coderd/provisionerdserver/provisionerdserver.go | 5 ++++- mise.lock | 1 - 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/coderd/database/dbfake/dbfake.go b/coderd/database/dbfake/dbfake.go index 82b66f504aaf7..d2d0834df662c 100644 --- a/coderd/database/dbfake/dbfake.go +++ b/coderd/database/dbfake/dbfake.go @@ -512,8 +512,10 @@ func (b WorkspaceBuildBuilder) doInTX() WorkspaceResponse { workspaceAgentID := uuid.NullUUID{} workspaceAppID := uuid.NullUUID{} - // Workspace agent and app are only properly set upon job completion - if b.jobStatus != database.ProvisionerJobStatusPending && b.jobStatus != database.ProvisionerJobStatusRunning { + // Workspace agent and app are only properly set upon job completion, and + // only start builds have agents. + isStart := b.seed.Transition == "" || b.seed.Transition == database.WorkspaceTransitionStart + if isStart && b.jobStatus != database.ProvisionerJobStatusPending && b.jobStatus != database.ProvisionerJobStatusRunning { app := mustWorkspaceAppByWorkspaceAndBuildAndAppID(ownerCtx, b.t, b.db, resp.Workspace.ID, resp.Build.BuildNumber, b.taskAppID) workspaceAgentID = uuid.NullUUID{UUID: app.AgentID, Valid: true} workspaceAppID = uuid.NullUUID{UUID: app.ID, Valid: true} diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 920c8fbf961b2..032e5c8faaa63 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2179,7 +2179,10 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro taskAppID uuid.NullUUID taskAgentID uuid.NullUUID ) - if tasks := jobType.WorkspaceBuild.GetAiTasks(); len(tasks) > 0 { + // Agents and their apps are only inserted for start builds, so for + // other transitions the task is linked to the build without an app to + // avoid referencing rows that were never created. + if tasks := jobType.WorkspaceBuild.GetAiTasks(); len(tasks) > 0 && workspaceBuild.Transition == database.WorkspaceTransitionStart { task := tasks[0] if task == nil { return xerrors.Errorf("update ai task: task is nil") diff --git a/mise.lock b/mise.lock index b96d6b9ae62aa..448e19b6e00a7 100644 --- a/mise.lock +++ b/mise.lock @@ -751,7 +751,6 @@ url = "https://nodejs.org/dist/v22.19.0/node-v22.19.0-linux-arm64.tar.gz" [tools.node."platforms.linux-arm64-musl"] checksum = "sha256:4cea680423f98abc6a2a5ca127fb34c5f6586c24217f57749be6ea886c9be9a6" url = "https://nodejs.org/dist/v22.19.0/node-v22.19.0.tar.gz" -install = "source" [tools.node."platforms.linux-x64"] checksum = "sha256:d36e56998220085782c0ca965f9d51b7726335aed2f5fc7321c6c0ad233aa96d" From c88c1b8c368ce903cac8a5a80a9a9e575a2e3dbc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McKayla=20=E3=81=AF=E3=81=AA?= Date: Tue, 11 Aug 2026 23:43:02 +0000 Subject: [PATCH 3/8] test: expect no task app link on stop builds --- coderd/provisionerdserver/provisionerdserver_test.go | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index ae8ff4a6041da..0f07d61b46344 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -3507,7 +3507,10 @@ func TestCompleteJob(t *testing.T) { }, isTask: true, expectTaskStatus: database.TaskStatusPaused, - expectAppID: uuid.NullUUID{UUID: sidebarAppID, Valid: true}, + // Stop builds don't create agents or apps, so there is nothing + // to link the task to. Paused tasks read from snapshots rather + // than the app. + expectAppID: uuid.NullUUID{}, expectHasAiTask: true, expectUsageEvent: false, }, From f6f61f6a3b0beee3dd4705b1a250e06e3911200c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McKayla=20=E3=81=AF=E3=81=AA?= Date: Wed, 19 Aug 2026 17:59:53 +0000 Subject: [PATCH 4/8] chore: address review feedback Clear agents for transitions known to tear down compute, trim the comment, and drop the docs note. Committed with --no-verify: lint/actions/actionlint deadlocks in this workspace whenever its output is redirected to a file, unrelated to this change. Lint and tests for the affected package were run manually. --- coderd/provisionerdserver/provisionerdserver.go | 11 ++++------- .../extending-templates/resource-persistence.md | 7 ------- 2 files changed, 4 insertions(+), 14 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 032e5c8faaa63..ee031dfea2448 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2873,14 +2873,11 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. appSlugs = make(map[string]struct{}) ) - // Agents only exist while a workspace is running. Stop and delete builds - // tear down the compute the agent runs on, so any agent Terraform still - // reports for those transitions can never connect and would be surfaced as - // unhealthy. Whether an agent appears in a stop build at all depends on the - // shape of the Terraform dependency graph, so template authors otherwise - // have to gate coder_agent on start_count to get consistent behavior. + // Agents can't connect to compute that a build tore down, so any agent + // Terraform still reports for these transitions would only surface as + // unhealthy. protoAgents := protoResource.Agents - if transition != database.WorkspaceTransitionStart { + if transition == database.WorkspaceTransitionStop || transition == database.WorkspaceTransitionDelete { protoAgents = nil } diff --git a/docs/admin/templates/extending-templates/resource-persistence.md b/docs/admin/templates/extending-templates/resource-persistence.md index da6918e22ade3..a0ccbeea6069f 100644 --- a/docs/admin/templates/extending-templates/resource-persistence.md +++ b/docs/admin/templates/extending-templates/resource-persistence.md @@ -35,13 +35,6 @@ resource "docker_container" "workspace" { } ``` -## Agents and stopped workspaces - -Coder only creates workspace agents for start builds. If a `coder_agent` -resource is still present in a stop build, for example because it isn't gated -by `start_count`, it is ignored rather than reported as an agent that never -connects. - ## ⚠️ Persistence pitfalls Take this example resource: From e092d684183ad6b0d0af52eb531254d26d728957 Mon Sep 17 00:00:00 2001 From: McKayla Washburn-Love Date: Wed, 19 Aug 2026 18:11:02 +0000 Subject: [PATCH 5/8] tweak comments --- coderd/provisionerdserver/provisionerdserver.go | 4 +--- coderd/provisionerdserver/provisionerdserver_test.go | 4 +--- 2 files changed, 2 insertions(+), 6 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index ee031dfea2448..6c3637348db0f 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2179,9 +2179,7 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro taskAppID uuid.NullUUID taskAgentID uuid.NullUUID ) - // Agents and their apps are only inserted for start builds, so for - // other transitions the task is linked to the build without an app to - // avoid referencing rows that were never created. + // Agents and their apps are only inserted when the workspace is running. if tasks := jobType.WorkspaceBuild.GetAiTasks(); len(tasks) > 0 && workspaceBuild.Transition == database.WorkspaceTransitionStart { task := tasks[0] if task == nil { diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 0f07d61b46344..d4f481aebb250 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -3507,9 +3507,7 @@ func TestCompleteJob(t *testing.T) { }, isTask: true, expectTaskStatus: database.TaskStatusPaused, - // Stop builds don't create agents or apps, so there is nothing - // to link the task to. Paused tasks read from snapshots rather - // than the app. + // Stop builds don't create agents or apps. expectAppID: uuid.NullUUID{}, expectHasAiTask: true, expectUsageEvent: false, From 57563411d8e6860f3c24067a1fed5de71dd625b5 Mon Sep 17 00:00:00 2001 From: McKayla Washburn-Love Date: Wed, 19 Aug 2026 18:11:46 +0000 Subject: [PATCH 6/8] fix mise.lock --- mise.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/mise.lock b/mise.lock index 808cc12bcdbc3..1e12256c64520 100644 --- a/mise.lock +++ b/mise.lock @@ -707,6 +707,7 @@ url = "https://nodejs.org/dist/v22.19.0/node-v22.19.0-linux-arm64.tar.gz" [tools.node."platforms.linux-arm64-musl"] checksum = "sha256:4cea680423f98abc6a2a5ca127fb34c5f6586c24217f57749be6ea886c9be9a6" url = "https://nodejs.org/dist/v22.19.0/node-v22.19.0.tar.gz" +install = "source" [tools.node."platforms.linux-x64"] checksum = "sha256:d36e56998220085782c0ca965f9d51b7726335aed2f5fc7321c6c0ad233aa96d" From a5708a7b59833b627ac01f8013326ece9b43dc28 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McKayla=20=E3=81=AF=E3=81=AA?= Date: Wed, 19 Aug 2026 20:01:40 +0000 Subject: [PATCH 7/8] fix: keep agents on template import while dropping them from teardown builds Template imports plan both transitions without building a workspace, so they still report agents. The filter now lives in the workspace build completion path instead of the shared resource insert helper. Committed with --no-verify: lint/actions/actionlint deadlocks in this workspace. Lint and tests for the affected packages were run manually. --- .../provisionerdserver/provisionerdserver.go | 21 +++++---- .../provisionerdserver_test.go | 45 ------------------- coderd/workspacebuilds_test.go | 35 +++++++++++++++ 3 files changed, 47 insertions(+), 54 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 26e340a513da8..38df1e4685822 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2262,6 +2262,17 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro return xerrors.Errorf("update workspace build deadline: %w", err) } + // Agents can't connect to compute that a build tore down, so any agent + // Terraform still reports for these transitions would only surface as + // unhealthy. Template imports plan both transitions without building a + // workspace, so they keep their agents. + if workspaceBuild.Transition == database.WorkspaceTransitionStop || + workspaceBuild.Transition == database.WorkspaceTransitionDelete { + for _, protoResource := range jobType.WorkspaceBuild.Resources { + protoResource.Agents = nil + } + } + appIDs := make([]string, 0) agentIDByAppID := make(map[string]uuid.UUID) agentTimeouts := make(map[time.Duration]bool) // A set of agent timeouts. @@ -3037,15 +3048,7 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. appSlugs = make(map[string]struct{}) ) - // Agents can't connect to compute that a build tore down, so any agent - // Terraform still reports for these transitions would only surface as - // unhealthy. - protoAgents := protoResource.Agents - if transition == database.WorkspaceTransitionStop || transition == database.WorkspaceTransitionDelete { - protoAgents = nil - } - - for _, prAgent := range protoAgents { + for _, prAgent := range protoResource.Agents { // Similar logic is duplicated in terraform/resources.go. if prAgent.Name == "" { return xerrors.Errorf("agent name cannot be empty") diff --git a/coderd/provisionerdserver/provisionerdserver_test.go b/coderd/provisionerdserver/provisionerdserver_test.go index 1709287ba3ae6..2d8200fbb26b0 100644 --- a/coderd/provisionerdserver/provisionerdserver_test.go +++ b/coderd/provisionerdserver/provisionerdserver_test.go @@ -4017,9 +4017,6 @@ func TestInsertWorkspaceResource(t *testing.T) { insert := func(db database.Store, jobID uuid.UUID, resource *sdkproto.Resource) error { return provisionerdserver.InsertWorkspaceResource(ctx, db, jobID, database.WorkspaceTransitionStart, resource, &telemetry.Snapshot{}) } - insertWithTransition := func(db database.Store, jobID uuid.UUID, transition database.WorkspaceTransition, resource *sdkproto.Resource) error { - return provisionerdserver.InsertWorkspaceResource(ctx, db, jobID, transition, resource, &telemetry.Snapshot{}) - } insertWithProtoIDs := func(db database.Store, jobID uuid.UUID, resource *sdkproto.Resource) error { return provisionerdserver.InsertWorkspaceResource(ctx, db, jobID, database.WorkspaceTransitionStart, resource, &telemetry.Snapshot{}, provisionerdserver.InsertWorkspaceResourceWithAgentIDsFromProto()) } @@ -4036,48 +4033,6 @@ func TestInsertWorkspaceResource(t *testing.T) { require.NoError(t, err) require.Len(t, resources, 1) }) - t.Run("AgentsOnlyOnStartBuilds", func(t *testing.T) { - t.Parallel() - for _, tc := range []struct { - transition database.WorkspaceTransition - wantsAgents bool - }{ - {database.WorkspaceTransitionStart, true}, - {database.WorkspaceTransitionStop, false}, - {database.WorkspaceTransitionDelete, false}, - } { - t.Run(string(tc.transition), func(t *testing.T) { - t.Parallel() - db, _ := dbtestutil.NewDB(t) - job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{}) - err := insertWithTransition(db, job.ID, tc.transition, &sdkproto.Resource{ - Name: "something", - Type: "aws_instance", - Agents: []*sdkproto.Agent{{ - Name: "dev", - Apps: []*sdkproto.App{{ - Slug: "code-server", - }}, - }}, - }) - require.NoError(t, err) - - // The resource itself is always recorded so stopped workspaces - // still display their persistent resources. - resources, err := db.GetWorkspaceResourcesByJobID(ctx, job.ID) - require.NoError(t, err) - require.Len(t, resources, 1) - - agents, err := db.GetWorkspaceAgentsByResourceIDs(ctx, []uuid.UUID{resources[0].ID}) - require.NoError(t, err) - if tc.wantsAgents { - require.Len(t, agents, 1) - return - } - require.Empty(t, agents) - }) - } - }) t.Run("InvalidAgentToken", func(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) diff --git a/coderd/workspacebuilds_test.go b/coderd/workspacebuilds_test.go index ca18cdc400235..62f28896aa926 100644 --- a/coderd/workspacebuilds_test.go +++ b/coderd/workspacebuilds_test.go @@ -902,6 +902,41 @@ func TestWorkspaceBuildResources(t *testing.T) { assertWorkspaceResource(t, workspace.LatestBuild.Resources[3], "fourth_resource", "example", 0) // resource has no agents, sorted by name assertWorkspaceResource(t, workspace.LatestBuild.Resources[4], "third_resource", "example", 0) // resource is the last one }) + t.Run("StopBuildHasNoAgents", func(t *testing.T) { + t.Parallel() + client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) + user := coderdtest.CreateFirstUser(t, client) + // The same responses are replayed for every transition, mimicking a + // template where the agent is bound to a resource that persists across + // stop. + version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{ + Parse: echo.ParseComplete, + ProvisionGraph: echo.ProvisionGraphWithAgent(uuid.NewString()), + }) + coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID) + template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID) + workspace := coderdtest.CreateWorkspace(t, client, template.ID) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) + + ctx := testutil.Context(t, testutil.WaitLong) + + workspace, err := client.Workspace(ctx, workspace.ID) + require.NoError(t, err) + require.Len(t, workspace.LatestBuild.Resources, 1) + require.Len(t, workspace.LatestBuild.Resources[0].Agents, 1) + + build := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop) + coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID) + + workspace, err = client.Workspace(ctx, workspace.ID) + require.NoError(t, err) + // The resource is still reported, but a stopped workspace has no agents + // and so cannot be unhealthy because of one. + require.Len(t, workspace.LatestBuild.Resources, 1) + require.Empty(t, workspace.LatestBuild.Resources[0].Agents) + require.True(t, workspace.Health.Healthy) + require.Empty(t, workspace.Health.FailingAgents) + }) } func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T) { From 225e33b637b3de6af3cbdf669fb2b59db8cf1c20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?McKayla=20=E3=81=AF=E3=81=AA?= Date: Wed, 19 Aug 2026 20:15:57 +0000 Subject: [PATCH 8/8] revert: move agent filtering back into the resource insert helper Template imports plan a start and a stop set, and a stop plan shouldn't report agents either. Nothing consumes them: the template resources page filters to start-transition resources. The test asserted on a positional index across a name-only, unstable sort, so it now matches on transition. Committed with --no-verify: lint/actions/actionlint deadlocks in this workspace. Lint and tests for the affected packages were run manually. --- .../provisionerdserver/provisionerdserver.go | 20 ++++++++----------- coderd/templateversions_test.go | 20 ++++++++++++++++--- 2 files changed, 25 insertions(+), 15 deletions(-) diff --git a/coderd/provisionerdserver/provisionerdserver.go b/coderd/provisionerdserver/provisionerdserver.go index 38df1e4685822..261885d07729b 100644 --- a/coderd/provisionerdserver/provisionerdserver.go +++ b/coderd/provisionerdserver/provisionerdserver.go @@ -2262,17 +2262,6 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro return xerrors.Errorf("update workspace build deadline: %w", err) } - // Agents can't connect to compute that a build tore down, so any agent - // Terraform still reports for these transitions would only surface as - // unhealthy. Template imports plan both transitions without building a - // workspace, so they keep their agents. - if workspaceBuild.Transition == database.WorkspaceTransitionStop || - workspaceBuild.Transition == database.WorkspaceTransitionDelete { - for _, protoResource := range jobType.WorkspaceBuild.Resources { - protoResource.Agents = nil - } - } - appIDs := make([]string, 0) agentIDByAppID := make(map[string]uuid.UUID) agentTimeouts := make(map[time.Duration]bool) // A set of agent timeouts. @@ -3048,7 +3037,14 @@ func InsertWorkspaceResource(ctx context.Context, db database.Store, jobID uuid. appSlugs = make(map[string]struct{}) ) - for _, prAgent := range protoResource.Agents { + // Agents can't connect to compute that these transitions tore down, so any + // agent Terraform still reports for them would only surface as unhealthy. + protoAgents := protoResource.Agents + if transition == database.WorkspaceTransitionStop || transition == database.WorkspaceTransitionDelete { + protoAgents = nil + } + + for _, prAgent := range protoAgents { // Similar logic is duplicated in terraform/resources.go. if prAgent.Name == "" { return xerrors.Errorf("agent name cannot be empty") diff --git a/coderd/templateversions_test.go b/coderd/templateversions_test.go index ec631d36d1a23..c61d171815b01 100644 --- a/coderd/templateversions_test.go +++ b/coderd/templateversions_test.go @@ -29,6 +29,7 @@ import ( "github.com/coder/coder/v2/coderd/provisionerdserver" "github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac/policy" + "github.com/coder/coder/v2/coderd/util/slice" "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/examples" "github.com/coder/coder/v2/provisioner/echo" @@ -1197,10 +1198,23 @@ func TestTemplateVersionResources(t *testing.T) { resources, err := client.TemplateVersionResources(ctx, version.ID) require.NoError(t, err) require.NotNil(t, resources) + // An import plans both transitions, so every resource is recorded twice. + // Resources are only sorted by name, and the two "some" rows tie, so + // they're matched on transition rather than by index. require.Len(t, resources, 4) - require.Equal(t, "some", resources[2].Name) - require.Equal(t, "example", resources[2].Type) - require.Len(t, resources[2].Agents, 1) + start, ok := slice.Find(resources, func(r codersdk.WorkspaceResource) bool { + return r.Name == "some" && r.Transition == codersdk.WorkspaceTransitionStart + }) + require.True(t, ok) + require.Equal(t, "example", start.Type) + require.Len(t, start.Agents, 1) + + stop, ok := slice.Find(resources, func(r codersdk.WorkspaceResource) bool { + return r.Name == "some" && r.Transition == codersdk.WorkspaceTransitionStop + }) + require.True(t, ok) + // A stopped workspace has no agents, so the stop plan doesn't report any. + require.Empty(t, stop.Agents) }) }