mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix!: stop workspace before update (#18425)
Fixes https://github.com/coder/coder/issues/17840 NOTE: calling this out as a breaking change so that it is highly visible in the changelog. * CLI: Modifies `coder update` to stop the workspace if already running. * UI: Modifies "update" button to always stop the workspace if already running.
This commit is contained in:
+1
-1
@@ -358,7 +358,7 @@ func TestStartAutoUpdate(t *testing.T) {
|
|||||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
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
@@ -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)
|
||||||
|
}
|
||||||
|
|||||||
Vendored
+2
-1
@@ -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
@@ -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
@@ -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
@@ -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")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
@@ -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
@@ -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"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
Generated
+44
-44
@@ -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
|
||||||
|
|
||||||
|
|||||||
Generated
+1
-1
@@ -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
|
||||||
|
|
||||||
|
|||||||
@@ -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,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
@@ -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: [],
|
||||||
|
},
|
||||||
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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…</> : <>Update…</>}
|
data-testid="workspace-update-button"
|
||||||
</TopbarButton>
|
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> = ({
|
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…
|
|
||||||
</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…
|
<CloudIcon
|
||||||
|
className="size-icon-sm"
|
||||||
|
data-testid="bulk-action-update"
|
||||||
|
/>{" "}
|
||||||
|
Update…
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="text-content-destructive focus:text-content-destructive"
|
className="text-content-destructive focus:text-content-destructive"
|
||||||
|
|||||||
Reference in New Issue
Block a user