mirror of
https://github.com/coder/coder.git
synced 2026-06-03 04:58:23 +00:00
e5707a13d6
> This PR was authored by Mux on behalf of Mike. ## Summary Adds support for multiple peer root workspace agents sharing the same `auth_instance_id`, so AWS, Azure, and GCP instance-identity auth can issue the correct session token for a selected agent instead of assuming a single root agent per instance. ## Problem When a Terraform template attaches two or more `coder_agent` resources (with `auth = "aws-instance-identity"`) to a single compute instance, every agent shares the same cloud instance ID. The existing singular lookup picks whichever agent was created most recently, silently ignoring the others. ## Solution Introduce an optional pre-auth agent selector (`CODER_AGENT_NAME`) and make the server-side lookup ambiguity-aware. **Database layer:** - `GetWorkspaceAgentsByInstanceID` (`:many`): returns all matching root agents for an instance ID. - `GetWorkspaceAgentByInstanceIDAndName` (`:one`): returns the named root agent for disambiguation. **SDK and CLI:** - `agent_name` field added to AWS, Azure, and GCP request structs (`omitempty` for backward compatibility). - `CODER_AGENT_NAME` env var and `--agent-name` flag wired into the agent bootstrap before instance-identity auth runs. **Server handler (`handleAuthInstanceID`):** - When `agent_name` is present: direct lookup by (instance ID, name). - When absent: legacy lookup, then resource-scoped ambiguity check. Returns 409 with available agent names if multiple root agents match. - Whitespace-only names are trimmed and treated as unspecified. - Sub-agents remain excluded (`parent_id IS NULL` filter). **Verification template:** - `examples/templates/aws-multi-agent/` provisions one EC2 instance with two agents (`main` and `dev`), both using instance-identity auth with `CODER_AGENT_NAME` set in the cloud-init user data. ## Backward compatibility Existing single-agent deployments work unchanged. The `agent_name` field is optional with `omitempty`, and the unnamed path preserves today's behavior when only one root agent matches.
488 lines
12 KiB
SQL
488 lines
12 KiB
SQL
-- name: GetWorkspaceAgentByID :one
|
|
SELECT
|
|
*
|
|
FROM
|
|
workspace_agents
|
|
WHERE
|
|
id = $1
|
|
-- Filter out deleted sub agents.
|
|
AND deleted = FALSE;
|
|
|
|
-- name: GetWorkspaceAgentsByInstanceID :many
|
|
SELECT
|
|
*
|
|
FROM
|
|
workspace_agents
|
|
WHERE
|
|
auth_instance_id = @auth_instance_id :: TEXT
|
|
-- Filter out deleted agents.
|
|
AND deleted = FALSE
|
|
-- Filter out sub agents, they do not authenticate with auth_instance_id.
|
|
AND parent_id IS NULL
|
|
ORDER BY
|
|
created_at DESC;
|
|
|
|
-- name: GetWorkspaceAgentsByResourceIDs :many
|
|
SELECT
|
|
*
|
|
FROM
|
|
workspace_agents
|
|
WHERE
|
|
resource_id = ANY(@ids :: uuid [ ])
|
|
-- Filter out deleted sub agents.
|
|
AND deleted = FALSE;
|
|
|
|
-- name: GetWorkspaceAgentsCreatedAfter :many
|
|
SELECT * FROM workspace_agents
|
|
WHERE
|
|
created_at > $1
|
|
-- Filter out deleted sub agents.
|
|
AND deleted = FALSE;
|
|
|
|
-- name: InsertWorkspaceAgent :one
|
|
INSERT INTO
|
|
workspace_agents (
|
|
id,
|
|
parent_id,
|
|
created_at,
|
|
updated_at,
|
|
name,
|
|
resource_id,
|
|
auth_token,
|
|
auth_instance_id,
|
|
architecture,
|
|
environment_variables,
|
|
operating_system,
|
|
directory,
|
|
instance_metadata,
|
|
resource_metadata,
|
|
connection_timeout_seconds,
|
|
troubleshooting_url,
|
|
motd_file,
|
|
display_apps,
|
|
display_order,
|
|
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 *;
|
|
|
|
-- name: UpdateWorkspaceAgentConnectionByID :exec
|
|
UPDATE
|
|
workspace_agents
|
|
SET
|
|
first_connected_at = $2,
|
|
last_connected_at = $3,
|
|
last_connected_replica_id = $4,
|
|
disconnected_at = $5,
|
|
updated_at = $6
|
|
WHERE
|
|
id = $1;
|
|
|
|
-- name: UpdateWorkspaceAgentStartupByID :exec
|
|
UPDATE
|
|
workspace_agents
|
|
SET
|
|
version = $2,
|
|
expanded_directory = $3,
|
|
subsystems = $4,
|
|
api_version = $5
|
|
WHERE
|
|
id = $1;
|
|
|
|
-- name: GetWorkspaceAgentLifecycleStateByID :one
|
|
SELECT
|
|
lifecycle_state,
|
|
started_at,
|
|
ready_at
|
|
FROM
|
|
workspace_agents
|
|
WHERE
|
|
id = $1;
|
|
|
|
|
|
-- name: UpdateWorkspaceAgentLifecycleStateByID :exec
|
|
UPDATE
|
|
workspace_agents
|
|
SET
|
|
lifecycle_state = $2,
|
|
started_at = $3,
|
|
ready_at = $4
|
|
WHERE
|
|
id = $1;
|
|
|
|
-- name: InsertWorkspaceAgentMetadata :exec
|
|
INSERT INTO
|
|
workspace_agent_metadata (
|
|
workspace_agent_id,
|
|
display_name,
|
|
key,
|
|
script,
|
|
timeout,
|
|
interval,
|
|
display_order
|
|
)
|
|
VALUES
|
|
($1, $2, $3, $4, $5, $6, $7);
|
|
|
|
-- name: UpdateWorkspaceAgentMetadata :exec
|
|
WITH metadata AS (
|
|
SELECT
|
|
unnest(sqlc.arg('key')::text[]) AS key,
|
|
unnest(sqlc.arg('value')::text[]) AS value,
|
|
unnest(sqlc.arg('error')::text[]) AS error,
|
|
unnest(sqlc.arg('collected_at')::timestamptz[]) AS collected_at
|
|
)
|
|
UPDATE
|
|
workspace_agent_metadata wam
|
|
SET
|
|
value = m.value,
|
|
error = m.error,
|
|
collected_at = m.collected_at
|
|
FROM
|
|
metadata m
|
|
WHERE
|
|
wam.workspace_agent_id = $1
|
|
AND wam.key = m.key;
|
|
|
|
-- name: BatchUpdateWorkspaceAgentMetadata :exec
|
|
WITH metadata AS (
|
|
SELECT
|
|
unnest(sqlc.arg('workspace_agent_id')::uuid[]) AS workspace_agent_id,
|
|
unnest(sqlc.arg('key')::text[]) AS key,
|
|
unnest(sqlc.arg('value')::text[]) AS value,
|
|
unnest(sqlc.arg('error')::text[]) AS error,
|
|
unnest(sqlc.arg('collected_at')::timestamptz[]) AS collected_at
|
|
)
|
|
UPDATE
|
|
workspace_agent_metadata wam
|
|
SET
|
|
value = m.value,
|
|
error = m.error,
|
|
collected_at = m.collected_at
|
|
FROM
|
|
metadata m
|
|
WHERE
|
|
wam.workspace_agent_id = m.workspace_agent_id
|
|
AND wam.key = m.key;
|
|
|
|
-- name: GetWorkspaceAgentMetadata :many
|
|
SELECT
|
|
*
|
|
FROM
|
|
workspace_agent_metadata
|
|
WHERE
|
|
workspace_agent_id = $1
|
|
AND CASE WHEN COALESCE(array_length(sqlc.arg('keys')::text[], 1), 0) > 0 THEN key = ANY(sqlc.arg('keys')::text[]) ELSE TRUE END;
|
|
|
|
-- name: UpdateWorkspaceAgentLogOverflowByID :exec
|
|
UPDATE
|
|
workspace_agents
|
|
SET
|
|
logs_overflowed = $2
|
|
WHERE
|
|
id = $1;
|
|
|
|
-- name: UpdateWorkspaceAgentDisplayAppsByID :exec
|
|
UPDATE
|
|
workspace_agents
|
|
SET
|
|
display_apps = $2, updated_at = $3
|
|
WHERE
|
|
id = $1;
|
|
|
|
-- name: UpdateWorkspaceAgentDirectoryByID :exec
|
|
UPDATE
|
|
workspace_agents
|
|
SET
|
|
directory = $2, updated_at = $3
|
|
WHERE
|
|
id = $1;
|
|
|
|
-- name: GetWorkspaceAgentLogsAfter :many
|
|
SELECT
|
|
*
|
|
FROM
|
|
workspace_agent_logs
|
|
WHERE
|
|
agent_id = $1
|
|
AND (
|
|
id > @created_after
|
|
) ORDER BY id ASC;
|
|
|
|
-- name: InsertWorkspaceAgentLogs :many
|
|
WITH new_length AS (
|
|
UPDATE workspace_agents SET
|
|
logs_length = logs_length + @output_length WHERE workspace_agents.id = @agent_id
|
|
)
|
|
INSERT INTO
|
|
workspace_agent_logs (agent_id, created_at, output, level, log_source_id)
|
|
SELECT
|
|
@agent_id :: uuid AS agent_id,
|
|
@created_at :: timestamptz AS created_at,
|
|
unnest(@output :: VARCHAR(1024) [ ]) AS output,
|
|
unnest(@level :: log_level [ ]) AS level,
|
|
@log_source_id :: uuid AS log_source_id
|
|
RETURNING workspace_agent_logs.*;
|
|
|
|
-- name: InsertWorkspaceAgentLogSources :many
|
|
INSERT INTO
|
|
workspace_agent_log_sources (workspace_agent_id, created_at, id, display_name, icon)
|
|
SELECT
|
|
@workspace_agent_id :: uuid AS workspace_agent_id,
|
|
@created_at :: timestamptz AS created_at,
|
|
unnest(@id :: uuid [ ]) AS id,
|
|
unnest(@display_name :: VARCHAR(127) [ ]) AS display_name,
|
|
unnest(@icon :: text [ ]) AS icon
|
|
RETURNING workspace_agent_log_sources.*;
|
|
|
|
-- name: GetWorkspaceAgentLogSourcesByAgentIDs :many
|
|
SELECT * FROM workspace_agent_log_sources WHERE workspace_agent_id = ANY(@ids :: uuid [ ]);
|
|
|
|
-- If an agent hasn't connected within the retention period, we purge its logs.
|
|
-- Exception: if the logs are related to the latest build, we keep those around.
|
|
-- Logs can take up a lot of space, so it's important we clean up frequently.
|
|
-- name: DeleteOldWorkspaceAgentLogs :execrows
|
|
WITH
|
|
latest_builds AS (
|
|
SELECT
|
|
workspace_id, max(build_number) AS max_build_number
|
|
FROM
|
|
workspace_builds
|
|
GROUP BY
|
|
workspace_id
|
|
),
|
|
old_agents AS (
|
|
SELECT
|
|
wa.id
|
|
FROM
|
|
workspace_agents AS wa
|
|
JOIN
|
|
workspace_resources AS wr
|
|
ON
|
|
wa.resource_id = wr.id
|
|
JOIN
|
|
workspace_builds AS wb
|
|
ON
|
|
wb.job_id = wr.job_id
|
|
LEFT JOIN
|
|
latest_builds
|
|
ON
|
|
latest_builds.workspace_id = wb.workspace_id
|
|
AND
|
|
latest_builds.max_build_number = wb.build_number
|
|
WHERE
|
|
-- Filter out the latest builds for each workspace.
|
|
latest_builds.workspace_id IS NULL
|
|
AND CASE
|
|
-- If the last time the agent connected was before @threshold
|
|
WHEN wa.last_connected_at IS NOT NULL THEN
|
|
wa.last_connected_at < @threshold :: timestamptz
|
|
-- The agent never connected, and was created before @threshold
|
|
ELSE wa.created_at < @threshold :: timestamptz
|
|
END
|
|
)
|
|
DELETE FROM workspace_agent_logs WHERE agent_id IN (SELECT id FROM old_agents);
|
|
|
|
-- name: GetWorkspaceAgentsInLatestBuildByWorkspaceID :many
|
|
SELECT
|
|
workspace_agents.*
|
|
FROM
|
|
workspace_agents
|
|
JOIN
|
|
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
|
|
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 = (
|
|
SELECT
|
|
MAX(build_number)
|
|
FROM
|
|
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
|
|
workspace_agents.*
|
|
FROM
|
|
workspace_agents
|
|
JOIN
|
|
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
|
|
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
|
|
-- Filter out deleted sub agents.
|
|
AND workspace_agents.deleted = FALSE;
|
|
|
|
-- GetAuthenticatedWorkspaceAgentAndBuildByAuthToken returns an authenticated
|
|
-- workspace agent and its associated build. During normal operation, this is
|
|
-- the latest build. During shutdown, this may be the previous START build while
|
|
-- the STOP build is executing, allowing shutdown scripts to authenticate (see
|
|
-- issue #19467).
|
|
-- name: GetAuthenticatedWorkspaceAgentAndBuildByAuthToken :one
|
|
SELECT
|
|
sqlc.embed(workspaces),
|
|
sqlc.embed(workspace_agents),
|
|
sqlc.embed(workspace_build_with_user),
|
|
tasks.id AS task_id
|
|
FROM
|
|
workspace_agents
|
|
JOIN
|
|
workspace_resources
|
|
ON
|
|
workspace_agents.resource_id = workspace_resources.id
|
|
JOIN
|
|
workspace_build_with_user
|
|
ON
|
|
workspace_resources.job_id = workspace_build_with_user.job_id
|
|
JOIN
|
|
workspaces
|
|
ON
|
|
workspace_build_with_user.workspace_id = workspaces.id
|
|
LEFT JOIN
|
|
tasks
|
|
ON
|
|
tasks.workspace_id = workspaces.id
|
|
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, with exception for shutdown case.
|
|
-- Use CASE for short-circuiting: check normal case first (most common), then shutdown case.
|
|
AND CASE
|
|
-- Normal case: Agent's build is the latest build.
|
|
WHEN workspace_build_with_user.build_number = (
|
|
SELECT
|
|
MAX(build_number)
|
|
FROM
|
|
workspace_builds
|
|
WHERE
|
|
workspace_id = workspace_build_with_user.workspace_id
|
|
) THEN TRUE
|
|
-- Shutdown case: Agent from previous START build during STOP build execution.
|
|
WHEN workspace_build_with_user.transition = 'start'
|
|
-- Agent's START build job succeeded.
|
|
AND (SELECT job_status FROM provisioner_jobs WHERE id = workspace_build_with_user.job_id) = 'succeeded'
|
|
-- Latest build is a STOP build whose job is still active,
|
|
-- and agent's build is immediately previous.
|
|
AND EXISTS (
|
|
SELECT 1
|
|
FROM workspace_builds latest
|
|
JOIN provisioner_jobs pj ON pj.id = latest.job_id
|
|
WHERE latest.workspace_id = workspace_build_with_user.workspace_id
|
|
AND latest.build_number = workspace_build_with_user.build_number + 1
|
|
AND latest.build_number = (
|
|
SELECT MAX(build_number)
|
|
FROM workspace_builds l2
|
|
WHERE l2.workspace_id = latest.workspace_id
|
|
)
|
|
AND latest.transition = 'stop'
|
|
AND pj.job_status IN ('pending', 'running')
|
|
) THEN TRUE
|
|
ELSE FALSE
|
|
END
|
|
;
|
|
|
|
-- name: InsertWorkspaceAgentScriptTimings :one
|
|
INSERT INTO
|
|
workspace_agent_script_timings (
|
|
script_id,
|
|
started_at,
|
|
ended_at,
|
|
exit_code,
|
|
stage,
|
|
status
|
|
)
|
|
VALUES
|
|
($1, $2, $3, $4, $5, $6)
|
|
RETURNING workspace_agent_script_timings.*;
|
|
|
|
-- name: GetWorkspaceAgentScriptTimingsByBuildID :many
|
|
SELECT
|
|
DISTINCT ON (workspace_agent_script_timings.script_id) workspace_agent_script_timings.*,
|
|
workspace_agent_scripts.display_name,
|
|
workspace_agents.id as workspace_agent_id,
|
|
workspace_agents.name as workspace_agent_name
|
|
FROM workspace_agent_script_timings
|
|
INNER JOIN workspace_agent_scripts ON workspace_agent_scripts.id = workspace_agent_script_timings.script_id
|
|
INNER JOIN workspace_agents ON workspace_agents.id = workspace_agent_scripts.workspace_agent_id
|
|
INNER JOIN workspace_resources ON workspace_resources.id = workspace_agents.resource_id
|
|
INNER JOIN workspace_builds ON workspace_builds.job_id = workspace_resources.job_id
|
|
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
|
|
AND deleted = FALSE;
|
|
|
|
-- name: DeleteWorkspaceSubAgentByID :exec
|
|
UPDATE
|
|
workspace_agents
|
|
SET
|
|
deleted = TRUE
|
|
WHERE
|
|
id = $1
|
|
AND parent_id IS NOT NULL
|
|
AND deleted = FALSE;
|
|
|
|
-- name: GetWorkspaceAgentsForMetrics :many
|
|
SELECT
|
|
w.id as workspace_id,
|
|
w.name as workspace_name,
|
|
u.username as owner_username,
|
|
t.name as template_name,
|
|
tv.name as template_version_name,
|
|
sqlc.embed(workspace_agents)
|
|
FROM workspaces w
|
|
JOIN users u ON w.owner_id = u.id
|
|
JOIN templates t ON w.template_id = t.id
|
|
JOIN workspace_builds wb ON w.id = wb.workspace_id
|
|
LEFT JOIN template_versions tv ON wb.template_version_id = tv.id
|
|
JOIN workspace_resources wr ON wb.job_id = wr.job_id
|
|
JOIN workspace_agents ON wr.id = workspace_agents.resource_id
|
|
WHERE w.deleted = false
|
|
AND wb.build_number = (
|
|
SELECT MAX(wb2.build_number)
|
|
FROM workspace_builds wb2
|
|
WHERE wb2.workspace_id = w.id
|
|
)
|
|
AND workspace_agents.deleted = FALSE;
|
|
|
|
-- name: GetWorkspaceAgentAndWorkspaceByID :one
|
|
SELECT
|
|
sqlc.embed(workspace_agents),
|
|
sqlc.embed(workspaces),
|
|
users.username as owner_username
|
|
FROM
|
|
workspace_agents
|
|
JOIN
|
|
workspace_resources ON workspace_agents.resource_id = workspace_resources.id
|
|
JOIN
|
|
provisioner_jobs ON workspace_resources.job_id = provisioner_jobs.id
|
|
JOIN
|
|
workspace_builds ON provisioner_jobs.id = workspace_builds.job_id
|
|
JOIN
|
|
workspaces ON workspace_builds.workspace_id = workspaces.id
|
|
JOIN
|
|
users ON workspaces.owner_id = users.id
|
|
WHERE
|
|
workspace_agents.id = @id
|
|
AND workspace_agents.deleted = FALSE
|
|
AND provisioner_jobs.type = 'workspace_build'::provisioner_job_type
|
|
AND workspaces.deleted = FALSE
|
|
AND users.deleted = FALSE
|
|
LIMIT 1;
|