From 6abde1e4ba1c31f3ba58df9823ea414681752acf Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 29 Aug 2025 08:45:03 +0000 Subject: [PATCH 1/4] chore: refactor codersdk.Client creation into builder --- codersdk/agentsdk/agentsdk.go | 6 +-- codersdk/builder.go | 97 +++++++++++++++++++++++++++++++++++ codersdk/client.go | 15 +++--- 3 files changed, 108 insertions(+), 10 deletions(-) create mode 100644 codersdk/builder.go diff --git a/codersdk/agentsdk/agentsdk.go b/codersdk/agentsdk/agentsdk.go index 3b865f04ad8a6..cd0a03ed5420e 100644 --- a/codersdk/agentsdk/agentsdk.go +++ b/codersdk/agentsdk/agentsdk.go @@ -46,9 +46,9 @@ var ExternalLogSourceID = uuid.MustParse("3b579bf4-1ed8-4b99-87a8-e9a1e3410410") type SessionTokenSetup func(client *codersdk.Client) RefreshableSessionTokenProvider func New(serverURL *url.URL, setup SessionTokenSetup) *Client { - c := codersdk.New(serverURL) - provider := setup(c) - c.SessionTokenProvider = provider + b := codersdk.NewClientBuilder(serverURL) + provider := setup(b.DangerouslyGetUnbuiltClient()) + c := b.SessionTokenProvider(provider).Build() return &Client{ SDK: c, RefreshableSessionTokenProvider: provider, diff --git a/codersdk/builder.go b/codersdk/builder.go new file mode 100644 index 0000000000000..8a7c005e71c53 --- /dev/null +++ b/codersdk/builder.go @@ -0,0 +1,97 @@ +package codersdk + +import ( + "io" + "net/http" + "net/url" + + "cdr.dev/slog" +) + +// ClientBuilder is a builder for a Client. +// @typescript-ignore ClientBuilder +type ClientBuilder struct { + serverURL *url.URL + httpClient *http.Client + sessionTokenProvider SessionTokenProvider + logger slog.Logger + logBodies bool + plainLogger io.Writer + trace bool + disableDirectConnections bool + + // unbuiltClient is the client that is built when Build is called. Used for cases where the code building the client + // needs to reference the client while building it. + unbuiltClient *Client +} + +func NewClientBuilder(serverURL *url.URL) *ClientBuilder { + return &ClientBuilder{ + serverURL: serverURL, + httpClient: &http.Client{}, + sessionTokenProvider: FixedSessionTokenProvider{}, + unbuiltClient: &Client{}, + } +} + +func (b *ClientBuilder) SessionToken(sessionToken string) *ClientBuilder { + b.sessionTokenProvider = FixedSessionTokenProvider{SessionToken: sessionToken} + return b +} + +func (b *ClientBuilder) SessionTokenProvider(sessionTokenProvider SessionTokenProvider) *ClientBuilder { + b.sessionTokenProvider = sessionTokenProvider + return b +} + +func (b *ClientBuilder) Logger(logger slog.Logger) *ClientBuilder { + b.logger = logger + return b +} + +func (b *ClientBuilder) LogBodies(logBodies bool) *ClientBuilder { + b.logBodies = logBodies + return b +} + +func (b *ClientBuilder) PlainLogger(plainLogger io.Writer) *ClientBuilder { + b.plainLogger = plainLogger + return b +} + +func (b *ClientBuilder) Trace() *ClientBuilder { + b.trace = true + return b +} + +func (b *ClientBuilder) DisableDirectConnections() *ClientBuilder { + b.disableDirectConnections = true + return b +} + +func (b *ClientBuilder) HTTPClient(httpClient *http.Client) *ClientBuilder { + b.httpClient = httpClient + return b +} + +// DangerouslyGetUnbuiltClient returns the unbuilt client. +// +// This is dangerous to use because the client can be modified after it is built. Only use this if you need a reference +// to the client during the building process, e.g. to give to a client component like the SessionTokenProvider. +func (b *ClientBuilder) DangerouslyGetUnbuiltClient() *Client { + return b.unbuiltClient +} + +func (b *ClientBuilder) Build() *Client { + client := b.unbuiltClient + b.unbuiltClient = &Client{} // clear this so that the client cannot be modified after it is built + client.URL = b.serverURL + client.HTTPClient = b.httpClient + client.SessionTokenProvider = b.sessionTokenProvider + client.logger = b.logger + client.logBodies = b.logBodies + client.PlainLogger = b.plainLogger + client.Trace = b.trace + client.DisableDirectConnections = b.disableDirectConnections + return client +} diff --git a/codersdk/client.go b/codersdk/client.go index b6f10465e3a07..e40af48e1f710 100644 --- a/codersdk/client.go +++ b/codersdk/client.go @@ -106,6 +106,7 @@ var loggableMimeTypes = map[string]struct{}{ } // New creates a Coder client for the provided URL. +// Deprecated: Use the ClientBuilder to create a client. func New(serverURL *url.URL) *Client { return &Client{ URL: serverURL, @@ -129,15 +130,18 @@ type Client struct { // PlainLogger may be set to log HTTP traffic in a human-readable form. // It uses the LogBodies option. + // Deprecated: Use the ClientBuilder to set this. PlainLogger io.Writer // Trace can be enabled to propagate tracing spans to the Coder API. // This is useful for tracking a request end-to-end. + // Deprecated: Use the ClientBuilder to set this. Trace bool // DisableDirectConnections forces any connections to workspaces to go // through DERP, regardless of the BlockEndpoints setting on each // connection. + // Deprecated: Use the ClientBuilder to set this. DisableDirectConnections bool } @@ -149,6 +153,7 @@ func (c *Client) Logger() slog.Logger { } // SetLogger sets the logger for the client. +// Deprecated: Use the ClientBuilder to set this. func (c *Client) SetLogger(logger slog.Logger) { c.mu.Lock() defer c.mu.Unlock() @@ -163,6 +168,7 @@ func (c *Client) LogBodies() bool { } // SetLogBodies sets whether to log request and response bodies. +// Deprecated: Use the ClientBuilder to set this. func (c *Client) SetLogBodies(logBodies bool) { c.mu.Lock() defer c.mu.Unlock() @@ -177,16 +183,11 @@ func (c *Client) SessionToken() string { } // SetSessionToken sets a fixed token for the client. -// Deprecated: Create a new client instead of changing the token after creation. +// Deprecated: Build a new client using the ClientBuilder instead of changing the token after creation. func (c *Client) SetSessionToken(token string) { - c.SetSessionTokenProvider(FixedSessionTokenProvider{SessionToken: token}) -} - -// SetSessionTokenProvider sets the session token provider for the client. -func (c *Client) SetSessionTokenProvider(provider SessionTokenProvider) { c.mu.Lock() defer c.mu.Unlock() - c.SessionTokenProvider = provider + c.SessionTokenProvider = FixedSessionTokenProvider{SessionToken: token} } func prefixLines(prefix, s []byte) []byte { From a6173095c9ca6df3c23df5d836d4f41f34ad035f Mon Sep 17 00:00:00 2001 From: Spike Curtis Date: Fri, 29 Aug 2025 12:12:32 +0000 Subject: [PATCH 2/4] chore: refactor to directly create Client in Command Handlers --- cli/autoupdate.go | 9 +- cli/configssh.go | 8 +- cli/create.go | 8 +- cli/delete.go | 7 +- cli/exp_mcp.go | 9 +- cli/exp_rpty.go | 10 +- cli/exp_scaletest.go | 54 +++---- cli/exp_task_create.go | 7 +- cli/exp_task_delete.go | 9 +- cli/exp_task_list.go | 7 +- cli/exp_task_status.go | 7 +- cli/favorite.go | 15 +- cli/list.go | 7 +- cli/logout.go | 11 +- cli/netcheck.go | 11 +- cli/notifications.go | 25 ++- cli/open.go | 22 +-- cli/organization.go | 8 +- cli/organizationmanage.go | 10 +- cli/organizationmembers.go | 30 ++-- cli/organizationroles.go | 22 ++- cli/organizationsettings.go | 19 +-- cli/ping.go | 20 +-- cli/portforward.go | 9 +- cli/provisionerjobs.go | 16 +- cli/provisioners.go | 7 +- cli/publickey.go | 12 +- cli/rename.go | 10 +- cli/restart.go | 7 +- cli/root.go | 188 +++++++++++------------ cli/schedule.go | 24 ++- cli/sharing.go | 15 +- cli/show.go | 7 +- cli/speedtest.go | 20 +-- cli/ssh.go | 12 +- cli/start.go | 7 +- cli/state.go | 13 +- cli/stop.go | 9 +- cli/support.go | 6 +- cli/templatecreate.go | 6 +- cli/templatedelete.go | 9 +- cli/templateedit.go | 6 +- cli/templatelist.go | 8 +- cli/templatepresets.go | 6 +- cli/templatepull.go | 6 +- cli/templatepush.go | 6 +- cli/templateversionarchive.go | 18 +-- cli/templateversions.go | 14 +- cli/tokens.go | 22 ++- cli/update.go | 7 +- cli/usercreate.go | 7 +- cli/userdelete.go | 7 +- cli/usereditroles.go | 8 +- cli/userlist.go | 14 +- cli/userstatus.go | 7 +- cli/whoami.go | 7 +- enterprise/cli/externalworkspaces.go | 25 ++- enterprise/cli/features.go | 13 +- enterprise/cli/groupcreate.go | 6 +- enterprise/cli/groupdelete.go | 7 +- enterprise/cli/groupedit.go | 6 +- enterprise/cli/grouplist.go | 6 +- enterprise/cli/licenses.go | 22 ++- enterprise/cli/prebuilds.go | 18 ++- enterprise/cli/provisionerdaemonstart.go | 11 +- enterprise/cli/provisionerkeys.go | 18 ++- enterprise/cli/workspaceproxy.go | 31 ++-- 67 files changed, 594 insertions(+), 424 deletions(-) diff --git a/cli/autoupdate.go b/cli/autoupdate.go index 5e3db8f2fe0c3..52ed0ffd64327 100644 --- a/cli/autoupdate.go +++ b/cli/autoupdate.go @@ -12,18 +12,21 @@ import ( ) func (r *RootCmd) autoupdate() *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "autoupdate ", Short: "Toggle auto-update policy for a workspace", Middleware: serpent.Chain( serpent.RequireNArgs(2), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + policy := strings.ToLower(inv.Args[1]) - err := validateAutoUpdatePolicy(policy) + err = validateAutoUpdatePolicy(policy) if err != nil { return xerrors.Errorf("validate policy: %w", err) } diff --git a/cli/configssh.go b/cli/configssh.go index b12b9d5c3d5cd..7676e82c4a7cb 100644 --- a/cli/configssh.go +++ b/cli/configssh.go @@ -236,7 +236,6 @@ func (r *RootCmd) configSSH() *serpent.Command { dryRun bool coderCliPath string ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "config-ssh", @@ -253,9 +252,13 @@ func (r *RootCmd) configSSH() *serpent.Command { ), Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() if sshConfigOpts.waitEnum != "auto" && sshConfigOpts.skipProxyCommand { @@ -280,7 +283,6 @@ func (r *RootCmd) configSSH() *serpent.Command { out = inv.Stderr } - var err error coderBinary := coderCliPath if coderBinary == "" { coderBinary, err = currentBinPath(out) diff --git a/cli/create.go b/cli/create.go index 59ab0ba0fa6d7..05fe0824b5be1 100644 --- a/cli/create.go +++ b/cli/create.go @@ -50,7 +50,6 @@ func (r *RootCmd) Create(opts CreateOptions) *serpent.Command { // shares the same name across multiple organizations. orgContext = NewOrganizationContext() ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "create [workspace]", @@ -61,9 +60,12 @@ func (r *RootCmd) Create(opts CreateOptions) *serpent.Command { Command: "coder create /", }, ), - Middleware: serpent.Chain(r.InitClient(client)), Handler: func(inv *serpent.Invocation) error { - var err error + client, err := r.InitClient(inv) + if err != nil { + return err + } + workspaceOwner := codersdk.Me if len(inv.Args) >= 1 { workspaceOwner, workspaceName, err = splitNamedWorkspace(inv.Args[0]) diff --git a/cli/delete.go b/cli/delete.go index a0988bb4cdeff..88e56405d6835 100644 --- a/cli/delete.go +++ b/cli/delete.go @@ -16,7 +16,6 @@ func (r *RootCmd) deleteWorkspace() *serpent.Command { orphan bool prov buildFlags ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "delete ", @@ -29,9 +28,13 @@ func (r *RootCmd) deleteWorkspace() *serpent.Command { ), Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err diff --git a/cli/exp_mcp.go b/cli/exp_mcp.go index ddc31d9b61179..dfeac3669e28c 100644 --- a/cli/exp_mcp.go +++ b/cli/exp_mcp.go @@ -399,7 +399,6 @@ type mcpServer struct { func (r *RootCmd) mcpServer() *serpent.Command { var ( - client = new(codersdk.Client) instructions string allowedTools []string appStatusSlug string @@ -409,6 +408,11 @@ func (r *RootCmd) mcpServer() *serpent.Command { cmd := &serpent.Command{ Use: "server", Handler: func(inv *serpent.Invocation) error { + client, err := r.TryInitClient(inv) + if err != nil { + return err + } + var lastReport taskReport // Create a queue that skips duplicates and preserves summaries. queue := cliutil.NewQueue[taskReport](512).WithPredicate(func(report taskReport) (taskReport, bool) { @@ -548,9 +552,6 @@ func (r *RootCmd) mcpServer() *serpent.Command { return srv.startServer(ctx, inv, instructions, allowedTools) }, Short: "Start the Coder MCP server.", - Middleware: serpent.Chain( - r.TryInitClient(client), - ), Options: []serpent.Option{ { Name: "instructions", diff --git a/cli/exp_rpty.go b/cli/exp_rpty.go index 196328b64732c..e289a793b8491 100644 --- a/cli/exp_rpty.go +++ b/cli/exp_rpty.go @@ -22,16 +22,17 @@ import ( ) func (r *RootCmd) rptyCommand() *serpent.Command { - var ( - client = new(codersdk.Client) - args handleRPTYArgs - ) + var args handleRPTYArgs cmd := &serpent.Command{ Handler: func(inv *serpent.Invocation) error { if r.disableDirect { return xerrors.New("direct connections are disabled, but you can try websocat ;-)") } + client, err := r.InitClient(inv) + if err != nil { + return err + } args.NamedWorkspace = inv.Args[0] args.Command = inv.Args[1:] return handleRPTY(inv, client, args) @@ -39,7 +40,6 @@ func (r *RootCmd) rptyCommand() *serpent.Command { Long: "Establish an RPTY session with a workspace/agent. This uses the same mechanism as the Web Terminal.", Middleware: serpent.Chain( serpent.RequireRangeArgs(1, -1), - r.InitClient(client), ), Options: []serpent.Option{ { diff --git a/cli/exp_scaletest.go b/cli/exp_scaletest.go index 4580ff3e1bc8a..1afc2c351f766 100644 --- a/cli/exp_scaletest.go +++ b/cli/exp_scaletest.go @@ -395,18 +395,17 @@ func (r *userCleanupRunner) Run(ctx context.Context, _ string, _ io.Writer) erro func (r *RootCmd) scaletestCleanup() *serpent.Command { var template string - cleanupStrategy := &scaletestStrategyFlags{cleanup: true} - client := new(codersdk.Client) - cmd := &serpent.Command{ Use: "cleanup", Short: "Cleanup scaletest workspaces, then cleanup scaletest users.", Long: "The strategy flags will apply to each stage of the cleanup process.", - Middleware: serpent.Chain( - r.InitClient(client), - ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() me, err := requireAdmin(ctx, client) @@ -551,14 +550,16 @@ func (r *RootCmd) scaletestCreateWorkspaces() *serpent.Command { output = &scaletestOutputFlags{} ) - client := new(codersdk.Client) - cmd := &serpent.Command{ - Use: "create-workspaces", - Short: "Creates many users, then creates a workspace for each user and waits for them finish building and fully come online. Optionally runs a command inside each workspace, and connects to the workspace over WireGuard.", - Long: `It is recommended that all rate limits are disabled on the server before running this scaletest. This test generates many login events which will be rate limited against the (most likely single) IP.`, - Middleware: r.InitClient(client), + Use: "create-workspaces", + Short: "Creates many users, then creates a workspace for each user and waits for them finish building and fully come online. Optionally runs a command inside each workspace, and connects to the workspace over WireGuard.", + Long: `It is recommended that all rate limits are disabled on the server before running this scaletest. This test generates many login events which will be rate limited against the (most likely single) IP.`, Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() me, err := requireAdmin(ctx, client) @@ -871,7 +872,6 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command { targetWorkspaces string workspaceProxyURL string - client = &codersdk.Client{} tracingFlags = &scaletestTracingFlags{} strategy = &scaletestStrategyFlags{} cleanupStrategy = &scaletestStrategyFlags{cleanup: true} @@ -882,10 +882,12 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command { cmd := &serpent.Command{ Use: "workspace-traffic", Short: "Generate traffic to scaletest workspaces through coderd", - Middleware: serpent.Chain( - r.InitClient(client), - ), Handler: func(inv *serpent.Invocation) (err error) { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() notifyCtx, stop := signal.NotifyContext(ctx, StopSignals...) // Checked later. @@ -1160,13 +1162,11 @@ func (r *RootCmd) scaletestWorkspaceTraffic() *serpent.Command { func (r *RootCmd) scaletestDashboard() *serpent.Command { var ( - interval time.Duration - jitter time.Duration - headless bool - randSeed int64 - targetUsers string - - client = &codersdk.Client{} + interval time.Duration + jitter time.Duration + headless bool + randSeed int64 + targetUsers string tracingFlags = &scaletestTracingFlags{} strategy = &scaletestStrategyFlags{} cleanupStrategy = &scaletestStrategyFlags{cleanup: true} @@ -1177,10 +1177,12 @@ func (r *RootCmd) scaletestDashboard() *serpent.Command { cmd := &serpent.Command{ Use: "dashboard", Short: "Generate traffic to the HTTP API to simulate use of the dashboard.", - Middleware: serpent.Chain( - r.InitClient(client), - ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + if !(interval > 0) { return xerrors.Errorf("--interval must be greater than zero") } diff --git a/cli/exp_task_create.go b/cli/exp_task_create.go index ad5d51a3a42c3..992be74ffcd3d 100644 --- a/cli/exp_task_create.go +++ b/cli/exp_task_create.go @@ -16,7 +16,6 @@ import ( func (r *RootCmd) taskCreate() *serpent.Command { var ( orgContext = NewOrganizationContext() - client = new(codersdk.Client) templateName string templateVersionName string @@ -30,7 +29,6 @@ func (r *RootCmd) taskCreate() *serpent.Command { Short: "Create an experimental task", Middleware: serpent.Chain( serpent.RequireRangeArgs(0, 1), - r.InitClient(client), ), Options: serpent.OptionSet{ { @@ -67,6 +65,11 @@ func (r *RootCmd) taskCreate() *serpent.Command { }, }, Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + var ( ctx = inv.Context() expClient = codersdk.NewExperimentalClient(client) diff --git a/cli/exp_task_delete.go b/cli/exp_task_delete.go index 5429ef2809123..361a0cfe7c576 100644 --- a/cli/exp_task_delete.go +++ b/cli/exp_task_delete.go @@ -16,20 +16,21 @@ import ( ) func (r *RootCmd) taskDelete() *serpent.Command { - client := new(codersdk.Client) - cmd := &serpent.Command{ Use: "delete [ ...]", Short: "Delete experimental tasks", Middleware: serpent.Chain( serpent.RequireRangeArgs(1, -1), - r.InitClient(client), ), Options: serpent.OptionSet{ cliui.SkipPromptOption(), }, Handler: func(inv *serpent.Invocation) error { ctx := inv.Context() + client, err := r.InitClient(inv) + if err != nil { + return err + } exp := codersdk.NewExperimentalClient(client) type toDelete struct { @@ -70,7 +71,7 @@ func (r *RootCmd) taskDelete() *serpent.Command { for _, it := range items { displayList = append(displayList, it.Display) } - _, err := cliui.Prompt(inv, cliui.PromptOptions{ + _, err = cliui.Prompt(inv, cliui.PromptOptions{ Text: fmt.Sprintf("Delete these tasks: %s?", pretty.Sprint(cliui.DefaultStyles.Code, strings.Join(displayList, ", "))), IsConfirm: true, Default: cliui.ConfirmNo, diff --git a/cli/exp_task_list.go b/cli/exp_task_list.go index 70d68f8121c9a..18b2bec95db91 100644 --- a/cli/exp_task_list.go +++ b/cli/exp_task_list.go @@ -38,7 +38,6 @@ func (r *RootCmd) taskList() *serpent.Command { user string quiet bool - client = new(codersdk.Client) formatter = cliui.NewOutputFormatter( cliui.TableFormat( []taskListRow{}, @@ -73,7 +72,6 @@ func (r *RootCmd) taskList() *serpent.Command { Aliases: []string{"ls"}, Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Options: serpent.OptionSet{ { @@ -108,6 +106,11 @@ func (r *RootCmd) taskList() *serpent.Command { }, }, Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() exp := codersdk.NewExperimentalClient(client) diff --git a/cli/exp_task_status.go b/cli/exp_task_status.go index 7b4b75c1a8ef9..328499d9d33e0 100644 --- a/cli/exp_task_status.go +++ b/cli/exp_task_status.go @@ -15,7 +15,6 @@ import ( func (r *RootCmd) taskStatus() *serpent.Command { var ( - client = new(codersdk.Client) formatter = cliui.NewOutputFormatter( cliui.TableFormat( []taskStatusRow{}, @@ -66,9 +65,13 @@ func (r *RootCmd) taskStatus() *serpent.Command { }, Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), ), Handler: func(i *serpent.Invocation) error { + client, err := r.InitClient(i) + if err != nil { + return err + } + ctx := i.Context() ec := codersdk.NewExperimentalClient(client) identifier := i.Args[0] diff --git a/cli/favorite.go b/cli/favorite.go index efb731abb34a3..7fdf47270ee0c 100644 --- a/cli/favorite.go +++ b/cli/favorite.go @@ -5,12 +5,10 @@ import ( "golang.org/x/xerrors" - "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" ) func (r *RootCmd) favorite() *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Aliases: []string{"fav", "favou" + "rite"}, Annotations: workspaceCommand, @@ -18,9 +16,13 @@ func (r *RootCmd) favorite() *serpent.Command { Short: "Add a workspace to your favorites", Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ws, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return xerrors.Errorf("get workspace: %w", err) @@ -37,7 +39,6 @@ func (r *RootCmd) favorite() *serpent.Command { } func (r *RootCmd) unfavorite() *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Aliases: []string{"unfav", "unfavou" + "rite"}, Annotations: workspaceCommand, @@ -45,9 +46,13 @@ func (r *RootCmd) unfavorite() *serpent.Command { Short: "Remove a workspace from your favorites", Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ws, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return xerrors.Errorf("get workspace: %w", err) diff --git a/cli/list.go b/cli/list.go index 278895dfd7218..be808435346e9 100644 --- a/cli/list.go +++ b/cli/list.go @@ -96,7 +96,6 @@ func (r *RootCmd) list() *serpent.Command { cliui.JSONFormat(), ) ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "list", @@ -104,9 +103,13 @@ func (r *RootCmd) list() *serpent.Command { Aliases: []string{"ls"}, Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + res, err := QueryConvertWorkspaces(inv.Context(), client, filter.Filter(), WorkspaceListRowFromWorkspace) if err != nil { return err diff --git a/cli/logout.go b/cli/logout.go index 6540003650919..33cd55cc81042 100644 --- a/cli/logout.go +++ b/cli/logout.go @@ -8,24 +8,23 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/cli/cliui" - "github.com/coder/coder/v2/codersdk" "github.com/coder/serpent" ) func (r *RootCmd) logout() *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "logout", Short: "Unauthenticate your local session", - Middleware: serpent.Chain( - r.InitClient(client), - ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + var errors []error config := r.createConfig() - var err error _, err = cliui.Prompt(inv, cliui.PromptOptions{ Text: "Are you sure you want to log out?", IsConfirm: true, diff --git a/cli/netcheck.go b/cli/netcheck.go index 490ed25ce20b2..58a3dfe2adeb9 100644 --- a/cli/netcheck.go +++ b/cli/netcheck.go @@ -9,22 +9,21 @@ import ( "golang.org/x/xerrors" "github.com/coder/coder/v2/coderd/healthcheck/derphealth" - "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/healthsdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/serpent" ) func (r *RootCmd) netcheck() *serpent.Command { - client := new(codersdk.Client) - cmd := &serpent.Command{ Use: "netcheck", Short: "Print network debug information for DERP and STUN", - Middleware: serpent.Chain( - r.InitClient(client), - ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx, cancel := context.WithTimeout(inv.Context(), 30*time.Second) defer cancel() diff --git a/cli/notifications.go b/cli/notifications.go index 1769ef3aa154a..5bacf66448800 100644 --- a/cli/notifications.go +++ b/cli/notifications.go @@ -42,16 +42,19 @@ func (r *RootCmd) notifications() *serpent.Command { } func (r *RootCmd) pauseNotifications() *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "pause", Short: "Pause notifications", Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { - err := client.PutNotificationsSettings(inv.Context(), codersdk.NotificationsSettings{ + client, err := r.InitClient(inv) + if err != nil { + return err + } + + err = client.PutNotificationsSettings(inv.Context(), codersdk.NotificationsSettings{ NotifierPaused: true, }) if err != nil { @@ -66,16 +69,19 @@ func (r *RootCmd) pauseNotifications() *serpent.Command { } func (r *RootCmd) resumeNotifications() *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "resume", Short: "Resume notifications", Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { - err := client.PutNotificationsSettings(inv.Context(), codersdk.NotificationsSettings{ + client, err := r.InitClient(inv) + if err != nil { + return err + } + + err = client.PutNotificationsSettings(inv.Context(), codersdk.NotificationsSettings{ NotifierPaused: false, }) if err != nil { @@ -90,15 +96,18 @@ func (r *RootCmd) resumeNotifications() *serpent.Command { } func (r *RootCmd) testNotifications() *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "test", Short: "Send a test notification", Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + if err := client.PostTestNotification(inv.Context()); err != nil { return xerrors.Errorf("unable to post test notification: %w", err) } diff --git a/cli/open.go b/cli/open.go index 83569e87e241a..89e30e4c6de84 100644 --- a/cli/open.go +++ b/cli/open.go @@ -41,24 +41,25 @@ const vscodeDesktopName = "VS Code Desktop" func (r *RootCmd) openVSCode() *serpent.Command { var ( - generateToken bool - testOpenError bool - appearanceConfig codersdk.AppearanceConfig + generateToken bool + testOpenError bool ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "vscode []", Short: fmt.Sprintf("Open a workspace in %s", vscodeDesktopName), Middleware: serpent.Chain( serpent.RequireRangeArgs(1, 2), - r.InitClient(client), - initAppearance(client, &appearanceConfig), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } ctx, cancel := context.WithCancel(inv.Context()) defer cancel() + appearanceConfig := initAppearance(ctx, client) // Check if we're inside a workspace, and especially inside _this_ // workspace so we can perform path resolution/expansion. Generally, @@ -299,15 +300,16 @@ func (r *RootCmd) openApp() *serpent.Command { testOpenError bool ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "app ", Short: "Open a workspace application.", - Middleware: serpent.Chain( - r.InitClient(client), - ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx, cancel := context.WithCancel(inv.Context()) defer cancel() diff --git a/cli/organization.go b/cli/organization.go index 941219a0a6739..9395b21b00e4c 100644 --- a/cli/organization.go +++ b/cli/organization.go @@ -37,7 +37,6 @@ func (r *RootCmd) organizations() *serpent.Command { func (r *RootCmd) showOrganization(orgContext *OrganizationContext) *serpent.Command { var ( stringFormat func(orgs []codersdk.Organization) (string, error) - client = new(codersdk.Client) formatter = cliui.NewOutputFormatter( cliui.ChangeFormatterData(cliui.TextFormat(), func(data any) (any, error) { typed, ok := data.([]codersdk.Organization) @@ -77,7 +76,6 @@ func (r *RootCmd) showOrganization(orgContext *OrganizationContext) *serpent.Com }, ), Middleware: serpent.Chain( - r.InitClient(client), serpent.RequireRangeArgs(0, 1), ), Options: serpent.OptionSet{ @@ -90,13 +88,17 @@ func (r *RootCmd) showOrganization(orgContext *OrganizationContext) *serpent.Com }, }, Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + orgArg := "selected" if len(inv.Args) >= 1 { orgArg = inv.Args[0] } var orgs []codersdk.Organization - var err error switch strings.ToLower(orgArg) { case "selected": stringFormat = func(orgs []codersdk.Organization) (string, error) { diff --git a/cli/organizationmanage.go b/cli/organizationmanage.go index 7baf323aa1168..ce196a1682d7d 100644 --- a/cli/organizationmanage.go +++ b/cli/organizationmanage.go @@ -12,22 +12,24 @@ import ( ) func (r *RootCmd) createOrganization() *serpent.Command { - client := new(codersdk.Client) - cmd := &serpent.Command{ Use: "create ", Short: "Create a new organization.", Middleware: serpent.Chain( - r.InitClient(client), serpent.RequireNArgs(1), ), Options: serpent.OptionSet{ cliui.SkipPromptOption(), }, Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + orgName := inv.Args[0] - err := codersdk.NameValid(orgName) + err = codersdk.NameValid(orgName) if err != nil { return xerrors.Errorf("organization name %q is invalid: %w", orgName, err) } diff --git a/cli/organizationmembers.go b/cli/organizationmembers.go index 26208cb5db906..60dca731da2bb 100644 --- a/cli/organizationmembers.go +++ b/cli/organizationmembers.go @@ -31,16 +31,17 @@ func (r *RootCmd) organizationMembers(orgContext *OrganizationContext) *serpent. } func (r *RootCmd) removeOrganizationMember(orgContext *OrganizationContext) *serpent.Command { - client := new(codersdk.Client) - cmd := &serpent.Command{ Use: "remove ", Short: "Remove a new member to the current organization", Middleware: serpent.Chain( - r.InitClient(client), serpent.RequireNArgs(1), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } ctx := inv.Context() organization, err := orgContext.Selected(inv, client) if err != nil { @@ -62,16 +63,17 @@ func (r *RootCmd) removeOrganizationMember(orgContext *OrganizationContext) *ser } func (r *RootCmd) addOrganizationMember(orgContext *OrganizationContext) *serpent.Command { - client := new(codersdk.Client) - cmd := &serpent.Command{ Use: "add ", Short: "Add a new member to the current organization", Middleware: serpent.Chain( - r.InitClient(client), serpent.RequireNArgs(1), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } ctx := inv.Context() organization, err := orgContext.Selected(inv, client) if err != nil { @@ -93,16 +95,15 @@ func (r *RootCmd) addOrganizationMember(orgContext *OrganizationContext) *serpen } func (r *RootCmd) assignOrganizationRoles(orgContext *OrganizationContext) *serpent.Command { - client := new(codersdk.Client) - cmd := &serpent.Command{ Use: "edit-roles [roles...]", Aliases: []string{"edit-role"}, Short: "Edit organization member's roles", - Middleware: serpent.Chain( - r.InitClient(client), - ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } ctx := inv.Context() organization, err := orgContext.Selected(inv, client) if err != nil { @@ -141,15 +142,18 @@ func (r *RootCmd) listOrganizationMembers(orgContext *OrganizationContext) *serp cliui.JSONFormat(), ) - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "list", Short: "List all organization members", Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() organization, err := orgContext.Selected(inv, client) if err != nil { diff --git a/cli/organizationroles.go b/cli/organizationroles.go index 3651baea88d2f..d6d867c6eef78 100644 --- a/cli/organizationroles.go +++ b/cli/organizationroles.go @@ -54,14 +54,15 @@ func (r *RootCmd) showOrganizationRoles(orgContext *OrganizationContext) *serpen cliui.JSONFormat(), ) - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "show [role_names ...]", Short: "Show role(s)", - Middleware: serpent.Chain( - r.InitClient(client), - ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() org, err := orgContext.Selected(inv, client) if err != nil { @@ -117,7 +118,6 @@ func (r *RootCmd) createOrganizationRole(orgContext *OrganizationContext) *serpe jsonInput bool ) - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "create ", Short: "Create a new organization custom role", @@ -144,10 +144,13 @@ func (r *RootCmd) createOrganizationRole(orgContext *OrganizationContext) *serpe }, Middleware: serpent.Chain( serpent.RequireRangeArgs(0, 1), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { ctx := inv.Context() + client, err := r.InitClient(inv) + if err != nil { + return err + } org, err := orgContext.Selected(inv, client) if err != nil { return err @@ -240,7 +243,6 @@ func (r *RootCmd) updateOrganizationRole(orgContext *OrganizationContext) *serpe jsonInput bool ) - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "update ", Short: "Update an organization custom role", @@ -267,9 +269,13 @@ func (r *RootCmd) updateOrganizationRole(orgContext *OrganizationContext) *serpe }, Middleware: serpent.Chain( serpent.RequireRangeArgs(0, 1), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() org, err := orgContext.Selected(inv, client) if err != nil { diff --git a/cli/organizationsettings.go b/cli/organizationsettings.go index 391a4f72e27fd..b2934ef006ea2 100644 --- a/cli/organizationsettings.go +++ b/cli/organizationsettings.go @@ -95,7 +95,6 @@ type organizationSetting struct { } func (r *RootCmd) setOrganizationSettings(orgContext *OrganizationContext, settings []organizationSetting) *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "set", Short: "Update specified organization setting.", @@ -108,7 +107,6 @@ func (r *RootCmd) setOrganizationSettings(orgContext *OrganizationContext, setti Options: []serpent.Option{}, Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { return inv.Command.HelpHandler(inv) @@ -124,12 +122,15 @@ func (r *RootCmd) setOrganizationSettings(orgContext *OrganizationContext, setti Options: []serpent.Option{}, Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() var org codersdk.Organization - var err error if !set.DisableOrgContext { org, err = orgContext.Selected(inv, client) @@ -170,7 +171,6 @@ func (r *RootCmd) setOrganizationSettings(orgContext *OrganizationContext, setti } func (r *RootCmd) printOrganizationSetting(orgContext *OrganizationContext, settings []organizationSetting) *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "show", Short: "Outputs specified organization setting.", @@ -183,7 +183,6 @@ func (r *RootCmd) printOrganizationSetting(orgContext *OrganizationContext, sett Options: []serpent.Option{}, Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { return inv.Command.HelpHandler(inv) @@ -199,13 +198,15 @@ func (r *RootCmd) printOrganizationSetting(orgContext *OrganizationContext, sett Options: []serpent.Option{}, Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() var org codersdk.Organization - var err error - if !set.DisableOrgContext { org, err = orgContext.Selected(inv, client) if err != nil { diff --git a/cli/ping.go b/cli/ping.go index 29af06affeaee..f97f9ec0ae5be 100644 --- a/cli/ping.go +++ b/cli/ping.go @@ -84,28 +84,28 @@ func (s *pingSummary) Write(w io.Writer) { func (r *RootCmd) ping() *serpent.Command { var ( - pingNum int64 - pingTimeout time.Duration - pingWait time.Duration - pingTimeLocal bool - pingTimeUTC bool - appearanceConfig codersdk.AppearanceConfig + pingNum int64 + pingTimeout time.Duration + pingWait time.Duration + pingTimeLocal bool + pingTimeUTC bool ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "ping ", Short: "Ping a workspace", Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), - initAppearance(client, &appearanceConfig), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } ctx, cancel := context.WithCancel(inv.Context()) defer cancel() - + appearanceConfig := initAppearance(ctx, client) notifyCtx, notifyCancel := inv.SignalNotifyContext(ctx, StopSignals...) defer notifyCancel() diff --git a/cli/portforward.go b/cli/portforward.go index 1b055d9e4362e..8c07eee2feeb6 100644 --- a/cli/portforward.go +++ b/cli/portforward.go @@ -38,9 +38,7 @@ func (r *RootCmd) portForward() *serpent.Command { tcpForwards []string // : udpForwards []string // : disableAutostart bool - appearanceConfig codersdk.AppearanceConfig ) - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "port-forward ", Short: `Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R".`, @@ -69,12 +67,15 @@ func (r *RootCmd) portForward() *serpent.Command { ), Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), - initAppearance(client, &appearanceConfig), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } ctx, cancel := context.WithCancel(inv.Context()) defer cancel() + appearanceConfig := initAppearance(ctx, client) specs, err := parsePortForwards(tcpForwards, udpForwards) if err != nil { diff --git a/cli/provisionerjobs.go b/cli/provisionerjobs.go index 2ddd04c5b6a29..3ce7da20b7dcb 100644 --- a/cli/provisionerjobs.go +++ b/cli/provisionerjobs.go @@ -38,7 +38,6 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { } var ( - client = new(codersdk.Client) orgContext = NewOrganizationContext() formatter = cliui.NewOutputFormatter( cliui.TableFormat([]provisionerJobRow{}, []string{"created at", "id", "type", "template display name", "status", "queue", "tags"}), @@ -54,10 +53,13 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { Aliases: []string{"ls"}, Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { ctx := inv.Context() + client, err := r.InitClient(inv) + if err != nil { + return err + } org, err := orgContext.Selected(inv, client) if err != nil { return xerrors.Errorf("current organization: %w", err) @@ -129,19 +131,19 @@ func (r *RootCmd) provisionerJobsList() *serpent.Command { } func (r *RootCmd) provisionerJobsCancel() *serpent.Command { - var ( - client = new(codersdk.Client) - orgContext = NewOrganizationContext() - ) + orgContext := NewOrganizationContext() cmd := &serpent.Command{ Use: "cancel ", Short: "Cancel a provisioner job", Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { ctx := inv.Context() + client, err := r.InitClient(inv) + if err != nil { + return err + } org, err := orgContext.Selected(inv, client) if err != nil { return xerrors.Errorf("current organization: %w", err) diff --git a/cli/provisioners.go b/cli/provisioners.go index 77f5e7705edd5..4198809c1f6de 100644 --- a/cli/provisioners.go +++ b/cli/provisioners.go @@ -35,7 +35,6 @@ func (r *RootCmd) provisionerList() *serpent.Command { OrganizationName string `json:"organization_name" table:"organization"` } var ( - client = new(codersdk.Client) orgContext = NewOrganizationContext() formatter = cliui.NewOutputFormatter( cliui.TableFormat([]provisionerDaemonRow{}, []string{"created at", "last seen at", "key name", "name", "version", "status", "tags"}), @@ -53,11 +52,13 @@ func (r *RootCmd) provisionerList() *serpent.Command { Aliases: []string{"ls"}, Middleware: serpent.Chain( serpent.RequireNArgs(0), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { ctx := inv.Context() - + client, err := r.InitClient(inv) + if err != nil { + return err + } org, err := orgContext.Selected(inv, client) if err != nil { return xerrors.Errorf("current organization: %w", err) diff --git a/cli/publickey.go b/cli/publickey.go index 320ed86b2c697..4862edf760c4c 100644 --- a/cli/publickey.go +++ b/cli/publickey.go @@ -14,13 +14,15 @@ import ( func (r *RootCmd) publickey() *serpent.Command { var reset bool - client := new(codersdk.Client) cmd := &serpent.Command{ - Use: "publickey", - Aliases: []string{"pubkey"}, - Short: "Output your Coder public key used for Git operations", - Middleware: r.InitClient(client), + Use: "publickey", + Aliases: []string{"pubkey"}, + Short: "Output your Coder public key used for Git operations", Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } if reset { // Confirm prompt if using --reset. We don't want to accidentally // reset our public key. diff --git a/cli/rename.go b/cli/rename.go index 3bafa176d22a6..1e7413fed5728 100644 --- a/cli/rename.go +++ b/cli/rename.go @@ -13,18 +13,20 @@ import ( ) func (r *RootCmd) rename() *serpent.Command { - var appearanceConfig codersdk.AppearanceConfig - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "rename ", Short: "Rename a workspace", Middleware: serpent.Chain( serpent.RequireNArgs(2), - r.InitClient(client), - initAppearance(client, &appearanceConfig), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + appearanceConfig := initAppearance(inv.Context(), client) + workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return xerrors.Errorf("get workspace: %w", err) diff --git a/cli/restart.go b/cli/restart.go index 20ee0b9b9de9d..dff3897221306 100644 --- a/cli/restart.go +++ b/cli/restart.go @@ -19,17 +19,20 @@ func (r *RootCmd) restart() *serpent.Command { bflags buildFlags ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "restart ", Short: "Restart a workspace", Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), ), Options: serpent.OptionSet{cliui.SkipPromptOption()}, Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + ctx := inv.Context() out := inv.Stdout diff --git a/cli/root.go b/cli/root.go index cb11545ce4523..09763fce4425f 100644 --- a/cli/root.go +++ b/cli/root.go @@ -494,110 +494,106 @@ type RootCmd struct { noFeatureWarning bool } -// InitClient authenticates the client with files from disk -// and injects header middlewares for telemetry, authentication, +// InitClient creates and configures a new client with authentication, telemetry, // and version checks. -func (r *RootCmd) InitClient(client *codersdk.Client) serpent.MiddlewareFunc { - return func(next serpent.HandlerFunc) serpent.HandlerFunc { - return func(inv *serpent.Invocation) error { - conf := r.createConfig() - var err error - // Read the client URL stored on disk. - if r.clientURL == nil || r.clientURL.String() == "" { - rawURL, err := conf.URL().Read() - // If the configuration files are absent, the user is logged out - if os.IsNotExist(err) { - binPath, err := os.Executable() - if err != nil { - binPath = "coder" - } - return xerrors.Errorf(notLoggedInMessage, binPath) - } - if err != nil { - return err - } - - r.clientURL, err = url.Parse(strings.TrimSpace(rawURL)) - if err != nil { - return err - } - } - // Read the token stored on disk. - if r.token == "" { - r.token, err = conf.Session().Read() - // Even if there isn't a token, we don't care. - // Some API routes can be unauthenticated. - if err != nil && !os.IsNotExist(err) { - return err - } - } - - err = r.configureClient(inv.Context(), client, r.clientURL, inv) +func (r *RootCmd) InitClient(inv *serpent.Invocation) (*codersdk.Client, error) { + conf := r.createConfig() + var err error + // Read the client URL stored on disk. + if r.clientURL == nil || r.clientURL.String() == "" { + rawURL, err := conf.URL().Read() + // If the configuration files are absent, the user is logged out + if os.IsNotExist(err) { + binPath, err := os.Executable() if err != nil { - return err + binPath = "coder" } - client.SetSessionToken(r.token) + return nil, xerrors.Errorf(notLoggedInMessage, binPath) + } + if err != nil { + return nil, err + } - if r.debugHTTP { - client.PlainLogger = os.Stderr - client.SetLogBodies(true) - } - client.DisableDirectConnections = r.disableDirect - return next(inv) + r.clientURL, err = url.Parse(strings.TrimSpace(rawURL)) + if err != nil { + return nil, err + } + } + // Read the token stored on disk. + if r.token == "" { + r.token, err = conf.Session().Read() + // Even if there isn't a token, we don't care. + // Some API routes can be unauthenticated. + if err != nil && !os.IsNotExist(err) { + return nil, err } } + + client := &codersdk.Client{} + err = r.configureClient(inv.Context(), client, r.clientURL, inv) + if err != nil { + return nil, err + } + client.SetSessionToken(r.token) + + if r.debugHTTP { + client.PlainLogger = os.Stderr + client.SetLogBodies(true) + } + client.DisableDirectConnections = r.disableDirect + return client, nil } // TryInitClient is similar to InitClient but doesn't error when credentials are missing. // This allows commands to run without requiring authentication, but still use auth if available. -func (r *RootCmd) TryInitClient(client *codersdk.Client) serpent.MiddlewareFunc { - return func(next serpent.HandlerFunc) serpent.HandlerFunc { - return func(inv *serpent.Invocation) error { - conf := r.createConfig() - var err error - // Read the client URL stored on disk. - if r.clientURL == nil || r.clientURL.String() == "" { - rawURL, err := conf.URL().Read() - // If the configuration files are absent, just continue without URL - if err != nil { - // Continue with a nil or empty URL - if !os.IsNotExist(err) { - return err - } - } else { - r.clientURL, err = url.Parse(strings.TrimSpace(rawURL)) - if err != nil { - return err - } - } +func (r *RootCmd) TryInitClient(inv *serpent.Invocation) (*codersdk.Client, error) { + conf := r.createConfig() + var err error + // Read the client URL stored on disk. + if r.clientURL == nil || r.clientURL.String() == "" { + rawURL, err := conf.URL().Read() + // If the configuration files are absent, just continue without URL + if err != nil { + // Continue with a nil or empty URL + if !os.IsNotExist(err) { + return nil, err } - // Read the token stored on disk. - if r.token == "" { - r.token, err = conf.Session().Read() - // Even if there isn't a token, we don't care. - // Some API routes can be unauthenticated. - if err != nil && !os.IsNotExist(err) { - return err - } + } else { + r.clientURL, err = url.Parse(strings.TrimSpace(rawURL)) + if err != nil { + return nil, err } + } + } + // Read the token stored on disk. + if r.token == "" { + r.token, err = conf.Session().Read() + // Even if there isn't a token, we don't care. + // Some API routes can be unauthenticated. + if err != nil && !os.IsNotExist(err) { + return nil, err + } + } - // Only configure the client if we have a URL - if r.clientURL != nil && r.clientURL.String() != "" { - err = r.configureClient(inv.Context(), client, r.clientURL, inv) - if err != nil { - return err - } - client.SetSessionToken(r.token) + // Only configure the client if we have a URL + if r.clientURL != nil && r.clientURL.String() != "" { + client := &codersdk.Client{} + err = r.configureClient(inv.Context(), client, r.clientURL, inv) + if err != nil { + return nil, err + } + client.SetSessionToken(r.token) - if r.debugHTTP { - client.PlainLogger = os.Stderr - client.SetLogBodies(true) - } - client.DisableDirectConnections = r.disableDirect - } - return next(inv) + if r.debugHTTP { + client.PlainLogger = os.Stderr + client.SetLogBodies(true) } + client.DisableDirectConnections = r.disableDirect + return client, nil } + + // Return a minimal client if no URL is available + return &codersdk.Client{}, nil } // HeaderTransport creates a new transport that executes `--header-command` @@ -817,17 +813,13 @@ func namedWorkspace(ctx context.Context, client *codersdk.Client, identifier str return client.WorkspaceByOwnerAndName(ctx, owner, name, codersdk.WorkspaceOptions{}) } -func initAppearance(client *codersdk.Client, outConfig *codersdk.AppearanceConfig) serpent.MiddlewareFunc { - return func(next serpent.HandlerFunc) serpent.HandlerFunc { - return func(inv *serpent.Invocation) error { - cfg, _ := client.Appearance(inv.Context()) - if cfg.DocsURL == "" { - cfg.DocsURL = codersdk.DefaultDocsURL() - } - *outConfig = cfg - return next(inv) - } +func initAppearance(ctx context.Context, client *codersdk.Client) codersdk.AppearanceConfig { + // best effort + cfg, _ := client.Appearance(ctx) + if cfg.DocsURL == "" { + cfg.DocsURL = codersdk.DefaultDocsURL() } + return cfg } // createConfig consumes the global configuration flag to produce a config root. diff --git a/cli/schedule.go b/cli/schedule.go index b7d1ff9b1f2bf..15f837bc16779 100644 --- a/cli/schedule.go +++ b/cli/schedule.go @@ -90,16 +90,18 @@ func (r *RootCmd) scheduleShow() *serpent.Command { cliui.JSONFormat(), ) ) - client := new(codersdk.Client) showCmd := &serpent.Command{ Use: "show | --all>", Short: "Show workspace schedules", Long: scheduleShowDescriptionLong, Middleware: serpent.Chain( serpent.RequireRangeArgs(0, 1), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } // To preserve existing behavior, if an argument is passed we will // only show the schedule for that workspace. // This will clobber the search query if one is passed. @@ -137,7 +139,6 @@ func (r *RootCmd) scheduleShow() *serpent.Command { } func (r *RootCmd) scheduleStart() *serpent.Command { - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "start { [day-of-week] [location] | manual }", Long: scheduleStartDescriptionLong + "\n" + FormatExamples( @@ -149,9 +150,12 @@ func (r *RootCmd) scheduleStart() *serpent.Command { Short: "Edit workspace start schedule", Middleware: serpent.Chain( serpent.RequireRangeArgs(2, 4), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err @@ -193,7 +197,6 @@ func (r *RootCmd) scheduleStart() *serpent.Command { } func (r *RootCmd) scheduleStop() *serpent.Command { - client := new(codersdk.Client) return &serpent.Command{ Use: "stop { | manual }", Long: scheduleStopDescriptionLong + "\n" + FormatExamples( @@ -204,9 +207,12 @@ func (r *RootCmd) scheduleStop() *serpent.Command { Short: "Edit workspace stop schedule", Middleware: serpent.Chain( serpent.RequireNArgs(2), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err @@ -244,7 +250,6 @@ func (r *RootCmd) scheduleStop() *serpent.Command { } func (r *RootCmd) scheduleExtend() *serpent.Command { - client := new(codersdk.Client) extendCmd := &serpent.Command{ Use: "extend ", Aliases: []string{"override-stop"}, @@ -256,9 +261,12 @@ func (r *RootCmd) scheduleExtend() *serpent.Command { ), Middleware: serpent.Chain( serpent.RequireNArgs(2), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } extendDuration, err := parseDuration(inv.Args[1]) if err != nil { return err diff --git a/cli/sharing.go b/cli/sharing.go index aa1678e7a9e81..9810dfd22622c 100644 --- a/cli/sharing.go +++ b/cli/sharing.go @@ -36,17 +36,19 @@ func (r *RootCmd) sharing() *serpent.Command { } func (r *RootCmd) statusWorkspaceSharing() *serpent.Command { - client := new(codersdk.Client) - cmd := &serpent.Command{ Use: "status ", Short: "List all users and groups the given Workspace is shared with.", Aliases: []string{"list"}, Middleware: serpent.Chain( - r.InitClient(client), serpent.RequireNArgs(1), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return xerrors.Errorf("unable to fetch Workspace %s: %w", inv.Args[0], err) @@ -74,7 +76,6 @@ func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Comma var ( // Username regex taken from codersdk/name.go nameRoleRegex = regexp.MustCompile(`(^[a-zA-Z0-9]+(?:-[a-zA-Z0-9]+)*)+(?::([A-Za-z0-9-]+))?`) - client = new(codersdk.Client) users []string groups []string ) @@ -97,10 +98,14 @@ func (r *RootCmd) shareWorkspace(orgContext *OrganizationContext) *serpent.Comma }, }, Middleware: serpent.Chain( - r.InitClient(client), serpent.RequireNArgs(1), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + if len(users) == 0 && len(groups) == 0 { return xerrors.New("at least one user or group must be provided") } diff --git a/cli/show.go b/cli/show.go index 284e8581f5dda..0a78a9e86180d 100644 --- a/cli/show.go +++ b/cli/show.go @@ -15,7 +15,6 @@ import ( ) func (r *RootCmd) show() *serpent.Command { - client := new(codersdk.Client) var details bool return &serpent.Command{ Use: "show ", @@ -30,9 +29,13 @@ func (r *RootCmd) show() *serpent.Command { }, Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + buildInfo, err := client.BuildInfo(inv.Context()) if err != nil { return xerrors.Errorf("get server version: %w", err) diff --git a/cli/speedtest.go b/cli/speedtest.go index 86d0e6a9ee63c..29f991bbcca31 100644 --- a/cli/speedtest.go +++ b/cli/speedtest.go @@ -13,7 +13,6 @@ import ( "cdr.dev/slog" "cdr.dev/slog/sloggers/sloghuman" "github.com/coder/coder/v2/cli/cliui" - "github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk/workspacesdk" "github.com/coder/serpent" ) @@ -36,12 +35,11 @@ type speedtestTableItem struct { func (r *RootCmd) speedtest() *serpent.Command { var ( - direct bool - duration time.Duration - direction string - pcapFile string - appearanceConfig codersdk.AppearanceConfig - formatter = cliui.NewOutputFormatter( + direct bool + duration time.Duration + direction string + pcapFile string + formatter = cliui.NewOutputFormatter( cliui.ChangeFormatterData(cliui.TableFormat([]speedtestTableItem{}, []string{"Interval", "Throughput"}), func(data any) (any, error) { res, ok := data.(SpeedtestResult) if !ok { @@ -65,19 +63,21 @@ func (r *RootCmd) speedtest() *serpent.Command { cliui.JSONFormat(), ) ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "speedtest ", Short: "Run upload and download tests from your machine to a workspace", Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), - initAppearance(client, &appearanceConfig), ), Handler: func(inv *serpent.Invocation) error { ctx, cancel := context.WithCancel(inv.Context()) defer cancel() + client, err := r.InitClient(inv) + if err != nil { + return err + } + appearanceConfig := initAppearance(ctx, client) if direct && r.disableDirect { return xerrors.Errorf("--direct (-d) is incompatible with --%s", varDisableDirect) diff --git a/cli/ssh.go b/cli/ssh.go index a2f0db7327bef..609670b690b9b 100644 --- a/cli/ssh.go +++ b/cli/ssh.go @@ -79,15 +79,12 @@ func (r *RootCmd) ssh() *serpent.Command { env []string usageApp string disableAutostart bool - appearanceConfig codersdk.AppearanceConfig networkInfoDir string networkInfoInterval time.Duration containerName string containerUser string ) - client := new(codersdk.Client) - wsClient := workspacesdk.New(client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "ssh [command]", @@ -111,10 +108,15 @@ func (r *RootCmd) ssh() *serpent.Command { return next(i) } }, - r.InitClient(client), - initAppearance(client, &appearanceConfig), ), Handler: func(inv *serpent.Invocation) (retErr error) { + client, err := r.InitClient(inv) + if err != nil { + return err + } + appearanceConfig := initAppearance(inv.Context(), client) + wsClient := workspacesdk.New(client) + command := strings.Join(inv.Args[1:], " ") // Before dialing the SSH server over TCP, capture Interrupt signals diff --git a/cli/start.go b/cli/start.go index 66c96cc9c4d75..28fc1512060ad 100644 --- a/cli/start.go +++ b/cli/start.go @@ -21,14 +21,12 @@ func (r *RootCmd) start() *serpent.Command { noWait bool ) - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "start ", Short: "Start a workspace", Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), ), Options: serpent.OptionSet{ { @@ -40,6 +38,11 @@ func (r *RootCmd) start() *serpent.Command { cliui.SkipPromptOption(), }, Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } + workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err diff --git a/cli/state.go b/cli/state.go index 7469c77d6f666..2b8e7f8cc6389 100644 --- a/cli/state.go +++ b/cli/state.go @@ -28,16 +28,17 @@ func (r *RootCmd) state() *serpent.Command { func (r *RootCmd) statePull() *serpent.Command { var buildNumber int64 - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "pull [file]", Short: "Pull a Terraform state file from a workspace.", Middleware: serpent.Chain( serpent.RequireRangeArgs(1, 2), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { - var err error + client, err := r.InitClient(inv) + if err != nil { + return err + } var build codersdk.WorkspaceBuild if buildNumber == 0 { workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) @@ -86,15 +87,17 @@ func buildNumberOption(n *int64) serpent.Option { func (r *RootCmd) statePush() *serpent.Command { var buildNumber int64 - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "push ", Short: "Push a Terraform state file to a workspace.", Middleware: serpent.Chain( serpent.RequireNArgs(2), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } workspace, err := namedWorkspace(inv.Context(), client, inv.Args[0]) if err != nil { return err diff --git a/cli/stop.go b/cli/stop.go index ef4813ff0a1a0..fb35e4a5e07fc 100644 --- a/cli/stop.go +++ b/cli/stop.go @@ -12,20 +12,23 @@ import ( func (r *RootCmd) stop() *serpent.Command { var bflags buildFlags - client := new(codersdk.Client) cmd := &serpent.Command{ Annotations: workspaceCommand, Use: "stop ", Short: "Stop a workspace", Middleware: serpent.Chain( serpent.RequireNArgs(1), - r.InitClient(client), ), Options: serpent.OptionSet{ cliui.SkipPromptOption(), }, Handler: func(inv *serpent.Invocation) error { - _, err := cliui.Prompt(inv, cliui.PromptOptions{ + client, err := r.InitClient(inv) + if err != nil { + return err + } + + _, err = cliui.Prompt(inv, cliui.PromptOptions{ Text: "Confirm stop workspace?", IsConfirm: true, }) diff --git a/cli/support.go b/cli/support.go index c55bab92cd6ff..9e55c1d6d98ae 100644 --- a/cli/support.go +++ b/cli/support.go @@ -62,16 +62,18 @@ var supportBundleBlurb = cliui.Bold("This will collect the following information func (r *RootCmd) supportBundle() *serpent.Command { var outputPath string var coderURLOverride string - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "bundle []", Short: "Generate a support bundle to troubleshoot issues connecting to a workspace.", Long: `This command generates a file containing detailed troubleshooting information about the Coder deployment and workspace connections. You must specify a single workspace (and optionally an agent name).`, Middleware: serpent.Chain( serpent.RequireRangeArgs(0, 2), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } var cliLogBuf bytes.Buffer cliLogW := sloghuman.Sink(&cliLogBuf) cliLog := slog.Make(cliLogW).Leveled(slog.LevelDebug) diff --git a/cli/templatecreate.go b/cli/templatecreate.go index c45277bec5837..bd4f076d179ea 100644 --- a/cli/templatecreate.go +++ b/cli/templatecreate.go @@ -33,7 +33,6 @@ func (r *RootCmd) templateCreate() *serpent.Command { uploadFlags templateUploadFlags orgContext = NewOrganizationContext() ) - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "create [name]", Short: "DEPRECATED: Create a template from the current directory or as specified by flag", @@ -43,9 +42,12 @@ func (r *RootCmd) templateCreate() *serpent.Command { "Use `coder templates push` command for creating and updating templates. \n"+ "Use `coder templates edit` command for editing template settings. ", ), - r.InitClient(client), ), Handler: func(inv *serpent.Invocation) error { + client, err := r.InitClient(inv) + if err != nil { + return err + } isTemplateSchedulingOptionsSet := failureTTL != 0 || dormancyThreshold != 0 || dormancyAutoDeletion != 0 if isTemplateSchedulingOptionsSet || requireActiveVersion { diff --git a/cli/templatedelete.go b/cli/templatedelete.go index 120693b952eef..0b2d0b91d0b66 100644 --- a/cli/templatedelete.go +++ b/cli/templatedelete.go @@ -16,13 +16,9 @@ import ( func (r *RootCmd) templateDelete() *serpent.Command { orgContext := NewOrganizationContext() - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "delete [name...]", Short: "Delete templates", - Middleware: serpent.Chain( - r.InitClient(client), - ), Options: serpent.OptionSet{ cliui.SkipPromptOption(), }, @@ -32,7 +28,10 @@ func (r *RootCmd) templateDelete() *serpent.Command { templateNames = []string{} templates = []codersdk.Template{} ) - + client, err := r.InitClient(inv) + if err != nil { + return err + } organization, err := orgContext.Selected(inv, client) if err != nil { return err diff --git a/cli/templateedit.go b/cli/templateedit.go index fe0323449c9be..1f8c7ff5b1259 100644 --- a/cli/templateedit.go +++ b/cli/templateedit.go @@ -37,16 +37,18 @@ func (r *RootCmd) templateEdit() *serpent.Command { disableEveryone bool orgContext = NewOrganizationContext() ) - client := new(codersdk.Client) cmd := &serpent.Command{ Use: "edit