Skip to content

Commit 7b1dcd9

Browse files
authored
feat(cli): add enterprise external-workspaces CLI command (#19287)
This pull request introduces support for external workspace management, allowing users to register and manage workspaces that are provisioned and managed outside of the Coder. * coder external-workspaces create - Creates a new external workspace (this command extends coder create) * Example: coder external-workspaces create ext-workspace --template=externally-managed-workspace -y * Checks if template has coder_external_agent resource before creating a workspace * coder external-workspaces list - Lists all external workspaces * coder external-workspaces agent-instructions <workspace name> <agent name> - Retrieves agent connection instruction * Example: coder external-workspaces agent-instructions ext-workspace main --output=json
1 parent 9edceef commit 7b1dcd9

25 files changed

+1277
-73
lines changed

cli/create.go

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,12 @@ const PresetNone = "none"
2929

3030
var ErrNoPresetFound = xerrors.New("no preset found")
3131

32-
func (r *RootCmd) create() *serpent.Command {
32+
type CreateOptions struct {
33+
BeforeCreate func(ctx context.Context, client *codersdk.Client, template codersdk.Template, templateVersionID uuid.UUID) error
34+
AfterCreate func(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, workspace codersdk.Workspace) error
35+
}
36+
37+
func (r *RootCmd) Create(opts CreateOptions) *serpent.Command {
3338
var (
3439
templateName string
3540
templateVersion string
@@ -305,6 +310,13 @@ func (r *RootCmd) create() *serpent.Command {
305310
_, _ = fmt.Fprintf(inv.Stdout, "%s", cliui.Bold("No preset applied."))
306311
}
307312

313+
if opts.BeforeCreate != nil {
314+
err = opts.BeforeCreate(inv.Context(), client, template, templateVersionID)
315+
if err != nil {
316+
return xerrors.Errorf("before create: %w", err)
317+
}
318+
}
319+
308320
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
309321
Action: WorkspaceCreate,
310322
TemplateVersionID: templateVersionID,
@@ -366,6 +378,14 @@ func (r *RootCmd) create() *serpent.Command {
366378
cliui.Keyword(workspace.Name),
367379
cliui.Timestamp(time.Now()),
368380
)
381+
382+
if opts.AfterCreate != nil {
383+
err = opts.AfterCreate(inv.Context(), inv, client, workspace)
384+
if err != nil {
385+
return err
386+
}
387+
}
388+
369389
return nil
370390
},
371391
}

cli/exp_rpty.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ func handleRPTY(inv *serpent.Invocation, client *codersdk.Client, args handleRPT
9797
reconnectID = uuid.New()
9898
}
9999

100-
ws, agt, _, err := getWorkspaceAndAgent(ctx, inv, client, true, args.NamedWorkspace)
100+
ws, agt, _, err := GetWorkspaceAndAgent(ctx, inv, client, true, args.NamedWorkspace)
101101
if err != nil {
102102
return err
103103
}

cli/list.go

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import (
1818
// workspaceListRow is the type provided to the OutputFormatter. This is a bit
1919
// dodgy but it's the only way to do complex display code for one format vs. the
2020
// other.
21-
type workspaceListRow struct {
21+
type WorkspaceListRow struct {
2222
// For JSON format:
2323
codersdk.Workspace `table:"-"`
2424

@@ -40,7 +40,7 @@ type workspaceListRow struct {
4040
DailyCost string `json:"-" table:"daily cost"`
4141
}
4242

43-
func workspaceListRowFromWorkspace(now time.Time, workspace codersdk.Workspace) workspaceListRow {
43+
func WorkspaceListRowFromWorkspace(now time.Time, workspace codersdk.Workspace) WorkspaceListRow {
4444
status := codersdk.WorkspaceDisplayStatus(workspace.LatestBuild.Job.Status, workspace.LatestBuild.Transition)
4545

4646
lastBuilt := now.UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
@@ -55,7 +55,7 @@ func workspaceListRowFromWorkspace(now time.Time, workspace codersdk.Workspace)
5555
favIco = "★"
5656
}
5757
workspaceName := favIco + " " + workspace.OwnerName + "/" + workspace.Name
58-
return workspaceListRow{
58+
return WorkspaceListRow{
5959
Favorite: workspace.Favorite,
6060
Workspace: workspace,
6161
WorkspaceName: workspaceName,
@@ -80,7 +80,7 @@ func (r *RootCmd) list() *serpent.Command {
8080
filter cliui.WorkspaceFilter
8181
formatter = cliui.NewOutputFormatter(
8282
cliui.TableFormat(
83-
[]workspaceListRow{},
83+
[]WorkspaceListRow{},
8484
[]string{
8585
"workspace",
8686
"template",
@@ -107,7 +107,7 @@ func (r *RootCmd) list() *serpent.Command {
107107
r.InitClient(client),
108108
),
109109
Handler: func(inv *serpent.Invocation) error {
110-
res, err := queryConvertWorkspaces(inv.Context(), client, filter.Filter(), workspaceListRowFromWorkspace)
110+
res, err := QueryConvertWorkspaces(inv.Context(), client, filter.Filter(), WorkspaceListRowFromWorkspace)
111111
if err != nil {
112112
return err
113113
}
@@ -137,9 +137,9 @@ func (r *RootCmd) list() *serpent.Command {
137137
// queryConvertWorkspaces is a helper function for converting
138138
// codersdk.Workspaces to a different type.
139139
// It's used by the list command to convert workspaces to
140-
// workspaceListRow, and by the schedule command to
140+
// WorkspaceListRow, and by the schedule command to
141141
// convert workspaces to scheduleListRow.
142-
func queryConvertWorkspaces[T any](ctx context.Context, client *codersdk.Client, filter codersdk.WorkspaceFilter, convertF func(time.Time, codersdk.Workspace) T) ([]T, error) {
142+
func QueryConvertWorkspaces[T any](ctx context.Context, client *codersdk.Client, filter codersdk.WorkspaceFilter, convertF func(time.Time, codersdk.Workspace) T) ([]T, error) {
143143
var empty []T
144144
workspaces, err := client.Workspaces(ctx, filter)
145145
if err != nil {

cli/open.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
7272
// need to wait for the agent to start.
7373
workspaceQuery := inv.Args[0]
7474
autostart := true
75-
workspace, workspaceAgent, otherWorkspaceAgents, err := getWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
75+
workspace, workspaceAgent, otherWorkspaceAgents, err := GetWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
7676
if err != nil {
7777
return xerrors.Errorf("get workspace and agent: %w", err)
7878
}
@@ -316,7 +316,7 @@ func (r *RootCmd) openApp() *serpent.Command {
316316
}
317317

318318
workspaceName := inv.Args[0]
319-
ws, agt, _, err := getWorkspaceAndAgent(ctx, inv, client, false, workspaceName)
319+
ws, agt, _, err := GetWorkspaceAndAgent(ctx, inv, client, false, workspaceName)
320320
if err != nil {
321321
var sdkErr *codersdk.Error
322322
if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {

cli/ping.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ func (r *RootCmd) ping() *serpent.Command {
110110
defer notifyCancel()
111111

112112
workspaceName := inv.Args[0]
113-
_, workspaceAgent, _, err := getWorkspaceAndAgent(
113+
_, workspaceAgent, _, err := GetWorkspaceAndAgent(
114114
ctx, inv, client,
115115
false, // Do not autostart for a ping.
116116
workspaceName,

cli/portforward.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ func (r *RootCmd) portForward() *serpent.Command {
8484
return xerrors.New("no port-forwards requested")
8585
}
8686

87-
workspace, workspaceAgent, _, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
87+
workspace, workspaceAgent, _, err := GetWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
8888
if err != nil {
8989
return err
9090
}

cli/root.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
108108
// Workspace Commands
109109
r.autoupdate(),
110110
r.configSSH(),
111-
r.create(),
111+
r.Create(CreateOptions{}),
112112
r.deleteWorkspace(),
113113
r.favorite(),
114114
r.list(),

cli/schedule.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ func (r *RootCmd) scheduleShow() *serpent.Command {
117117
f.FilterQuery = fmt.Sprintf("owner:me name:%s", inv.Args[0])
118118
}
119119
}
120-
res, err := queryConvertWorkspaces(inv.Context(), client, f, scheduleListRowFromWorkspace)
120+
res, err := QueryConvertWorkspaces(inv.Context(), client, f, scheduleListRowFromWorkspace)
121121
if err != nil {
122122
return err
123123
}
@@ -307,7 +307,7 @@ func (r *RootCmd) scheduleExtend() *serpent.Command {
307307
}
308308

309309
func displaySchedule(ws codersdk.Workspace, out io.Writer) error {
310-
rows := []workspaceListRow{workspaceListRowFromWorkspace(time.Now(), ws)}
310+
rows := []WorkspaceListRow{WorkspaceListRowFromWorkspace(time.Now(), ws)}
311311
rendered, err := cliui.DisplayTable(rows, "workspace", []string{
312312
"workspace", "starts at", "starts next", "stops after", "stops next",
313313
})

cli/speedtest.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ func (r *RootCmd) speedtest() *serpent.Command {
8383
return xerrors.Errorf("--direct (-d) is incompatible with --%s", varDisableDirect)
8484
}
8585

86-
_, workspaceAgent, _, err := getWorkspaceAndAgent(ctx, inv, client, false, inv.Args[0])
86+
_, workspaceAgent, _, err := GetWorkspaceAndAgent(ctx, inv, client, false, inv.Args[0])
8787
if err != nil {
8888
return err
8989
}

cli/ssh.go

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -754,7 +754,7 @@ func findWorkspaceAndAgentByHostname(
754754
hostname = strings.TrimSuffix(hostname, qualifiedSuffix)
755755
}
756756
hostname = normalizeWorkspaceInput(hostname)
757-
ws, agent, _, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, hostname)
757+
ws, agent, _, err := GetWorkspaceAndAgent(ctx, inv, client, !disableAutostart, hostname)
758758
return ws, agent, err
759759
}
760760

@@ -827,11 +827,11 @@ startWatchLoop:
827827
}
828828
}
829829

830-
// getWorkspaceAgent returns the workspace and agent selected using either the
830+
// GetWorkspaceAndAgent returns the workspace and agent selected using either the
831831
// `<workspace>[.<agent>]` syntax via `in`. It will also return any other agents
832832
// in the workspace as a slice for use in child->parent lookups.
833833
// If autoStart is true, the workspace will be started if it is not already running.
834-
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, input string) (codersdk.Workspace, codersdk.WorkspaceAgent, []codersdk.WorkspaceAgent, error) { //nolint:revive
834+
func GetWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, input string) (codersdk.Workspace, codersdk.WorkspaceAgent, []codersdk.WorkspaceAgent, error) { //nolint:revive
835835
var (
836836
workspace codersdk.Workspace
837837
// The input will be `owner/name.agent`
@@ -880,7 +880,7 @@ func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *
880880
switch cerr.StatusCode() {
881881
case http.StatusConflict:
882882
_, _ = fmt.Fprintln(inv.Stderr, "Unable to start the workspace due to conflict, the workspace may be starting, retrying without autostart...")
883-
return getWorkspaceAndAgent(ctx, inv, client, false, input)
883+
return GetWorkspaceAndAgent(ctx, inv, client, false, input)
884884

885885
case http.StatusForbidden:
886886
_, err = startWorkspace(inv, client, workspace, workspaceParameterFlags{}, buildFlags{}, WorkspaceUpdate)

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy