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:
Cian Johnston
2025-06-23 09:12:37 +01:00
committed by GitHub
parent 725bc3792e
commit 49fcffc266
24 changed files with 429 additions and 235 deletions
+1 -1
View File
@@ -358,7 +358,7 @@ func TestStartAutoUpdate(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
if c.Cmd == "start" { 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) { version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(stringRichParameters), func(ctvr *codersdk.CreateTemplateVersionRequest) {
ctvr.TemplateID = template.ID ctvr.TemplateID = template.ID
+27 -24
View File
@@ -37,32 +37,11 @@ func (r *RootCmd) stop() *serpent.Command {
if err != nil { if err != nil {
return err 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{ build, err := stopWorkspace(inv, client, workspace, bflags)
Transition: codersdk.WorkspaceTransitionStop,
}
if bflags.provisionerLogDebug {
wbr.LogLevel = codersdk.ProvisionerLogLevelDebug
}
build, err := client.CreateWorkspaceBuild(inv.Context(), workspace.ID, wbr)
if err != nil { if err != nil {
return err return err
} }
cliutil.WarnMatchedProvisioners(inv.Stderr, build.MatchedProvisioners, build.Job)
err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID) err = cliui.WorkspaceBuild(inv.Context(), inv.Stdout, client, build.ID)
if err != nil { if err != nil {
@@ -71,8 +50,8 @@ func (r *RootCmd) stop() *serpent.Command {
_, _ = fmt.Fprintf( _, _ = fmt.Fprintf(
inv.Stdout, 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()), cliui.Timestamp(time.Now()),
) )
return nil return nil
@@ -82,3 +61,27 @@ func (r *RootCmd) stop() *serpent.Command {
return cmd 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)
}
+2 -1
View File
@@ -57,7 +57,8 @@ SUBCOMMANDS:
tokens Manage personal access tokens tokens Manage personal access tokens
unfavorite Remove a workspace from your favorites unfavorite Remove a workspace from your favorites
update Will update and start a given workspace if it is out of 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 users Manage users
version Show coder version version Show coder version
whoami Fetch authenticated user info for Coder deployment whoami Fetch authenticated user info for Coder deployment
+2 -1
View File
@@ -3,7 +3,8 @@ coder v0.0.0-devel
USAGE: USAGE:
coder update [flags] <workspace> 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. Use --always-prompt to change the parameter values of the workspace.
+16 -1
View File
@@ -5,6 +5,7 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/coder/coder/v2/cli/cliui"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/serpent" "github.com/coder/serpent"
) )
@@ -18,7 +19,7 @@ func (r *RootCmd) update() *serpent.Command {
cmd := &serpent.Command{ cmd := &serpent.Command{
Annotations: workspaceCommand, Annotations: workspaceCommand,
Use: "update <workspace>", 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.", Long: "Use --always-prompt to change the parameter values of the workspace.",
Middleware: serpent.Chain( Middleware: serpent.Chain(
serpent.RequireNArgs(1), serpent.RequireNArgs(1),
@@ -34,6 +35,20 @@ func (r *RootCmd) update() *serpent.Command {
return nil 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) build, err := startWorkspace(inv, client, workspace, parameterFlags, bflags, WorkspaceUpdate)
if err != nil { if err != nil {
return xerrors.Errorf("start workspace: %w", err) return xerrors.Errorf("start workspace: %w", err)
+97 -21
View File
@@ -34,28 +34,21 @@ func TestUpdate(t *testing.T) {
t.Run("OK", func(t *testing.T) { t.Run("OK", func(t *testing.T) {
t.Parallel() t.Parallel()
// Given: a workspace exists on the latest template version.
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true}) client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client) 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) version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID) coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID) template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
inv, root := clitest.New(t, "create", ws := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
"my-workspace", cwr.Name = "my-workspace"
"--template", template.Name, })
"-y", require.False(t, ws.Outdated, "newly created workspace with active template version must not be outdated")
)
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())
// Given: the template version is updated
version2 := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{ version2 := coderdtest.UpdateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, Parse: echo.ParseComplete,
ProvisionApply: echo.ApplyComplete, ProvisionApply: echo.ApplyComplete,
@@ -63,20 +56,103 @@ func TestUpdate(t *testing.T) {
}, template.ID) }, template.ID)
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.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, 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) clitest.SetupConfig(t, member, root)
err = inv.Run() 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{}) // Then: the workspace is no longer 'outdated'
require.NoError(t, err) ws, err = member.WorkspaceByOwnerAndName(ctx, codersdk.Me, "my-workspace", codersdk.WorkspaceOptions{})
require.Equal(t, version2.ID.String(), ws.LatestBuild.TemplateVersionID.String()) 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")
}) })
} }
+12 -12
View File
@@ -47,7 +47,7 @@ func TestExecutorAutostartOK(t *testing.T) {
}) })
) )
// Given: workspace is stopped // 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 // When: the autobuild executor ticks after the scheduled time
go func() { go func() {
@@ -105,7 +105,7 @@ func TestMultipleLifecycleExecutors(t *testing.T) {
) )
// Have the workspace stopped so we can perform an autostart // 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 // Get both clients to perform a lifecycle execution tick
next := sched.Next(workspace.LatestBuild.CreatedAt) next := sched.Next(workspace.LatestBuild.CreatedAt)
@@ -203,7 +203,7 @@ func TestExecutorAutostartTemplateUpdated(t *testing.T) {
) )
// Given: workspace is stopped // Given: workspace is stopped
workspace = coderdtest.MustTransitionWorkspace( 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()) orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
require.NoError(t, err) require.NoError(t, err)
@@ -344,7 +344,7 @@ func TestExecutorAutostartNotEnabled(t *testing.T) {
require.Empty(t, workspace.AutostartSchedule) require.Empty(t, workspace.AutostartSchedule)
// Given: workspace is stopped // 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 // When: the autobuild executor ticks way into the future
go func() { go func() {
@@ -384,7 +384,7 @@ func TestExecutorAutostartUserSuspended(t *testing.T) {
workspace = coderdtest.MustWorkspace(t, userClient, workspace.ID) workspace = coderdtest.MustWorkspace(t, userClient, workspace.ID)
// Given: workspace is stopped, and the user is suspended. // 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) ctx := testutil.Context(t, testutil.WaitShort)
@@ -507,7 +507,7 @@ func TestExecutorAutostopAlreadyStopped(t *testing.T) {
) )
// Given: workspace is stopped // 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 // When: the autobuild executor ticks past the TTL
go func() { go func() {
@@ -578,7 +578,7 @@ func TestExecutorWorkspaceDeleted(t *testing.T) {
) )
// Given: workspace is deleted // 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 // When: the autobuild executor ticks
go func() { go func() {
@@ -767,7 +767,7 @@ func TestExecutorAutostartMultipleOK(t *testing.T) {
}) })
) )
// Given: workspace is stopped // 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 // When: the autobuild executor ticks past the scheduled time
go func() { go func() {
@@ -832,7 +832,7 @@ func TestExecutorAutostartWithParameters(t *testing.T) {
}) })
) )
// Given: workspace is stopped // 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 // When: the autobuild executor ticks after the scheduled time
go func() { go func() {
@@ -882,7 +882,7 @@ func TestExecutorAutostartTemplateDisabled(t *testing.T) {
}) })
) )
// Given: workspace is stopped // 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 // When: the autobuild executor ticks before the next scheduled time
go func() { go func() {
@@ -1001,7 +1001,7 @@ func TestExecutorRequireActiveVersion(t *testing.T) {
cwr.AutostartSchedule = ptr.Ref(sched.String()) cwr.AutostartSchedule = ptr.Ref(sched.String())
}) })
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, ownerClient, ws.LatestBuild.ID) _ = 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 req.TemplateVersionID = inactiveVersion.ID
}) })
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID) require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
@@ -1159,7 +1159,7 @@ func TestNotifications(t *testing.T) {
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
// Stop workspace // 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) _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
// Wait for workspace to become dormant // Wait for workspace to become dormant
+4 -4
View File
@@ -1245,16 +1245,16 @@ func CreateWorkspace(t testing.TB, client *codersdk.Client, templateID uuid.UUID
} }
// TransitionWorkspace is a convenience method for transitioning a workspace from one state to another. // TransitionWorkspace is a convenience method for transitioning a workspace from one state to another.
func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID uuid.UUID, from, to database.WorkspaceTransition, muts ...func(req *codersdk.CreateWorkspaceBuildRequest)) codersdk.Workspace { func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID uuid.UUID, from, to codersdk.WorkspaceTransition, muts ...func(req *codersdk.CreateWorkspaceBuildRequest)) codersdk.Workspace {
t.Helper() t.Helper()
ctx := context.Background() ctx := context.Background()
workspace, err := client.Workspace(ctx, workspaceID) workspace, err := client.Workspace(ctx, workspaceID)
require.NoError(t, err, "unexpected error fetching workspace") require.NoError(t, err, "unexpected error fetching workspace")
require.Equal(t, workspace.LatestBuild.Transition, codersdk.WorkspaceTransition(from), "expected workspace state: %s got: %s", from, workspace.LatestBuild.Transition) require.Equal(t, workspace.LatestBuild.Transition, from, "expected workspace state: %s got: %s", from, workspace.LatestBuild.Transition)
req := codersdk.CreateWorkspaceBuildRequest{ req := codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: workspace.LatestBuild.TemplateVersionID, TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
Transition: codersdk.WorkspaceTransition(to), Transition: to,
} }
for _, mut := range muts { for _, mut := range muts {
@@ -1267,7 +1267,7 @@ func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID
_ = AwaitWorkspaceBuildJobCompleted(t, client, build.ID) _ = AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
updated := MustWorkspace(t, client, workspace.ID) updated := MustWorkspace(t, client, workspace.ID)
require.Equal(t, codersdk.WorkspaceTransition(to), updated.LatestBuild.Transition, "expected workspace to be in state %s but got %s", to, updated.LatestBuild.Transition) require.Equal(t, to, updated.LatestBuild.Transition, "expected workspace to be in state %s but got %s", to, updated.LatestBuild.Transition)
return updated return updated
} }
+1 -2
View File
@@ -27,7 +27,6 @@ import (
"golang.org/x/xerrors" "golang.org/x/xerrors"
"github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/jwtutils" "github.com/coder/coder/v2/coderd/jwtutils"
"github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/coderd/workspaceapps" "github.com/coder/coder/v2/coderd/workspaceapps"
@@ -1820,7 +1819,7 @@ func Run(t *testing.T, appHostIsPrimary bool, factory DeploymentFactory) {
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong) ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel() defer cancel()
_ = coderdtest.MustTransitionWorkspace(t, appDetails.SDKClient, appDetails.Workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) _ = coderdtest.MustTransitionWorkspace(t, appDetails.SDKClient, appDetails.Workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
u := appDetails.PathAppURL(appDetails.Apps.Owner) u := appDetails.PathAppURL(appDetails.Apps.Owner)
resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, u.String(), nil) resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, u.String(), nil)
+5 -5
View File
@@ -585,7 +585,7 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T)
// Create a workspace using this template // Create a workspace using this template
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID) workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Create a new version of the template // Create a new version of the template
newVersion := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) { newVersion := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
@@ -598,7 +598,7 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T)
cwbr.TemplateVersionID = newVersion.ID cwbr.TemplateVersionID = newVersion.ID
}) })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID)
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Create the workspace build _again_. We are doing this to // Create the workspace build _again_. We are doing this to
// ensure we do not create _another_ notification. This is // ensure we do not create _another_ notification. This is
@@ -610,7 +610,7 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T)
cwbr.TemplateVersionID = newVersion.ID cwbr.TemplateVersionID = newVersion.ID
}) })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID)
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// We're going to have two notifications (one for the first user and one for the template admin) // We're going to have two notifications (one for the first user and one for the template admin)
// By ensuring we only have these two, we are sure the second build didn't trigger more // By ensuring we only have these two, we are sure the second build didn't trigger more
@@ -659,7 +659,7 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T)
// Create a workspace using this template // Create a workspace using this template
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID) workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Create a new version of the template // Create a new version of the template
newVersion := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) { newVersion := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
@@ -675,7 +675,7 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T)
}) })
require.NoError(t, err) require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID)
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Ensure we receive only 1 workspace manually updated notification and to the right user // Ensure we receive only 1 workspace manually updated notification and to the right user
sent := notify.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceManuallyUpdated)) sent := notify.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceManuallyUpdated))
+2 -2
View File
@@ -3992,7 +3992,7 @@ func TestWorkspaceDormant(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Should be able to stop a workspace while it is dormant. // Should be able to stop a workspace while it is dormant.
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Should not be able to start a workspace while it is dormant. // Should not be able to start a workspace while it is dormant.
_, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{ _, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
@@ -4005,7 +4005,7 @@ func TestWorkspaceDormant(t *testing.T) {
Dormant: false, Dormant: false,
}) })
require.NoError(t, err) require.NoError(t, err)
coderdtest.MustTransitionWorkspace(t, client, workspace.ID, database.WorkspaceTransitionStop, database.WorkspaceTransitionStart) coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionStart)
}) })
} }
+1 -1
View File
@@ -1699,7 +1699,7 @@
}, },
{ {
"title": "update", "title": "update",
"description": "Will update and start a given workspace if it is out of date", "description": "Will update and start a given workspace if it is out of date. If the workspace is already running, it will be stopped first.",
"path": "reference/cli/update.md" "path": "reference/cli/update.md"
}, },
{ {
+44 -44
View File
@@ -22,50 +22,50 @@ Coder — A tool for provisioning self-hosted development environments with Terr
## Subcommands ## Subcommands
| Name | Purpose | | Name | Purpose |
|----------------------------------------------------|-------------------------------------------------------------------------------------------------------| |----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
| [<code>completion</code>](./completion.md) | Install or update shell completion scripts for the detected or chosen shell. | | [<code>completion</code>](./completion.md) | Install or update shell completion scripts for the detected or chosen shell. |
| [<code>dotfiles</code>](./dotfiles.md) | Personalize your workspace by applying a canonical dotfiles repository | | [<code>dotfiles</code>](./dotfiles.md) | Personalize your workspace by applying a canonical dotfiles repository |
| [<code>external-auth</code>](./external-auth.md) | Manage external authentication | | [<code>external-auth</code>](./external-auth.md) | Manage external authentication |
| [<code>login</code>](./login.md) | Authenticate with Coder deployment | | [<code>login</code>](./login.md) | Authenticate with Coder deployment |
| [<code>logout</code>](./logout.md) | Unauthenticate your local session | | [<code>logout</code>](./logout.md) | Unauthenticate your local session |
| [<code>netcheck</code>](./netcheck.md) | Print network debug information for DERP and STUN | | [<code>netcheck</code>](./netcheck.md) | Print network debug information for DERP and STUN |
| [<code>notifications</code>](./notifications.md) | Manage Coder notifications | | [<code>notifications</code>](./notifications.md) | Manage Coder notifications |
| [<code>organizations</code>](./organizations.md) | Organization related commands | | [<code>organizations</code>](./organizations.md) | Organization related commands |
| [<code>port-forward</code>](./port-forward.md) | Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R". | | [<code>port-forward</code>](./port-forward.md) | Forward ports from a workspace to the local machine. For reverse port forwarding, use "coder ssh -R". |
| [<code>publickey</code>](./publickey.md) | Output your Coder public key used for Git operations | | [<code>publickey</code>](./publickey.md) | Output your Coder public key used for Git operations |
| [<code>reset-password</code>](./reset-password.md) | Directly connect to the database to reset a user's password | | [<code>reset-password</code>](./reset-password.md) | Directly connect to the database to reset a user's password |
| [<code>state</code>](./state.md) | Manually manage Terraform state to fix broken workspaces | | [<code>state</code>](./state.md) | Manually manage Terraform state to fix broken workspaces |
| [<code>templates</code>](./templates.md) | Manage templates | | [<code>templates</code>](./templates.md) | Manage templates |
| [<code>tokens</code>](./tokens.md) | Manage personal access tokens | | [<code>tokens</code>](./tokens.md) | Manage personal access tokens |
| [<code>users</code>](./users.md) | Manage users | | [<code>users</code>](./users.md) | Manage users |
| [<code>version</code>](./version.md) | Show coder version | | [<code>version</code>](./version.md) | Show coder version |
| [<code>autoupdate</code>](./autoupdate.md) | Toggle auto-update policy for a workspace | | [<code>autoupdate</code>](./autoupdate.md) | Toggle auto-update policy for a workspace |
| [<code>config-ssh</code>](./config-ssh.md) | Add an SSH Host entry for your workspaces "ssh workspace.coder" | | [<code>config-ssh</code>](./config-ssh.md) | Add an SSH Host entry for your workspaces "ssh workspace.coder" |
| [<code>create</code>](./create.md) | Create a workspace | | [<code>create</code>](./create.md) | Create a workspace |
| [<code>delete</code>](./delete.md) | Delete a workspace | | [<code>delete</code>](./delete.md) | Delete a workspace |
| [<code>favorite</code>](./favorite.md) | Add a workspace to your favorites | | [<code>favorite</code>](./favorite.md) | Add a workspace to your favorites |
| [<code>list</code>](./list.md) | List workspaces | | [<code>list</code>](./list.md) | List workspaces |
| [<code>open</code>](./open.md) | Open a workspace | | [<code>open</code>](./open.md) | Open a workspace |
| [<code>ping</code>](./ping.md) | Ping a workspace | | [<code>ping</code>](./ping.md) | Ping a workspace |
| [<code>rename</code>](./rename.md) | Rename a workspace | | [<code>rename</code>](./rename.md) | Rename a workspace |
| [<code>restart</code>](./restart.md) | Restart a workspace | | [<code>restart</code>](./restart.md) | Restart a workspace |
| [<code>schedule</code>](./schedule.md) | Schedule automated start and stop times for workspaces | | [<code>schedule</code>](./schedule.md) | Schedule automated start and stop times for workspaces |
| [<code>show</code>](./show.md) | Display details of a workspace's resources and agents | | [<code>show</code>](./show.md) | Display details of a workspace's resources and agents |
| [<code>speedtest</code>](./speedtest.md) | Run upload and download tests from your machine to a workspace | | [<code>speedtest</code>](./speedtest.md) | Run upload and download tests from your machine to a workspace |
| [<code>ssh</code>](./ssh.md) | Start a shell into a workspace or run a command | | [<code>ssh</code>](./ssh.md) | Start a shell into a workspace or run a command |
| [<code>start</code>](./start.md) | Start a workspace | | [<code>start</code>](./start.md) | Start a workspace |
| [<code>stat</code>](./stat.md) | Show resource usage for the current workspace. | | [<code>stat</code>](./stat.md) | Show resource usage for the current workspace. |
| [<code>stop</code>](./stop.md) | Stop a workspace | | [<code>stop</code>](./stop.md) | Stop a workspace |
| [<code>unfavorite</code>](./unfavorite.md) | Remove a workspace from your favorites | | [<code>unfavorite</code>](./unfavorite.md) | Remove a workspace from your favorites |
| [<code>update</code>](./update.md) | Will update and start a given workspace if it is out of date | | [<code>update</code>](./update.md) | Will update and start a given workspace if it is out of date. If the workspace is already running, it will be stopped first. |
| [<code>whoami</code>](./whoami.md) | Fetch authenticated user info for Coder deployment | | [<code>whoami</code>](./whoami.md) | Fetch authenticated user info for Coder deployment |
| [<code>support</code>](./support.md) | Commands for troubleshooting issues with a Coder deployment. | | [<code>support</code>](./support.md) | Commands for troubleshooting issues with a Coder deployment. |
| [<code>server</code>](./server.md) | Start a Coder server | | [<code>server</code>](./server.md) | Start a Coder server |
| [<code>features</code>](./features.md) | List Enterprise features | | [<code>features</code>](./features.md) | List Enterprise features |
| [<code>licenses</code>](./licenses.md) | Add, delete, and list licenses | | [<code>licenses</code>](./licenses.md) | Add, delete, and list licenses |
| [<code>groups</code>](./groups.md) | Manage groups | | [<code>groups</code>](./groups.md) | Manage groups |
| [<code>provisioner</code>](./provisioner.md) | View and manage provisioner daemons and jobs | | [<code>provisioner</code>](./provisioner.md) | View and manage provisioner daemons and jobs |
## Options ## Options
+1 -1
View File
@@ -1,7 +1,7 @@
<!-- DO NOT EDIT | GENERATED CONTENT --> <!-- DO NOT EDIT | GENERATED CONTENT -->
# update # update
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.
## Usage ## Usage
+2 -3
View File
@@ -9,7 +9,6 @@ import (
"github.com/coder/coder/v2/cli/clitest" "github.com/coder/coder/v2/cli/clitest"
"github.com/coder/coder/v2/coderd/coderdtest" "github.com/coder/coder/v2/coderd/coderdtest"
"github.com/coder/coder/v2/coderd/database"
"github.com/coder/coder/v2/coderd/rbac" "github.com/coder/coder/v2/coderd/rbac"
"github.com/coder/coder/v2/codersdk" "github.com/coder/coder/v2/codersdk"
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest" "github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
@@ -144,7 +143,7 @@ func TestStart(t *testing.T) {
if cmd == "start" { if cmd == "start" {
// Stop the workspace so that we can start it. // Stop the workspace so that we can start it.
coderdtest.MustTransitionWorkspace(t, c.Client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) coderdtest.MustTransitionWorkspace(t, c.Client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
} }
// Start the workspace. Every test permutation should // Start the workspace. Every test permutation should
// pass. // pass.
@@ -196,7 +195,7 @@ func TestStart(t *testing.T) {
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID) memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
workspace := coderdtest.CreateWorkspace(t, memberClient, template.ID) workspace := coderdtest.CreateWorkspace(t, memberClient, template.ID)
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace.LatestBuild.ID) _ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, memberClient, workspace.LatestBuild.ID)
_ = coderdtest.MustTransitionWorkspace(t, memberClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) _ = coderdtest.MustTransitionWorkspace(t, memberClient, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
err := memberClient.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{ err := memberClient.UpdateWorkspaceDormancy(ctx, workspace.ID, codersdk.UpdateWorkspaceDormancy{
Dormant: true, Dormant: true,
}) })
+9 -9
View File
@@ -962,7 +962,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
// Stop the workspace so we can assert autobuild does nothing // Stop the workspace so we can assert autobuild does nothing
// if we breach our inactivity threshold. // if we breach our inactivity threshold.
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Simulate not having accessed the workspace in a while. // Simulate not having accessed the workspace in a while.
ticker <- ws.LastUsedAt.Add(2 * inactiveTTL) ticker <- ws.LastUsedAt.Add(2 * inactiveTTL)
@@ -1150,7 +1150,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
cwr.AutostartSchedule = ptr.Ref(sched.String()) cwr.AutostartSchedule = ptr.Ref(sched.String())
}) })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Assert that autostart works when the workspace isn't dormant.. // Assert that autostart works when the workspace isn't dormant..
tickCh <- sched.Next(ws.LatestBuild.CreatedAt) tickCh <- sched.Next(ws.LatestBuild.CreatedAt)
@@ -1319,7 +1319,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
}) })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Create a new version so that we can assert we don't update // Create a new version so that we can assert we don't update
// to the latest by default. // to the latest by default.
@@ -1360,7 +1360,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
// Reset the workspace to the stopped state so we can try // Reset the workspace to the stopped state so we can try
// to autostart again. // to autostart again.
coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) { coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
req.TemplateVersionID = ws.LatestBuild.TemplateVersionID req.TemplateVersionID = ws.LatestBuild.TemplateVersionID
}) })
@@ -1420,7 +1420,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
}) })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
next := ws.LatestBuild.CreatedAt next := ws.LatestBuild.CreatedAt
// For each day of the week (Monday-Sunday) // For each day of the week (Monday-Sunday)
@@ -1448,7 +1448,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[ws.ID]) assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[ws.ID])
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
} }
// Ensure that there is a valid next start at and that is is after // Ensure that there is a valid next start at and that is is after
@@ -1511,7 +1511,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
}) })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Our next start at should be Monday // Our next start at should be Monday
require.NotNil(t, ws.NextStartAt) require.NotNil(t, ws.NextStartAt)
@@ -1573,7 +1573,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
}) })
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID) coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop) ws = coderdtest.MustTransitionWorkspace(t, client, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
// Check we have a 'NextStartAt' // Check we have a 'NextStartAt'
require.NotNil(t, ws.NextStartAt) require.NotNil(t, ws.NextStartAt)
@@ -2099,7 +2099,7 @@ func TestExecutorAutostartBlocked(t *testing.T) {
) )
// Given: workspace is stopped // 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 into the future // When: the autobuild executor ticks into the future
go func() { go func() {
+13
View File
@@ -1018,9 +1018,22 @@ export const updateWorkspace = async (
await fillParameters(page, richParameters, buildParameters); await fillParameters(page, richParameters, buildParameters);
await page.getByRole("button", { name: /update parameters/i }).click(); await page.getByRole("button", { name: /update parameters/i }).click();
// Wait for the update button to detach.
await page.waitForSelector(
"button[data-testid='workspace-update-button']:enabled",
{ state: "detached" },
);
// Wait for the workspace to be running again.
await page.waitForSelector("text=Workspace status: Running", { await page.waitForSelector("text=Workspace status: Running", {
state: "visible", state: "visible",
}); });
// Wait for the stop button to be enabled again
await page.waitForSelector(
"button[data-testid='workspace-stop-button']:enabled",
{
state: "visible",
},
);
}; };
export const updateWorkspaceParameters = async ( export const updateWorkspaceParameters = async (
+101 -52
View File
@@ -1,5 +1,7 @@
import { import {
MockStoppedWorkspace,
MockTemplate, MockTemplate,
MockTemplateVersion2,
MockTemplateVersionParameter1, MockTemplateVersionParameter1,
MockTemplateVersionParameter2, MockTemplateVersionParameter2,
MockWorkspace, MockWorkspace,
@@ -171,65 +173,112 @@ describe("api.ts", () => {
}); });
describe("update", () => { describe("update", () => {
it("creates a build with start and the latest template", async () => { describe("given a running workspace", () => {
jest it("stops with current version before starting with the latest version", async () => {
.spyOn(API, "postWorkspaceBuild") jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
.mockResolvedValueOnce(MockWorkspaceBuild); ...MockWorkspaceBuild,
jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate); transition: "stop",
await API.updateWorkspace(MockWorkspace); });
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
transition: "start", ...MockWorkspaceBuild,
template_version_id: MockTemplate.active_version_id, template_version_id: MockTemplateVersion2.id,
rich_parameter_values: [], transition: "start",
});
jest.spyOn(API, "getTemplate").mockResolvedValueOnce({
...MockTemplate,
active_version_id: MockTemplateVersion2.id,
});
await API.updateWorkspace(MockWorkspace);
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "stop",
log_level: undefined,
});
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "start",
template_version_id: MockTemplateVersion2.id,
rich_parameter_values: [],
});
}); });
});
it("fails when having missing parameters", async () => { it("fails when having missing parameters", async () => {
jest jest
.spyOn(API, "postWorkspaceBuild") .spyOn(API, "postWorkspaceBuild")
.mockResolvedValue(MockWorkspaceBuild); .mockResolvedValue(MockWorkspaceBuild);
jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate); jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]); jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]);
jest jest
.spyOn(API, "getTemplateVersionRichParameters") .spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValue([ .mockResolvedValue([
MockTemplateVersionParameter1,
{ ...MockTemplateVersionParameter2, mutable: false },
]);
let error = new Error();
try {
await API.updateWorkspace(MockWorkspace);
} catch (e) {
error = e as Error;
}
expect(error).toBeInstanceOf(MissingBuildParameters);
// Verify if the correct missing parameters are being passed
expect((error as MissingBuildParameters).parameters).toEqual([
MockTemplateVersionParameter1, MockTemplateVersionParameter1,
{ ...MockTemplateVersionParameter2, mutable: false }, { ...MockTemplateVersionParameter2, mutable: false },
]); ]);
});
let error = new Error(); it("creates a build with no parameters if it is already filled", async () => {
try { jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
await API.updateWorkspace(MockWorkspace); ...MockWorkspaceBuild,
} catch (e) { transition: "stop",
error = e as Error; });
} jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
...MockWorkspaceBuild,
expect(error).toBeInstanceOf(MissingBuildParameters); template_version_id: MockTemplateVersion2.id,
// Verify if the correct missing parameters are being passed transition: "start",
expect((error as MissingBuildParameters).parameters).toEqual([ });
MockTemplateVersionParameter1, jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
{ ...MockTemplateVersionParameter2, mutable: false }, jest
]); .spyOn(API, "getWorkspaceBuildParameters")
}); .mockResolvedValue([MockWorkspaceBuildParameter1]);
jest.spyOn(API, "getTemplateVersionRichParameters").mockResolvedValue([
it("creates a build with the no parameters if it is already filled", async () => { {
jest ...MockTemplateVersionParameter1,
.spyOn(API, "postWorkspaceBuild") required: true,
.mockResolvedValueOnce(MockWorkspaceBuild); mutable: false,
jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate); },
jest
.spyOn(API, "getWorkspaceBuildParameters")
.mockResolvedValue([MockWorkspaceBuildParameter1]);
jest
.spyOn(API, "getTemplateVersionRichParameters")
.mockResolvedValue([
{ ...MockTemplateVersionParameter1, required: true, mutable: false },
]); ]);
await API.updateWorkspace(MockWorkspace); await API.updateWorkspace(MockWorkspace);
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, { expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "start", transition: "stop",
template_version_id: MockTemplate.active_version_id, log_level: undefined,
rich_parameter_values: [], });
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
transition: "start",
template_version_id: MockTemplate.active_version_id,
rich_parameter_values: [],
});
});
});
describe("given a stopped workspace", () => {
it("creates a build with start and the latest template", async () => {
jest
.spyOn(API, "postWorkspaceBuild")
.mockResolvedValueOnce(MockWorkspaceBuild);
jest.spyOn(API, "getTemplate").mockResolvedValueOnce({
...MockTemplate,
active_version_id: MockTemplateVersion2.id,
});
await API.updateWorkspace(MockStoppedWorkspace);
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(
MockStoppedWorkspace.id,
{
transition: "start",
template_version_id: MockTemplateVersion2.id,
rich_parameter_values: [],
},
);
}); });
}); });
}); });
+14
View File
@@ -2237,6 +2237,7 @@ class ApiMethods {
* - Update the build parameters and check if there are missed parameters for * - Update the build parameters and check if there are missed parameters for
* the newest version * the newest version
* - If there are missing parameters raise an error * - If there are missing parameters raise an error
* - Stop the workspace with the current template version if it is already running
* - Create a build with the latest version and updated build parameters * - Create a build with the latest version and updated build parameters
*/ */
updateWorkspace = async ( updateWorkspace = async (
@@ -2274,6 +2275,19 @@ class ApiMethods {
throw new MissingBuildParameters(missingParameters, activeVersionId); throw new MissingBuildParameters(missingParameters, activeVersionId);
} }
// Stop the workspace if it is already running.
if (workspace.latest_build.status === "running") {
const stopBuild = await this.stopWorkspace(workspace.id);
const awaitedStopBuild = await this.waitForBuild(stopBuild);
// If the stop is canceled halfway through, we bail.
// This is the same behaviour as restartWorkspace.
if (awaitedStopBuild?.status === "canceled") {
return Promise.reject(
new Error("Workspace stop was canceled, not proceeding with update."),
);
}
}
return this.postWorkspaceBuild(workspace.id, { return this.postWorkspaceBuild(workspace.id, {
transition: "start", transition: "start",
template_version_id: activeVersionId, template_version_id: activeVersionId,
+11 -8
View File
@@ -6,16 +6,19 @@ import type { Workspace } from "api/typesGenerated";
const actionTypes = [ const actionTypes = [
"start", "start",
"starting", "starting",
// Replaces start when an update is required. // Appears beside start when an update is available.
"updateAndStart", "updateAndStart",
// Replaces start when an update is required.
"updateAndStartRequireActiveVersion",
"stop", "stop",
"stopping", "stopping",
"restart", "restart",
"restarting", "restarting",
// Replaces restart when an update is required. // Appears beside restart when an update is available.
"updateAndRestart", "updateAndRestart",
// Replaces restart when an update is required.
"updateAndRestartRequireActiveVersion",
"deleting", "deleting",
"update",
"updating", "updating",
"activate", "activate",
"activating", "activating",
@@ -74,10 +77,10 @@ export const abilitiesByWorkspaceStatus = (
const actions: ActionType[] = ["stop"]; const actions: ActionType[] = ["stop"];
if (workspace.template_require_active_version && workspace.outdated) { if (workspace.template_require_active_version && workspace.outdated) {
actions.push("updateAndRestart"); actions.push("updateAndRestartRequireActiveVersion");
} else { } else {
if (workspace.outdated) { if (workspace.outdated) {
actions.unshift("update"); actions.unshift("updateAndRestart");
} }
actions.push("restart"); actions.push("restart");
} }
@@ -99,10 +102,10 @@ export const abilitiesByWorkspaceStatus = (
const actions: ActionType[] = []; const actions: ActionType[] = [];
if (workspace.template_require_active_version && workspace.outdated) { if (workspace.template_require_active_version && workspace.outdated) {
actions.push("updateAndStart"); actions.push("updateAndStartRequireActiveVersion");
} else { } else {
if (workspace.outdated) { if (workspace.outdated) {
actions.unshift("update"); actions.unshift("updateAndStart");
} }
actions.push("start"); actions.push("start");
} }
@@ -128,7 +131,7 @@ export const abilitiesByWorkspaceStatus = (
} }
if (workspace.outdated) { if (workspace.outdated) {
actions.unshift("update"); actions.unshift("updateAndStart");
} }
return { return {
@@ -19,21 +19,41 @@ export interface ActionButtonProps {
handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void; handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void;
disabled?: boolean; disabled?: boolean;
tooltipText?: string; tooltipText?: string;
isRunning?: boolean;
requireActiveVersion?: boolean;
} }
export const UpdateButton: FC<ActionButtonProps> = ({ export const UpdateButton: FC<ActionButtonProps> = ({
handleAction, handleAction,
loading, loading,
isRunning,
requireActiveVersion,
}) => { }) => {
return ( return (
<TopbarButton <Tooltip
disabled={loading} title={
data-testid="workspace-update-button" requireActiveVersion
onClick={() => handleAction()} ? "This template requires automatic updates on workspace startup. Contact your administrator if you want to preserve the template version."
: isRunning
? "Stop workspace and restart it with the latest template version."
: "Start workspace with the latest template version."
}
> >
<CloudIcon /> <TopbarButton
{loading ? <>Updating&hellip;</> : <>Update&hellip;</>} data-testid="workspace-update-button"
</TopbarButton> disabled={loading}
onClick={() => handleAction()}
>
{requireActiveVersion ? <CirclePlayIcon /> : <CloudIcon />}
{loading ? (
<>Updating&hellip;</>
) : isRunning ? (
<>Update and restart&hellip;</>
) : (
<>Update and start&hellip;</>
)}
</TopbarButton>
</Tooltip>
); );
}; };
@@ -84,19 +104,6 @@ export const StartButton: FC<ActionButtonPropsWithWorkspace> = ({
); );
}; };
export const UpdateAndStartButton: FC<ActionButtonProps> = ({
handleAction,
}) => {
return (
<Tooltip title="This template requires automatic updates on workspace startup. Contact your administrator if you want to preserve the template version.">
<TopbarButton onClick={() => handleAction()}>
<CirclePlayIcon />
Update and start&hellip;
</TopbarButton>
</Tooltip>
);
};
export const StopButton: FC<ActionButtonProps> = ({ export const StopButton: FC<ActionButtonProps> = ({
handleAction, handleAction,
loading, loading,
@@ -138,19 +145,6 @@ export const RestartButton: FC<ActionButtonPropsWithWorkspace> = ({
); );
}; };
export const UpdateAndRestartButton: FC<ActionButtonProps> = ({
handleAction,
}) => {
return (
<Tooltip title="This template requires automatic updates on workspace startup. Contact your administrator if you want to preserve the template version.">
<TopbarButton onClick={() => handleAction()}>
<RotateCcwIcon />
Update and restart&hellip;
</TopbarButton>
</Tooltip>
);
};
export const CancelButton: FC<ActionButtonProps> = ({ handleAction }) => { export const CancelButton: FC<ActionButtonProps> = ({ handleAction }) => {
return ( return (
<TopbarButton onClick={() => handleAction()}> <TopbarButton onClick={() => handleAction()}>
@@ -18,8 +18,6 @@ import {
RestartButton, RestartButton,
StartButton, StartButton,
StopButton, StopButton,
UpdateAndRestartButton,
UpdateAndStartButton,
UpdateButton, UpdateButton,
} from "./Buttons"; } from "./Buttons";
import { DebugButton } from "./DebugButton"; import { DebugButton } from "./DebugButton";
@@ -81,9 +79,34 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
// A mapping of button type to the corresponding React component // A mapping of button type to the corresponding React component
const buttonMapping: Record<ActionType, ReactNode> = { const buttonMapping: Record<ActionType, ReactNode> = {
update: <UpdateButton handleAction={handleUpdate} />, updateAndStart: (
updateAndStart: <UpdateAndStartButton handleAction={handleUpdate} />, <UpdateButton
updateAndRestart: <UpdateAndRestartButton handleAction={handleUpdate} />, handleAction={handleUpdate}
isRunning={false}
requireActiveVersion={false}
/>
),
updateAndStartRequireActiveVersion: (
<UpdateButton
handleAction={handleUpdate}
isRunning={false}
requireActiveVersion={true}
/>
),
updateAndRestart: (
<UpdateButton
handleAction={handleUpdate}
isRunning={true}
requireActiveVersion={false}
/>
),
updateAndRestartRequireActiveVersion: (
<UpdateButton
handleAction={handleUpdate}
isRunning={true}
requireActiveVersion={true}
/>
),
updating: <UpdateButton loading handleAction={handleUpdate} />, updating: <UpdateButton loading handleAction={handleUpdate} />,
start: ( start: (
<StartButton <StartButton
@@ -107,7 +107,7 @@ describe("WorkspacesPage", () => {
} }
await user.click(screen.getByRole("button", { name: /bulk actions/i })); await user.click(screen.getByRole("button", { name: /bulk actions/i }));
const updateButton = await screen.findByText(/update/i); const updateButton = await screen.findByTestId("bulk-action-update");
await user.click(updateButton); await user.click(updateButton);
// One click: no running workspaces warning, no dormant workspaces warning. // One click: no running workspaces warning, no dormant workspaces warning.
@@ -146,7 +146,7 @@ describe("WorkspacesPage", () => {
} }
await user.click(screen.getByRole("button", { name: /bulk actions/i })); await user.click(screen.getByRole("button", { name: /bulk actions/i }));
const updateButton = await screen.findByText(/update/i); const updateButton = await screen.findByTestId("bulk-action-update");
await user.click(updateButton); await user.click(updateButton);
// Two clicks: 1 running workspace, no dormant workspaces warning. // Two clicks: 1 running workspace, no dormant workspaces warning.
@@ -184,7 +184,7 @@ describe("WorkspacesPage", () => {
} }
await user.click(screen.getByRole("button", { name: /bulk actions/i })); await user.click(screen.getByRole("button", { name: /bulk actions/i }));
const updateButton = await screen.findByText(/update/i); const updateButton = await screen.findByTestId("bulk-action-update");
await user.click(updateButton); await user.click(updateButton);
// Two clicks: no running workspaces warning, 1 dormant workspace. // Two clicks: no running workspaces warning, 1 dormant workspace.
@@ -224,7 +224,7 @@ describe("WorkspacesPage", () => {
} }
await user.click(screen.getByRole("button", { name: /bulk actions/i })); await user.click(screen.getByRole("button", { name: /bulk actions/i }));
const updateButton = await screen.findByText(/update/i); const updateButton = await screen.findByTestId("bulk-action-update");
await user.click(updateButton); await user.click(updateButton);
// Three clicks: 1 running workspace, 1 dormant workspace. // Three clicks: 1 running workspace, 1 dormant workspace.
@@ -171,7 +171,11 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<DropdownMenuItem onClick={onUpdateAll}> <DropdownMenuItem onClick={onUpdateAll}>
<CloudIcon className="size-icon-sm" /> Update&hellip; <CloudIcon
className="size-icon-sm"
data-testid="bulk-action-update"
/>{" "}
Update&hellip;
</DropdownMenuItem> </DropdownMenuItem>
<DropdownMenuItem <DropdownMenuItem
className="text-content-destructive focus:text-content-destructive" className="text-content-destructive focus:text-content-destructive"