mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
Follow-up to #30199. `--template-version` on `coder create`, `start`, `stop`, and `update` (plus `--stop-template-version` on `update`) now takes a template version ID as well as a name. Passing a version UUID used to 404 with a pretty unhelpful `get template version by name` error. `resolveTemplateVersionID` still looks up by name first, so a version that happens to be named like a UUID resolves the same way it did before. If that 404s and the input parses as a UUID, it falls back to fetching the version by ID. Either way the version has to belong to the selected template. That check matters most for `create`, which only sends the version ID and lets the server derive the template from it, so without it `coder create --template foo --template-version <id of a bar version>` would quietly create a `bar` workspace. The name is now path-escaped before the lookup. Previously something like `../../../templateversions/<id>` could reach other API routes through the name lookup and skip the template check entirely. Not-found errors now read `template version "x" not found for template "foo"` instead of the raw SDK error, so anything matching on the old text will need updating. Flag help and the generated CLI docs say "name or ID" now.
145 lines
5.5 KiB
Go
145 lines
5.5 KiB
Go
package cli
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
|
|
"golang.org/x/xerrors"
|
|
|
|
"github.com/coder/coder/v2/cli/cliui"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/serpent"
|
|
)
|
|
|
|
func (r *RootCmd) update() *serpent.Command {
|
|
var (
|
|
parameterFlags workspaceParameterFlags
|
|
bflags buildFlags
|
|
templateVersion string
|
|
stopTemplateVersion string
|
|
)
|
|
cmd := &serpent.Command{
|
|
Annotations: serpent.Annotations(workspaceCommand).Mark(annotationClientSessionID, "").Mark(annotationFlightRecorder, ""),
|
|
Use: "update <workspace>",
|
|
Short: "Will update and start a given workspace if it is out of date. If the workspace is already running, it will be stopped first.",
|
|
Long: "Use --always-prompt to change the parameter values of the workspace.",
|
|
Middleware: serpent.Chain(
|
|
serpent.RequireNArgs(1),
|
|
),
|
|
Options: serpent.OptionSet{
|
|
cliui.SkipPromptOption(),
|
|
},
|
|
Handler: func(inv *serpent.Invocation) error {
|
|
client, err := r.InitClient(inv)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// The invocation logger records debug detail and emits it to stderr
|
|
// only if the command fails (see flightRecorderMiddleware).
|
|
client.SetLogger(inv.Logger)
|
|
|
|
workspace, err := client.ResolveWorkspace(inv.Context(), inv.Args[0])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
versionID, err := resolveTemplateVersionID(inv.Context(), client, workspace.TemplateID, workspace.TemplateName, templateVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
stopVersionID, err := resolveTemplateVersionID(inv.Context(), client, workspace.TemplateID, workspace.TemplateName, stopTemplateVersion)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
hasStartWork := templateVersion != "" || workspace.Outdated || parameterFlags.promptRichParameters || parameterFlags.promptEphemeralParameters || len(parameterFlags.ephemeralParameters) > 0
|
|
// The stop version only applies when update stops the workspace first.
|
|
willStop := workspace.LatestBuild.Transition == codersdk.WorkspaceTransitionStart
|
|
if stopTemplateVersion != "" && !willStop {
|
|
latest := workspace.LatestBuild
|
|
if latest.Transition != codersdk.WorkspaceTransitionStop || latest.Status != codersdk.WorkspaceStatusStopped {
|
|
return xerrors.Errorf("--stop-template-version only applies when update stops a started workspace; the latest build is a %s (%s). Use 'coder stop --template-version %s' to stop with that version", latest.Transition, latest.Status, stopTemplateVersion)
|
|
}
|
|
if !hasStartWork {
|
|
return xerrors.New("workspace is already stopped and up-to-date; --stop-template-version has nothing to stop")
|
|
}
|
|
cliui.Warn(inv.Stderr, "Workspace is already stopped, ignoring --stop-template-version.")
|
|
stopTemplateVersion = ""
|
|
}
|
|
if stopTemplateVersion == "" && !hasStartWork {
|
|
_, _ = fmt.Fprintf(inv.Stdout, "Workspace is up-to-date.\n")
|
|
return nil
|
|
}
|
|
|
|
stoppedForUpdate := false
|
|
// #17840: If the workspace is already running, we will stop it before
|
|
// updating. Simply performing a new start transition may not work if the
|
|
// template specifies ignore_changes.
|
|
if willStop {
|
|
if workspace.LatestBuild.Status == codersdk.WorkspaceStatusRunning {
|
|
targetVersion := "the active template version"
|
|
if templateVersion != "" {
|
|
targetVersion = fmt.Sprintf("template version %q", templateVersion)
|
|
}
|
|
_, err = cliui.Prompt(inv, cliui.PromptOptions{
|
|
Text: fmt.Sprintf("Updating your workspace will start the workspace on %s. This can delete non-persistent data. Continue?", targetVersion),
|
|
IsConfirm: true,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
build, err := stopWorkspace(inv, client, workspace, bflags, stopVersionID)
|
|
if err != nil {
|
|
return xerrors.Errorf("stop workspace: %w", err)
|
|
}
|
|
// Wait for the stop to complete.
|
|
if err := cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID); err != nil {
|
|
return xerrors.Errorf("wait for stop: %w", err)
|
|
}
|
|
stoppedForUpdate = true
|
|
}
|
|
|
|
build, err := startWorkspace(inv, client, workspace, parameterFlags, bflags, WorkspaceUpdate, versionID)
|
|
if err != nil {
|
|
if stoppedForUpdate {
|
|
return xerrors.Errorf("start workspace after stopping (workspace is stopped; run 'coder start' to retry): %w", err)
|
|
}
|
|
return xerrors.Errorf("start workspace: %w", err)
|
|
}
|
|
|
|
logs, closer, err := client.WorkspaceBuildLogsAfter(inv.Context(), build.ID, 0)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer closer.Close()
|
|
for {
|
|
log, ok := <-logs
|
|
if !ok {
|
|
break
|
|
}
|
|
_, _ = fmt.Fprintf(inv.Stdout, "Output: %s\n", log.Output)
|
|
}
|
|
if err := cliui.WorkspaceBuild(inv.Context(), io.Discard, client, build.ID); err != nil {
|
|
if stoppedForUpdate {
|
|
return xerrors.Errorf("wait for start after stopping (workspace did not restart successfully; run 'coder start' to retry): %w", err)
|
|
}
|
|
return xerrors.Errorf("wait for start workspace: %w", err)
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
|
|
cmd.Options = append(cmd.Options, serpent.Option{
|
|
Flag: "template-version",
|
|
Description: "Start with a version of the workspace template, by name or ID. Defaults to the active version.",
|
|
Value: serpent.StringOf(&templateVersion),
|
|
}, serpent.Option{
|
|
Flag: "stop-template-version",
|
|
Description: "Stop with a version of the workspace template, by name or ID. Only applies when the workspace is started. Defaults to the workspace's current version.",
|
|
Value: serpent.StringOf(&stopTemplateVersion),
|
|
})
|
|
cmd.Options = append(cmd.Options, parameterFlags.allOptions()...)
|
|
cmd.Options = append(cmd.Options, bflags.cliOptions()...)
|
|
return cmd
|
|
}
|