From 191dd230ae9b4d55de208f090a3ba2f4ee7f4b4e Mon Sep 17 00:00:00 2001 From: Callum Styan Date: Fri, 15 May 2026 14:36:54 -0700 Subject: [PATCH] feat: add agentfake scaletest subcommand (#25072) This PR builds on top of https://github.com/coder/coder/pull/25070 to add a way of running the larger "fake agent" manager via the existing CLI, pulling in the URL/credentials already set. With this, we can run a pod per scaletest region to act as all the workspaces in that region. This is in a new subcommand `scaletest agentfake` currently. --------- Signed-off-by: Callum Styan --- cli/exp_scaletest.go | 14 +-- cli/exp_scaletest_bridge.go | 2 +- cli/exp_scaletest_dynamicparameters.go | 2 +- cli/exp_scaletest_notifications.go | 2 +- cli/exp_scaletest_prebuilds.go | 2 +- cli/exp_scaletest_taskstatus.go | 2 +- enterprise/cli/exp_scaletest_agentfake.go | 100 ++++++++++++++++++++++ 7 files changed, 112 insertions(+), 12 deletions(-) create mode 100644 enterprise/cli/exp_scaletest_agentfake.go diff --git a/cli/exp_scaletest.go b/cli/exp_scaletest.go index 1a6922c747..06af372e15 100644 --- a/cli/exp_scaletest.go +++ b/cli/exp_scaletest.go @@ -472,7 +472,7 @@ func (f *workspaceTargetFlags) getTargetedWorkspaces(ctx context.Context, client return workspaces[targetStart:targetEnd], nil } -func requireAdmin(ctx context.Context, client *codersdk.Client) (codersdk.User, error) { +func RequireAdmin(ctx context.Context, client *codersdk.Client) (codersdk.User, error) { me, err := client.User(ctx, codersdk.Me) if err != nil { return codersdk.User{}, xerrors.Errorf("fetch current user: %w", err) @@ -617,7 +617,7 @@ func (r *RootCmd) scaletestCleanup() *serpent.Command { ctx := inv.Context() - me, err := requireAdmin(ctx, client) + me, err := RequireAdmin(ctx, client) if err != nil { return err } @@ -851,7 +851,7 @@ func (r *RootCmd) scaletestCreateWorkspaces() *serpent.Command { ctx := inv.Context() - me, err := requireAdmin(ctx, client) + me, err := RequireAdmin(ctx, client) if err != nil { return err } @@ -1051,7 +1051,7 @@ func (r *RootCmd) scaletestCreateWorkspaces() *serpent.Command { { Flag: "no-wait-for-agents", Env: "CODER_SCALETEST_NO_WAIT_FOR_AGENTS", - Description: `Do not wait for agents to start before marking the test as succeeded. This can be useful if you are running the test against a template that does not start the agent quickly.`, + Description: `Do not wait for agents to start before marking the test as succeeded. This can be useful if you are running the test against a template that does not start the agent quickly. This is REQUIRED for templates whose workspaces use coder_external_agent resources, since external agents never connect on their own; pair with "coder exp scaletest agentfake" to drive those agents.`, Value: serpent.BoolOf(&noWaitForAgents), }, { @@ -1177,7 +1177,7 @@ func (r *RootCmd) scaletestWorkspaceUpdates() *serpent.Command { defer stop() ctx = notifyCtx - me, err := requireAdmin(ctx, client) + me, err := RequireAdmin(ctx, client) if err != nil { return err } @@ -1473,7 +1473,7 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command { defer stop() ctx = notifyCtx - me, err := requireAdmin(ctx, client) + me, err := RequireAdmin(ctx, client) if err != nil { return err } @@ -1925,7 +1925,7 @@ func (r *RootCmd) scaletestAutostart() *serpent.Command { defer stop() ctx = notifyCtx - me, err := requireAdmin(ctx, client) + me, err := RequireAdmin(ctx, client) if err != nil { return err } diff --git a/cli/exp_scaletest_bridge.go b/cli/exp_scaletest_bridge.go index c3a040e697..0e6a86d837 100644 --- a/cli/exp_scaletest_bridge.go +++ b/cli/exp_scaletest_bridge.go @@ -90,7 +90,7 @@ Examples: var userConfig createusers.Config if bridge.RequestMode(mode) == bridge.RequestModeBridge { - me, err := requireAdmin(ctx, client) + me, err := RequireAdmin(ctx, client) if err != nil { return err } diff --git a/cli/exp_scaletest_dynamicparameters.go b/cli/exp_scaletest_dynamicparameters.go index 40e11dac61..9624c98755 100644 --- a/cli/exp_scaletest_dynamicparameters.go +++ b/cli/exp_scaletest_dynamicparameters.go @@ -65,7 +65,7 @@ func (r *RootCmd) scaletestDynamicParameters() *serpent.Command { return err } - _, err = requireAdmin(ctx, client) + _, err = RequireAdmin(ctx, client) if err != nil { return err } diff --git a/cli/exp_scaletest_notifications.go b/cli/exp_scaletest_notifications.go index b2e4ba6cf0..6b765bc7d6 100644 --- a/cli/exp_scaletest_notifications.go +++ b/cli/exp_scaletest_notifications.go @@ -61,7 +61,7 @@ func (r *RootCmd) scaletestNotifications() *serpent.Command { defer stop() ctx = notifyCtx - me, err := requireAdmin(ctx, client) + me, err := RequireAdmin(ctx, client) if err != nil { return err } diff --git a/cli/exp_scaletest_prebuilds.go b/cli/exp_scaletest_prebuilds.go index a2d3fd920c..da65c32364 100644 --- a/cli/exp_scaletest_prebuilds.go +++ b/cli/exp_scaletest_prebuilds.go @@ -52,7 +52,7 @@ func (r *RootCmd) scaletestPrebuilds() *serpent.Command { defer stop() ctx = notifyCtx - me, err := requireAdmin(ctx, client) + me, err := RequireAdmin(ctx, client) if err != nil { return err } diff --git a/cli/exp_scaletest_taskstatus.go b/cli/exp_scaletest_taskstatus.go index 578e6e8e12..9d97f05ca9 100644 --- a/cli/exp_scaletest_taskstatus.go +++ b/cli/exp_scaletest_taskstatus.go @@ -67,7 +67,7 @@ After all runners connect, it waits for the baseline duration before triggering return err } - _, err = requireAdmin(ctx, client) + _, err = RequireAdmin(ctx, client) if err != nil { return err } diff --git a/enterprise/cli/exp_scaletest_agentfake.go b/enterprise/cli/exp_scaletest_agentfake.go new file mode 100644 index 0000000000..a6c2e88649 --- /dev/null +++ b/enterprise/cli/exp_scaletest_agentfake.go @@ -0,0 +1,100 @@ +//go:build !slim + +package cli + +import ( + "os/signal" + + "golang.org/x/xerrors" + + agplcli "github.com/coder/coder/v2/cli" + "github.com/coder/coder/v2/enterprise/scaletest/agentfake" + "github.com/coder/serpent" +) + +// AGPLExperimental shadows the embedded RootCmd.AGPLExperimental to inject the +// enterprise-only agentfake scaletest subcommand into the scaletest subtree. +func (r *RootCmd) AGPLExperimental() []*serpent.Command { + cmds := r.RootCmd.AGPLExperimental() + for _, cmd := range cmds { + if cmd.Use == "scaletest" { + cmd.Children = append(cmd.Children, r.scaletestAgentFake()) + } + } + return cmds +} + +func (r *RootCmd) scaletestAgentFake() *serpent.Command { + var ( + template string + owner string + ) + + cmd := &serpent.Command{ + Use: "agentfake", + Short: "Run fake external agents against workspaces of the given template.", + Long: agplcli.FormatExamples( + agplcli.Example{ + Description: "Connect a fake agent for every external-agent workspace built from the template named " + + "\"agentfake-runner\".", + Command: "coder exp scaletest agentfake --template agentfake-runner", + }, + ) + "\n\n" + + "Enumerates external-agent workspaces matching --template (optionally filtered by --owner), " + + "fetches each workspace agent's external-agent credentials, and supervises one in-process fake " + + "agent per token until the command is interrupted.\n\n" + + "Requires a session token whose user is template-admin (or higher) on a deployment licensed " + + "for the workspace external-agent feature; both the workspace builds and the credentials " + + "endpoint are gated server-side. Pair with `coder exp scaletest create-workspaces " + + "--no-wait-for-agents` to seed the workspaces this command will pick up. Workspaces created " + + "after this command starts are NOT picked up; rerun the command after seeding more.", + Handler: func(inv *serpent.Invocation) error { + ctx := inv.Context() + client, err := r.InitClient(inv) + if err != nil { + return err + } + + notifyCtx, stop := signal.NotifyContext(ctx, agplcli.StopSignals...) + defer stop() + ctx = notifyCtx + + if _, err := agplcli.RequireAdmin(ctx, client); err != nil { + return err + } + + if template == "" { + return xerrors.New("--template is required") + } + + logger := inv.Logger + mgr := agentfake.NewManager(client, logger, agentfake.ManagerOptions{ + Template: template, + Owner: owner, + }) + defer mgr.Close() + + if err := mgr.Run(ctx); err != nil { + return xerrors.Errorf("run agentfake manager: %w", err) + } + return nil + }, + } + + cmd.Options = serpent.OptionSet{ + { + Flag: "template", + Env: "CODER_SCALETEST_AGENTFAKE_TEMPLATE", + Description: "Name of the template whose external-agent workspaces should be supervised. Required.", + Value: serpent.StringOf(&template), + }, + { + Flag: "owner", + Env: "CODER_SCALETEST_AGENTFAKE_OWNER", + Description: "Optional workspace-owner filter (username). When empty, all owners' workspaces of the template are included.", + Value: serpent.StringOf(&owner), + }, + } + + return cmd +}