cli/workspace_template_version_test.go
Rama main · réplica de lectura del repositorio original.
package cli_test
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"testing"
"github.com/google/uuid"
"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"
)
func TestWorkspaceBuildTemplateVersion(t *testing.T) {
t.Parallel()
for _, tc := range []struct {
name string
command string
startPin bool
stopPin bool
stopped bool
sameVersion bool
missing bool
byID bool
wantErr string
}{
{name: "Stop", command: "stop", startPin: true},
{name: "StartRunning", command: "start", startPin: true},
{name: "StartStopped", command: "start", startPin: true, stopped: true},
{name: "StartSameVersion", command: "start", startPin: true, sameVersion: true},
{name: "UpdateStart", command: "update", startPin: true},
{name: "UpdateStop", command: "update", stopPin: true},
{name: "UpdateBoth", command: "update", startPin: true, stopPin: true},
{name: "UpdateStopped", command: "update", startPin: true, stopPin: true, stopped: true},
{name: "UpdateStopOnlyStopped", command: "update", stopPin: true, stopped: true, wantErr: "already stopped and up-to-date"},
{name: "StopByID", command: "stop", startPin: true, byID: true},
{name: "StartByID", command: "start", startPin: true, byID: true},
{name: "UpdateBothByID", command: "update", startPin: true, stopPin: true, byID: true},
{name: "StartMissingName", command: "start", startPin: true, missing: true},
{name: "UpdateMissingStart", command: "update", startPin: true, stopPin: true, missing: true, byID: true},
{name: "UpdateMissingStop", command: "update", stopPin: true, missing: true, byID: true},
} {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
missingVersion := "missing-version"
if tc.byID {
missingVersion = uuid.NewString()
}
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, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
req.AutomaticUpdates = codersdk.AutomaticUpdatesAlways
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
if tc.stopped {
coderdtest.MustTransitionWorkspace(t, member, workspace.ID,
codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
}
target := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID,
prepareEchoResponses([]*proto.RichParameter{
{Name: "target_parameter", Type: "string", Mutable: true, DefaultValue: "target"},
}), func(req *codersdk.CreateTemplateVersionRequest) {
req.TemplateID = template.ID
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, target.ID)
stopTarget := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil,
func(req *codersdk.CreateTemplateVersionRequest) {
req.TemplateID = template.ID
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, stopTarget.ID)
// Keep the workspace current so explicit update flags must bypass
// the up-to-date shortcut. Targets remain non-active.
ctx := testutil.Context(t, testutil.WaitLong)
workspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.False(t, workspace.Outdated)
before := workspace.LatestBuild.BuildNumber
args := []string{tc.command, workspace.Name, "-y"}
startID := version.ID
if tc.startPin {
name := target.Name
if tc.byID {
name = target.ID.String()
}
startID = target.ID
if tc.sameVersion {
name = version.Name
startID = version.ID
}
if tc.missing {
name = missingVersion
}
args = append(args, "--template-version", name)
}
if tc.stopPin {
name := stopTarget.Name
if tc.byID {
name = stopTarget.ID.String()
}
if tc.missing && !tc.startPin {
name = missingVersion
}
args = append(args, "--stop-template-version", name)
}
if tc.command != "stop" {
args = append(args, "--use-parameter-defaults")
if tc.startPin && !tc.sameVersion && !tc.missing {
args = append(args, "--parameter", "target_parameter=chosen")
}
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
var stderr bytes.Buffer
inv.Stderr = &stderr
err = inv.Run()
if tc.wantErr != "" {
require.ErrorContains(t, err, tc.wantErr)
after, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.Equal(t, before, after.LatestBuild.BuildNumber)
require.Equal(t, codersdk.WorkspaceTransitionStop, after.LatestBuild.Transition)
return
}
if tc.missing {
require.Error(t, err)
require.ErrorContains(t, err, fmt.Sprintf("not found for template %q", template.Name))
after, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.Equal(t, before, after.LatestBuild.BuildNumber)
return
}
require.NoError(t, err)
after, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
if tc.command == "update" && tc.stopPin && tc.stopped {
require.Contains(t, stderr.String(), "ignoring --stop-template-version")
}
if tc.command == "stop" {
require.Equal(t, before+1, after.LatestBuild.BuildNumber)
require.Equal(t, codersdk.WorkspaceTransitionStop, after.LatestBuild.Transition)
require.Equal(t, target.ID, after.LatestBuild.TemplateVersionID)
return
}
expectedBuilds := before + 1
if tc.command == "update" && !tc.stopped {
expectedBuilds++
stopBuild, err := client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
ctx, workspace.OwnerName, workspace.Name, fmt.Sprint(before+1))
require.NoError(t, err)
require.Equal(t, codersdk.WorkspaceTransitionStop, stopBuild.Transition)
stopID := version.ID
if tc.stopPin {
stopID = stopTarget.ID
}
require.Equal(t, stopID, stopBuild.TemplateVersionID)
}
require.Equal(t, expectedBuilds, after.LatestBuild.BuildNumber)
require.Equal(t, codersdk.WorkspaceTransitionStart, after.LatestBuild.Transition)
require.Equal(t, startID, after.LatestBuild.TemplateVersionID)
if startID == target.ID {
parameters, err := client.WorkspaceBuildParameters(ctx, after.LatestBuild.ID)
require.NoError(t, err)
require.Contains(t, parameters, codersdk.WorkspaceBuildParameter{
Name: "target_parameter", Value: "chosen",
})
}
})
}
}
func TestTemplateVersionClassicParameterContinuity(t *testing.T) {
t.Parallel()
for _, required := range []bool{false, true} {
for _, mode := range []string{"Update", "SeparateCommands", "InterruptedUpdate"} {
t.Run(fmt.Sprintf("%s/Required=%t", mode, required), 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)
prior := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID,
prepareEchoResponses([]*proto.RichParameter{{
Name: "disk_size", Type: "string", Required: required, DefaultValue: "64",
}}))
coderdtest.AwaitTemplateVersionJobCompleted(t, client, prior.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, prior.ID,
func(req *codersdk.CreateTemplateRequest) {
classic := true
req.UseClassicParameterFlow = &classic
})
workspace := coderdtest.CreateWorkspace(t, member, template.ID,
func(req *codersdk.CreateWorkspaceRequest) {
req.RichParameterValues = []codersdk.WorkspaceBuildParameter{
{Name: "disk_size", Value: "128"},
}
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
stopTarget := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil,
func(req *codersdk.CreateTemplateVersionRequest) { req.TemplateID = template.ID })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, stopTarget.ID)
run := func(args ...string) error {
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
return inv.Run()
}
if mode == "SeparateCommands" {
require.NoError(t, run("stop", workspace.Name, "--template-version", stopTarget.Name, "-y"))
} else {
args := []string{
"update", workspace.Name, "--stop-template-version", stopTarget.Name,
"--use-parameter-defaults", "-y",
}
if mode == "InterruptedUpdate" {
args = append(args, "--parameter", "unknown=value")
err := run(args...)
require.Error(t, err)
require.Contains(t, err.Error(), "workspace is stopped")
} else {
require.NoError(t, run(args...))
}
}
ctx := testutil.Context(t, testutil.WaitLong)
stop, err := client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
ctx, workspace.OwnerName, workspace.Name, "2")
require.NoError(t, err)
require.Equal(t, stopTarget.ID, stop.TemplateVersionID)
require.Equal(t, codersdk.ProvisionerJobSucceeded, stop.Job.Status)
params, err := client.WorkspaceBuildParameters(ctx, stop.ID)
require.NoError(t, err)
require.Contains(t, params, codersdk.WorkspaceBuildParameter{Name: "disk_size", Value: "128"})
if mode != "Update" {
require.NoError(t, run("start", workspace.Name, "--template-version", prior.Name,
"--use-parameter-defaults", "-y"))
}
after := coderdtest.MustWorkspace(t, client, workspace.ID)
require.Equal(t, int32(3), after.LatestBuild.BuildNumber)
require.Equal(t, prior.ID, after.LatestBuild.TemplateVersionID)
require.Equal(t, codersdk.WorkspaceStatusRunning, after.LatestBuild.Status)
params, err = client.WorkspaceBuildParameters(ctx, after.LatestBuild.ID)
require.NoError(t, err)
require.Contains(t, params, codersdk.WorkspaceBuildParameter{Name: "disk_size", Value: "128"})
})
}
}
}
func TestUpdateTemplateVersionApplyFailure(t *testing.T) {
t.Parallel()
for _, pinned := range []bool{false, true} {
t.Run(fmt.Sprint(pinned), 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)
prior := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, prior.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, prior.ID)
workspace := coderdtest.CreateWorkspace(t, member, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
const failure = "real start apply failed"
const partialState = "partial start state"
target := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete, ProvisionGraph: echo.GraphComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_START: {
{Type: &proto.Response_Apply{Apply: &proto.ApplyComplete{
Error: failure, State: []byte(partialState),
}}},
},
},
}, func(req *codersdk.CreateTemplateVersionRequest) {
req.TemplateID = template.ID
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, target.ID)
args := []string{"update", workspace.Name, "--use-parameter-defaults", "-y"}
if pinned {
args = append(args, "--template-version", target.Name)
} else {
coderdtest.UpdateActiveTemplateVersion(t, client, template.ID, target.ID)
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
err := inv.Run()
require.Error(t, err)
require.Contains(t, err.Error(), failure)
require.Contains(t, err.Error(), "workspace did not restart successfully")
require.Contains(t, err.Error(), "run 'coder start' to retry")
ctx := testutil.Context(t, testutil.WaitLong)
after := coderdtest.MustWorkspace(t, client, workspace.ID)
require.Equal(t, int32(3), after.LatestBuild.BuildNumber)
require.Equal(t, target.ID, after.LatestBuild.TemplateVersionID)
require.Equal(t, codersdk.WorkspaceStatusFailed, after.LatestBuild.Status)
require.Equal(t, failure, after.LatestBuild.Job.Error)
state, err := client.WorkspaceBuildState(ctx, after.LatestBuild.ID)
require.NoError(t, err)
require.Equal(t, partialState, string(state))
stop, err := client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
ctx, workspace.OwnerName, workspace.Name, "2")
require.NoError(t, err)
require.Equal(t, prior.ID, stop.TemplateVersionID)
require.Equal(t, codersdk.WorkspaceTransitionStop, stop.Transition)
require.Equal(t, codersdk.ProvisionerJobSucceeded, stop.Job.Status)
})
}
}
func TestStartTemplateVersionBuildInProgress(t *testing.T) {
t.Parallel()
for _, starting := range []bool{false, true} {
t.Run(fmt.Sprint(starting), func(t *testing.T) {
t.Parallel()
store, ps := dbtestutil.NewDB(t)
client := coderdtest.New(t, &coderdtest.Options{Database: store, Pubsub: ps})
owner := coderdtest.CreateFirstUser(t, client)
member, memberUser := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
builder := dbfake.WorkspaceBuild(t, store, database.WorkspaceTable{
OwnerID: memberUser.ID, OrganizationID: owner.OrganizationID,
})
if starting {
builder = builder.Starting()
} else {
builder = builder.Pending()
}
ws := builder.Do()
ctx := testutil.Context(t, testutil.WaitShort)
version, err := client.TemplateVersion(ctx, ws.Build.TemplateVersionID)
require.NoError(t, err)
inv, root := clitest.New(t, "start", ws.Workspace.Name,
"--template-version", version.Name, "-y")
clitest.SetupConfig(t, member, root)
err = inv.Run()
require.Error(t, err)
require.Contains(t, err.Error(), "wait for the current build to finish")
after := coderdtest.MustWorkspace(t, client, ws.Workspace.ID)
require.Equal(t, ws.Build.ID, after.LatestBuild.ID)
})
}
}
func TestUpdateStopTemplateVersionFailedStop(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)
const failure = "stop apply failed"
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete, ProvisionInit: echo.InitComplete,
ProvisionPlan: echo.PlanComplete, ProvisionGraph: echo.GraphComplete,
ProvisionApply: echo.ApplyComplete,
ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{
proto.WorkspaceTransition_STOP: {
{Type: &proto.Response_Apply{Apply: &proto.ApplyComplete{Error: failure}}},
},
},
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, member, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
stopTarget := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil,
func(req *codersdk.CreateTemplateVersionRequest) { req.TemplateID = template.ID })
coderdtest.AwaitTemplateVersionJobCompleted(t, client, stopTarget.ID)
ctx := testutil.Context(t, testutil.WaitLong)
stop, err := member.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStop,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, stop.ID)
before := coderdtest.MustWorkspace(t, client, workspace.ID)
require.Equal(t, codersdk.WorkspaceTransitionStop, before.LatestBuild.Transition)
require.Equal(t, codersdk.WorkspaceStatusFailed, before.LatestBuild.Status)
inv, root := clitest.New(t, "update", workspace.Name, "--stop-template-version", stopTarget.Name,
"--template-version", version.Name, "--use-parameter-defaults", "-y")
clitest.SetupConfig(t, member, root)
err = inv.Run()
require.ErrorContains(t, err, "the latest build is a stop (failed)")
require.ErrorContains(t, err, "coder stop --template-version "+stopTarget.Name)
after := coderdtest.MustWorkspace(t, client, workspace.ID)
require.Equal(t, before.LatestBuild.ID, after.LatestBuild.ID)
}
func TestTemplateVersionNameOrID(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)
createVersion := func(mutators ...func(*codersdk.CreateTemplateVersionRequest)) codersdk.TemplateVersion {
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil, mutators...)
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
return version
}
active := createVersion()
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, active.ID)
inTemplate := func(req *codersdk.CreateTemplateVersionRequest) { req.TemplateID = template.ID }
target := createVersion(inTemplate)
// Named after the active version's ID, so that ID resolves to this
// version by name.
namedLikeID := createVersion(func(req *codersdk.CreateTemplateVersionRequest) {
req.TemplateID = template.ID
req.Name = active.ID.String()
})
otherVersion := createVersion()
coderdtest.CreateTemplate(t, client, owner.OrganizationID, otherVersion.ID)
// A file whose body decodes as a version of this template that points at
// the other template's version.
forged, err := json.Marshal(codersdk.TemplateVersion{ID: otherVersion.ID, TemplateID: &template.ID})
require.NoError(t, err)
forgedFile, err := member.Upload(testutil.Context(t, testutil.WaitShort), codersdk.ContentTypeTar, bytes.NewReader(forged))
require.NoError(t, err)
// Error formats take the --template-version input and the template name.
const (
notFound = "template version %q not found for template %q"
notOwned = "template version %q does not belong to template %q"
)
for _, command := range []string{"create", "start", "stop"} {
for _, tc := range []struct {
name string
version string
wantID uuid.UUID
wantErr string
}{
{name: "ID", version: target.ID.String(), wantID: target.ID},
{name: "NameBeforeID", version: active.ID.String(), wantID: namedLikeID.ID},
{name: "OtherTemplateID", version: otherVersion.ID.String(), wantErr: notOwned},
{name: "OtherTemplatePathName", version: "../../../templateversions/" + otherVersion.ID.String(), wantErr: notFound},
{name: "ForgedFilePathName", version: "../../../files/" + forgedFile.ID.String(), wantErr: notFound},
{name: "ParentSegment", version: "..", wantErr: notFound},
{name: "CurrentSegment", version: ".", wantErr: notFound},
{name: "MissingID", version: uuid.NewString(), wantErr: notFound},
{name: "MissingName", version: "missing-version", wantErr: notFound},
} {
t.Run(command+"/"+tc.name, func(t *testing.T) {
t.Parallel()
ctx := testutil.Context(t, testutil.WaitLong)
workspaceName := testutil.GetRandomNameHyphenated(t)
args := []string{command, workspaceName, "--template-version", tc.version, "-y"}
var before codersdk.Workspace
if command == "create" {
args = append(args, "--template", template.Name)
} else {
before = coderdtest.CreateWorkspace(t, member, template.ID, func(req *codersdk.CreateWorkspaceRequest) {
req.Name = workspaceName
})
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, before.LatestBuild.ID)
}
if command != "stop" {
args = append(args, "--use-parameter-defaults")
}
inv, root := clitest.New(t, args...)
clitest.SetupConfig(t, member, root)
err := inv.Run()
if tc.wantErr != "" {
require.ErrorContains(t, err, fmt.Sprintf(tc.wantErr, tc.version, template.Name))
if command == "create" {
_, err := member.WorkspaceByOwnerAndName(ctx, codersdk.Me, workspaceName, codersdk.WorkspaceOptions{})
var sdkErr *codersdk.Error
require.ErrorAs(t, err, &sdkErr)
require.Equal(t, http.StatusNotFound, sdkErr.StatusCode())
return
}
after := coderdtest.MustWorkspace(t, member, before.ID)
require.Equal(t, before.LatestBuild.ID, after.LatestBuild.ID)
return
}
require.NoError(t, err)
after, err := member.WorkspaceByOwnerAndName(ctx, codersdk.Me, workspaceName, codersdk.WorkspaceOptions{})
require.NoError(t, err)
require.Equal(t, template.ID, after.TemplateID)
require.Equal(t, tc.wantID, after.LatestBuild.TemplateVersionID)
})
}
}
}