From 7d73585206cbed44d266a3179ef5ec312830b310 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Tue, 17 Jun 2025 15:37:47 +0000 Subject: [PATCH 1/6] fix(coderd): mark sub agent deletion via boolean instead of delete Fixes coder/internal#685 --- coderd/agentapi/subagent_test.go | 13 ++-- coderd/database/dbgen/dbgen.go | 31 ++++++++- coderd/database/dbmem/dbmem.go | 22 +++++-- coderd/database/dump.sql | 6 +- ...use_deleted_boolean_for_subagents.down.sql | 52 +++++++++++++++ ...8_use_deleted_boolean_for_subagents.up.sql | 55 ++++++++++++++++ coderd/database/models.go | 2 + coderd/database/queries.sql.go | 64 +++++++++++++++---- coderd/database/queries/workspaceagents.sql | 43 +++++++++++-- coderd/database/queries/workspaces.sql | 6 +- docs/admin/security/audit-logs.md | 2 +- enterprise/audit/table.go | 1 + 12 files changed, 260 insertions(+), 37 deletions(-) create mode 100644 coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql create mode 100644 coderd/database/migrations/000338_use_deleted_boolean_for_subagents.up.sql diff --git a/coderd/agentapi/subagent_test.go b/coderd/agentapi/subagent_test.go index cd7c892189fa5..3fa2bed1ead85 100644 --- a/coderd/agentapi/subagent_test.go +++ b/coderd/agentapi/subagent_test.go @@ -875,14 +875,9 @@ func TestSubAgentAPI(t *testing.T) { require.NoError(t, err) }) - t.Run("DeletesWorkspaceApps", func(t *testing.T) { + t.Run("DeleteRetainsWorkspaceApps", func(t *testing.T) { t.Parallel() - // Skip test on in-memory database since CASCADE DELETE is not implemented - if !dbtestutil.WillUsePostgres() { - t.Skip("CASCADE DELETE behavior requires PostgreSQL") - } - log := testutil.Logger(t) ctx := testutil.Context(t, testutil.WaitShort) clock := quartz.NewMock(t) @@ -931,11 +926,11 @@ func TestSubAgentAPI(t *testing.T) { _, err = api.Database.GetWorkspaceAgentByID(dbauthz.AsSystemRestricted(ctx), subAgentID) //nolint:gocritic // this is a test. require.ErrorIs(t, err, sql.ErrNoRows) - // And: The apps are also deleted (due to CASCADE DELETE) - // Use raw database since authorization layer requires agent to exist + // And: The apps are *retained* to avoid causing issues + // where the resources are expected to be present. appsAfterDeletion, err := db.GetWorkspaceAppsByAgentID(ctx, subAgentID) require.NoError(t, err) - require.Empty(t, appsAfterDeletion) + require.NotEmpty(t, appsAfterDeletion) }) }) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index aabce08b717d7..947c07bf37f36 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -209,7 +209,7 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen }, ConnectionTimeoutSeconds: takeFirst(orig.ConnectionTimeoutSeconds, 3600), TroubleshootingURL: takeFirst(orig.TroubleshootingURL, "https://example.com"), - MOTDFile: takeFirst(orig.TroubleshootingURL, ""), + MOTDFile: takeFirst(orig.MOTDFile, ""), DisplayApps: append([]database.DisplayApp{}, orig.DisplayApps...), DisplayOrder: takeFirst(orig.DisplayOrder, 1), APIKeyScope: takeFirst(orig.APIKeyScope, database.AgentKeyScopeEnumAll), @@ -226,6 +226,35 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen }) require.NoError(t, err, "update workspace agent first connected at") } + + // Add a test antagonist. For every agent we add a deleted sub agent + // to discover cases where deletion should be handled. + subAgt, err := db.InsertWorkspaceAgent(genCtx, database.InsertWorkspaceAgentParams{ + ID: uuid.New(), + ParentID: uuid.NullUUID{UUID: agt.ID, Valid: true}, + CreatedAt: dbtime.Now(), + UpdatedAt: dbtime.Now(), + Name: testutil.GetRandomName(t), + ResourceID: agt.ResourceID, + AuthToken: uuid.New(), + AuthInstanceID: sql.NullString{}, + Architecture: agt.Architecture, + EnvironmentVariables: pqtype.NullRawMessage{}, + OperatingSystem: agt.OperatingSystem, + Directory: agt.Directory, + InstanceMetadata: pqtype.NullRawMessage{}, + ResourceMetadata: pqtype.NullRawMessage{}, + ConnectionTimeoutSeconds: agt.ConnectionTimeoutSeconds, + TroubleshootingURL: agt.TroubleshootingURL, + MOTDFile: "", + DisplayApps: nil, + DisplayOrder: agt.DisplayOrder, + APIKeyScope: agt.APIKeyScope, + }) + require.NoError(t, err, "insert workspace agent subagent antagonist") + err = db.DeleteWorkspaceSubAgentByID(genCtx, subAgt.ID) + require.NoError(t, err, "delete workspace agent subagent antagonist") + return agt } diff --git a/coderd/database/dbmem/dbmem.go b/coderd/database/dbmem/dbmem.go index ab2dd923dab47..0a7877b7022b7 100644 --- a/coderd/database/dbmem/dbmem.go +++ b/coderd/database/dbmem/dbmem.go @@ -792,7 +792,7 @@ func (q *FakeQuerier) getWorkspaceAgentByIDNoLock(_ context.Context, id uuid.UUI // The schema sorts this by created at, so we iterate the array backwards. for i := len(q.workspaceAgents) - 1; i >= 0; i-- { agent := q.workspaceAgents[i] - if agent.ID == id { + if !agent.Deleted && agent.ID == id { return agent, nil } } @@ -802,6 +802,9 @@ func (q *FakeQuerier) getWorkspaceAgentByIDNoLock(_ context.Context, id uuid.UUI func (q *FakeQuerier) getWorkspaceAgentsByResourceIDsNoLock(_ context.Context, resourceIDs []uuid.UUID) ([]database.WorkspaceAgent, error) { workspaceAgents := make([]database.WorkspaceAgent, 0) for _, agent := range q.workspaceAgents { + if agent.Deleted { + continue + } for _, resourceID := range resourceIDs { if agent.ResourceID != resourceID { continue @@ -2543,13 +2546,13 @@ func (q *FakeQuerier) DeleteWorkspaceAgentPortSharesByTemplate(_ context.Context return nil } -func (q *FakeQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error { +func (q *FakeQuerier) DeleteWorkspaceSubAgentByID(_ context.Context, id uuid.UUID) error { q.mutex.Lock() defer q.mutex.Unlock() for i, agent := range q.workspaceAgents { if agent.ID == id && agent.ParentID.Valid { - q.workspaceAgents = slices.Delete(q.workspaceAgents, i, i+1) + q.workspaceAgents[i].Deleted = true return nil } } @@ -7066,6 +7069,10 @@ func (q *FakeQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(_ context.Conte latestBuildNumber := make(map[uuid.UUID]int32) for _, agt := range q.workspaceAgents { + if agt.Deleted { + continue + } + // get the related workspace and user for _, res := range q.workspaceResources { if agt.ResourceID != res.ID { @@ -7135,7 +7142,7 @@ func (q *FakeQuerier) GetWorkspaceAgentByInstanceID(_ context.Context, instanceI // The schema sorts this by created at, so we iterate the array backwards. for i := len(q.workspaceAgents) - 1; i >= 0; i-- { agent := q.workspaceAgents[i] - if agent.AuthInstanceID.Valid && agent.AuthInstanceID.String == instanceID { + if !agent.Deleted && agent.AuthInstanceID.Valid && agent.AuthInstanceID.String == instanceID { return agent, nil } } @@ -7695,13 +7702,13 @@ func (q *FakeQuerier) GetWorkspaceAgentUsageStatsAndLabels(_ context.Context, cr return stats, nil } -func (q *FakeQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]database.WorkspaceAgent, error) { +func (q *FakeQuerier) GetWorkspaceAgentsByParentID(_ context.Context, parentID uuid.UUID) ([]database.WorkspaceAgent, error) { q.mutex.RLock() defer q.mutex.RUnlock() workspaceAgents := make([]database.WorkspaceAgent, 0) for _, agent := range q.workspaceAgents { - if !agent.ParentID.Valid || agent.ParentID.UUID != parentID { + if !agent.ParentID.Valid || agent.ParentID.UUID != parentID || agent.Deleted { continue } @@ -7748,6 +7755,9 @@ func (q *FakeQuerier) GetWorkspaceAgentsCreatedAfter(_ context.Context, after ti workspaceAgents := make([]database.WorkspaceAgent, 0) for _, agent := range q.workspaceAgents { + if agent.Deleted { + continue + } if agent.CreatedAt.After(after) { workspaceAgents = append(workspaceAgents, agent) } diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index 457ba8e65ce5a..a39cea6070026 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -358,7 +358,8 @@ BEGIN 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.id != NEW.id + AND workspace_agents.deleted = FALSE; -- Ensure we only count non-deleted agents. -- If there's already an agent with this name, raise an error IF agents_with_name > 0 THEN @@ -1916,6 +1917,7 @@ CREATE TABLE workspace_agents ( display_order integer DEFAULT 0 NOT NULL, parent_id uuid, api_key_scope agent_key_scope_enum DEFAULT 'all'::agent_key_scope_enum NOT NULL, + deleted boolean DEFAULT false NOT NULL, CONSTRAINT max_logs_length CHECK ((logs_length <= 1048576)), CONSTRAINT subsystems_not_none CHECK ((NOT ('none'::workspace_agent_subsystem = ANY (subsystems)))) ); @@ -1944,6 +1946,8 @@ COMMENT ON COLUMN workspace_agents.display_order IS 'Specifies the order in whic COMMENT ON COLUMN workspace_agents.api_key_scope IS 'Defines the scope of the API key associated with the agent. ''all'' allows access to everything, ''no_user_data'' restricts it to exclude user data.'; +COMMENT ON COLUMN workspace_agents.deleted IS 'Indicates whether or not the agent has been deleted. This is currently only applicable to sub agents.'; + CREATE UNLOGGED TABLE workspace_app_audit_sessions ( agent_id uuid NOT NULL, app_id uuid NOT NULL, diff --git a/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql new file mode 100644 index 0000000000000..4db9d9a97902f --- /dev/null +++ b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql @@ -0,0 +1,52 @@ +ALTER TABLE workspace_agents + DROP COLUMN deleted; + +-- Restore trigger without deleted check. +DROP TRIGGER IF EXISTS workspace_agent_name_unique_trigger ON workspace_agents; +DROP FUNCTION IF EXISTS check_workspace_agent_name_unique(); + +CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique() +RETURNS TRIGGER AS $$ +DECLARE + workspace_build_id uuid; + agents_with_name int; +BEGIN + -- Find the workspace build the workspace agent is being inserted into. + SELECT workspace_builds.id INTO workspace_build_id + FROM workspace_resources + JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id + WHERE workspace_resources.id = NEW.resource_id; + + -- 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 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 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 workspace build', 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(); + +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.'; diff --git a/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.up.sql b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.up.sql new file mode 100644 index 0000000000000..933069c52c545 --- /dev/null +++ b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.up.sql @@ -0,0 +1,55 @@ +ALTER TABLE workspace_agents + ADD COLUMN deleted BOOLEAN NOT NULL DEFAULT FALSE; + +COMMENT ON COLUMN workspace_agents.deleted IS 'Indicates whether or not the agent has been deleted. This is currently only applicable to sub agents.'; + +-- Recreate the trigger with deleted check. +DROP TRIGGER IF EXISTS workspace_agent_name_unique_trigger ON workspace_agents; +DROP FUNCTION IF EXISTS check_workspace_agent_name_unique(); + +CREATE OR REPLACE FUNCTION check_workspace_agent_name_unique() +RETURNS TRIGGER AS $$ +DECLARE + workspace_build_id uuid; + agents_with_name int; +BEGIN + -- Find the workspace build the workspace agent is being inserted into. + SELECT workspace_builds.id INTO workspace_build_id + FROM workspace_resources + JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id + WHERE workspace_resources.id = NEW.resource_id; + + -- 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 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 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.deleted = FALSE; -- Ensure we only count non-deleted agents. + + -- 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 build', 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(); + +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.'; diff --git a/coderd/database/models.go b/coderd/database/models.go index c54a218d4b41d..831055cfcb314 100644 --- a/coderd/database/models.go +++ b/coderd/database/models.go @@ -3628,6 +3628,8 @@ type WorkspaceAgent struct { ParentID uuid.NullUUID `db:"parent_id" json:"parent_id"` // Defines the scope of the API key associated with the agent. 'all' allows access to everything, 'no_user_data' restricts it to exclude user data. APIKeyScope AgentKeyScopeEnum `db:"api_key_scope" json:"api_key_scope"` + // Indicates whether or not the agent has been deleted. This is currently only applicable to sub agents. + Deleted bool `db:"deleted" json:"deleted"` } // Workspace agent devcontainer configuration diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index 3b44aae2d294f..f4aa1418f7546 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -14176,7 +14176,14 @@ func (q *sqlQuerier) DeleteOldWorkspaceAgentLogs(ctx context.Context, threshold } const deleteWorkspaceSubAgentByID = `-- name: DeleteWorkspaceSubAgentByID :exec -DELETE FROM workspace_agents WHERE id = $1 AND parent_id IS NOT NULL +UPDATE + workspace_agents +SET + deleted = TRUE +WHERE + id = $1 + AND parent_id IS NOT NULL + AND deleted = FALSE ` func (q *sqlQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UUID) error { @@ -14187,7 +14194,7 @@ func (q *sqlQuerier) DeleteWorkspaceSubAgentByID(ctx context.Context, id uuid.UU const getWorkspaceAgentAndLatestBuildByAuthToken = `-- name: GetWorkspaceAgentAndLatestBuildByAuthToken :one SELECT workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, - workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, + workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted, workspace_build_with_user.id, workspace_build_with_user.created_at, workspace_build_with_user.updated_at, workspace_build_with_user.workspace_id, workspace_build_with_user.template_version_id, workspace_build_with_user.build_number, workspace_build_with_user.transition, workspace_build_with_user.initiator_id, workspace_build_with_user.provisioner_state, workspace_build_with_user.job_id, workspace_build_with_user.deadline, workspace_build_with_user.reason, workspace_build_with_user.daily_cost, workspace_build_with_user.max_deadline, workspace_build_with_user.template_version_preset_id, workspace_build_with_user.has_ai_task, workspace_build_with_user.ai_tasks_sidebar_app_id, workspace_build_with_user.initiator_by_avatar_url, workspace_build_with_user.initiator_by_username, workspace_build_with_user.initiator_by_name FROM workspace_agents @@ -14207,6 +14214,8 @@ WHERE -- This should only match 1 agent, so 1 returned row or 0. workspace_agents.auth_token = $1::uuid AND workspaces.deleted = FALSE + -- Filter out deleted sub agents. + AND workspace_agents.deleted = FALSE -- Filter out builds that are not the latest. AND workspace_build_with_user.build_number = ( -- Select from workspace_builds as it's one less join compared @@ -14279,6 +14288,7 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont &i.WorkspaceAgent.DisplayOrder, &i.WorkspaceAgent.ParentID, &i.WorkspaceAgent.APIKeyScope, + &i.WorkspaceAgent.Deleted, &i.WorkspaceBuild.ID, &i.WorkspaceBuild.CreatedAt, &i.WorkspaceBuild.UpdatedAt, @@ -14305,11 +14315,13 @@ func (q *sqlQuerier) GetWorkspaceAgentAndLatestBuildByAuthToken(ctx context.Cont const getWorkspaceAgentByID = `-- name: GetWorkspaceAgentByID :one SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted FROM workspace_agents WHERE id = $1 + -- Filter out deleted sub agents. + AND deleted = FALSE ` func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (WorkspaceAgent, error) { @@ -14349,17 +14361,20 @@ func (q *sqlQuerier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (W &i.DisplayOrder, &i.ParentID, &i.APIKeyScope, + &i.Deleted, ) return i, err } const getWorkspaceAgentByInstanceID = `-- name: GetWorkspaceAgentByInstanceID :one SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted FROM workspace_agents WHERE auth_instance_id = $1 :: TEXT + -- Filter out deleted sub agents. + AND deleted = FALSE ORDER BY created_at DESC ` @@ -14401,6 +14416,7 @@ func (q *sqlQuerier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInst &i.DisplayOrder, &i.ParentID, &i.APIKeyScope, + &i.Deleted, ) return i, err } @@ -14619,7 +14635,13 @@ func (q *sqlQuerier) GetWorkspaceAgentScriptTimingsByBuildID(ctx context.Context } const getWorkspaceAgentsByParentID = `-- name: GetWorkspaceAgentsByParentID :many -SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE parent_id = $1::uuid +SELECT + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted +FROM + workspace_agents +WHERE + parent_id = $1::uuid + AND deleted = FALSE ` func (q *sqlQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]WorkspaceAgent, error) { @@ -14665,6 +14687,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID &i.DisplayOrder, &i.ParentID, &i.APIKeyScope, + &i.Deleted, ); err != nil { return nil, err } @@ -14681,11 +14704,13 @@ func (q *sqlQuerier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID const getWorkspaceAgentsByResourceIDs = `-- name: GetWorkspaceAgentsByResourceIDs :many SELECT - id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope + id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted FROM workspace_agents WHERE resource_id = ANY($1 :: uuid [ ]) + -- Filter out deleted sub agents. + AND deleted = FALSE ` func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids []uuid.UUID) ([]WorkspaceAgent, error) { @@ -14731,6 +14756,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] &i.DisplayOrder, &i.ParentID, &i.APIKeyScope, + &i.Deleted, ); err != nil { return nil, err } @@ -14747,7 +14773,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByResourceIDs(ctx context.Context, ids [] const getWorkspaceAgentsByWorkspaceAndBuildNumber = `-- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many SELECT - workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope + workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted FROM workspace_agents JOIN @@ -14757,6 +14783,8 @@ JOIN WHERE workspace_builds.workspace_id = $1 :: uuid AND workspace_builds.build_number = $2 :: int + -- Filter out deleted sub agents. + AND workspace_agents.deleted = FALSE ` type GetWorkspaceAgentsByWorkspaceAndBuildNumberParams struct { @@ -14807,6 +14835,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Con &i.DisplayOrder, &i.ParentID, &i.APIKeyScope, + &i.Deleted, ); err != nil { return nil, err } @@ -14822,7 +14851,11 @@ func (q *sqlQuerier) GetWorkspaceAgentsByWorkspaceAndBuildNumber(ctx context.Con } const getWorkspaceAgentsCreatedAfter = `-- name: GetWorkspaceAgentsCreatedAfter :many -SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope FROM workspace_agents WHERE created_at > $1 +SELECT id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted FROM workspace_agents +WHERE + created_at > $1 + -- Filter out deleted sub agents. + AND deleted = FALSE ` func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, createdAt time.Time) ([]WorkspaceAgent, error) { @@ -14868,6 +14901,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created &i.DisplayOrder, &i.ParentID, &i.APIKeyScope, + &i.Deleted, ); err != nil { return nil, err } @@ -14884,7 +14918,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsCreatedAfter(ctx context.Context, created const getWorkspaceAgentsInLatestBuildByWorkspaceID = `-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many SELECT - workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope + workspace_agents.id, workspace_agents.created_at, workspace_agents.updated_at, workspace_agents.name, workspace_agents.first_connected_at, workspace_agents.last_connected_at, workspace_agents.disconnected_at, workspace_agents.resource_id, workspace_agents.auth_token, workspace_agents.auth_instance_id, workspace_agents.architecture, workspace_agents.environment_variables, workspace_agents.operating_system, workspace_agents.instance_metadata, workspace_agents.resource_metadata, workspace_agents.directory, workspace_agents.version, workspace_agents.last_connected_replica_id, workspace_agents.connection_timeout_seconds, workspace_agents.troubleshooting_url, workspace_agents.motd_file, workspace_agents.lifecycle_state, workspace_agents.expanded_directory, workspace_agents.logs_length, workspace_agents.logs_overflowed, workspace_agents.started_at, workspace_agents.ready_at, workspace_agents.subsystems, workspace_agents.display_apps, workspace_agents.api_version, workspace_agents.display_order, workspace_agents.parent_id, workspace_agents.api_key_scope, workspace_agents.deleted FROM workspace_agents JOIN @@ -14901,6 +14935,8 @@ WHERE WHERE wb.workspace_id = $1 :: uuid ) + -- Filter out deleted sub agents. + AND workspace_agents.deleted = FALSE ` func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Context, workspaceID uuid.UUID) ([]WorkspaceAgent, error) { @@ -14946,6 +14982,7 @@ func (q *sqlQuerier) GetWorkspaceAgentsInLatestBuildByWorkspaceID(ctx context.Co &i.DisplayOrder, &i.ParentID, &i.APIKeyScope, + &i.Deleted, ); err != nil { return nil, err } @@ -14985,7 +15022,7 @@ INSERT INTO api_key_scope ) VALUES - ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope + ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20) RETURNING id, created_at, updated_at, name, first_connected_at, last_connected_at, disconnected_at, resource_id, auth_token, auth_instance_id, architecture, environment_variables, operating_system, instance_metadata, resource_metadata, directory, version, last_connected_replica_id, connection_timeout_seconds, troubleshooting_url, motd_file, lifecycle_state, expanded_directory, logs_length, logs_overflowed, started_at, ready_at, subsystems, display_apps, api_version, display_order, parent_id, api_key_scope, deleted ` type InsertWorkspaceAgentParams struct { @@ -15069,6 +15106,7 @@ func (q *sqlQuerier) InsertWorkspaceAgent(ctx context.Context, arg InsertWorkspa &i.DisplayOrder, &i.ParentID, &i.APIKeyScope, + &i.Deleted, ) return i, err } @@ -18757,6 +18795,7 @@ WHERE WHERE workspace_resources.job_id = latest_build.provisioner_job_id AND latest_build.transition = 'start'::workspace_transition AND + workspace_agents.deleted = FALSE AND $13 = ( CASE WHEN workspace_agents.first_connected_at IS NULL THEN @@ -19059,7 +19098,10 @@ LEFT JOIN LATERAL ( workspace_agents.name as agent_name, job_id FROM workspace_resources - JOIN workspace_agents ON workspace_agents.resource_id = workspace_resources.id + JOIN workspace_agents ON ( + workspace_agents.resource_id = workspace_resources.id + AND workspace_agents.deleted = FALSE + ) WHERE job_id = latest_build.job_id ) resources ON true WHERE diff --git a/coderd/database/queries/workspaceagents.sql b/coderd/database/queries/workspaceagents.sql index f831ff8e3cae2..c67435d7cbd06 100644 --- a/coderd/database/queries/workspaceagents.sql +++ b/coderd/database/queries/workspaceagents.sql @@ -4,7 +4,9 @@ SELECT FROM workspace_agents WHERE - id = $1; + id = $1 + -- Filter out deleted sub agents. + AND deleted = FALSE; -- name: GetWorkspaceAgentByInstanceID :one SELECT @@ -13,6 +15,8 @@ FROM workspace_agents WHERE auth_instance_id = @auth_instance_id :: TEXT + -- Filter out deleted sub agents. + AND deleted = FALSE ORDER BY created_at DESC; @@ -22,10 +26,16 @@ SELECT FROM workspace_agents WHERE - resource_id = ANY(@ids :: uuid [ ]); + resource_id = ANY(@ids :: uuid [ ]) + -- Filter out deleted sub agents. + AND deleted = FALSE; -- name: GetWorkspaceAgentsCreatedAfter :many -SELECT * FROM workspace_agents WHERE created_at > $1; +SELECT * FROM workspace_agents +WHERE + created_at > $1 + -- Filter out deleted sub agents. + AND deleted = FALSE; -- name: InsertWorkspaceAgent :one INSERT INTO @@ -252,7 +262,9 @@ WHERE workspace_builds AS wb WHERE wb.workspace_id = @workspace_id :: uuid - ); + ) + -- Filter out deleted sub agents. + AND workspace_agents.deleted = FALSE; -- name: GetWorkspaceAgentsByWorkspaceAndBuildNumber :many SELECT @@ -265,7 +277,9 @@ JOIN workspace_builds ON workspace_resources.job_id = workspace_builds.job_id WHERE workspace_builds.workspace_id = @workspace_id :: uuid AND - workspace_builds.build_number = @build_number :: int; + workspace_builds.build_number = @build_number :: int + -- Filter out deleted sub agents. + AND workspace_agents.deleted = FALSE; -- name: GetWorkspaceAgentAndLatestBuildByAuthToken :one SELECT @@ -290,6 +304,8 @@ WHERE -- This should only match 1 agent, so 1 returned row or 0. workspace_agents.auth_token = @auth_token::uuid AND workspaces.deleted = FALSE + -- Filter out deleted sub agents. + AND workspace_agents.deleted = FALSE -- Filter out builds that are not the latest. AND workspace_build_with_user.build_number = ( -- Select from workspace_builds as it's one less join compared @@ -332,7 +348,20 @@ WHERE workspace_builds.id = $1 ORDER BY workspace_agent_script_timings.script_id, workspace_agent_script_timings.started_at; -- name: GetWorkspaceAgentsByParentID :many -SELECT * FROM workspace_agents WHERE parent_id = @parent_id::uuid; +SELECT + * +FROM + workspace_agents +WHERE + parent_id = @parent_id::uuid + AND deleted = FALSE; -- name: DeleteWorkspaceSubAgentByID :exec -DELETE FROM workspace_agents WHERE id = $1 AND parent_id IS NOT NULL; +UPDATE + workspace_agents +SET + deleted = TRUE +WHERE + id = $1 + AND parent_id IS NOT NULL + AND deleted = FALSE; diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index d439ae2aa9944..12c92bfd1af32 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -301,6 +301,7 @@ WHERE WHERE workspace_resources.job_id = latest_build.provisioner_job_id AND latest_build.transition = 'start'::workspace_transition AND + workspace_agents.deleted = FALSE AND @has_agent = ( CASE WHEN workspace_agents.first_connected_at IS NULL THEN @@ -822,7 +823,10 @@ LEFT JOIN LATERAL ( workspace_agents.name as agent_name, job_id FROM workspace_resources - JOIN workspace_agents ON workspace_agents.resource_id = workspace_resources.id + JOIN workspace_agents ON ( + workspace_agents.resource_id = workspace_resources.id + AND workspace_agents.deleted = FALSE + ) WHERE job_id = latest_build.job_id ) resources ON true WHERE diff --git a/docs/admin/security/audit-logs.md b/docs/admin/security/audit-logs.md index 080e864fcb866..7b0b852419f21 100644 --- a/docs/admin/security/audit-logs.md +++ b/docs/admin/security/audit-logs.md @@ -29,7 +29,7 @@ We track the following resources: | Template
write, delete | |
FieldTracked
active_version_idtrue
activity_bumptrue
allow_user_autostarttrue
allow_user_autostoptrue
allow_user_cancel_workspace_jobstrue
autostart_block_days_of_weektrue
autostop_requirement_days_of_weektrue
autostop_requirement_weekstrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
default_ttltrue
deletedfalse
deprecatedtrue
descriptiontrue
display_nametrue
failure_ttltrue
group_acltrue
icontrue
idtrue
max_port_sharing_leveltrue
nametrue
organization_display_namefalse
organization_iconfalse
organization_idfalse
organization_namefalse
provisionertrue
require_active_versiontrue
time_til_dormanttrue
time_til_dormant_autodeletetrue
updated_atfalse
use_classic_parameter_flowtrue
user_acltrue
| | TemplateVersion
create, write | |
FieldTracked
archivedtrue
created_atfalse
created_bytrue
created_by_avatar_urlfalse
created_by_namefalse
created_by_usernamefalse
external_auth_providersfalse
has_ai_taskfalse
idtrue
job_idfalse
messagefalse
nametrue
organization_idfalse
readmetrue
source_example_idfalse
template_idtrue
updated_atfalse
| | User
create, write, delete | |
FieldTracked
avatar_urlfalse
created_atfalse
deletedtrue
emailtrue
github_com_user_idfalse
hashed_one_time_passcodefalse
hashed_passwordtrue
idtrue
is_systemtrue
last_seen_atfalse
login_typetrue
nametrue
one_time_passcode_expires_attrue
quiet_hours_scheduletrue
rbac_rolestrue
statustrue
updated_atfalse
usernametrue
| -| WorkspaceAgent
connect, disconnect | |
FieldTracked
api_key_scopefalse
api_versionfalse
architecturefalse
auth_instance_idfalse
auth_tokenfalse
connection_timeout_secondsfalse
created_atfalse
directoryfalse
disconnected_atfalse
display_appsfalse
display_orderfalse
environment_variablesfalse
expanded_directoryfalse
first_connected_atfalse
idfalse
instance_metadatafalse
last_connected_atfalse
last_connected_replica_idfalse
lifecycle_statefalse
logs_lengthfalse
logs_overflowedfalse
motd_filefalse
namefalse
operating_systemfalse
parent_idfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| +| WorkspaceAgent
connect, disconnect | |
FieldTracked
api_key_scopefalse
api_versionfalse
architecturefalse
auth_instance_idfalse
auth_tokenfalse
connection_timeout_secondsfalse
created_atfalse
deletedfalse
directoryfalse
disconnected_atfalse
display_appsfalse
display_orderfalse
environment_variablesfalse
expanded_directoryfalse
first_connected_atfalse
idfalse
instance_metadatafalse
last_connected_atfalse
last_connected_replica_idfalse
lifecycle_statefalse
logs_lengthfalse
logs_overflowedfalse
motd_filefalse
namefalse
operating_systemfalse
parent_idfalse
ready_atfalse
resource_idfalse
resource_metadatafalse
started_atfalse
subsystemsfalse
troubleshooting_urlfalse
updated_atfalse
versionfalse
| | WorkspaceApp
open, close | |
FieldTracked
agent_idfalse
commandfalse
created_atfalse
display_groupfalse
display_namefalse
display_orderfalse
externalfalse
healthfalse
healthcheck_intervalfalse
healthcheck_thresholdfalse
healthcheck_urlfalse
hiddenfalse
iconfalse
idfalse
open_infalse
sharing_levelfalse
slugfalse
subdomainfalse
urlfalse
| | WorkspaceBuild
start, stop | |
FieldTracked
ai_tasks_sidebar_app_idfalse
build_numberfalse
created_atfalse
daily_costfalse
deadlinefalse
has_ai_taskfalse
idfalse
initiator_by_avatar_urlfalse
initiator_by_namefalse
initiator_by_usernamefalse
initiator_idfalse
job_idfalse
max_deadlinefalse
provisioner_statefalse
reasonfalse
template_version_idtrue
template_version_preset_idfalse
transitionfalse
updated_atfalse
workspace_idfalse
| | WorkspaceProxy
| |
FieldTracked
created_attrue
deletedfalse
derp_enabledtrue
derp_onlytrue
display_nametrue
icontrue
idtrue
nametrue
region_idtrue
token_hashed_secrettrue
updated_atfalse
urltrue
versiontrue
wildcard_hostnametrue
| diff --git a/enterprise/audit/table.go b/enterprise/audit/table.go index ffb79810ee2c3..bd4987bae24e2 100644 --- a/enterprise/audit/table.go +++ b/enterprise/audit/table.go @@ -351,6 +351,7 @@ var auditableResourcesTypes = map[any]map[string]Action{ "display_order": ActionIgnore, "parent_id": ActionIgnore, "api_key_scope": ActionIgnore, + "deleted": ActionIgnore, }, &database.WorkspaceApp{}: { "id": ActionIgnore, From 106e8caf84fa9a090771d28a1b239304f7a3ca1e Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 18 Jun 2025 11:38:40 +0000 Subject: [PATCH 2/6] fix prebuilds view --- coderd/database/dump.sql | 2 +- ...use_deleted_boolean_for_subagents.down.sql | 43 ++++++++++++++++++ ...8_use_deleted_boolean_for_subagents.up.sql | 44 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) diff --git a/coderd/database/dump.sql b/coderd/database/dump.sql index a39cea6070026..74c5b00bfb2b7 100644 --- a/coderd/database/dump.sql +++ b/coderd/database/dump.sql @@ -2220,7 +2220,7 @@ CREATE VIEW workspace_prebuilds AS FROM (((workspaces w JOIN workspace_latest_builds wlb ON ((wlb.workspace_id = w.id))) JOIN workspace_resources wr ON ((wr.job_id = wlb.job_id))) - JOIN workspace_agents wa ON ((wa.resource_id = wr.id))) + JOIN workspace_agents wa ON (((wa.resource_id = wr.id) AND (wa.deleted = false)))) WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) GROUP BY w.id ), current_presets AS ( diff --git a/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql index 4db9d9a97902f..e2ee4dbbab1db 100644 --- a/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql +++ b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql @@ -50,3 +50,46 @@ 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.'; + +-- Restore prebuilds, previously modified in 000323_workspace_latest_builds_optimization.up.sql. +DROP VIEW workspace_prebuilds; + +CREATE VIEW workspace_prebuilds AS + WITH all_prebuilds AS ( + SELECT w.id, + w.name, + w.template_id, + w.created_at + FROM workspaces w + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + ), workspaces_with_latest_presets AS ( + SELECT DISTINCT ON (workspace_builds.workspace_id) workspace_builds.workspace_id, + workspace_builds.template_version_preset_id + FROM workspace_builds + WHERE (workspace_builds.template_version_preset_id IS NOT NULL) + ORDER BY workspace_builds.workspace_id, workspace_builds.build_number DESC + ), workspaces_with_agents_status AS ( + SELECT w.id AS workspace_id, + bool_and((wa.lifecycle_state = 'ready'::workspace_agent_lifecycle_state)) AS ready + FROM (((workspaces w + JOIN workspace_latest_builds wlb ON ((wlb.workspace_id = w.id))) + JOIN workspace_resources wr ON ((wr.job_id = wlb.job_id))) + JOIN workspace_agents wa ON ((wa.resource_id = wr.id))) + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + GROUP BY w.id + ), current_presets AS ( + SELECT w.id AS prebuild_id, + wlp.template_version_preset_id + FROM (workspaces w + JOIN workspaces_with_latest_presets wlp ON ((wlp.workspace_id = w.id))) + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + ) + SELECT p.id, + p.name, + p.template_id, + p.created_at, + COALESCE(a.ready, false) AS ready, + cp.template_version_preset_id AS current_preset_id + FROM ((all_prebuilds p + LEFT JOIN workspaces_with_agents_status a ON ((a.workspace_id = p.id))) + JOIN current_presets cp ON ((cp.prebuild_id = p.id))); diff --git a/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.up.sql b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.up.sql index 933069c52c545..7c558e9f4fb74 100644 --- a/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.up.sql +++ b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.up.sql @@ -53,3 +53,47 @@ 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.'; + +-- Handle agent deletion in prebuilds, previously modified in 000323_workspace_latest_builds_optimization.up.sql. +DROP VIEW workspace_prebuilds; + +CREATE VIEW workspace_prebuilds AS + WITH all_prebuilds AS ( + SELECT w.id, + w.name, + w.template_id, + w.created_at + FROM workspaces w + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + ), workspaces_with_latest_presets AS ( + SELECT DISTINCT ON (workspace_builds.workspace_id) workspace_builds.workspace_id, + workspace_builds.template_version_preset_id + FROM workspace_builds + WHERE (workspace_builds.template_version_preset_id IS NOT NULL) + ORDER BY workspace_builds.workspace_id, workspace_builds.build_number DESC + ), workspaces_with_agents_status AS ( + SELECT w.id AS workspace_id, + bool_and((wa.lifecycle_state = 'ready'::workspace_agent_lifecycle_state)) AS ready + FROM (((workspaces w + JOIN workspace_latest_builds wlb ON ((wlb.workspace_id = w.id))) + JOIN workspace_resources wr ON ((wr.job_id = wlb.job_id))) + -- ADD: deleted check for sub agents. + JOIN workspace_agents wa ON ((wa.resource_id = wr.id AND wa.deleted = FALSE))) + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + GROUP BY w.id + ), current_presets AS ( + SELECT w.id AS prebuild_id, + wlp.template_version_preset_id + FROM (workspaces w + JOIN workspaces_with_latest_presets wlp ON ((wlp.workspace_id = w.id))) + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + ) + SELECT p.id, + p.name, + p.template_id, + p.created_at, + COALESCE(a.ready, false) AS ready, + cp.template_version_preset_id AS current_preset_id + FROM ((all_prebuilds p + LEFT JOIN workspaces_with_agents_status a ON ((a.workspace_id = p.id))) + JOIN current_presets cp ON ((cp.prebuild_id = p.id))); From 1c8443336d75448188164f94c9ae78da879f39fe Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 18 Jun 2025 11:48:04 +0000 Subject: [PATCH 3/6] fix order --- ...use_deleted_boolean_for_subagents.down.sql | 87 ++++++++++--------- 1 file changed, 44 insertions(+), 43 deletions(-) diff --git a/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql index e2ee4dbbab1db..bc2e791cf10df 100644 --- a/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql +++ b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql @@ -1,5 +1,45 @@ -ALTER TABLE workspace_agents - DROP COLUMN deleted; +-- Restore prebuilds, previously modified in 000323_workspace_latest_builds_optimization.up.sql. +DROP VIEW workspace_prebuilds; + +CREATE VIEW workspace_prebuilds AS + WITH all_prebuilds AS ( + SELECT w.id, + w.name, + w.template_id, + w.created_at + FROM workspaces w + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + ), workspaces_with_latest_presets AS ( + SELECT DISTINCT ON (workspace_builds.workspace_id) workspace_builds.workspace_id, + workspace_builds.template_version_preset_id + FROM workspace_builds + WHERE (workspace_builds.template_version_preset_id IS NOT NULL) + ORDER BY workspace_builds.workspace_id, workspace_builds.build_number DESC + ), workspaces_with_agents_status AS ( + SELECT w.id AS workspace_id, + bool_and((wa.lifecycle_state = 'ready'::workspace_agent_lifecycle_state)) AS ready + FROM (((workspaces w + JOIN workspace_latest_builds wlb ON ((wlb.workspace_id = w.id))) + JOIN workspace_resources wr ON ((wr.job_id = wlb.job_id))) + JOIN workspace_agents wa ON ((wa.resource_id = wr.id))) + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + GROUP BY w.id + ), current_presets AS ( + SELECT w.id AS prebuild_id, + wlp.template_version_preset_id + FROM (workspaces w + JOIN workspaces_with_latest_presets wlp ON ((wlp.workspace_id = w.id))) + WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) + ) + SELECT p.id, + p.name, + p.template_id, + p.created_at, + COALESCE(a.ready, false) AS ready, + cp.template_version_preset_id AS current_preset_id + FROM ((all_prebuilds p + LEFT JOIN workspaces_with_agents_status a ON ((a.workspace_id = p.id))) + JOIN current_presets cp ON ((cp.prebuild_id = p.id))); -- Restore trigger without deleted check. DROP TRIGGER IF EXISTS workspace_agent_name_unique_trigger ON workspace_agents; @@ -51,45 +91,6 @@ COMMENT ON TRIGGER workspace_agent_name_unique_trigger ON workspace_agents IS the uniqueness requirement. A trigger allows us to enforce uniqueness going forward without requiring a migration to clean up historical data.'; --- Restore prebuilds, previously modified in 000323_workspace_latest_builds_optimization.up.sql. -DROP VIEW workspace_prebuilds; -CREATE VIEW workspace_prebuilds AS - WITH all_prebuilds AS ( - SELECT w.id, - w.name, - w.template_id, - w.created_at - FROM workspaces w - WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) - ), workspaces_with_latest_presets AS ( - SELECT DISTINCT ON (workspace_builds.workspace_id) workspace_builds.workspace_id, - workspace_builds.template_version_preset_id - FROM workspace_builds - WHERE (workspace_builds.template_version_preset_id IS NOT NULL) - ORDER BY workspace_builds.workspace_id, workspace_builds.build_number DESC - ), workspaces_with_agents_status AS ( - SELECT w.id AS workspace_id, - bool_and((wa.lifecycle_state = 'ready'::workspace_agent_lifecycle_state)) AS ready - FROM (((workspaces w - JOIN workspace_latest_builds wlb ON ((wlb.workspace_id = w.id))) - JOIN workspace_resources wr ON ((wr.job_id = wlb.job_id))) - JOIN workspace_agents wa ON ((wa.resource_id = wr.id))) - WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) - GROUP BY w.id - ), current_presets AS ( - SELECT w.id AS prebuild_id, - wlp.template_version_preset_id - FROM (workspaces w - JOIN workspaces_with_latest_presets wlp ON ((wlp.workspace_id = w.id))) - WHERE (w.owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'::uuid) - ) - SELECT p.id, - p.name, - p.template_id, - p.created_at, - COALESCE(a.ready, false) AS ready, - cp.template_version_preset_id AS current_preset_id - FROM ((all_prebuilds p - LEFT JOIN workspaces_with_agents_status a ON ((a.workspace_id = p.id))) - JOIN current_presets cp ON ((cp.prebuild_id = p.id))); +ALTER TABLE workspace_agents + DROP COLUMN deleted; From 774337d1f896b3bba63160404e7e050045a229a9 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 18 Jun 2025 11:53:21 +0000 Subject: [PATCH 4/6] add log --- coderd/database/dbgen/dbgen.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index 947c07bf37f36..e3a33d813bfbd 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -255,6 +255,8 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen err = db.DeleteWorkspaceSubAgentByID(genCtx, subAgt.ID) require.NoError(t, err, "delete workspace agent subagent antagonist") + t.Logf("inserted workspace agent %s (%v) with deleted subagent antagonist %s (%v)", agt.Name, agt.ID, subAgt.Name, subAgt.ID) + return agt } From 609f4077d65b77e2edca63dc8f307d7be7154447 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 18 Jun 2025 12:18:06 +0000 Subject: [PATCH 5/6] add comment to a few more places --- coderd/database/queries.sql.go | 2 ++ coderd/database/queries/workspaces.sql | 2 ++ 2 files changed, 4 insertions(+) diff --git a/coderd/database/queries.sql.go b/coderd/database/queries.sql.go index f4aa1418f7546..8b4f5abfd0486 100644 --- a/coderd/database/queries.sql.go +++ b/coderd/database/queries.sql.go @@ -18795,6 +18795,7 @@ WHERE WHERE workspace_resources.job_id = latest_build.provisioner_job_id AND latest_build.transition = 'start'::workspace_transition AND + -- Filter out deleted sub agents. workspace_agents.deleted = FALSE AND $13 = ( CASE @@ -19100,6 +19101,7 @@ LEFT JOIN LATERAL ( FROM workspace_resources JOIN workspace_agents ON ( workspace_agents.resource_id = workspace_resources.id + -- Filter out deleted sub agents. AND workspace_agents.deleted = FALSE ) WHERE job_id = latest_build.job_id diff --git a/coderd/database/queries/workspaces.sql b/coderd/database/queries/workspaces.sql index 12c92bfd1af32..88ccbbae30bbf 100644 --- a/coderd/database/queries/workspaces.sql +++ b/coderd/database/queries/workspaces.sql @@ -301,6 +301,7 @@ WHERE WHERE workspace_resources.job_id = latest_build.provisioner_job_id AND latest_build.transition = 'start'::workspace_transition AND + -- Filter out deleted sub agents. workspace_agents.deleted = FALSE AND @has_agent = ( CASE @@ -825,6 +826,7 @@ LEFT JOIN LATERAL ( FROM workspace_resources JOIN workspace_agents ON ( workspace_agents.resource_id = workspace_resources.id + -- Filter out deleted sub agents. AND workspace_agents.deleted = FALSE ) WHERE job_id = latest_build.job_id From bc370a8f0a24395d5c7398775a753ba5f1421293 Mon Sep 17 00:00:00 2001 From: Mathias Fredriksson Date: Wed, 18 Jun 2025 12:40:21 +0000 Subject: [PATCH 6/6] troubleshooting "url" --- coderd/database/dbgen/dbgen.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/coderd/database/dbgen/dbgen.go b/coderd/database/dbgen/dbgen.go index e3a33d813bfbd..8614a80843fba 100644 --- a/coderd/database/dbgen/dbgen.go +++ b/coderd/database/dbgen/dbgen.go @@ -245,7 +245,7 @@ func WorkspaceAgent(t testing.TB, db database.Store, orig database.WorkspaceAgen InstanceMetadata: pqtype.NullRawMessage{}, ResourceMetadata: pqtype.NullRawMessage{}, ConnectionTimeoutSeconds: agt.ConnectionTimeoutSeconds, - TroubleshootingURL: agt.TroubleshootingURL, + TroubleshootingURL: "I AM A TEST ANTAGONIST AND I AM HERE TO MESS UP YOUR TESTS. IF YOU SEE ME, SOMETHING IS WRONG AND SUB AGENT DELETION MAY NOT BE HANDLED CORRECTLY IN A QUERY.", MOTDFile: "", DisplayApps: nil, DisplayOrder: agt.DisplayOrder,