mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +08:00
fix!: stop workspace before update (#18425)
Fixes https://github.com/coder/coder/issues/17840 NOTE: calling this out as a breaking change so that it is highly visible in the changelog. * CLI: Modifies `coder update` to stop the workspace if already running. * UI: Modifies "update" button to always stop the workspace if already running.
This commit is contained in:
+1
-1
@@ -358,7 +358,7 @@ func TestStartAutoUpdate(t *testing.T) {
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
if c.Cmd == "start" {
|
||||
coderdtest.MustTransitionWorkspace(t, member, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
||||
coderdtest.MustTransitionWorkspace(t, member, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||||
}
|
||||
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(stringRichParameters), func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||||
ctvr.TemplateID = template.ID
|
||||
|
||||
+27
-24
@@ -37,32 +37,11 @@ func (r *RootCmd) stop() *serpent.Command {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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 err
|
||||
}
|
||||
}
|
||||
|
||||
wbr := codersdk.CreateWorkspaceBuildRequest{
|
||||
Transition: codersdk.WorkspaceTransitionStop,
|
||||
}
|
||||
if bflags.provisionerLogDebug {
|
||||
wbr.LogLevel = codersdk.ProvisionerLogLevelDebug
|
||||
}
|
||||
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, wbr)
|
||||
build, err := stopWorkspace(inv, client, workspace, bflags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
cliutil.WarnMatchedProvisioners(inv.Stderr, build.MatchedProvisioners, build.Job)
|
||||
|
||||
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID)
|
||||
if err != nil {
|
||||
@@ -71,8 +50,8 @@ func (r *RootCmd) stop() *serpent.Command {
|
||||
|
||||
_, _ = fmt.Fprintf(
|
||||
inv.Stdout,
|
||||
"\nThe %s workspace has been stopped at %s!\n", cliui.Keyword(workspace.Name),
|
||||
|
||||
"\nThe %s workspace has been stopped at %s!\n",
|
||||
cliui.Keyword(workspace.Name),
|
||||
cliui.Timestamp(time.Now()),
|
||||
)
|
||||
return nil
|
||||
@@ -82,3 +61,27 @@ func (r *RootCmd) stop() *serpent.Command {
|
||||
|
||||
return cmd
|
||||
}
|
||||
|
||||
func stopWorkspace(inv *serpent.Invocation, client *codersdk.Client, workspace codersdk.Workspace, bflags buildFlags) (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,
|
||||
}
|
||||
if bflags.provisionerLogDebug {
|
||||
wbr.LogLevel = codersdk.ProvisionerLogLevelDebug
|
||||
}
|
||||
return client.CreateWorkspaceBuild(inv.Context(), workspace.ID, wbr)
|
||||
}
|
||||
|
||||
Vendored
+2
-1
@@ -57,7 +57,8 @@ SUBCOMMANDS:
|
||||
tokens Manage personal access tokens
|
||||
unfavorite Remove a workspace from your favorites
|
||||
update Will update and start a given workspace if it is out of
|
||||
date
|
||||
date. If the workspace is already running, it will be
|
||||
stopped first.
|
||||
users Manage users
|
||||
version Show coder version
|
||||
whoami Fetch authenticated user info for Coder deployment
|
||||
|
||||
+2
-1
@@ -3,7 +3,8 @@ coder v0.0.0-devel
|
||||
USAGE:
|
||||
coder update [flags] <workspace>
|
||||
|
||||
Will update and start a given workspace if it is out of date
|
||||
Will update and start a given workspace if it is out of date. If the workspace
|
||||
is already running, it will be stopped first.
|
||||
|
||||
Use --always-prompt to change the parameter values of the workspace.
|
||||
|
||||
|
||||
+16
-1
@@ -5,6 +5,7 @@ import (
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/cli/cliui"
|
||||
"github.com/coder/coder/v2/codersdk"
|
||||
"github.com/coder/serpent"
|
||||
)
|
||||
@@ -18,7 +19,7 @@ func (r *RootCmd) update() *serpent.Command {
|
||||
cmd := &serpent.Command{
|
||||
Annotations: workspaceCommand,
|
||||
Use: "update <workspace>",
|
||||
Short: "Will update and start a given workspace if it is out of date",
|
||||
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),
|
||||
@@ -34,6 +35,20 @@ func (r *RootCmd) update() *serpent.Command {
|
||||
return nil
|
||||
}
|
||||
|
||||
// #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 workspace.LatestBuild.Transition == codersdk.WorkspaceTransitionStart {
|
||||
build, err := stopWorkspace(inv, client, workspace, bflags)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
build, err := startWorkspace(inv, client, workspace, parameterFlags, bflags, WorkspaceUpdate)
|
||||
if err != nil {
|
||||
return xerrors.Errorf("start workspace: %w", err)
|
||||
|
||||
+97
-21
@@ -34,28 +34,21 @@ func TestUpdate(t *testing.T) {
|
||||
t.Run("OK", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given: a workspace exists on the latest template version.
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
|
||||
|
||||
inv, root := clitest.New(t, "create",
|
||||
"my-workspace",
|
||||
"--template", template.Name,
|
||||
"-y",
|
||||
)
|
||||
clitest.SetupConfig(t, member, root)
|
||||
|
||||
err := inv.Run()
|
||||
require.NoError(t, err)
|
||||
|
||||
ws, err := client.WorkspaceByOwnerAndName(context.Background(), memberUser.Username, "my-workspace", codersdk.WorkspaceOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, version1.ID.String(), ws.LatestBuild.TemplateVersionID.String())
|
||||
ws := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.Name = "my-workspace"
|
||||
})
|
||||
require.False(t, ws.Outdated, "newly created workspace with active template version must not be outdated")
|
||||
|
||||
// Given: the template version is updated
|
||||
version2 := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
@@ -63,20 +56,103 @@ func TestUpdate(t *testing.T) {
|
||||
}, template.ID)
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
|
||||
|
||||
err = client.UpdateActiveTemplateVersion(context.Background(), template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
err := client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version2.ID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err, "failed to update active template version")
|
||||
|
||||
inv, root = clitest.New(t, "update", ws.Name)
|
||||
// Then: the workspace is marked as 'outdated'
|
||||
ws, err = member.WorkspaceByOwnerAndName(ctx, codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
|
||||
require.NoError(t, err, "member failed to get workspace they themselves own")
|
||||
require.True(t, ws.Outdated, "workspace must be outdated after template version update")
|
||||
|
||||
// When: the workspace is updated
|
||||
inv, root := clitest.New(t, "update", ws.Name)
|
||||
clitest.SetupConfig(t, member, root)
|
||||
|
||||
err = inv.Run()
|
||||
require.NoError(t, err)
|
||||
require.NoError(t, err, "update command failed")
|
||||
|
||||
ws, err = member.WorkspaceByOwnerAndName(context.Background(), memberUser.Username, "my-workspace", codersdk.WorkspaceOptions{})
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, version2.ID.String(), ws.LatestBuild.TemplateVersionID.String())
|
||||
// Then: the workspace is no longer 'outdated'
|
||||
ws, err = member.WorkspaceByOwnerAndName(ctx, codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
|
||||
require.NoError(t, err, "member failed to get workspace they themselves own after update")
|
||||
require.Equal(t, version2.ID.String(), ws.LatestBuild.TemplateVersionID.String(), "workspace must have latest template version after update")
|
||||
require.False(t, ws.Outdated, "workspace must not be outdated after update")
|
||||
|
||||
// Then: the workspace must have been started with the new template version
|
||||
require.Equal(t, int32(3), ws.LatestBuild.BuildNumber, "workspace must have 3 builds after update")
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, ws.LatestBuild.Transition, "latest build must be a start transition")
|
||||
|
||||
// Then: the previous workspace build must be a stop transition with the old
|
||||
// template version.
|
||||
// This is important to ensure that the workspace resources are recreated
|
||||
// correctly. Simply running a start transition with the new template
|
||||
// version may not recreate resources that were changed in the new
|
||||
// template version. This can happen, for example, if a user specifies
|
||||
// ignore_changes in the template.
|
||||
prevBuild, err := member.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(ctx, codersdk.Me, ws.Name, "2")
|
||||
require.NoError(t, err, "failed to get previous workspace build")
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStop, prevBuild.Transition, "previous build must be a stop transition")
|
||||
require.Equal(t, version1.ID.String(), prevBuild.TemplateVersionID.String(), "previous build must have the old template version")
|
||||
})
|
||||
|
||||
t.Run("Stopped", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Given: a workspace exists on the latest template version.
|
||||
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
||||
owner := coderdtest.CreateFirstUser(t, client)
|
||||
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
||||
version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||||
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
|
||||
|
||||
ws := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||||
cwr.Name = "my-workspace"
|
||||
})
|
||||
require.False(t, ws.Outdated, "newly created workspace with active template version must not be outdated")
|
||||
|
||||
// Given: the template version is updated
|
||||
version2 := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionApply: echo.ApplyComplete,
|
||||
ProvisionPlan: echo.PlanComplete,
|
||||
}, template.ID)
|
||||
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
|
||||
|
||||
ctx := testutil.Context(t, testutil.WaitShort)
|
||||
err := client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{
|
||||
ID: version2.ID,
|
||||
})
|
||||
require.NoError(t, err, "failed to update active template version")
|
||||
|
||||
// Given: the workspace is in a stopped state.
|
||||
coderdtest.MustTransitionWorkspace(t, member, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||||
|
||||
// Then: the workspace is marked as 'outdated'
|
||||
ws, err = member.WorkspaceByOwnerAndName(ctx, codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
|
||||
require.NoError(t, err, "member failed to get workspace they themselves own")
|
||||
require.True(t, ws.Outdated, "workspace must be outdated after template version update")
|
||||
|
||||
// When: the workspace is updated
|
||||
inv, root := clitest.New(t, "update", ws.Name)
|
||||
clitest.SetupConfig(t, member, root)
|
||||
|
||||
err = inv.Run()
|
||||
require.NoError(t, err, "update command failed")
|
||||
|
||||
// Then: the workspace is no longer 'outdated'
|
||||
ws, err = member.WorkspaceByOwnerAndName(ctx, codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
|
||||
require.NoError(t, err, "member failed to get workspace they themselves own after update")
|
||||
require.Equal(t, version2.ID.String(), ws.LatestBuild.TemplateVersionID.String(), "workspace must have latest template version after update")
|
||||
require.False(t, ws.Outdated, "workspace must not be outdated after update")
|
||||
|
||||
// Then: the workspace must have been started with the new template version
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, ws.LatestBuild.Transition, "latest build must be a start transition")
|
||||
// Then: we expect 3 builds, as we manually stopped the workspace.
|
||||
require.Equal(t, int32(3), ws.LatestBuild.BuildNumber, "workspace must have 3 builds after update")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user