mirror of
https://github.com/coder/coder.git
synced 2026-06-02 20:48:20 +00: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")
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
@@ -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)
|
||||
@@ -203,7 +203,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)
|
||||
@@ -344,7 +344,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() {
|
||||
@@ -384,7 +384,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)
|
||||
|
||||
@@ -507,7 +507,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() {
|
||||
@@ -578,7 +578,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() {
|
||||
@@ -767,7 +767,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() {
|
||||
@@ -832,7 +832,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() {
|
||||
@@ -882,7 +882,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() {
|
||||
@@ -1001,7 +1001,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)
|
||||
@@ -1159,7 +1159,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
|
||||
|
||||
@@ -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.
|
||||
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()
|
||||
ctx := context.Background()
|
||||
workspace, err := client.Workspace(ctx, workspaceID)
|
||||
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{
|
||||
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
|
||||
Transition: codersdk.WorkspaceTransition(to),
|
||||
Transition: to,
|
||||
}
|
||||
|
||||
for _, mut := range muts {
|
||||
@@ -1267,7 +1267,7 @@ func MustTransitionWorkspace(t testing.TB, client *codersdk.Client, workspaceID
|
||||
_ = AwaitWorkspaceBuildJobCompleted(t, client, build.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
|
||||
}
|
||||
|
||||
|
||||
@@ -27,7 +27,6 @@ import (
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"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/rbac"
|
||||
"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)
|
||||
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)
|
||||
resp, err := appDetails.AppClient(t).Request(ctx, http.MethodGet, u.String(), nil)
|
||||
|
||||
@@ -585,7 +585,7 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T)
|
||||
// Create a workspace using this template
|
||||
workspace := coderdtest.CreateWorkspace(t, userClient, template.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
|
||||
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
|
||||
})
|
||||
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
|
||||
// ensure we do not create _another_ notification. This is
|
||||
@@ -610,7 +610,7 @@ func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T)
|
||||
cwbr.TemplateVersionID = newVersion.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)
|
||||
// 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
|
||||
workspace := coderdtest.CreateWorkspace(t, userClient, template.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
|
||||
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)
|
||||
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
|
||||
sent := notify.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceManuallyUpdated))
|
||||
|
||||
@@ -3992,7 +3992,7 @@ func TestWorkspaceDormant(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// 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.
|
||||
_, err = client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
||||
@@ -4005,7 +4005,7 @@ func TestWorkspaceDormant(t *testing.T) {
|
||||
Dormant: false,
|
||||
})
|
||||
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
@@ -1699,7 +1699,7 @@
|
||||
},
|
||||
{
|
||||
"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"
|
||||
},
|
||||
{
|
||||
|
||||
Generated
+44
-44
@@ -22,50 +22,50 @@ Coder — A tool for provisioning self-hosted development environments with Terr
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Name | Purpose |
|
||||
|----------------------------------------------------|-------------------------------------------------------------------------------------------------------|
|
||||
| [<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>external-auth</code>](./external-auth.md) | Manage external authentication |
|
||||
| [<code>login</code>](./login.md) | Authenticate with Coder deployment |
|
||||
| [<code>logout</code>](./logout.md) | Unauthenticate your local session |
|
||||
| [<code>netcheck</code>](./netcheck.md) | Print network debug information for DERP and STUN |
|
||||
| [<code>notifications</code>](./notifications.md) | Manage Coder notifications |
|
||||
| [<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>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>state</code>](./state.md) | Manually manage Terraform state to fix broken workspaces |
|
||||
| [<code>templates</code>](./templates.md) | Manage templates |
|
||||
| [<code>tokens</code>](./tokens.md) | Manage personal access tokens |
|
||||
| [<code>users</code>](./users.md) | Manage users |
|
||||
| [<code>version</code>](./version.md) | Show coder version |
|
||||
| [<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>create</code>](./create.md) | Create a workspace |
|
||||
| [<code>delete</code>](./delete.md) | Delete a workspace |
|
||||
| [<code>favorite</code>](./favorite.md) | Add a workspace to your favorites |
|
||||
| [<code>list</code>](./list.md) | List workspaces |
|
||||
| [<code>open</code>](./open.md) | Open a workspace |
|
||||
| [<code>ping</code>](./ping.md) | Ping a workspace |
|
||||
| [<code>rename</code>](./rename.md) | Rename a workspace |
|
||||
| [<code>restart</code>](./restart.md) | Restart a workspace |
|
||||
| [<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>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>start</code>](./start.md) | Start a workspace |
|
||||
| [<code>stat</code>](./stat.md) | Show resource usage for the current workspace. |
|
||||
| [<code>stop</code>](./stop.md) | Stop a workspace |
|
||||
| [<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>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>server</code>](./server.md) | Start a Coder server |
|
||||
| [<code>features</code>](./features.md) | List Enterprise features |
|
||||
| [<code>licenses</code>](./licenses.md) | Add, delete, and list licenses |
|
||||
| [<code>groups</code>](./groups.md) | Manage groups |
|
||||
| [<code>provisioner</code>](./provisioner.md) | View and manage provisioner daemons and jobs |
|
||||
| Name | Purpose |
|
||||
|----------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------|
|
||||
| [<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>external-auth</code>](./external-auth.md) | Manage external authentication |
|
||||
| [<code>login</code>](./login.md) | Authenticate with Coder deployment |
|
||||
| [<code>logout</code>](./logout.md) | Unauthenticate your local session |
|
||||
| [<code>netcheck</code>](./netcheck.md) | Print network debug information for DERP and STUN |
|
||||
| [<code>notifications</code>](./notifications.md) | Manage Coder notifications |
|
||||
| [<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>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>state</code>](./state.md) | Manually manage Terraform state to fix broken workspaces |
|
||||
| [<code>templates</code>](./templates.md) | Manage templates |
|
||||
| [<code>tokens</code>](./tokens.md) | Manage personal access tokens |
|
||||
| [<code>users</code>](./users.md) | Manage users |
|
||||
| [<code>version</code>](./version.md) | Show coder version |
|
||||
| [<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>create</code>](./create.md) | Create a workspace |
|
||||
| [<code>delete</code>](./delete.md) | Delete a workspace |
|
||||
| [<code>favorite</code>](./favorite.md) | Add a workspace to your favorites |
|
||||
| [<code>list</code>](./list.md) | List workspaces |
|
||||
| [<code>open</code>](./open.md) | Open a workspace |
|
||||
| [<code>ping</code>](./ping.md) | Ping a workspace |
|
||||
| [<code>rename</code>](./rename.md) | Rename a workspace |
|
||||
| [<code>restart</code>](./restart.md) | Restart a workspace |
|
||||
| [<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>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>start</code>](./start.md) | Start a workspace |
|
||||
| [<code>stat</code>](./stat.md) | Show resource usage for the current workspace. |
|
||||
| [<code>stop</code>](./stop.md) | Stop a workspace |
|
||||
| [<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. If the workspace is already running, it will be stopped first. |
|
||||
| [<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>server</code>](./server.md) | Start a Coder server |
|
||||
| [<code>features</code>](./features.md) | List Enterprise features |
|
||||
| [<code>licenses</code>](./licenses.md) | Add, delete, and list licenses |
|
||||
| [<code>groups</code>](./groups.md) | Manage groups |
|
||||
| [<code>provisioner</code>](./provisioner.md) | View and manage provisioner daemons and jobs |
|
||||
|
||||
## Options
|
||||
|
||||
|
||||
Generated
+1
-1
@@ -1,7 +1,7 @@
|
||||
<!-- DO NOT EDIT | GENERATED CONTENT -->
|
||||
# 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
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ import (
|
||||
|
||||
"github.com/coder/coder/v2/cli/clitest"
|
||||
"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/codersdk"
|
||||
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
|
||||
@@ -144,7 +143,7 @@ func TestStart(t *testing.T) {
|
||||
|
||||
if cmd == "start" {
|
||||
// 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
|
||||
// pass.
|
||||
@@ -196,7 +195,7 @@ func TestStart(t *testing.T) {
|
||||
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||||
workspace := coderdtest.CreateWorkspace(t, memberClient, template.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{
|
||||
Dormant: true,
|
||||
})
|
||||
|
||||
@@ -962,7 +962,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
|
||||
// Stop the workspace so we can assert autobuild does nothing
|
||||
// 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.
|
||||
ticker <- ws.LastUsedAt.Add(2 * inactiveTTL)
|
||||
@@ -1150,7 +1150,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||||
})
|
||||
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..
|
||||
tickCh <- sched.Next(ws.LatestBuild.CreatedAt)
|
||||
@@ -1319,7 +1319,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
})
|
||||
|
||||
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
|
||||
// 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
|
||||
// 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
|
||||
})
|
||||
|
||||
@@ -1420,7 +1420,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
})
|
||||
|
||||
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
|
||||
|
||||
// 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])
|
||||
|
||||
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
|
||||
@@ -1511,7 +1511,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
})
|
||||
|
||||
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
|
||||
require.NotNil(t, ws.NextStartAt)
|
||||
@@ -1573,7 +1573,7 @@ func TestWorkspaceAutobuild(t *testing.T) {
|
||||
})
|
||||
|
||||
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'
|
||||
require.NotNil(t, ws.NextStartAt)
|
||||
@@ -2099,7 +2099,7 @@ func TestExecutorAutostartBlocked(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 into the future
|
||||
go func() {
|
||||
|
||||
@@ -1018,9 +1018,22 @@ export const updateWorkspace = async (
|
||||
await fillParameters(page, richParameters, buildParameters);
|
||||
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", {
|
||||
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 (
|
||||
|
||||
+101
-52
@@ -1,5 +1,7 @@
|
||||
import {
|
||||
MockStoppedWorkspace,
|
||||
MockTemplate,
|
||||
MockTemplateVersion2,
|
||||
MockTemplateVersionParameter1,
|
||||
MockTemplateVersionParameter2,
|
||||
MockWorkspace,
|
||||
@@ -171,65 +173,112 @@ describe("api.ts", () => {
|
||||
});
|
||||
|
||||
describe("update", () => {
|
||||
it("creates a build with start and the latest template", async () => {
|
||||
jest
|
||||
.spyOn(API, "postWorkspaceBuild")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild);
|
||||
jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
|
||||
await API.updateWorkspace(MockWorkspace);
|
||||
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
|
||||
transition: "start",
|
||||
template_version_id: MockTemplate.active_version_id,
|
||||
rich_parameter_values: [],
|
||||
describe("given a running workspace", () => {
|
||||
it("stops with current version before starting with the latest version", async () => {
|
||||
jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
|
||||
...MockWorkspaceBuild,
|
||||
transition: "stop",
|
||||
});
|
||||
jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
|
||||
...MockWorkspaceBuild,
|
||||
template_version_id: MockTemplateVersion2.id,
|
||||
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 () => {
|
||||
jest
|
||||
.spyOn(API, "postWorkspaceBuild")
|
||||
.mockResolvedValue(MockWorkspaceBuild);
|
||||
jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
|
||||
jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]);
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValue([
|
||||
it("fails when having missing parameters", async () => {
|
||||
jest
|
||||
.spyOn(API, "postWorkspaceBuild")
|
||||
.mockResolvedValue(MockWorkspaceBuild);
|
||||
jest.spyOn(API, "getTemplate").mockResolvedValue(MockTemplate);
|
||||
jest.spyOn(API, "getWorkspaceBuildParameters").mockResolvedValue([]);
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.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,
|
||||
{ ...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,
|
||||
{ ...MockTemplateVersionParameter2, mutable: false },
|
||||
]);
|
||||
});
|
||||
|
||||
it("creates a build with the no parameters if it is already filled", async () => {
|
||||
jest
|
||||
.spyOn(API, "postWorkspaceBuild")
|
||||
.mockResolvedValueOnce(MockWorkspaceBuild);
|
||||
jest.spyOn(API, "getTemplate").mockResolvedValueOnce(MockTemplate);
|
||||
jest
|
||||
.spyOn(API, "getWorkspaceBuildParameters")
|
||||
.mockResolvedValue([MockWorkspaceBuildParameter1]);
|
||||
jest
|
||||
.spyOn(API, "getTemplateVersionRichParameters")
|
||||
.mockResolvedValue([
|
||||
{ ...MockTemplateVersionParameter1, required: true, mutable: false },
|
||||
it("creates a build with no parameters if it is already filled", async () => {
|
||||
jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
|
||||
...MockWorkspaceBuild,
|
||||
transition: "stop",
|
||||
});
|
||||
jest.spyOn(API, "postWorkspaceBuild").mockResolvedValueOnce({
|
||||
...MockWorkspaceBuild,
|
||||
template_version_id: MockTemplateVersion2.id,
|
||||
transition: "start",
|
||||
});
|
||||
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);
|
||||
expect(API.postWorkspaceBuild).toHaveBeenCalledWith(MockWorkspace.id, {
|
||||
transition: "start",
|
||||
template_version_id: MockTemplate.active_version_id,
|
||||
rich_parameter_values: [],
|
||||
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: 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: [],
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -2237,6 +2237,7 @@ class ApiMethods {
|
||||
* - Update the build parameters and check if there are missed parameters for
|
||||
* the newest version
|
||||
* - 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
|
||||
*/
|
||||
updateWorkspace = async (
|
||||
@@ -2274,6 +2275,19 @@ class ApiMethods {
|
||||
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, {
|
||||
transition: "start",
|
||||
template_version_id: activeVersionId,
|
||||
|
||||
@@ -6,16 +6,19 @@ import type { Workspace } from "api/typesGenerated";
|
||||
const actionTypes = [
|
||||
"start",
|
||||
"starting",
|
||||
// Replaces start when an update is required.
|
||||
// Appears beside start when an update is available.
|
||||
"updateAndStart",
|
||||
// Replaces start when an update is required.
|
||||
"updateAndStartRequireActiveVersion",
|
||||
"stop",
|
||||
"stopping",
|
||||
"restart",
|
||||
"restarting",
|
||||
// Replaces restart when an update is required.
|
||||
// Appears beside restart when an update is available.
|
||||
"updateAndRestart",
|
||||
// Replaces restart when an update is required.
|
||||
"updateAndRestartRequireActiveVersion",
|
||||
"deleting",
|
||||
"update",
|
||||
"updating",
|
||||
"activate",
|
||||
"activating",
|
||||
@@ -74,10 +77,10 @@ export const abilitiesByWorkspaceStatus = (
|
||||
const actions: ActionType[] = ["stop"];
|
||||
|
||||
if (workspace.template_require_active_version && workspace.outdated) {
|
||||
actions.push("updateAndRestart");
|
||||
actions.push("updateAndRestartRequireActiveVersion");
|
||||
} else {
|
||||
if (workspace.outdated) {
|
||||
actions.unshift("update");
|
||||
actions.unshift("updateAndRestart");
|
||||
}
|
||||
actions.push("restart");
|
||||
}
|
||||
@@ -99,10 +102,10 @@ export const abilitiesByWorkspaceStatus = (
|
||||
const actions: ActionType[] = [];
|
||||
|
||||
if (workspace.template_require_active_version && workspace.outdated) {
|
||||
actions.push("updateAndStart");
|
||||
actions.push("updateAndStartRequireActiveVersion");
|
||||
} else {
|
||||
if (workspace.outdated) {
|
||||
actions.unshift("update");
|
||||
actions.unshift("updateAndStart");
|
||||
}
|
||||
actions.push("start");
|
||||
}
|
||||
@@ -128,7 +131,7 @@ export const abilitiesByWorkspaceStatus = (
|
||||
}
|
||||
|
||||
if (workspace.outdated) {
|
||||
actions.unshift("update");
|
||||
actions.unshift("updateAndStart");
|
||||
}
|
||||
|
||||
return {
|
||||
|
||||
@@ -19,21 +19,41 @@ export interface ActionButtonProps {
|
||||
handleAction: (buildParameters?: WorkspaceBuildParameter[]) => void;
|
||||
disabled?: boolean;
|
||||
tooltipText?: string;
|
||||
isRunning?: boolean;
|
||||
requireActiveVersion?: boolean;
|
||||
}
|
||||
|
||||
export const UpdateButton: FC<ActionButtonProps> = ({
|
||||
handleAction,
|
||||
loading,
|
||||
isRunning,
|
||||
requireActiveVersion,
|
||||
}) => {
|
||||
return (
|
||||
<TopbarButton
|
||||
disabled={loading}
|
||||
data-testid="workspace-update-button"
|
||||
onClick={() => handleAction()}
|
||||
<Tooltip
|
||||
title={
|
||||
requireActiveVersion
|
||||
? "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 />
|
||||
{loading ? <>Updating…</> : <>Update…</>}
|
||||
</TopbarButton>
|
||||
<TopbarButton
|
||||
data-testid="workspace-update-button"
|
||||
disabled={loading}
|
||||
onClick={() => handleAction()}
|
||||
>
|
||||
{requireActiveVersion ? <CirclePlayIcon /> : <CloudIcon />}
|
||||
{loading ? (
|
||||
<>Updating…</>
|
||||
) : isRunning ? (
|
||||
<>Update and restart…</>
|
||||
) : (
|
||||
<>Update and start…</>
|
||||
)}
|
||||
</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…
|
||||
</TopbarButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const StopButton: FC<ActionButtonProps> = ({
|
||||
handleAction,
|
||||
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…
|
||||
</TopbarButton>
|
||||
</Tooltip>
|
||||
);
|
||||
};
|
||||
|
||||
export const CancelButton: FC<ActionButtonProps> = ({ handleAction }) => {
|
||||
return (
|
||||
<TopbarButton onClick={() => handleAction()}>
|
||||
|
||||
@@ -18,8 +18,6 @@ import {
|
||||
RestartButton,
|
||||
StartButton,
|
||||
StopButton,
|
||||
UpdateAndRestartButton,
|
||||
UpdateAndStartButton,
|
||||
UpdateButton,
|
||||
} from "./Buttons";
|
||||
import { DebugButton } from "./DebugButton";
|
||||
@@ -81,9 +79,34 @@ export const WorkspaceActions: FC<WorkspaceActionsProps> = ({
|
||||
|
||||
// A mapping of button type to the corresponding React component
|
||||
const buttonMapping: Record<ActionType, ReactNode> = {
|
||||
update: <UpdateButton handleAction={handleUpdate} />,
|
||||
updateAndStart: <UpdateAndStartButton handleAction={handleUpdate} />,
|
||||
updateAndRestart: <UpdateAndRestartButton handleAction={handleUpdate} />,
|
||||
updateAndStart: (
|
||||
<UpdateButton
|
||||
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} />,
|
||||
start: (
|
||||
<StartButton
|
||||
|
||||
@@ -107,7 +107,7 @@ describe("WorkspacesPage", () => {
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
// 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 }));
|
||||
const updateButton = await screen.findByText(/update/i);
|
||||
const updateButton = await screen.findByTestId("bulk-action-update");
|
||||
await user.click(updateButton);
|
||||
|
||||
// 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 }));
|
||||
const updateButton = await screen.findByText(/update/i);
|
||||
const updateButton = await screen.findByTestId("bulk-action-update");
|
||||
await user.click(updateButton);
|
||||
|
||||
// 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 }));
|
||||
const updateButton = await screen.findByText(/update/i);
|
||||
const updateButton = await screen.findByTestId("bulk-action-update");
|
||||
await user.click(updateButton);
|
||||
|
||||
// Three clicks: 1 running workspace, 1 dormant workspace.
|
||||
|
||||
@@ -171,7 +171,11 @@ export const WorkspacesPageView: FC<WorkspacesPageViewProps> = ({
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem onClick={onUpdateAll}>
|
||||
<CloudIcon className="size-icon-sm" /> Update…
|
||||
<CloudIcon
|
||||
className="size-icon-sm"
|
||||
data-testid="bulk-action-update"
|
||||
/>{" "}
|
||||
Update…
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
className="text-content-destructive focus:text-content-destructive"
|
||||
|
||||
Reference in New Issue
Block a user