Files
coder/cli/start_test.go
dylanhuff-at-coderandNick Vigilante b4d51012da feat: allow template version selection for workspace builds (#30199)
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>
2026-10-07 13:17:12 -07:00

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