Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

fix!: stop workspace before update#18425

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Draft
johnstcn wants to merge15 commits intomain
base:main
Choose a base branch
Loading
fromcj/prebuild-template-upgrade
Draft
Show file tree
Hide file tree
Changes fromall commits
Commits
Show all changes
15 commits
Select commitHold shift + click to select a range
9443453
chore: TestUpdate: remove extraneous cli invocation
johnstcnJun 17, 2025
594d1d7
assert previous build state when updating workspace
johnstcnJun 17, 2025
873794f
chore: coderdtest: change argument types to remove unnecessary conver…
johnstcnJun 17, 2025
f73b3f6
cli: stop before starting on update
johnstcnJun 17, 2025
8a48d85
chore: refactor and extract stopWorkspace function
johnstcnJun 18, 2025
123b51f
update cli usage doc
johnstcnJun 18, 2025
3d6106f
confirm before stop
johnstcnJun 18, 2025
f25c333
Revert "confirm before stop"
johnstcnJun 18, 2025
9bf5141
ui: make update button stop before start
johnstcnJun 18, 2025
8dd13d3
make fmt
johnstcnJun 18, 2025
be0c176
site: fix some tests
johnstcnJun 18, 2025
1c703ca
fixup! site: fix some tests
johnstcnJun 18, 2025
da8bd12
reduce button explosion
johnstcnJun 19, 2025
ed6399a
adjust icons
johnstcnJun 19, 2025
0cf0110
add testid to bulk update button and fix bulk update tests
johnstcnJun 19, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletioncli/start_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -359,7 +359,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
Expand Down
51 changes: 27 additions & 24 deletionscli/stop.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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 {
Expand All@@ -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
Expand All@@ -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)
}
3 changes: 2 additions & 1 deletioncli/testdata/coder_--help.golden
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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
Expand Down
3 changes: 2 additions & 1 deletioncli/testdata/coder_update_--help.golden
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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.

Expand Down
17 changes: 16 additions & 1 deletioncli/update.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -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"
)
Expand All@@ -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),
Expand All@@ -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)
Expand Down
114 changes: 95 additions & 19 deletionscli/update_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -34,49 +34,125 @@ 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",
)
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")

// 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)
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, 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")
})

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())
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)

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")

// Given: the workspace is in a stopped state.
coderdtest.MustTransitionWorkspace(t, member, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)

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)

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())
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")
})
}

Expand Down
24 changes: 12 additions & 12 deletionscoderd/autobuild/lifecycle_executor_test.go
View file
Open in desktop
Original file line numberDiff line numberDiff line change
Expand Up@@ -47,7 +47,7 @@ func TestExecutorAutostartOK(t *testing.T) {
})
)
// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)

// When: the autobuild executor ticks after the scheduled time
go func() {
Expand DownExpand Up@@ -105,7 +105,7 @@ func TestMultipleLifecycleExecutors(t *testing.T) {
)

// Have the workspace stopped so we can perform an autostart
workspace = coderdtest.MustTransitionWorkspace(t, clientA, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
workspace = coderdtest.MustTransitionWorkspace(t, clientA, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)

// Get both clients to perform a lifecycle execution tick
next := sched.Next(workspace.LatestBuild.CreatedAt)
Expand DownExpand Up@@ -204,7 +204,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
)
// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(
t, client, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
t, client, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)

orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
require.NoError(t, err)
Expand DownExpand Up@@ -345,7 +345,7 @@ func TestExecutorAutostartNotEnabled(t *testing.T) {
require.Empty(t, workspace.AutostartSchedule)

// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)

// When: the autobuild executor ticks way into the future
go func() {
Expand DownExpand Up@@ -385,7 +385,7 @@ func TestExecutorAutostartUserSuspended(t *testing.T) {
workspace = coderdtest.MustWorkspace(t, userClient, workspace.ID)

// Given: workspace is stopped, and the user is suspended.
workspace = coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
workspace = coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)

ctx := testutil.Context(t, testutil.WaitShort)

Expand DownExpand Up@@ -508,7 +508,7 @@ func TestExecutorAutostopAlreadyStopped(t *testing.T) {
)

// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)

// When: the autobuild executor ticks past the TTL
go func() {
Expand DownExpand Up@@ -579,7 +579,7 @@ func TestExecutorWorkspaceDeleted(t *testing.T) {
)

// Given: workspace is deleted
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionDelete)
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionDelete)

// When: the autobuild executor ticks
go func() {
Expand DownExpand Up@@ -768,7 +768,7 @@ func TestExecutorAutostartMultipleOK(t *testing.T) {
})
)
// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)

// When: the autobuild executor ticks past the scheduled time
go func() {
Expand DownExpand Up@@ -833,7 +833,7 @@ func TestExecutorAutostartWithParameters(t *testing.T) {
})
)
// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)

// When: the autobuild executor ticks after the scheduled time
go func() {
Expand DownExpand Up@@ -883,7 +883,7 @@ func TestExecutorAutostartTemplateDisabled(t *testing.T) {
})
)
// Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)

// When: the autobuild executor ticks before the next scheduled time
go func() {
Expand DownExpand Up@@ -1002,7 +1002,7 @@ func TestExecutorRequireActiveVersion(t *testing.T) {
cwr.AutostartSchedule = ptr.Ref(sched.String())
})
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, ownerClient, ws.LatestBuild.ID)
ws = coderdtest.MustTransitionWorkspace(t, memberClient, ws.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
ws = coderdtest.MustTransitionWorkspace(t, memberClient, ws.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
req.TemplateVersionID = inactiveVersion.ID
})
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
Expand DownExpand Up@@ -1160,7 +1160,7 @@ func TestNotifications(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)

// Stop workspace
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,database.WorkspaceTransitionStart,database.WorkspaceTransitionStop)
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID,codersdk.WorkspaceTransitionStart,codersdk.WorkspaceTransitionStop)
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)

// Wait for workspace to become dormant
Expand Down
Loading
Loading

[8]ページ先頭

©2009-2025 Movatter.jp