Ctrl K
ComunidadGuías…

cli/workspace_template_version_test.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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)
			})
		}
	}
}