mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: support multiple agents with shared instance-identity auth (#24325)
> 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.
This commit is contained in:
@@ -4422,22 +4422,6 @@ func (q *querier) GetWorkspaceAgentByID(ctx context.Context, id uuid.UUID) (data
|
||||
return q.db.GetWorkspaceAgentByID(ctx, id)
|
||||
}
|
||||
|
||||
// GetWorkspaceAgentByInstanceID might want to be a system call? Unsure exactly,
|
||||
// but this will fail. Need to figure out what AuthInstanceID is, and if it
|
||||
// is essentially an auth token. But the caller using this function is not
|
||||
// an authenticated user. So this authz check will fail.
|
||||
func (q *querier) GetWorkspaceAgentByInstanceID(ctx context.Context, authInstanceID string) (database.WorkspaceAgent, error) {
|
||||
agent, err := q.db.GetWorkspaceAgentByInstanceID(ctx, authInstanceID)
|
||||
if err != nil {
|
||||
return database.WorkspaceAgent{}, err
|
||||
}
|
||||
_, err = q.GetWorkspaceByAgentID(ctx, agent.ID)
|
||||
if err != nil {
|
||||
return database.WorkspaceAgent{}, err
|
||||
}
|
||||
return agent, nil
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspaceAgentDevcontainersByAgentID(ctx context.Context, workspaceAgentID uuid.UUID) ([]database.WorkspaceAgentDevcontainer, error) {
|
||||
_, err := q.GetWorkspaceAgentByID(ctx, workspaceAgentID)
|
||||
if err != nil {
|
||||
@@ -4527,6 +4511,33 @@ func (q *querier) GetWorkspaceAgentUsageStatsAndLabels(ctx context.Context, crea
|
||||
return q.db.GetWorkspaceAgentUsageStatsAndLabels(ctx, createdAt)
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspaceAgentsByInstanceID(ctx context.Context, authInstanceID string) ([]database.WorkspaceAgent, error) {
|
||||
if err := q.authorizeContext(ctx, policy.ActionRead, rbac.ResourceSystem); err == nil {
|
||||
return q.db.GetWorkspaceAgentsByInstanceID(ctx, authInstanceID)
|
||||
}
|
||||
|
||||
agents, err := q.db.GetWorkspaceAgentsByInstanceID(ctx, authInstanceID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Filter to agents whose workspace is accessible. Template-version
|
||||
// agents can share the same instance ID but do not belong to a
|
||||
// workspace, so GetWorkspaceByAgentID returns sql.ErrNoRows for
|
||||
// them. Exclude those agents rather than failing the entire lookup.
|
||||
filtered := make([]database.WorkspaceAgent, 0, len(agents))
|
||||
for _, agent := range agents {
|
||||
_, err = q.GetWorkspaceByAgentID(ctx, agent.ID)
|
||||
if err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
continue
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
filtered = append(filtered, agent)
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func (q *querier) GetWorkspaceAgentsByParentID(ctx context.Context, parentID uuid.UUID) ([]database.WorkspaceAgent, error) {
|
||||
workspace, err := q.db.GetWorkspaceByAgentID(ctx, parentID)
|
||||
if err != nil {
|
||||
|
||||
@@ -3012,13 +3012,16 @@ func (s *MethodTestSuite) TestWorkspace() {
|
||||
dbm.EXPECT().BatchUpdateWorkspaceAgentMetadata(gomock.Any(), arg).Return(nil).AnyTimes()
|
||||
check.Args(arg).Asserts(rbac.ResourceWorkspace.All(), policy.ActionUpdate).Returns()
|
||||
}))
|
||||
s.Run("GetWorkspaceAgentByInstanceID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
s.Run("GetWorkspaceAgentsByInstanceID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
w := testutil.Fake(s.T(), faker, database.Workspace{})
|
||||
agt := testutil.Fake(s.T(), faker, database.WorkspaceAgent{})
|
||||
authInstanceID := "instance-id"
|
||||
dbm.EXPECT().GetWorkspaceAgentByInstanceID(gomock.Any(), authInstanceID).Return(agt, nil).AnyTimes()
|
||||
dbm.EXPECT().GetWorkspaceAgentsByInstanceID(gomock.Any(), authInstanceID).Return([]database.WorkspaceAgent{agt}, nil).AnyTimes()
|
||||
dbm.EXPECT().GetWorkspaceByAgentID(gomock.Any(), agt.ID).Return(w, nil).AnyTimes()
|
||||
check.Args(authInstanceID).Asserts(w, policy.ActionRead).Returns(agt)
|
||||
check.Args(authInstanceID).
|
||||
Asserts(rbac.ResourceSystem, policy.ActionRead, w, policy.ActionRead).
|
||||
Returns([]database.WorkspaceAgent{agt}).
|
||||
FailSystemObjectChecks()
|
||||
}))
|
||||
s.Run("UpdateWorkspaceAgentLifecycleStateByID", s.Mocked(func(dbm *dbmock.MockStore, faker *gofakeit.Faker, check *expects) {
|
||||
w := testutil.Fake(s.T(), faker, database.Workspace{})
|
||||
|
||||
Reference in New Issue
Block a user