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..8614a80843fba 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,37 @@ 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: "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,
+ 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")
+
+ t.Logf("inserted workspace agent %s (%v) with deleted subagent antagonist %s (%v)", agt.Name, agt.ID, subAgt.Name, subAgt.ID)
+
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..74c5b00bfb2b7 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,
@@ -2216,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
new file mode 100644
index 0000000000000..bc2e791cf10df
--- /dev/null
+++ b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.down.sql
@@ -0,0 +1,96 @@
+-- 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;
+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.';
+
+
+ALTER TABLE workspace_agents
+ DROP COLUMN deleted;
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..7c558e9f4fb74
--- /dev/null
+++ b/coderd/database/migrations/000338_use_deleted_boolean_for_subagents.up.sql
@@ -0,0 +1,99 @@
+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.';
+
+-- 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)));
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..8b4f5abfd0486 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,8 @@ 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
WHEN workspace_agents.first_connected_at IS NULL THEN
@@ -19059,7 +19099,11 @@ 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
+ -- Filter out deleted sub agents.
+ 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..88ccbbae30bbf 100644
--- a/coderd/database/queries/workspaces.sql
+++ b/coderd/database/queries/workspaces.sql
@@ -301,6 +301,8 @@ 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
WHEN workspace_agents.first_connected_at IS NULL THEN
@@ -822,7 +824,11 @@ 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
+ -- Filter out deleted sub agents.
+ 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 |
Field | Tracked |
| active_version_id | true |
activity_bump | true |
allow_user_autostart | true |
allow_user_autostop | true |
allow_user_cancel_workspace_jobs | true |
autostart_block_days_of_week | true |
autostop_requirement_days_of_week | true |
autostop_requirement_weeks | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_name | false |
created_by_username | false |
default_ttl | true |
deleted | false |
deprecated | true |
description | true |
display_name | true |
failure_ttl | true |
group_acl | true |
icon | true |
id | true |
max_port_sharing_level | true |
name | true |
organization_display_name | false |
organization_icon | false |
organization_id | false |
organization_name | false |
provisioner | true |
require_active_version | true |
time_til_dormant | true |
time_til_dormant_autodelete | true |
updated_at | false |
use_classic_parameter_flow | true |
user_acl | true |
|
| TemplateVersion
create, write | Field | Tracked |
| archived | true |
created_at | false |
created_by | true |
created_by_avatar_url | false |
created_by_name | false |
created_by_username | false |
external_auth_providers | false |
has_ai_task | false |
id | true |
job_id | false |
message | false |
name | true |
organization_id | false |
readme | true |
source_example_id | false |
template_id | true |
updated_at | false |
|
| User
create, write, delete | Field | Tracked |
| avatar_url | false |
created_at | false |
deleted | true |
email | true |
github_com_user_id | false |
hashed_one_time_passcode | false |
hashed_password | true |
id | true |
is_system | true |
last_seen_at | false |
login_type | true |
name | true |
one_time_passcode_expires_at | true |
quiet_hours_schedule | true |
rbac_roles | true |
status | true |
updated_at | false |
username | true |
|
-| WorkspaceAgent
connect, disconnect | Field | Tracked |
| api_key_scope | false |
api_version | false |
architecture | false |
auth_instance_id | false |
auth_token | false |
connection_timeout_seconds | false |
created_at | false |
directory | false |
disconnected_at | false |
display_apps | false |
display_order | false |
environment_variables | false |
expanded_directory | false |
first_connected_at | false |
id | false |
instance_metadata | false |
last_connected_at | false |
last_connected_replica_id | false |
lifecycle_state | false |
logs_length | false |
logs_overflowed | false |
motd_file | false |
name | false |
operating_system | false |
parent_id | false |
ready_at | false |
resource_id | false |
resource_metadata | false |
started_at | false |
subsystems | false |
troubleshooting_url | false |
updated_at | false |
version | false |
|
+| WorkspaceAgent
connect, disconnect | Field | Tracked |
| api_key_scope | false |
api_version | false |
architecture | false |
auth_instance_id | false |
auth_token | false |
connection_timeout_seconds | false |
created_at | false |
deleted | false |
directory | false |
disconnected_at | false |
display_apps | false |
display_order | false |
environment_variables | false |
expanded_directory | false |
first_connected_at | false |
id | false |
instance_metadata | false |
last_connected_at | false |
last_connected_replica_id | false |
lifecycle_state | false |
logs_length | false |
logs_overflowed | false |
motd_file | false |
name | false |
operating_system | false |
parent_id | false |
ready_at | false |
resource_id | false |
resource_metadata | false |
started_at | false |
subsystems | false |
troubleshooting_url | false |
updated_at | false |
version | false |
|
| WorkspaceApp
open, close | Field | Tracked |
| agent_id | false |
command | false |
created_at | false |
display_group | false |
display_name | false |
display_order | false |
external | false |
health | false |
healthcheck_interval | false |
healthcheck_threshold | false |
healthcheck_url | false |
hidden | false |
icon | false |
id | false |
open_in | false |
sharing_level | false |
slug | false |
subdomain | false |
url | false |
|
| WorkspaceBuild
start, stop | Field | Tracked |
| ai_tasks_sidebar_app_id | false |
build_number | false |
created_at | false |
daily_cost | false |
deadline | false |
has_ai_task | false |
id | false |
initiator_by_avatar_url | false |
initiator_by_name | false |
initiator_by_username | false |
initiator_id | false |
job_id | false |
max_deadline | false |
provisioner_state | false |
reason | false |
template_version_id | true |
template_version_preset_id | false |
transition | false |
updated_at | false |
workspace_id | false |
|
| WorkspaceProxy
| Field | Tracked |
| created_at | true |
deleted | false |
derp_enabled | true |
derp_only | true |
display_name | true |
icon | true |
id | true |
name | true |
region_id | true |
token_hashed_secret | true |
updated_at | false |
url | true |
version | true |
wildcard_hostname | true |
|
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,