From fb3cf7b95e9e4fa1032d4f52c362c80e740279dd Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 27 May 2025 11:34:35 +0000 Subject: [PATCH 01/12] chore(coderd/database): create db trigger to enforce agent name uniqueness This PR creates a new database trigger to ensure an inserted workspace agent has a unique name within the scope of its workspace. --- coderd/database/dump.sql | 42 +++++ ...rkspace_agent_name_unique_trigger.down.sql | 2 + ...workspace_agent_name_unique_trigger.up.sql | 43 +++++ coderd/database/querier_test.go | 175 ++++++++++++++++++ 4 files changed, 262 insertions(+) create mode 100644 coderd/database/migrations/000330_workspace_agent_name_unique_trigger.down.sql create mode 100644 coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 5e79149ff6f52..a53002ae12557 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -316,6 +316,46 @@ CREATE TYPE workspace_transition AS ENUM ( 'delete' ); +CREATE FUNCTION check_workspace_agent_name_unique() RETURNS trigger + LANGUAGE plpgsql + AS $$ +DECLARE + workspace_id_var uuid; + existing_count integer; +BEGIN + -- Get the workspace_id for this agent by following the relationship chain: + -- workspace_agents -> workspace_resources -> provisioner_jobs -> workspace_builds -> workspaces + SELECT wb.workspace_id INTO workspace_id_var + FROM workspace_resources wr + JOIN provisioner_jobs pj ON wr.job_id = pj.id + JOIN workspace_builds wb ON pj.id = wb.job_id + WHERE wr.id = NEW.resource_id; + + -- If we couldn't find a workspace_id, allow the insert (might be a template import or other edge case) + IF workspace_id_var IS NULL THEN + RETURN NEW; + END IF; + + -- Check if there's already an agent with this name in this workspace + SELECT COUNT(*) INTO existing_count + FROM workspace_agents wa + JOIN workspace_resources wr ON wa.resource_id = wr.id + JOIN provisioner_jobs pj ON wr.job_id = pj.id + JOIN workspace_builds wb ON pj.id = wb.job_id + WHERE wb.workspace_id = workspace_id_var + AND wa.name = NEW.name + AND wa.id != NEW.id; -- Exclude the current agent (for updates) + + -- If there's already an agent with this name, raise an error + IF existing_count > 0 THEN + RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace', NEW.name + USING ERRCODE = 'unique_violation'; + END IF; + + RETURN NEW; +END; +$$; + CREATE FUNCTION compute_notification_message_dedupe_hash() RETURNS trigger LANGUAGE plpgsql AS $$ @@ -2772,6 +2812,8 @@ CREATE TRIGGER update_notification_message_dedupe_hash BEFORE INSERT OR UPDATE O CREATE TRIGGER user_status_change_trigger AFTER INSERT OR UPDATE ON users FOR EACH ROW EXECUTE FUNCTION record_user_status_change(); +CREATE TRIGGER workspace_agent_name_unique_trigger BEFORE INSERT OR UPDATE OF name, resource_id ON workspace_agents FOR EACH ROW EXECUTE FUNCTION check_workspace_agent_name_unique(); + ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.down.sql b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.down.sql new file mode 100644 index 0000000000000..916e1d469ed69 --- /dev/null +++ b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.down.sql @@ -0,0 +1,2 @@ +DROP TRIGGER IF EXISTS workspace_agent_name_unique_trigger ON workspace_agents; +DROP FUNCTION IF EXISTS check_workspace_agent_name_unique(); diff --git a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql new file mode 100644 index 0000000000000..468a416aac1e1 --- /dev/null +++ b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql @@ -0,0 +1,43 @@ +CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique() +RETURNS TRIGGER AS $$ +DECLARE + workspace_id_var uuid; + existing_count integer; +BEGIN + -- Get the workspace_id for this agent by following the relationship chain: + -- workspace_agents -> workspace_resources -> provisioner_jobs -> workspace_builds -> workspaces + SELECT wb.workspace_id INTO workspace_id_var + FROM workspace_resources wr + JOIN provisioner_jobs pj ON wr.job_id = pj.id + JOIN workspace_builds wb ON pj.id = wb.job_id + WHERE wr.id = NEW.resource_id; + + -- If we couldn't find a workspace_id, allow the insert (might be a template import or other edge case) + IF workspace_id_var IS NULL THEN + RETURN NEW; + END IF; + + -- Check if there's already an agent with this name in this workspace + SELECT COUNT(*) INTO existing_count + FROM workspace_agents wa + JOIN workspace_resources wr ON wa.resource_id = wr.id + JOIN provisioner_jobs pj ON wr.job_id = pj.id + JOIN workspace_builds wb ON pj.id = wb.job_id + WHERE wb.workspace_id = workspace_id_var + AND wa.name = NEW.name + AND wa.id != NEW.id; -- Exclude the current agent (for updates) + + -- If there's already an agent with this name, raise an error + IF existing_count > 0 THEN + RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace', NEW.name + USING ERRCODE = 'unique_violation'; + END IF; + + RETURN NEW; +END; +$$ LANGUAGE plpgsql; + +CREATE TRIGGER workspace_agent_name_unique_trigger + BEFORE INSERT OR UPDATE OF name, resource_id ON workspace_agents + FOR EACH ROW + EXECUTE FUNCTION check_workspace_agent_name_unique(); diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 5bafa58796b7a..a3b12b3effdfb 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -4,12 +4,15 @@ import ( "context" "database/sql" "encoding/json" + "errors" "fmt" "sort" + "sync/atomic" "testing" "time" "github.com/google/uuid" + "github.com/lib/pq" "github.com/prometheus/client_golang/prometheus" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -4705,6 +4708,178 @@ func TestGetPresetsAtFailureLimit(t *testing.T) { }) } +func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { + t.Parallel() + + var builds atomic.Int32 + + if !dbtestutil.WillUsePostgres() { + t.Skip("This test makes use of a database trigger not implemented in dbmem") + } + + createWorkspaceWithAgent := func(t *testing.T, db database.Store, org database.Organization, agentName string) (database.WorkspaceTable, database.TemplateVersion, database.WorkspaceAgent) { + t.Helper() + + user := dbgen.User(t, db, database.User{}) + template := dbgen.Template(t, db, database.Template{ + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + templateVersion := dbgen.TemplateVersion(t, db, database.TemplateVersion{ + TemplateID: uuid.NullUUID{Valid: true, UUID: template.ID}, + OrganizationID: org.ID, + CreatedBy: user.ID, + }) + workspace := dbgen.Workspace(t, db, database.WorkspaceTable{ + OrganizationID: org.ID, + TemplateID: template.ID, + OwnerID: user.ID, + }) + job := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeWorkspaceBuild, + OrganizationID: org.ID, + }) + build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + BuildNumber: builds.Add(1), + JobID: job.ID, + WorkspaceID: workspace.ID, + TemplateVersionID: templateVersion.ID, + }) + resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build.JobID, + }) + agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource.ID, + Name: agentName, + }) + + return workspace, templateVersion, agent + } + + t.Run("DuplicateNamesInSameWorkspace", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + ctx := testutil.Context(t, testutil.WaitShort) + + // Given: A workspace with an agent + workspace1, templateVersion1, agent1 := createWorkspaceWithAgent(t, db, org, "duplicate-agent") + require.Equal(t, "duplicate-agent", agent1.Name) + + // When: Another agent is created for that workspace with the same name. + job2 := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeWorkspaceBuild, + OrganizationID: org.ID, + }) + build2 := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ + BuildNumber: builds.Add(1), + JobID: job2.ID, + WorkspaceID: workspace1.ID, + TemplateVersionID: templateVersion1.ID, + }) + resource2 := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: build2.JobID, + }) + _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ + ID: uuid.New(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "duplicate-agent", // Same name as agent1 + ResourceID: resource2.ID, + AuthToken: uuid.New(), + Architecture: "amd64", + OperatingSystem: "linux", + APIKeyScope: database.AgentKeyScopeEnumAll, + }) + + // Then: We expect it to fail. + require.Error(t, err) + var pqErr *pq.Error + require.True(t, errors.As(err, &pqErr)) + require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation + require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this workspace`) + }) + + t.Run("SameNamesInDifferentWorkspaces", func(t *testing.T) { + t.Parallel() + + agentName := "same-name-different-workspace" + + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + + // Given: A workspace with an agent + _, _, agent1 := createWorkspaceWithAgent(t, db, org, agentName) + require.Equal(t, agentName, agent1.Name) + + // When: A second workspace is created with an agent having the same name + _, _, agent2 := createWorkspaceWithAgent(t, db, org, agentName) + require.Equal(t, agentName, agent2.Name) + + // Then: We expect there to be different agents with the same name. + require.NotEqual(t, agent1.ID, agent2.ID) + require.Equal(t, agent1.Name, agent2.Name) + }) + + t.Run("NullWorkspaceID", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + ctx := testutil.Context(t, testutil.WaitShort) + + // Given: A resource that does not belong to a workspace build (simulating template import) + orphanJob := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeTemplateVersionImport, + OrganizationID: org.ID, + }) + orphanResource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: orphanJob.ID, + }) + + // And this resource has a workspace agent. + agent1, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ + ID: uuid.New(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "orphan-agent", + ResourceID: orphanResource.ID, + AuthToken: uuid.New(), + Architecture: "amd64", + OperatingSystem: "linux", + APIKeyScope: database.AgentKeyScopeEnumAll, + }) + require.NoError(t, err) + require.Equal(t, "orphan-agent", agent1.Name) + + // When: We created another resource that does not belong to a workspace build. + orphanJob2 := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ + Type: database.ProvisionerJobTypeTemplateVersionImport, + OrganizationID: org.ID, + }) + orphanResource2 := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ + JobID: orphanJob2.ID, + }) + + // Then: We expect to be able to create an agent in this new resource that has the same name. + agent2, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ + ID: uuid.New(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: "orphan-agent", // Same name as agent1 + ResourceID: orphanResource2.ID, + AuthToken: uuid.New(), + Architecture: "amd64", + OperatingSystem: "linux", + APIKeyScope: database.AgentKeyScopeEnumAll, + }) + require.NoError(t, err) + require.Equal(t, "orphan-agent", agent2.Name) + require.NotEqual(t, agent1.ID, agent2.ID) + }) +} + func requireUsersMatch(t testing.TB, expected []database.User, found []database.GetUsersRow, msg string) { t.Helper() require.ElementsMatch(t, expected, database.ConvertUserRows(found), msg) From 3c47f69956fd1dc43dd2083fa8b6a0994c9087fa Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 27 May 2025 12:56:43 +0000 Subject: [PATCH 02/12] fix: unique per build not workspace --- coderd/database/dump.sql | 16 ++++++++-------- ...30_workspace_agent_name_unique_trigger.up.sql | 16 ++++++++-------- 2 files changed, 16 insertions(+), 16 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index a53002ae12557..7ca70c0944c8a 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -320,29 +320,29 @@ CREATE FUNCTION check_workspace_agent_name_unique() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE - workspace_id_var uuid; + workspace_build_id uuid; existing_count integer; BEGIN - -- Get the workspace_id for this agent by following the relationship chain: - -- workspace_agents -> workspace_resources -> provisioner_jobs -> workspace_builds -> workspaces - SELECT wb.workspace_id INTO workspace_id_var + -- Get the workspace_build.id for this agent by following the relationship chain: + -- workspace_agents -> workspace_resources -> provisioner_jobs -> workspace_builds + SELECT wb.id INTO workspace_build_id FROM workspace_resources wr JOIN provisioner_jobs pj ON wr.job_id = pj.id JOIN workspace_builds wb ON pj.id = wb.job_id WHERE wr.id = NEW.resource_id; - -- If we couldn't find a workspace_id, allow the insert (might be a template import or other edge case) - IF workspace_id_var IS NULL THEN + -- If we couldn't find a workspace_build_id, allow the insert (might be a template import or other edge case) + IF workspace_build_id IS NULL THEN RETURN NEW; END IF; - -- Check if there's already an agent with this name in this workspace + -- Check if there's already an agent with this name for this workspace build SELECT COUNT(*) INTO existing_count FROM workspace_agents wa JOIN workspace_resources wr ON wa.resource_id = wr.id JOIN provisioner_jobs pj ON wr.job_id = pj.id JOIN workspace_builds wb ON pj.id = wb.job_id - WHERE wb.workspace_id = workspace_id_var + WHERE wb.id = workspace_build_id AND wa.name = NEW.name AND wa.id != NEW.id; -- Exclude the current agent (for updates) diff --git a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql index 468a416aac1e1..ea773e324fa45 100644 --- a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql +++ b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql @@ -1,29 +1,29 @@ CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique() RETURNS TRIGGER AS $$ DECLARE - workspace_id_var uuid; + workspace_build_id uuid; existing_count integer; BEGIN - -- Get the workspace_id for this agent by following the relationship chain: - -- workspace_agents -> workspace_resources -> provisioner_jobs -> workspace_builds -> workspaces - SELECT wb.workspace_id INTO workspace_id_var + -- Get the workspace_build.id for this agent by following the relationship chain: + -- workspace_agents -> workspace_resources -> provisioner_jobs -> workspace_builds + SELECT wb.id INTO workspace_build_id FROM workspace_resources wr JOIN provisioner_jobs pj ON wr.job_id = pj.id JOIN workspace_builds wb ON pj.id = wb.job_id WHERE wr.id = NEW.resource_id; - -- If we couldn't find a workspace_id, allow the insert (might be a template import or other edge case) - IF workspace_id_var IS NULL THEN + -- If we couldn't find a workspace_build_id, allow the insert (might be a template import or other edge case) + IF workspace_build_id IS NULL THEN RETURN NEW; END IF; - -- Check if there's already an agent with this name in this workspace + -- Check if there's already an agent with this name for this workspace build SELECT COUNT(*) INTO existing_count FROM workspace_agents wa JOIN workspace_resources wr ON wa.resource_id = wr.id JOIN provisioner_jobs pj ON wr.job_id = pj.id JOIN workspace_builds wb ON pj.id = wb.job_id - WHERE wb.workspace_id = workspace_id_var + WHERE wb.id = workspace_build_id AND wa.name = NEW.name AND wa.id != NEW.id; -- Exclude the current agent (for updates) From 043005a591a68b3351e89709cfff5d24f7659541 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 27 May 2025 13:58:09 +0000 Subject: [PATCH 03/12] fix: unique per resource not build --- coderd/database/dump.sql | 64 +++++++++++-------- ...workspace_agent_name_unique_trigger.up.sql | 62 ++++++++++-------- coderd/database/querier_test.go | 32 +++------- 3 files changed, 83 insertions(+), 75 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 7ca70c0944c8a..3a0565f8767dd 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -320,37 +320,49 @@ CREATE FUNCTION check_workspace_agent_name_unique() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE - workspace_build_id uuid; - existing_count integer; + provisioner_job_id uuid; + agents_with_name integer; BEGIN - -- Get the workspace_build.id for this agent by following the relationship chain: - -- workspace_agents -> workspace_resources -> provisioner_jobs -> workspace_builds - SELECT wb.id INTO workspace_build_id - FROM workspace_resources wr - JOIN provisioner_jobs pj ON wr.job_id = pj.id - JOIN workspace_builds wb ON pj.id = wb.job_id - WHERE wr.id = NEW.resource_id; - - -- If we couldn't find a workspace_build_id, allow the insert (might be a template import or other edge case) - IF workspace_build_id IS NULL THEN - RETURN NEW; + -- Count how many agents in this resource already have + -- the given agent ID. + SELECT COUNT(*) INTO agents_with_name + FROM workspace_agents + WHERE workspace_agents.resource_id = NEW.resource_id + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; + + -- If there's already an agent with this name, raise an error + IF agents_with_name > 0 THEN + RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace resource', NEW.name + USING ERRCODE = 'unique_violation'; END IF; - -- Check if there's already an agent with this name for this workspace build - SELECT COUNT(*) INTO existing_count - FROM workspace_agents wa - JOIN workspace_resources wr ON wa.resource_id = wr.id - JOIN provisioner_jobs pj ON wr.job_id = pj.id - JOIN workspace_builds wb ON pj.id = wb.job_id - WHERE wb.id = workspace_build_id - AND wa.name = NEW.name - AND wa.id != NEW.id; -- Exclude the current agent (for updates) + -- Get the provisioner_jobs.id for this agent by following the relationship chain: + -- workspace_agents -> workspace_resources -> provisioner_jobs + -- SELECT pj.id INTO provisioner_job_id + -- FROM workspace_resources wr + -- JOIN provisioner_jobs pj ON wr.job_id = pj.id + -- WHERE wr.id = NEW.resource_id; + + -- If we couldn't find a provisioner_job.id, allow the insert (might be a template import or other edge case) + -- IF provisioner_job_id IS NULL THEN + -- RETURN NEW; + -- END IF; + + -- Check if there's already an agent with this name for this provisioner job + -- SELECT COUNT(*) INTO existing_count + -- FROM workspace_agents wa + -- JOIN workspace_resources wr ON wa.resource_id = wr.id + -- JOIN provisioner_jobs pj ON wr.job_id = pj.id + -- WHERE pj.id = provisioner_job_id + -- AND wa.name = NEW.name + -- AND wa.id != NEW.id; -- If there's already an agent with this name, raise an error - IF existing_count > 0 THEN - RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace', NEW.name - USING ERRCODE = 'unique_violation'; - END IF; + -- IF existing_count > 0 THEN + -- RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name + -- USING ERRCODE = 'unique_violation'; + -- END IF; RETURN NEW; END; diff --git a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql index ea773e324fa45..f1743e4e9c35c 100644 --- a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql +++ b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql @@ -1,37 +1,49 @@ CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique() RETURNS TRIGGER AS $$ DECLARE - workspace_build_id uuid; - existing_count integer; + provisioner_job_id uuid; + agents_with_name integer; BEGIN - -- Get the workspace_build.id for this agent by following the relationship chain: - -- workspace_agents -> workspace_resources -> provisioner_jobs -> workspace_builds - SELECT wb.id INTO workspace_build_id - FROM workspace_resources wr - JOIN provisioner_jobs pj ON wr.job_id = pj.id - JOIN workspace_builds wb ON pj.id = wb.job_id - WHERE wr.id = NEW.resource_id; + -- Count how many agents in this resource already have + -- the given agent ID. + SELECT COUNT(*) INTO agents_with_name + FROM workspace_agents + WHERE workspace_agents.resource_id = NEW.resource_id + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; - -- If we couldn't find a workspace_build_id, allow the insert (might be a template import or other edge case) - IF workspace_build_id IS NULL THEN - RETURN NEW; + -- If there's already an agent with this name, raise an error + IF agents_with_name > 0 THEN + RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace resource', NEW.name + USING ERRCODE = 'unique_violation'; END IF; - -- Check if there's already an agent with this name for this workspace build - SELECT COUNT(*) INTO existing_count - FROM workspace_agents wa - JOIN workspace_resources wr ON wa.resource_id = wr.id - JOIN provisioner_jobs pj ON wr.job_id = pj.id - JOIN workspace_builds wb ON pj.id = wb.job_id - WHERE wb.id = workspace_build_id - AND wa.name = NEW.name - AND wa.id != NEW.id; -- Exclude the current agent (for updates) + -- Get the provisioner_jobs.id for this agent by following the relationship chain: + -- workspace_agents -> workspace_resources -> provisioner_jobs + -- SELECT pj.id INTO provisioner_job_id + -- FROM workspace_resources wr + -- JOIN provisioner_jobs pj ON wr.job_id = pj.id + -- WHERE wr.id = NEW.resource_id; + + -- If we couldn't find a provisioner_job.id, allow the insert (might be a template import or other edge case) + -- IF provisioner_job_id IS NULL THEN + -- RETURN NEW; + -- END IF; + + -- Check if there's already an agent with this name for this provisioner job + -- SELECT COUNT(*) INTO existing_count + -- FROM workspace_agents wa + -- JOIN workspace_resources wr ON wa.resource_id = wr.id + -- JOIN provisioner_jobs pj ON wr.job_id = pj.id + -- WHERE pj.id = provisioner_job_id + -- AND wa.name = NEW.name + -- AND wa.id != NEW.id; -- If there's already an agent with this name, raise an error - IF existing_count > 0 THEN - RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace', NEW.name - USING ERRCODE = 'unique_violation'; - END IF; + -- IF existing_count > 0 THEN + -- RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name + -- USING ERRCODE = 'unique_violation'; + -- END IF; RETURN NEW; END; diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index a3b12b3effdfb..f2d2cc36dc391 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -7,7 +7,6 @@ import ( "errors" "fmt" "sort" - "sync/atomic" "testing" "time" @@ -4711,13 +4710,11 @@ func TestGetPresetsAtFailureLimit(t *testing.T) { func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { t.Parallel() - var builds atomic.Int32 - if !dbtestutil.WillUsePostgres() { t.Skip("This test makes use of a database trigger not implemented in dbmem") } - createWorkspaceWithAgent := func(t *testing.T, db database.Store, org database.Organization, agentName string) (database.WorkspaceTable, database.TemplateVersion, database.WorkspaceAgent) { + createWorkspaceWithAgent := func(t *testing.T, db database.Store, org database.Organization, agentName string) (database.WorkspaceTable, database.WorkspaceResource, database.WorkspaceAgent) { t.Helper() user := dbgen.User(t, db, database.User{}) @@ -4740,7 +4737,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { OrganizationID: org.ID, }) build := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - BuildNumber: builds.Add(1), + BuildNumber: 1, JobID: job.ID, WorkspaceID: workspace.ID, TemplateVersionID: templateVersion.ID, @@ -4753,10 +4750,10 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { Name: agentName, }) - return workspace, templateVersion, agent + return workspace, resource, agent } - t.Run("DuplicateNamesInSameWorkspace", func(t *testing.T) { + t.Run("DuplicateNamesInSameWorkspaceBuild", func(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) @@ -4764,29 +4761,16 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { ctx := testutil.Context(t, testutil.WaitShort) // Given: A workspace with an agent - workspace1, templateVersion1, agent1 := createWorkspaceWithAgent(t, db, org, "duplicate-agent") - require.Equal(t, "duplicate-agent", agent1.Name) + _, resource, agent := createWorkspaceWithAgent(t, db, org, "duplicate-agent") + require.Equal(t, "duplicate-agent", agent.Name) // When: Another agent is created for that workspace with the same name. - job2 := dbgen.ProvisionerJob(t, db, nil, database.ProvisionerJob{ - Type: database.ProvisionerJobTypeWorkspaceBuild, - OrganizationID: org.ID, - }) - build2 := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{ - BuildNumber: builds.Add(1), - JobID: job2.ID, - WorkspaceID: workspace1.ID, - TemplateVersionID: templateVersion1.ID, - }) - resource2 := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{ - JobID: build2.JobID, - }) _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ ID: uuid.New(), CreatedAt: time.Now(), UpdatedAt: time.Now(), Name: "duplicate-agent", // Same name as agent1 - ResourceID: resource2.ID, + ResourceID: resource.ID, AuthToken: uuid.New(), Architecture: "amd64", OperatingSystem: "linux", @@ -4798,7 +4782,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { var pqErr *pq.Error require.True(t, errors.As(err, &pqErr)) require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation - require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this workspace`) + require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this workspace resource`) }) t.Run("SameNamesInDifferentWorkspaces", func(t *testing.T) { From e51c8bef4cfa77543c4d4a81571f584289bd61d2 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 27 May 2025 14:56:46 +0000 Subject: [PATCH 04/12] fix: for child agents, ensure unique across provisioner job Child agents are a new concept so there are very little existing tests for them. This means we can easily make the change for them without having to update a mountain of tests. So they get a special case for checking properly, whilst parent agents have less scrutiny due to the vast number of tests that appear to not work properly. --- coderd/database/dump.sql | 58 ++++++------- ...workspace_agent_name_unique_trigger.up.sql | 58 ++++++------- coderd/database/querier_test.go | 85 +++++++++++++++++-- 3 files changed, 128 insertions(+), 73 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 3a0565f8767dd..12e3fd5bf11ad 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -323,13 +323,30 @@ DECLARE provisioner_job_id uuid; agents_with_name integer; BEGIN - -- Count how many agents in this resource already have - -- the given agent ID. - SELECT COUNT(*) INTO agents_with_name - FROM workspace_agents - WHERE workspace_agents.resource_id = NEW.resource_id - AND workspace_agents.name = NEW.name - AND workspace_agents.id != NEW.id; + IF NEW.parent_id IS NULL THEN + -- Count how many agents in this resource already have + -- the given agent ID. + SELECT COUNT(*) INTO agents_with_name + FROM workspace_agents + WHERE workspace_agents.resource_id = NEW.resource_id + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; + ELSE + SELECT provisioner_jobs.id INTO provisioner_job_id + FROM workspace_resources + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + WHERE workspace_resources.id = NEW.resource_id; + + -- Count how many agents in this provisioner job already have + -- the given agent ID. + SELECT COUNT(*) INTO agents_with_name + FROM workspace_agents + JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + WHERE provisioner_jobs.id = provisioner_job_id + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; + END IF; -- If there's already an agent with this name, raise an error IF agents_with_name > 0 THEN @@ -337,33 +354,6 @@ BEGIN USING ERRCODE = 'unique_violation'; END IF; - -- Get the provisioner_jobs.id for this agent by following the relationship chain: - -- workspace_agents -> workspace_resources -> provisioner_jobs - -- SELECT pj.id INTO provisioner_job_id - -- FROM workspace_resources wr - -- JOIN provisioner_jobs pj ON wr.job_id = pj.id - -- WHERE wr.id = NEW.resource_id; - - -- If we couldn't find a provisioner_job.id, allow the insert (might be a template import or other edge case) - -- IF provisioner_job_id IS NULL THEN - -- RETURN NEW; - -- END IF; - - -- Check if there's already an agent with this name for this provisioner job - -- SELECT COUNT(*) INTO existing_count - -- FROM workspace_agents wa - -- JOIN workspace_resources wr ON wa.resource_id = wr.id - -- JOIN provisioner_jobs pj ON wr.job_id = pj.id - -- WHERE pj.id = provisioner_job_id - -- AND wa.name = NEW.name - -- AND wa.id != NEW.id; - - -- If there's already an agent with this name, raise an error - -- IF existing_count > 0 THEN - -- RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name - -- USING ERRCODE = 'unique_violation'; - -- END IF; - RETURN NEW; END; $$; diff --git a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql index f1743e4e9c35c..81d24f792a96c 100644 --- a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql +++ b/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql @@ -4,13 +4,30 @@ DECLARE provisioner_job_id uuid; agents_with_name integer; BEGIN - -- Count how many agents in this resource already have - -- the given agent ID. - SELECT COUNT(*) INTO agents_with_name - FROM workspace_agents - WHERE workspace_agents.resource_id = NEW.resource_id - AND workspace_agents.name = NEW.name - AND workspace_agents.id != NEW.id; + IF NEW.parent_id IS NULL THEN + -- Count how many agents in this resource already have + -- the given agent ID. + SELECT COUNT(*) INTO agents_with_name + FROM workspace_agents + WHERE workspace_agents.resource_id = NEW.resource_id + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; + ELSE + SELECT provisioner_jobs.id INTO provisioner_job_id + FROM workspace_resources + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + WHERE workspace_resources.id = NEW.resource_id; + + -- Count how many agents in this provisioner job already have + -- the given agent ID. + SELECT COUNT(*) INTO agents_with_name + FROM workspace_agents + JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + WHERE provisioner_jobs.id = provisioner_job_id + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; + END IF; -- If there's already an agent with this name, raise an error IF agents_with_name > 0 THEN @@ -18,33 +35,6 @@ BEGIN USING ERRCODE = 'unique_violation'; END IF; - -- Get the provisioner_jobs.id for this agent by following the relationship chain: - -- workspace_agents -> workspace_resources -> provisioner_jobs - -- SELECT pj.id INTO provisioner_job_id - -- FROM workspace_resources wr - -- JOIN provisioner_jobs pj ON wr.job_id = pj.id - -- WHERE wr.id = NEW.resource_id; - - -- If we couldn't find a provisioner_job.id, allow the insert (might be a template import or other edge case) - -- IF provisioner_job_id IS NULL THEN - -- RETURN NEW; - -- END IF; - - -- Check if there's already an agent with this name for this provisioner job - -- SELECT COUNT(*) INTO existing_count - -- FROM workspace_agents wa - -- JOIN workspace_resources wr ON wa.resource_id = wr.id - -- JOIN provisioner_jobs pj ON wr.job_id = pj.id - -- WHERE pj.id = provisioner_job_id - -- AND wa.name = NEW.name - -- AND wa.id != NEW.id; - - -- If there's already an agent with this name, raise an error - -- IF existing_count > 0 THEN - -- RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name - -- USING ERRCODE = 'unique_violation'; - -- END IF; - RETURN NEW; END; $$ LANGUAGE plpgsql; diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index f2d2cc36dc391..e7b67d8a6a740 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -4714,7 +4714,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { t.Skip("This test makes use of a database trigger not implemented in dbmem") } - createWorkspaceWithAgent := func(t *testing.T, db database.Store, org database.Organization, agentName string) (database.WorkspaceTable, database.WorkspaceResource, database.WorkspaceAgent) { + createWorkspaceWithAgent := func(t *testing.T, db database.Store, org database.Organization, agentName string) (database.WorkspaceBuild, database.WorkspaceResource, database.WorkspaceAgent) { t.Helper() user := dbgen.User(t, db, database.User{}) @@ -4750,10 +4750,10 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { Name: agentName, }) - return workspace, resource, agent + return build, resource, agent } - t.Run("DuplicateNamesInSameWorkspaceBuild", func(t *testing.T) { + t.Run("DuplicateNamesInSameWorkspaceResource", func(t *testing.T) { t.Parallel() db, _ := dbtestutil.NewDB(t) @@ -4761,8 +4761,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { ctx := testutil.Context(t, testutil.WaitShort) // Given: A workspace with an agent - _, resource, agent := createWorkspaceWithAgent(t, db, org, "duplicate-agent") - require.Equal(t, "duplicate-agent", agent.Name) + _, resource, _ := createWorkspaceWithAgent(t, db, org, "duplicate-agent") // When: Another agent is created for that workspace with the same name. _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ @@ -4785,6 +4784,82 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this workspace resource`) }) + t.Run("DuplicateNamesInSameProvisionerJob", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + ctx := testutil.Context(t, testutil.WaitShort) + + // Given: A workspace with an agent + _, resource, agent := createWorkspaceWithAgent(t, db, org, "duplicate-agent") + + // When: A child agent is created for that workspace with the same name. + _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ + ID: uuid.New(), + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: agent.Name, + ResourceID: resource.ID, + AuthToken: uuid.New(), + Architecture: "amd64", + OperatingSystem: "linux", + APIKeyScope: database.AgentKeyScopeEnumAll, + }) + + // Then: We expect it to fail. + require.Error(t, err) + var pqErr *pq.Error + require.True(t, errors.As(err, &pqErr)) + require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation + require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this workspace resource`) + }) + + t.Run("DuplicateChildNamesOverMultipleResources", func(t *testing.T) { + t.Parallel() + + db, _ := dbtestutil.NewDB(t) + org := dbgen.Organization(t, db, database.Organization{}) + ctx := testutil.Context(t, testutil.WaitShort) + + // Given: A workspace with two agents + _, resource1, agent1 := createWorkspaceWithAgent(t, db, org, "parent-agent-1") + + resource2 := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{JobID: resource1.JobID}) + agent2 := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ResourceID: resource2.ID, + Name: "parent-agent-2", + }) + + // Given: One agent has a child agent + agent1Child := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{ + ParentID: uuid.NullUUID{Valid: true, UUID: agent1.ID}, + Name: "child-agent", + ResourceID: resource1.ID, + }) + + // When: A child agent is inserted for the other parent. + _, err := db.InsertWorkspaceAgent(ctx, database.InsertWorkspaceAgentParams{ + ID: uuid.New(), + ParentID: uuid.NullUUID{Valid: true, UUID: agent2.ID}, + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + Name: agent1Child.Name, + ResourceID: resource2.ID, + AuthToken: uuid.New(), + Architecture: "amd64", + OperatingSystem: "linux", + APIKeyScope: database.AgentKeyScopeEnumAll, + }) + + // Then: We expect it to fail. + require.Error(t, err) + var pqErr *pq.Error + require.True(t, errors.As(err, &pqErr)) + require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation + require.Contains(t, pqErr.Message, `workspace agent name "child-agent" already exists in this workspace resource`) + }) + t.Run("SameNamesInDifferentWorkspaces", func(t *testing.T) { t.Parallel() From aae7049fc33cbe0b3dba6dec5f6c9053cb4279de Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 27 May 2025 15:03:11 +0000 Subject: [PATCH 05/12] chore: fix migration number --- ...wn.sql => 000331_workspace_agent_name_unique_trigger.down.sql} | 0 ...r.up.sql => 000331_workspace_agent_name_unique_trigger.up.sql} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename coderd/database/migrations/{000330_workspace_agent_name_unique_trigger.down.sql => 000331_workspace_agent_name_unique_trigger.down.sql} (100%) rename coderd/database/migrations/{000330_workspace_agent_name_unique_trigger.up.sql => 000331_workspace_agent_name_unique_trigger.up.sql} (100%) diff --git a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.down.sql b/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.down.sql similarity index 100% rename from coderd/database/migrations/000330_workspace_agent_name_unique_trigger.down.sql rename to coderd/database/migrations/000331_workspace_agent_name_unique_trigger.down.sql diff --git a/coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql similarity index 100% rename from coderd/database/migrations/000330_workspace_agent_name_unique_trigger.up.sql rename to coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql From 58b8a2f78c4d32c3036e22e33271298f29ba8b1b Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 27 May 2025 15:22:15 +0000 Subject: [PATCH 06/12] chore: ID -> name --- .../000331_workspace_agent_name_unique_trigger.up.sql | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql index 81d24f792a96c..067aa790d6800 100644 --- a/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql +++ b/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql @@ -6,7 +6,7 @@ DECLARE BEGIN IF NEW.parent_id IS NULL THEN -- Count how many agents in this resource already have - -- the given agent ID. + -- the given agent name. SELECT COUNT(*) INTO agents_with_name FROM workspace_agents WHERE workspace_agents.resource_id = NEW.resource_id @@ -19,7 +19,7 @@ BEGIN WHERE workspace_resources.id = NEW.resource_id; -- Count how many agents in this provisioner job already have - -- the given agent ID. + -- the given agent name. SELECT COUNT(*) INTO agents_with_name FROM workspace_agents JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id From f88f17beae4b8b65eb96f6e372e0e39f6e67a1d4 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Tue, 27 May 2025 16:32:37 +0000 Subject: [PATCH 07/12] fix: listen to dean The CI will fail for this as there are test failures for insights. --- coderd/database/dump.sql | 55 ++++++++++--------- ...workspace_agent_name_unique_trigger.up.sql | 53 ++++++++++-------- coderd/database/querier_test.go | 6 +- 3 files changed, 62 insertions(+), 52 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 12e3fd5bf11ad..9eaf66297fff3 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -321,36 +321,41 @@ CREATE FUNCTION check_workspace_agent_name_unique() RETURNS trigger AS $$ DECLARE provisioner_job_id uuid; - agents_with_name integer; + workspace_id_var uuid; + agents_with_name int; BEGIN - IF NEW.parent_id IS NULL THEN - -- Count how many agents in this resource already have - -- the given agent ID. - SELECT COUNT(*) INTO agents_with_name - FROM workspace_agents - WHERE workspace_agents.resource_id = NEW.resource_id - AND workspace_agents.name = NEW.name - AND workspace_agents.id != NEW.id; - ELSE - SELECT provisioner_jobs.id INTO provisioner_job_id - FROM workspace_resources - JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - WHERE workspace_resources.id = NEW.resource_id; - - -- Count how many agents in this provisioner job already have - -- the given agent ID. - SELECT COUNT(*) INTO agents_with_name - FROM workspace_agents - JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id - JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - WHERE provisioner_jobs.id = provisioner_job_id - AND workspace_agents.name = NEW.name - AND workspace_agents.id != NEW.id; + -- Find the provisioner job and workspace the agent is + -- being inserted into. + SELECT INTO provisioner_job_id, workspace_id_var + provisioner_jobs.id, workspaces.id + FROM workspace_resources + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id + JOIN workspaces ON workspaces.id = workspace_builds.workspace_id + WHERE workspace_resources.id = NEW.resource_id; + + -- If there is no workspace or provisioner job attached to the agent, + -- we will allow the insert to happen as there is no need to guarantee + -- uniqueness. + IF workspace_id_var IS NULL OR provisioner_job_id IS NULL THEN + RETURN NEW; END IF; + -- Count how many agents in this provisioner job already have + -- the given agent name. + SELECT COUNT(*) INTO agents_with_name + FROM workspace_agents + JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id + WHERE provisioner_jobs.id = provisioner_job_id + AND workspace_builds.workspace_id = workspace_id_var + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; + -- If there's already an agent with this name, raise an error IF agents_with_name > 0 THEN - RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace resource', NEW.name + RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name USING ERRCODE = 'unique_violation'; END IF; diff --git a/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql index 067aa790d6800..562e0163bd22e 100644 --- a/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql +++ b/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql @@ -2,36 +2,41 @@ CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique() RETURNS TRIGGER AS $$ DECLARE provisioner_job_id uuid; - agents_with_name integer; + workspace_id_var uuid; + agents_with_name int; BEGIN - IF NEW.parent_id IS NULL THEN - -- Count how many agents in this resource already have - -- the given agent name. - SELECT COUNT(*) INTO agents_with_name - FROM workspace_agents - WHERE workspace_agents.resource_id = NEW.resource_id - AND workspace_agents.name = NEW.name - AND workspace_agents.id != NEW.id; - ELSE - SELECT provisioner_jobs.id INTO provisioner_job_id - FROM workspace_resources - JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - WHERE workspace_resources.id = NEW.resource_id; + -- Find the provisioner job and workspace the agent is + -- being inserted into. + SELECT INTO provisioner_job_id, workspace_id_var + provisioner_jobs.id, workspaces.id + FROM workspace_resources + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id + JOIN workspaces ON workspaces.id = workspace_builds.workspace_id + WHERE workspace_resources.id = NEW.resource_id; - -- Count how many agents in this provisioner job already have - -- the given agent name. - SELECT COUNT(*) INTO agents_with_name - FROM workspace_agents - JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id - JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - WHERE provisioner_jobs.id = provisioner_job_id - AND workspace_agents.name = NEW.name - AND workspace_agents.id != NEW.id; + -- If there is no workspace or provisioner job attached to the agent, + -- we will allow the insert to happen as there is no need to guarantee + -- uniqueness. + IF workspace_id_var IS NULL OR provisioner_job_id IS NULL THEN + RETURN NEW; END IF; + -- Count how many agents in this provisioner job already have + -- the given agent name. + SELECT COUNT(*) INTO agents_with_name + FROM workspace_agents + JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id + JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id + WHERE provisioner_jobs.id = provisioner_job_id + AND workspace_builds.workspace_id = workspace_id_var + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; + -- If there's already an agent with this name, raise an error IF agents_with_name > 0 THEN - RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace resource', NEW.name + RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name USING ERRCODE = 'unique_violation'; END IF; diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index e7b67d8a6a740..9cc56e2ae1c5d 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -4781,7 +4781,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { var pqErr *pq.Error require.True(t, errors.As(err, &pqErr)) require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation - require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this workspace resource`) + require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this provisioner job`) }) t.Run("DuplicateNamesInSameProvisionerJob", func(t *testing.T) { @@ -4812,7 +4812,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { var pqErr *pq.Error require.True(t, errors.As(err, &pqErr)) require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation - require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this workspace resource`) + require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this provisioner job`) }) t.Run("DuplicateChildNamesOverMultipleResources", func(t *testing.T) { @@ -4857,7 +4857,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { var pqErr *pq.Error require.True(t, errors.As(err, &pqErr)) require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation - require.Contains(t, pqErr.Message, `workspace agent name "child-agent" already exists in this workspace resource`) + require.Contains(t, pqErr.Message, `workspace agent name "child-agent" already exists in this provisioner job`) }) t.Run("SameNamesInDifferentWorkspaces", func(t *testing.T) { From 74ab8f24c5a406a9a38703f3d7343ccdb0d2bd54 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 28 May 2025 09:36:11 +0000 Subject: [PATCH 08/12] fix: use unique name per agent --- coderd/insights_test.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/coderd/insights_test.go b/coderd/insights_test.go index 47a80df528501..693bb48811acc 100644 --- a/coderd/insights_test.go +++ b/coderd/insights_test.go @@ -609,8 +609,8 @@ func TestTemplateInsights_Golden(t *testing.T) { Name: "example", Type: "aws_instance", Agents: []*proto.Agent{{ - Id: uuid.NewString(), // Doesn't matter, not used in DB. - Name: "dev", + Id: uuid.NewString(), // Doesn't matter, not used in DB. + Name: fmt.Sprintf("dev-%d", len(resources)), // Ensure unique name per agent Auth: &proto.Agent_Token{ Token: authToken.String(), }, @@ -1525,8 +1525,8 @@ func TestUserActivityInsights_Golden(t *testing.T) { Name: "example", Type: "aws_instance", Agents: []*proto.Agent{{ - Id: uuid.NewString(), // Doesn't matter, not used in DB. - Name: "dev", + Id: uuid.NewString(), // Doesn't matter, not used in DB. + Name: fmt.Sprintf("dev-%d", len(resources)), // Ensure unique name per agent Auth: &proto.Agent_Token{ Token: authToken.String(), }, From 585b31183d7db953d361cd0e130bb6c1d8d12526 Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 28 May 2025 09:46:16 +0000 Subject: [PATCH 09/12] chore: feedback --- coderd/database/dump.sql | 14 +++++++------- ...2_workspace_agent_name_unique_trigger.down.sql} | 0 ...332_workspace_agent_name_unique_trigger.up.sql} | 14 +++++++------- 3 files changed, 14 insertions(+), 14 deletions(-) rename coderd/database/migrations/{000331_workspace_agent_name_unique_trigger.down.sql => 000332_workspace_agent_name_unique_trigger.down.sql} (100%) rename coderd/database/migrations/{000331_workspace_agent_name_unique_trigger.up.sql => 000332_workspace_agent_name_unique_trigger.up.sql} (83%) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index e71942e304f88..307fca8802124 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -320,13 +320,13 @@ CREATE FUNCTION check_workspace_agent_name_unique() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE - provisioner_job_id uuid; + provisioner_job_id_var uuid; workspace_id_var uuid; - agents_with_name int; + agents_with_name_var int; BEGIN -- Find the provisioner job and workspace the agent is -- being inserted into. - SELECT INTO provisioner_job_id, workspace_id_var + SELECT INTO provisioner_job_id_var, workspace_id_var provisioner_jobs.id, workspaces.id FROM workspace_resources JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id @@ -337,24 +337,24 @@ BEGIN -- If there is no workspace or provisioner job attached to the agent, -- we will allow the insert to happen as there is no need to guarantee -- uniqueness. - IF workspace_id_var IS NULL OR provisioner_job_id IS NULL THEN + IF workspace_id_var IS NULL OR provisioner_job_id_var IS NULL THEN RETURN NEW; END IF; -- Count how many agents in this provisioner job already have -- the given agent name. - SELECT COUNT(*) INTO agents_with_name + SELECT COUNT(*) INTO agents_with_name_var FROM workspace_agents JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id - WHERE provisioner_jobs.id = provisioner_job_id + WHERE provisioner_jobs.id = provisioner_job_id_var AND workspace_builds.workspace_id = workspace_id_var AND workspace_agents.name = NEW.name AND workspace_agents.id != NEW.id; -- If there's already an agent with this name, raise an error - IF agents_with_name > 0 THEN + IF agents_with_name_var > 0 THEN RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name USING ERRCODE = 'unique_violation'; END IF; diff --git a/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.down.sql b/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.down.sql similarity index 100% rename from coderd/database/migrations/000331_workspace_agent_name_unique_trigger.down.sql rename to coderd/database/migrations/000332_workspace_agent_name_unique_trigger.down.sql diff --git a/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql similarity index 83% rename from coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql rename to coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql index 562e0163bd22e..936991894e6cb 100644 --- a/coderd/database/migrations/000331_workspace_agent_name_unique_trigger.up.sql +++ b/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql @@ -1,13 +1,13 @@ CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique() RETURNS TRIGGER AS $$ DECLARE - provisioner_job_id uuid; + provisioner_job_id_var uuid; workspace_id_var uuid; - agents_with_name int; + agents_with_name_var int; BEGIN -- Find the provisioner job and workspace the agent is -- being inserted into. - SELECT INTO provisioner_job_id, workspace_id_var + SELECT INTO provisioner_job_id_var, workspace_id_var provisioner_jobs.id, workspaces.id FROM workspace_resources JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id @@ -18,24 +18,24 @@ BEGIN -- If there is no workspace or provisioner job attached to the agent, -- we will allow the insert to happen as there is no need to guarantee -- uniqueness. - IF workspace_id_var IS NULL OR provisioner_job_id IS NULL THEN + IF workspace_id_var IS NULL OR provisioner_job_id_var IS NULL THEN RETURN NEW; END IF; -- Count how many agents in this provisioner job already have -- the given agent name. - SELECT COUNT(*) INTO agents_with_name + SELECT COUNT(*) INTO agents_with_name_var FROM workspace_agents JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id - WHERE provisioner_jobs.id = provisioner_job_id + WHERE provisioner_jobs.id = provisioner_job_id_var AND workspace_builds.workspace_id = workspace_id_var AND workspace_agents.name = NEW.name AND workspace_agents.id != NEW.id; -- If there's already an agent with this name, raise an error - IF agents_with_name > 0 THEN + IF agents_with_name_var > 0 THEN RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name USING ERRCODE = 'unique_violation'; END IF; From 5164413a1e114119965420c0469ad2dfc03beccd Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 28 May 2025 10:25:07 +0000 Subject: [PATCH 10/12] chore: some changes - replace spaces with tabs. - remove _var suffixes. - check if provisioner job as a workspace build --- coderd/database/dump.sql | 42 ++++++++-------- ...workspace_agent_name_unique_trigger.up.sql | 48 +++++++++---------- 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 307fca8802124..e5ba58cc0ec83 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -320,46 +320,44 @@ CREATE FUNCTION check_workspace_agent_name_unique() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE - provisioner_job_id_var uuid; - workspace_id_var uuid; - agents_with_name_var int; + provisioner_job_id uuid; + workspace_build_count int; + agents_with_name int; BEGIN - -- Find the provisioner job and workspace the agent is - -- being inserted into. - SELECT INTO provisioner_job_id_var, workspace_id_var - provisioner_jobs.id, workspaces.id + -- Find the provisioner job the workspace agent is being inserted into. + SELECT provisioner_jobs.id INTO provisioner_job_id FROM workspace_resources JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id - JOIN workspaces ON workspaces.id = workspace_builds.workspace_id WHERE workspace_resources.id = NEW.resource_id; - -- If there is no workspace or provisioner job attached to the agent, - -- we will allow the insert to happen as there is no need to guarantee - -- uniqueness. - IF workspace_id_var IS NULL OR provisioner_job_id_var IS NULL THEN + -- Get whether the provisioner job has an associated workspace build + SELECT COUNT(*) INTO workspace_build_count + FROM workspace_builds + WHERE workspace_builds.job_id = provisioner_job_id; + + -- If the provisioner job doesn't have a workspace build, we'll just + -- allow this. + IF workspace_build_count = 0 THEN RETURN NEW; END IF; -- Count how many agents in this provisioner job already have -- the given agent name. - SELECT COUNT(*) INTO agents_with_name_var + SELECT COUNT(*) INTO agents_with_name FROM workspace_agents JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id - WHERE provisioner_jobs.id = provisioner_job_id_var - AND workspace_builds.workspace_id = workspace_id_var + WHERE provisioner_jobs.id = provisioner_job_id AND workspace_agents.name = NEW.name AND workspace_agents.id != NEW.id; -- If there's already an agent with this name, raise an error - IF agents_with_name_var > 0 THEN - RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name - USING ERRCODE = 'unique_violation'; - END IF; + IF agents_with_name > 0 THEN + RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name + USING ERRCODE = 'unique_violation'; + END IF; - RETURN NEW; + RETURN NEW; END; $$; diff --git a/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql index 936991894e6cb..a560ac6c29a75 100644 --- a/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql +++ b/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql @@ -1,50 +1,48 @@ CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique() RETURNS TRIGGER AS $$ DECLARE - provisioner_job_id_var uuid; - workspace_id_var uuid; - agents_with_name_var int; + provisioner_job_id uuid; + workspace_build_count int; + agents_with_name int; BEGIN - -- Find the provisioner job and workspace the agent is - -- being inserted into. - SELECT INTO provisioner_job_id_var, workspace_id_var - provisioner_jobs.id, workspaces.id + -- Find the provisioner job the workspace agent is being inserted into. + SELECT provisioner_jobs.id INTO provisioner_job_id FROM workspace_resources JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id - JOIN workspaces ON workspaces.id = workspace_builds.workspace_id WHERE workspace_resources.id = NEW.resource_id; - -- If there is no workspace or provisioner job attached to the agent, - -- we will allow the insert to happen as there is no need to guarantee - -- uniqueness. - IF workspace_id_var IS NULL OR provisioner_job_id_var IS NULL THEN + -- Get whether the provisioner job has an associated workspace build + SELECT COUNT(*) INTO workspace_build_count + FROM workspace_builds + WHERE workspace_builds.job_id = provisioner_job_id; + + -- If the provisioner job doesn't have a workspace build, we'll just + -- allow this. + IF workspace_build_count = 0 THEN RETURN NEW; END IF; -- Count how many agents in this provisioner job already have -- the given agent name. - SELECT COUNT(*) INTO agents_with_name_var + SELECT COUNT(*) INTO agents_with_name FROM workspace_agents JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - JOIN workspace_builds ON workspace_builds.job_id = provisioner_jobs.id - WHERE provisioner_jobs.id = provisioner_job_id_var - AND workspace_builds.workspace_id = workspace_id_var + WHERE provisioner_jobs.id = provisioner_job_id AND workspace_agents.name = NEW.name AND workspace_agents.id != NEW.id; -- If there's already an agent with this name, raise an error - IF agents_with_name_var > 0 THEN - RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name - USING ERRCODE = 'unique_violation'; - END IF; + IF agents_with_name > 0 THEN + RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name + USING ERRCODE = 'unique_violation'; + END IF; - RETURN NEW; + RETURN NEW; END; $$ LANGUAGE plpgsql; CREATE TRIGGER workspace_agent_name_unique_trigger - BEFORE INSERT OR UPDATE OF name, resource_id ON workspace_agents - FOR EACH ROW - EXECUTE FUNCTION check_workspace_agent_name_unique(); + BEFORE INSERT OR UPDATE OF name, resource_id ON workspace_agents + FOR EACH ROW + EXECUTE FUNCTION check_workspace_agent_name_unique(); From d6edbb5a601c1f43849d23099a2fbcf4d983b4db Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 28 May 2025 12:13:51 +0000 Subject: [PATCH 11/12] chore: improve sql query --- coderd/database/dump.sql | 28 +++++++------------ ...workspace_agent_name_unique_trigger.up.sql | 28 +++++++------------ coderd/database/querier_test.go | 6 ++-- 3 files changed, 23 insertions(+), 39 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index e5ba58cc0ec83..6dcbffc22267b 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -320,40 +320,32 @@ CREATE FUNCTION check_workspace_agent_name_unique() RETURNS trigger LANGUAGE plpgsql AS $$ DECLARE - provisioner_job_id uuid; - workspace_build_count int; + workspace_build_id uuid; agents_with_name int; BEGIN - -- Find the provisioner job the workspace agent is being inserted into. - SELECT provisioner_jobs.id INTO provisioner_job_id + -- Find the workspace build the workspace agent is being inserted into. + SELECT workspace_builds.id INTO workspace_build_id FROM workspace_resources - JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id WHERE workspace_resources.id = NEW.resource_id; - -- Get whether the provisioner job has an associated workspace build - SELECT COUNT(*) INTO workspace_build_count - FROM workspace_builds - WHERE workspace_builds.job_id = provisioner_job_id; - - -- If the provisioner job doesn't have a workspace build, we'll just - -- allow this. - IF workspace_build_count = 0 THEN + -- If the agent doesn't have a workspace build, we'll allow the insert. + IF workspace_build_id IS NULL THEN RETURN NEW; END IF; - -- Count how many agents in this provisioner job already have - -- the given agent name. + -- Count how many agents in this workspace build already have the given agent name. SELECT COUNT(*) INTO agents_with_name FROM workspace_agents JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id - JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - WHERE provisioner_jobs.id = provisioner_job_id + JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id + WHERE workspace_builds.id = workspace_build_id AND workspace_agents.name = NEW.name AND workspace_agents.id != NEW.id; -- If there's already an agent with this name, raise an error IF agents_with_name > 0 THEN - RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name + RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace build', NEW.name USING ERRCODE = 'unique_violation'; END IF; diff --git a/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql index a560ac6c29a75..695b6ec1d8479 100644 --- a/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql +++ b/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql @@ -1,40 +1,32 @@ CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique() RETURNS TRIGGER AS $$ DECLARE - provisioner_job_id uuid; - workspace_build_count int; + workspace_build_id uuid; agents_with_name int; BEGIN - -- Find the provisioner job the workspace agent is being inserted into. - SELECT provisioner_jobs.id INTO provisioner_job_id + -- Find the workspace build the workspace agent is being inserted into. + SELECT workspace_builds.id INTO workspace_build_id FROM workspace_resources - JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id + JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id WHERE workspace_resources.id = NEW.resource_id; - -- Get whether the provisioner job has an associated workspace build - SELECT COUNT(*) INTO workspace_build_count - FROM workspace_builds - WHERE workspace_builds.job_id = provisioner_job_id; - - -- If the provisioner job doesn't have a workspace build, we'll just - -- allow this. - IF workspace_build_count = 0 THEN + -- If the agent doesn't have a workspace build, we'll allow the insert. + IF workspace_build_id IS NULL THEN RETURN NEW; END IF; - -- Count how many agents in this provisioner job already have - -- the given agent name. + -- Count how many agents in this workspace build already have the given agent name. SELECT COUNT(*) INTO agents_with_name FROM workspace_agents JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id - JOIN provisioner_jobs ON provisioner_jobs.id = workspace_resources.job_id - WHERE provisioner_jobs.id = provisioner_job_id + JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id + WHERE workspace_builds.id = workspace_build_id AND workspace_agents.name = NEW.name AND workspace_agents.id != NEW.id; -- If there's already an agent with this name, raise an error IF agents_with_name > 0 THEN - RAISE EXCEPTION 'workspace agent name "%" already exists in this provisioner job', NEW.name + RAISE EXCEPTION 'workspace agent name "%" already exists in this workspace build', NEW.name USING ERRCODE = 'unique_violation'; END IF; diff --git a/coderd/database/querier_test.go b/coderd/database/querier_test.go index 9cc56e2ae1c5d..d482f2891a47c 100644 --- a/coderd/database/querier_test.go +++ b/coderd/database/querier_test.go @@ -4781,7 +4781,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { var pqErr *pq.Error require.True(t, errors.As(err, &pqErr)) require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation - require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this provisioner job`) + require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this workspace build`) }) t.Run("DuplicateNamesInSameProvisionerJob", func(t *testing.T) { @@ -4812,7 +4812,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { var pqErr *pq.Error require.True(t, errors.As(err, &pqErr)) require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation - require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this provisioner job`) + require.Contains(t, pqErr.Message, `workspace agent name "duplicate-agent" already exists in this workspace build`) }) t.Run("DuplicateChildNamesOverMultipleResources", func(t *testing.T) { @@ -4857,7 +4857,7 @@ func TestWorkspaceAgentNameUniqueTrigger(t *testing.T) { var pqErr *pq.Error require.True(t, errors.As(err, &pqErr)) require.Equal(t, pq.ErrorCode("23505"), pqErr.Code) // unique_violation - require.Contains(t, pqErr.Message, `workspace agent name "child-agent" already exists in this provisioner job`) + require.Contains(t, pqErr.Message, `workspace agent name "child-agent" already exists in this workspace build`) }) t.Run("SameNamesInDifferentWorkspaces", func(t *testing.T) { From 479681c99ecef690ac69c23e502f3acb9c3b4bad Mon Sep 17 00:00:00 2001 From: Danielle Maywood Date: Wed, 28 May 2025 13:09:14 +0000 Subject: [PATCH 12/12] chore: feedback - add a comment on trigger - spaces to tabs --- coderd/database/dump.sql | 8 ++++++-- .../000332_workspace_agent_name_unique_trigger.up.sql | 9 +++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 6dcbffc22267b..acb9780b82ea6 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -340,8 +340,8 @@ BEGIN JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id WHERE workspace_builds.id = workspace_build_id - AND workspace_agents.name = NEW.name - AND workspace_agents.id != NEW.id; + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; -- If there's already an agent with this name, raise an error IF agents_with_name > 0 THEN @@ -2812,6 +2812,10 @@ CREATE TRIGGER user_status_change_trigger AFTER INSERT OR UPDATE ON users FOR EA CREATE TRIGGER workspace_agent_name_unique_trigger BEFORE INSERT OR UPDATE OF name, resource_id ON workspace_agents FOR EACH ROW EXECUTE FUNCTION check_workspace_agent_name_unique(); +COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS 'Use a trigger instead of a unique constraint because existing data may violate +the uniqueness requirement. A trigger allows us to enforce uniqueness going +forward without requiring a migration to clean up historical data.'; + ALTER TABLE ONLY api_keys ADD CONSTRAINT api_keys_user_id_uuid_fkey FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE; diff --git a/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql b/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql index 695b6ec1d8479..7b10fcdc1dcde 100644 --- a/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql +++ b/coderd/database/migrations/000332_workspace_agent_name_unique_trigger.up.sql @@ -21,8 +21,8 @@ BEGIN JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id WHERE workspace_builds.id = workspace_build_id - AND workspace_agents.name = NEW.name - AND workspace_agents.id != NEW.id; + AND workspace_agents.name = NEW.name + AND workspace_agents.id != NEW.id; -- If there's already an agent with this name, raise an error IF agents_with_name > 0 THEN @@ -38,3 +38,8 @@ CREATE TRIGGER workspace_agent_name_unique_trigger BEFORE INSERT OR UPDATE OF name, resource_id ON workspace_agents FOR EACH ROW EXECUTE FUNCTION check_workspace_agent_name_unique(); + +COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS +'Use a trigger instead of a unique constraint because existing data may violate +the uniqueness requirement. A trigger allows us to enforce uniqueness going +forward without requiring a migration to clean up historical data.'; 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