mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
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
This commit is contained in:
+21
-1
@@ -29,7 +29,12 @@ const PresetNone = "none"
|
||||
|
||||
var ErrNoPresetFound = xerrors.New("no preset found")
|
||||
|
||||
func (r *RootCmd) create() *serpent.Command {
|
||||
type CreateOptions struct {
|
||||
BeforeCreate func(ctx context.Context, client *codersdk.Client, template codersdk.Template, templateVersionID uuid.UUID) error
|
||||
AfterCreate func(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, workspace codersdk.Workspace) error
|
||||
}
|
||||
|
||||
func (r *RootCmd) Create(opts CreateOptions) *serpent.Command {
|
||||
var (
|
||||
templateName string
|
||||
templateVersion string
|
||||
@@ -305,6 +310,13 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
_, _ = fmt.Fprintf(inv.Stdout, "%s", cliui.Bold("No preset applied."))
|
||||
}
|
||||
|
||||
if opts.BeforeCreate != nil {
|
||||
err = opts.BeforeCreate(inv.Context(), client, template, templateVersionID)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("before create: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
richParameters, err := prepWorkspaceBuild(inv, client, prepWorkspaceBuildArgs{
|
||||
Action: WorkspaceCreate,
|
||||
TemplateVersionID: templateVersionID,
|
||||
@@ -366,6 +378,14 @@ func (r *RootCmd) create() *serpent.Command {
|
||||
cliui.Keyword(workspace.Name),
|
||||
cliui.Timestamp(time.Now()),
|
||||
)
|
||||
|
||||
if opts.AfterCreate != nil {
|
||||
err = opts.AfterCreate(inv.Context(), inv, client, workspace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
+1
-1
@@ -97,7 +97,7 @@ func handleRPTY(inv *serpent.Invocation, client *codersdk.Client, args handleRPT
|
||||
reconnectID = uuid.New()
|
||||
}
|
||||
|
||||
ws, agt, _, err := getWorkspaceAndAgent(ctx, inv, client, true, args.NamedWorkspace)
|
||||
ws, agt, _, err := GetWorkspaceAndAgent(ctx, inv, client, true, args.NamedWorkspace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+7
-7
@@ -18,7 +18,7 @@ import (
|
||||
// workspaceListRow is the type provided to the OutputFormatter. This is a bit
|
||||
// dodgy but it's the only way to do complex display code for one format vs. the
|
||||
// other.
|
||||
type workspaceListRow struct {
|
||||
type WorkspaceListRow struct {
|
||||
// For JSON format:
|
||||
codersdk.Workspace `table:"-"`
|
||||
|
||||
@@ -40,7 +40,7 @@ type workspaceListRow struct {
|
||||
DailyCost string `json:"-" table:"daily cost"`
|
||||
}
|
||||
|
||||
func workspaceListRowFromWorkspace(now time.Time, workspace codersdk.Workspace) workspaceListRow {
|
||||
func WorkspaceListRowFromWorkspace(now time.Time, workspace codersdk.Workspace) WorkspaceListRow {
|
||||
status := codersdk.WorkspaceDisplayStatus(workspace.LatestBuild.Job.Status, workspace.LatestBuild.Transition)
|
||||
|
||||
lastBuilt := now.UTC().Sub(workspace.LatestBuild.Job.CreatedAt).Truncate(time.Second)
|
||||
@@ -55,7 +55,7 @@ func workspaceListRowFromWorkspace(now time.Time, workspace codersdk.Workspace)
|
||||
favIco = "★"
|
||||
}
|
||||
workspaceName := favIco + " " + workspace.OwnerName + "/" + workspace.Name
|
||||
return workspaceListRow{
|
||||
return WorkspaceListRow{
|
||||
Favorite: workspace.Favorite,
|
||||
Workspace: workspace,
|
||||
WorkspaceName: workspaceName,
|
||||
@@ -80,7 +80,7 @@ func (r *RootCmd) list() *serpent.Command {
|
||||
filter cliui.WorkspaceFilter
|
||||
formatter = cliui.NewOutputFormatter(
|
||||
cliui.TableFormat(
|
||||
[]workspaceListRow{},
|
||||
[]WorkspaceListRow{},
|
||||
[]string{
|
||||
"workspace",
|
||||
"template",
|
||||
@@ -107,7 +107,7 @@ func (r *RootCmd) list() *serpent.Command {
|
||||
r.InitClient(client),
|
||||
),
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
res, err := queryConvertWorkspaces(inv.Context(), client, filter.Filter(), workspaceListRowFromWorkspace)
|
||||
res, err := QueryConvertWorkspaces(inv.Context(), client, filter.Filter(), WorkspaceListRowFromWorkspace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -137,9 +137,9 @@ func (r *RootCmd) list() *serpent.Command {
|
||||
// queryConvertWorkspaces is a helper function for converting
|
||||
// codersdk.Workspaces to a different type.
|
||||
// It's used by the list command to convert workspaces to
|
||||
// workspaceListRow, and by the schedule command to
|
||||
// WorkspaceListRow, and by the schedule command to
|
||||
// convert workspaces to scheduleListRow.
|
||||
func queryConvertWorkspaces[T any](ctx context.Context, client *codersdk.Client, filter codersdk.WorkspaceFilter, convertF func(time.Time, codersdk.Workspace) T) ([]T, error) {
|
||||
func QueryConvertWorkspaces[T any](ctx context.Context, client *codersdk.Client, filter codersdk.WorkspaceFilter, convertF func(time.Time, codersdk.Workspace) T) ([]T, error) {
|
||||
var empty []T
|
||||
workspaces, err := client.Workspaces(ctx, filter)
|
||||
if err != nil {
|
||||
|
||||
+2
-2
@@ -72,7 +72,7 @@ func (r *RootCmd) openVSCode() *serpent.Command {
|
||||
// need to wait for the agent to start.
|
||||
workspaceQuery := inv.Args[0]
|
||||
autostart := true
|
||||
workspace, workspaceAgent, otherWorkspaceAgents, err := getWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
|
||||
workspace, workspaceAgent, otherWorkspaceAgents, err := GetWorkspaceAndAgent(ctx, inv, client, autostart, workspaceQuery)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("get workspace and agent: %w", err)
|
||||
}
|
||||
@@ -316,7 +316,7 @@ func (r *RootCmd) openApp() *serpent.Command {
|
||||
}
|
||||
|
||||
workspaceName := inv.Args[0]
|
||||
ws, agt, _, err := getWorkspaceAndAgent(ctx, inv, client, false, workspaceName)
|
||||
ws, agt, _, err := GetWorkspaceAndAgent(ctx, inv, client, false, workspaceName)
|
||||
if err != nil {
|
||||
var sdkErr *codersdk.Error
|
||||
if errors.As(err, &sdkErr) && sdkErr.StatusCode() == http.StatusNotFound {
|
||||
|
||||
+1
-1
@@ -110,7 +110,7 @@ func (r *RootCmd) ping() *serpent.Command {
|
||||
defer notifyCancel()
|
||||
|
||||
workspaceName := inv.Args[0]
|
||||
_, workspaceAgent, _, err := getWorkspaceAndAgent(
|
||||
_, workspaceAgent, _, err := GetWorkspaceAndAgent(
|
||||
ctx, inv, client,
|
||||
false, // Do not autostart for a ping.
|
||||
workspaceName,
|
||||
|
||||
+1
-1
@@ -84,7 +84,7 @@ func (r *RootCmd) portForward() *serpent.Command {
|
||||
return xerrors.New("no port-forwards requested")
|
||||
}
|
||||
|
||||
workspace, workspaceAgent, _, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
|
||||
workspace, workspaceAgent, _, err := GetWorkspaceAndAgent(ctx, inv, client, !disableAutostart, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ func (r *RootCmd) CoreSubcommands() []*serpent.Command {
|
||||
// Workspace Commands
|
||||
r.autoupdate(),
|
||||
r.configSSH(),
|
||||
r.create(),
|
||||
r.Create(CreateOptions{}),
|
||||
r.deleteWorkspace(),
|
||||
r.favorite(),
|
||||
r.list(),
|
||||
|
||||
+2
-2
@@ -117,7 +117,7 @@ func (r *RootCmd) scheduleShow() *serpent.Command {
|
||||
f.FilterQuery = fmt.Sprintf("owner:me name:%s", inv.Args[0])
|
||||
}
|
||||
}
|
||||
res, err := queryConvertWorkspaces(inv.Context(), client, f, scheduleListRowFromWorkspace)
|
||||
res, err := QueryConvertWorkspaces(inv.Context(), client, f, scheduleListRowFromWorkspace)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -307,7 +307,7 @@ func (r *RootCmd) scheduleExtend() *serpent.Command {
|
||||
}
|
||||
|
||||
func displaySchedule(ws codersdk.Workspace, out io.Writer) error {
|
||||
rows := []workspaceListRow{workspaceListRowFromWorkspace(time.Now(), ws)}
|
||||
rows := []WorkspaceListRow{WorkspaceListRowFromWorkspace(time.Now(), ws)}
|
||||
rendered, err := cliui.DisplayTable(rows, "workspace", []string{
|
||||
"workspace", "starts at", "starts next", "stops after", "stops next",
|
||||
})
|
||||
|
||||
+1
-1
@@ -83,7 +83,7 @@ func (r *RootCmd) speedtest() *serpent.Command {
|
||||
return xerrors.Errorf("--direct (-d) is incompatible with --%s", varDisableDirect)
|
||||
}
|
||||
|
||||
_, workspaceAgent, _, err := getWorkspaceAndAgent(ctx, inv, client, false, inv.Args[0])
|
||||
_, workspaceAgent, _, err := GetWorkspaceAndAgent(ctx, inv, client, false, inv.Args[0])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
+4
-4
@@ -754,7 +754,7 @@ func findWorkspaceAndAgentByHostname(
|
||||
hostname = strings.TrimSuffix(hostname, qualifiedSuffix)
|
||||
}
|
||||
hostname = normalizeWorkspaceInput(hostname)
|
||||
ws, agent, _, err := getWorkspaceAndAgent(ctx, inv, client, !disableAutostart, hostname)
|
||||
ws, agent, _, err := GetWorkspaceAndAgent(ctx, inv, client, !disableAutostart, hostname)
|
||||
return ws, agent, err
|
||||
}
|
||||
|
||||
@@ -827,11 +827,11 @@ startWatchLoop:
|
||||
}
|
||||
}
|
||||
|
||||
// getWorkspaceAgent returns the workspace and agent selected using either the
|
||||
// GetWorkspaceAndAgent returns the workspace and agent selected using either the
|
||||
// `<workspace>[.<agent>]` syntax via `in`. It will also return any other agents
|
||||
// in the workspace as a slice for use in child->parent lookups.
|
||||
// If autoStart is true, the workspace will be started if it is not already running.
|
||||
func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, input string) (codersdk.Workspace, codersdk.WorkspaceAgent, []codersdk.WorkspaceAgent, error) { //nolint:revive
|
||||
func GetWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *codersdk.Client, autostart bool, input string) (codersdk.Workspace, codersdk.WorkspaceAgent, []codersdk.WorkspaceAgent, error) { //nolint:revive
|
||||
var (
|
||||
workspace codersdk.Workspace
|
||||
// The input will be `owner/name.agent`
|
||||
@@ -880,7 +880,7 @@ func getWorkspaceAndAgent(ctx context.Context, inv *serpent.Invocation, client *
|
||||
switch cerr.StatusCode() {
|
||||
case http.StatusConflict:
|
||||
_, _ = fmt.Fprintln(inv.Stderr, "Unable to start the workspace due to conflict, the workspace may be starting, retrying without autostart...")
|
||||
return getWorkspaceAndAgent(ctx, inv, client, false, input)
|
||||
return GetWorkspaceAndAgent(ctx, inv, client, false, input)
|
||||
|
||||
case http.StatusForbidden:
|
||||
_, err = startWorkspace(inv, client, workspace, workspaceParameterFlags{}, buildFlags{}, WorkspaceUpdate)
|
||||
|
||||
+1
-1
@@ -102,7 +102,7 @@ func (r *RootCmd) vscodeSSH() *serpent.Command {
|
||||
// will call this command after the workspace is started.
|
||||
autostart := false
|
||||
|
||||
workspace, workspaceAgent, _, err := getWorkspaceAndAgent(ctx, inv, client, autostart, fmt.Sprintf("%s/%s", owner, name))
|
||||
workspace, workspaceAgent, _, err := GetWorkspaceAndAgent(ctx, inv, client, autostart, fmt.Sprintf("%s/%s", owner, name))
|
||||
if err != nil {
|
||||
return xerrors.Errorf("find workspace and agent: %w", err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user