Files
coder/cli/stop.go
T
dylanhuff-at-coderandNick Vigilante b4d51012da feat: allow template version selection for workspace builds (#30199)
Add named template version selection to `coder start` and `coder stop`,
and independent stop/start selection to `coder update` with
`--stop-template-version` and `--template-version`. Stop still defaults
to the workspace's current version, and update starts with the active
version by default. Explicit selections use the selected version for
parameter preparation and builds, keep server permissions authoritative,
and never silently fall back to the active version.

Classic stop builds retain previous parameter values omitted by the
selected stop version, so a later start can reuse them across separate
commands or an interrupted update. Declared parameters still follow
normal validation, and start builds still discard removed parameters.
Update now returns an error and retry guidance when a start apply fails
after stopping, including unflagged updates that previously returned
success.

Closes #29100


[PLAT-616](https://linear.app/codercom/issue/PLAT-616/cli-allow-specifying-a-template-version-for-stopstart-builds-and-both)

---------

Co-authored-by: Nick Vigilante <nickvigilante@users.noreply.github.com>
2026-10-07 13:17:12 -07:00

107 lines
3.1 KiB
Go

package cli
import (
"fmt"
"time"
"github.com/google/uuid"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/cli/cliutil"
"github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent"
)
func (r *RootCmd) stop() *serpent.Command {
var bflags buildFlags
var templateVersion string
cmd := &serpent.Command{
Annotations: serpent.Annotations(workspaceCommand).Mark(annotationClientSessionID, "").Mark(annotationFlightRecorder, ""),
Use: "stop <workspace>",
Short: "Stop a workspace",
Middleware: serpent.Chain(
serpent.RequireNArgs(1),
),
Options: serpent.OptionSet{
{
Flag: "template-version",
Description: "Stop with a named version of the workspace template. Defaults to the workspace's current version.",
Value: serpent.StringOf(&templateVersion),
},
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)
_, err = cliui.Prompt(inv, cliui.PromptOptions{
Text: "Confirm stop workspace?",
IsConfirm: true,
})
if err != nil {
return err
}
workspace, err := client.ResolveWorkspace(inv.Context(), inv.Args[0])
if err != nil {
return err
}
versionID, err := resolveTemplateVersionID(inv.Context(), client, workspace.TemplateID, templateVersion)
if err != nil {
return err
}
build, err := stopWorkspace(inv, client, workspace, bflags, versionID)
if err != nil {
return err
}
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID)
if err != nil {
return err
}
_, _ = fmt.Fprintf(
inv.Stdout,
"\nThe %s workspace has been stopped at %s!\n",
cliui.Keyword(workspace.Name),
cliui.Timestamp(time.Now()),
)
return nil
},
}
cmd.Options = append(cmd.Options, bflags.cliOptions()...)
return cmd
}
func stopWorkspace(inv *serpent.Invocation, client *codersdk.Client, workspace codersdk.Workspace, bflags buildFlags, templateVersionID uuid.UUID) (codersdk.WorkspaceBuild, error) {
if workspace.LatestBuild.Job.Status == codersdk.ProvisionerJobPending {
// cliutil.WarnMatchedProvisioners also checks if the job is pending
// but we still want to avoid users spamming multiple builds that will
// not be picked up.
cliui.Warn(inv.Stderr, "The workspace is already stopping!")
cliutil.WarnMatchedProvisioners(inv.Stderr, workspace.LatestBuild.MatchedProvisioners, workspace.LatestBuild.Job)
if _, err := cliui.Prompt(inv, cliui.PromptOptions{
Text: "Enqueue another stop?",
IsConfirm: true,
Default: cliui.ConfirmNo,
}); err != nil {
return codersdk.WorkspaceBuild{}, err
}
}
wbr := codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStop,
TemplateVersionID: templateVersionID,
}
if bflags.provisionerLogDebug {
wbr.LogLevel = codersdk.ProvisionerLogLevelDebug
}
return client.CreateWorkspaceBuild(inv.Context(), workspace.ID, wbr)
}