mirror of
https://github.com/coder/coder.git
synced 2026-10-16 14:48:56 +08:00
Add named template version selection to `coder start` and `coder stop`, and independent stop/start selection to `coder update` with `--stop-template-version` and `--template-version`. Stop still defaults to the workspace's current version, and update starts with the active version by default. Explicit selections use the selected version for parameter preparation and builds, keep server permissions authoritative, and never silently fall back to the active version. Classic stop builds retain previous parameter values omitted by the selected stop version, so a later start can reuse them across separate commands or an interrupted update. Declared parameters still follow normal validation, and start builds still discard removed parameters. Update now returns an error and retry guidance when a start apply fails after stopping, including unflagged updates that previously returned success. Closes #29100 [PLAT-616](https://linear.app/codercom/issue/PLAT-616/cli-allow-specifying-a-template-version-for-stopstart-builds-and-both) --------- Co-authored-by: Nick Vigilante <nickvigilante@users.noreply.github.com>
670 lines
24 KiB
Go
670 lines
24 KiB
Go
package cli_test
|
|
|
|
import (
|
|
"fmt"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"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/database/dbfake"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/codersdk"
|
|
"github.com/coder/coder/v2/provisioner/echo"
|
|
"github.com/coder/coder/v2/provisionersdk/proto"
|
|
"github.com/coder/coder/v2/testutil"
|
|
"github.com/coder/coder/v2/testutil/expecter"
|
|
)
|
|
|
|
const (
|
|
ephemeralParameterName = "ephemeral_parameter"
|
|
ephemeralParameterDescription = "This is ephemeral parameter"
|
|
ephemeralParameterValue = "3"
|
|
|
|
immutableParameterName = "immutable_parameter"
|
|
immutableParameterDescription = "This is immutable parameter"
|
|
immutableParameterValue = "abc"
|
|
|
|
mutableParameterName = "mutable_parameter"
|
|
mutableParameterValue = "hello"
|
|
)
|
|
|
|
func mutableParamsResponse() *echo.Responses {
|
|
return &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Parameters: []*proto.RichParameter{
|
|
{
|
|
Name: mutableParameterName,
|
|
Description: "This is a mutable parameter",
|
|
Required: true,
|
|
Mutable: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProvisionApply: echo.ApplyComplete,
|
|
}
|
|
}
|
|
|
|
func immutableParamsResponse() *echo.Responses {
|
|
return &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Parameters: []*proto.RichParameter{
|
|
{
|
|
Name: immutableParameterName,
|
|
Description: immutableParameterDescription,
|
|
Required: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProvisionApply: echo.ApplyComplete,
|
|
}
|
|
}
|
|
|
|
func TestStart(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
echoResponses := func() *echo.Responses {
|
|
return &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionInit: echo.InitComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionGraph: []*proto.Response{
|
|
{
|
|
Type: &proto.Response_Graph{
|
|
Graph: &proto.GraphComplete{
|
|
Parameters: []*proto.RichParameter{
|
|
{
|
|
Name: ephemeralParameterName,
|
|
Description: ephemeralParameterDescription,
|
|
Mutable: true,
|
|
Ephemeral: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
ProvisionApply: echo.ApplyComplete,
|
|
}
|
|
}
|
|
|
|
t.Run("BuildOptions", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
|
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
|
|
}
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
// Stop the workspace
|
|
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop, func(request *codersdk.CreateWorkspaceBuildRequest) {
|
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
|
|
}
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
|
|
|
|
inv, root := clitest.New(t, "start", workspace.Name, "--prompt-ephemeral-parameters")
|
|
clitest.SetupConfig(t, member, root)
|
|
doneChan := make(chan struct{})
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
matches := []string{
|
|
ephemeralParameterDescription, ephemeralParameterValue,
|
|
"workspace has been started", "",
|
|
}
|
|
for i := 0; i < len(matches); i += 2 {
|
|
match := matches[i]
|
|
value := matches[i+1]
|
|
stdout.ExpectMatch(ctx, match)
|
|
|
|
if value != "" {
|
|
stdin.WriteLine(value)
|
|
}
|
|
}
|
|
<-doneChan
|
|
|
|
// Verify if ephemeral parameter is set
|
|
workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
|
|
require.NoError(t, err)
|
|
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
|
|
require.NoError(t, err)
|
|
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
|
|
Name: ephemeralParameterName,
|
|
Value: ephemeralParameterValue,
|
|
})
|
|
})
|
|
|
|
t.Run("EphemeralParameterFlags", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, echoResponses())
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(request *codersdk.CreateWorkspaceRequest) {
|
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
|
|
}
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
// Stop the workspace
|
|
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop, func(request *codersdk.CreateWorkspaceBuildRequest) {
|
|
request.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{Name: ephemeralParameterName, Value: "foo"}, // Value is required, set it to something
|
|
}
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
|
|
|
|
inv, root := clitest.New(t, "start", workspace.Name,
|
|
"--ephemeral-parameter", fmt.Sprintf("%s=%s", ephemeralParameterName, ephemeralParameterValue))
|
|
clitest.SetupConfig(t, member, root)
|
|
doneChan := make(chan struct{})
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
stdout.ExpectMatch(ctx, "workspace has been started")
|
|
<-doneChan
|
|
|
|
// Verify if ephemeral parameter is set
|
|
workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
|
|
require.NoError(t, err)
|
|
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
|
|
require.NoError(t, err)
|
|
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
|
|
Name: ephemeralParameterName,
|
|
Value: ephemeralParameterValue,
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestStartWithParameters(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("DoNotAskForImmutables", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create the workspace
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, immutableParamsResponse())
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{
|
|
Name: immutableParameterName,
|
|
Value: immutableParameterValue,
|
|
},
|
|
}
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Stop the workspace
|
|
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
|
|
|
|
// Start the workspace again
|
|
inv, root := clitest.New(t, "start", workspace.Name)
|
|
clitest.SetupConfig(t, member, root)
|
|
doneChan := make(chan struct{})
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
stdout.ExpectMatch(ctx, "workspace has been started")
|
|
<-doneChan
|
|
|
|
// Verify if immutable parameter is set
|
|
workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
|
|
require.NoError(t, err)
|
|
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
|
|
require.NoError(t, err)
|
|
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
|
|
Name: immutableParameterName,
|
|
Value: immutableParameterValue,
|
|
})
|
|
})
|
|
|
|
t.Run("AlwaysPrompt", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
// Create the workspace
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, mutableParamsResponse())
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
|
{
|
|
Name: mutableParameterName,
|
|
Value: mutableParameterValue,
|
|
},
|
|
}
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Stop the workspace
|
|
workspaceBuild := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspaceBuild.ID)
|
|
|
|
// Start the workspace again
|
|
inv, root := clitest.New(t, "start", workspace.Name, "--always-prompt")
|
|
clitest.SetupConfig(t, member, root)
|
|
doneChan := make(chan struct{})
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
newValue := "xyz"
|
|
stdout.ExpectMatch(ctx, mutableParameterName)
|
|
stdin.WriteLine(newValue)
|
|
stdout.ExpectMatch(ctx, "workspace has been started")
|
|
<-doneChan
|
|
|
|
// Verify that the updated values are persisted.
|
|
workspace, err := client.WorkspaceByOwnerAndName(ctx, workspace.OwnerName, workspace.Name, codersdk.WorkspaceOptions{})
|
|
require.NoError(t, err)
|
|
actualParameters, err := client.WorkspaceBuildParameters(ctx, workspace.LatestBuild.ID)
|
|
require.NoError(t, err)
|
|
require.Contains(t, actualParameters, codersdk.WorkspaceBuildParameter{
|
|
Name: mutableParameterName,
|
|
Value: newValue,
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestStartUseParameterDefaults(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
member, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
// Create a template with no parameters and a workspace that
|
|
// auto-updates so `start` picks up the new active version.
|
|
version1 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Stop the workspace.
|
|
coderdtest.MustTransitionWorkspace(t, member, workspace.ID,
|
|
codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
|
|
|
// Push a new template version that adds a parameter with a default.
|
|
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID,
|
|
prepareEchoResponses([]*proto.RichParameter{
|
|
{Name: "new_param", Type: "string", Mutable: true, DefaultValue: "foobar"},
|
|
}), func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
|
ctvr.TemplateID = template.ID
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
err := client.UpdateActiveTemplateVersion(ctx, template.ID, codersdk.UpdateActiveTemplateVersion{ID: version2.ID})
|
|
require.NoError(t, err)
|
|
|
|
// Start the workspace with --use-parameter-defaults.
|
|
// The new parameter should be auto-accepted.
|
|
inv, root := clitest.New(t, "start", workspace.Name, "--use-parameter-defaults")
|
|
clitest.SetupConfig(t, member, root)
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
doneChan := make(chan struct{})
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
stdout.ExpectMatch(ctx, "workspace has been started")
|
|
_ = testutil.TryReceive(ctx, t, doneChan)
|
|
|
|
// Verify the new parameter was resolved to its default.
|
|
ws, err := member.WorkspaceByOwnerAndName(ctx, codersdk.Me, workspace.Name, codersdk.WorkspaceOptions{})
|
|
require.NoError(t, err)
|
|
buildParams, err := member.WorkspaceBuildParameters(ctx, ws.LatestBuild.ID)
|
|
require.NoError(t, err)
|
|
assert.Contains(t, buildParams, codersdk.WorkspaceBuildParameter{Name: "new_param", Value: "foobar"})
|
|
}
|
|
|
|
// TestStartAutoUpdate also tests restart since the flows are virtually identical.
|
|
func TestStartAutoUpdate(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
const (
|
|
stringParameterName = "myparam"
|
|
stringParameterValue = "abc"
|
|
)
|
|
|
|
stringRichParameters := []*proto.RichParameter{
|
|
{Name: stringParameterName, Type: "string", Mutable: true, Required: true},
|
|
}
|
|
|
|
type testcase struct {
|
|
Name string
|
|
Cmd string
|
|
}
|
|
|
|
cases := []testcase{
|
|
{
|
|
Name: "StartOK",
|
|
Cmd: "start",
|
|
},
|
|
{
|
|
Name: "RestartOK",
|
|
Cmd: "restart",
|
|
},
|
|
}
|
|
|
|
for _, c := range cases {
|
|
t.Run(c.Name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
logger := testutil.Logger(t)
|
|
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, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
|
ctvr.Name = "v1"
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version1.ID)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version1.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
|
cwr.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
if c.Cmd == "start" {
|
|
coderdtest.MustTransitionWorkspace(t, member, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
|
}
|
|
version2 := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, prepareEchoResponses(stringRichParameters), func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
|
ctvr.Name = "v2"
|
|
ctvr.TemplateID = template.ID
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version2.ID)
|
|
coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, version2.ID)
|
|
|
|
inv, root := clitest.New(t, c.Cmd, "-y", workspace.Name)
|
|
clitest.SetupConfig(t, member, root)
|
|
doneChan := make(chan struct{})
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
stdin := testutil.NewWriterAttachedToInvocation(t, logger.Named("stdin"), inv)
|
|
ctx := testutil.Context(t, testutil.WaitMedium)
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
stdout.ExpectMatch(ctx, stringParameterName)
|
|
stdin.WriteLine(stringParameterValue)
|
|
<-doneChan
|
|
|
|
workspace = coderdtest.MustWorkspace(t, member, workspace.ID)
|
|
require.Equal(t, version2.ID, workspace.LatestBuild.TemplateVersionID)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestStart_AlreadyRunning(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
client, db := coderdtest.NewWithDatabase(t, nil)
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
r := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
|
|
OwnerID: member.ID,
|
|
OrganizationID: owner.OrganizationID,
|
|
}).Do()
|
|
|
|
inv, root := clitest.New(t, "start", r.Workspace.Name)
|
|
clitest.SetupConfig(t, memberClient, root)
|
|
doneChan := make(chan struct{})
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
stdout.ExpectMatch(ctx, "workspace is already running")
|
|
_ = testutil.TryReceive(ctx, t, doneChan)
|
|
}
|
|
|
|
func TestStart_Starting(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
store, ps := dbtestutil.NewDB(t)
|
|
client := coderdtest.New(t, &coderdtest.Options{Pubsub: ps, Database: store})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
r := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
|
|
OwnerID: member.ID,
|
|
OrganizationID: owner.OrganizationID,
|
|
}).
|
|
Starting().
|
|
Do()
|
|
|
|
inv, root := clitest.New(t, "start", r.Workspace.Name)
|
|
clitest.SetupConfig(t, memberClient, root)
|
|
doneChan := make(chan struct{})
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
stdout.ExpectMatch(ctx, "workspace is already starting")
|
|
|
|
_ = dbfake.JobComplete(t, store, r.Build.JobID).Pubsub(ps).Do()
|
|
stdout.ExpectMatch(ctx, "workspace has been started")
|
|
|
|
_ = testutil.TryReceive(ctx, t, doneChan)
|
|
}
|
|
|
|
func TestStart_NoWait(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
// Prepare user, template, workspace
|
|
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)
|
|
workspace := coderdtest.CreateWorkspace(t, member, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Stop the workspace
|
|
build := coderdtest.CreateWorkspaceBuild(t, member, workspace, database.WorkspaceTransitionStop)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
|
|
|
// Start in no-wait mode
|
|
inv, root := clitest.New(t, "start", workspace.Name, "--no-wait")
|
|
clitest.SetupConfig(t, member, root)
|
|
doneChan := make(chan struct{})
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
stdout.ExpectMatch(ctx, "workspace has been started in no-wait mode")
|
|
_ = testutil.TryReceive(ctx, t, doneChan)
|
|
}
|
|
|
|
func TestStart_WithReason(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
|
|
// Prepare user, template, workspace
|
|
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)
|
|
workspace := coderdtest.CreateWorkspace(t, member, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Stop the workspace
|
|
build := coderdtest.CreateWorkspaceBuild(t, member, workspace, database.WorkspaceTransitionStop)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
|
|
|
// Start the workspace with reason
|
|
inv, root := clitest.New(t, "start", workspace.Name, "--reason", "cli")
|
|
clitest.SetupConfig(t, member, root)
|
|
doneChan := make(chan struct{})
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
stdout.ExpectMatch(ctx, "workspace has been started")
|
|
_ = testutil.TryReceive(ctx, t, doneChan)
|
|
|
|
workspace = coderdtest.MustWorkspace(t, member, workspace.ID)
|
|
require.Equal(t, codersdk.BuildReasonCLI, workspace.LatestBuild.Reason)
|
|
}
|
|
|
|
func TestStart_FailedStartCleansUp(t *testing.T) {
|
|
t.Parallel()
|
|
for _, name := range []string{"Default", "TemplateVersion"} {
|
|
t.Run(name, func(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
|
|
store, ps := dbtestutil.NewDB(t)
|
|
client := coderdtest.New(t, &coderdtest.Options{
|
|
Database: store,
|
|
Pubsub: ps,
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
memberClient, member := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, memberClient, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
target := version
|
|
if name == "TemplateVersion" {
|
|
target = coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil,
|
|
func(req *codersdk.CreateTemplateVersionRequest) {
|
|
req.TemplateID = template.ID
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, target.ID)
|
|
}
|
|
|
|
// Insert a failed start build directly into the database so that
|
|
// the workspace's latest build is a failed "start" transition.
|
|
failed := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
|
|
ID: workspace.ID,
|
|
OwnerID: member.ID,
|
|
OrganizationID: owner.OrganizationID,
|
|
TemplateID: template.ID,
|
|
}).
|
|
Seed(database.WorkspaceBuild{
|
|
TemplateVersionID: version.ID,
|
|
Transition: database.WorkspaceTransitionStart,
|
|
BuildNumber: workspace.LatestBuild.BuildNumber + 1,
|
|
}).
|
|
Failed().
|
|
Do()
|
|
|
|
args := []string{"start", workspace.Name}
|
|
if name == "TemplateVersion" {
|
|
args = append(args, "--template-version", target.Name)
|
|
}
|
|
inv, root := clitest.New(t, args...)
|
|
clitest.SetupConfig(t, memberClient, root)
|
|
stdout := expecter.NewAttachedToInvocation(t, inv)
|
|
doneChan := make(chan struct{})
|
|
go func() {
|
|
defer close(doneChan)
|
|
err := inv.Run()
|
|
assert.NoError(t, err)
|
|
}()
|
|
|
|
// The CLI should detect the failed start and clean up first.
|
|
stdout.ExpectMatch(ctx, "Cleaning up before retrying")
|
|
stdout.ExpectMatch(ctx, "workspace has been started")
|
|
|
|
_ = testutil.TryReceive(ctx, t, doneChan)
|
|
cleanup, err := memberClient.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
|
|
ctx, workspace.OwnerName, workspace.Name, fmt.Sprint(failed.Build.BuildNumber+1))
|
|
require.NoError(t, err)
|
|
require.Equal(t, codersdk.WorkspaceTransitionStop, cleanup.Transition)
|
|
require.Equal(t, version.ID, cleanup.TemplateVersionID)
|
|
after := coderdtest.MustWorkspace(t, memberClient, workspace.ID)
|
|
require.Equal(t, failed.Build.BuildNumber+2, after.LatestBuild.BuildNumber)
|
|
require.Equal(t, codersdk.WorkspaceTransitionStart, after.LatestBuild.Transition)
|
|
require.Equal(t, target.ID, after.LatestBuild.TemplateVersionID)
|
|
})
|
|
}
|
|
}
|