coder/coderd/workspacebuilds_test.go

974 lines
37 KiB
Go

package coderd_test
import (
"context"
"errors"
"fmt"
"net/http"
"strconv"
"testing"
"time"
"github.com/google/uuid"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/coder/coder/coderd/audit"
"github.com/coder/coder/coderd/coderdtest"
"github.com/coder/coder/coderd/database"
"github.com/coder/coder/codersdk"
"github.com/coder/coder/provisioner/echo"
"github.com/coder/coder/provisionersdk/proto"
"github.com/coder/coder/testutil"
)
func TestWorkspaceBuild(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, err := client.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
}
func TestWorkspaceBuildByBuildNumber(t *testing.T) {
t.Parallel()
t.Run("Successful", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
first := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err, "fetch me")
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
_, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
ctx,
user.Username,
workspace.Name,
strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10),
)
require.NoError(t, err)
})
t.Run("BuildNumberNotInt", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
first := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err, "fetch me")
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
_, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
ctx,
user.Username,
workspace.Name,
"buildNumber",
)
var apiError *codersdk.Error
require.ErrorAs(t, err, &apiError)
require.Equal(t, http.StatusBadRequest, apiError.StatusCode())
require.ErrorContains(t, apiError, "Failed to parse build number as integer.")
})
t.Run("WorkspaceNotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
first := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err, "fetch me")
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
_, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
ctx,
user.Username,
"workspaceName",
strconv.FormatInt(int64(workspace.LatestBuild.BuildNumber), 10),
)
var apiError *codersdk.Error
require.ErrorAs(t, err, &apiError)
require.Equal(t, http.StatusNotFound, apiError.StatusCode())
require.ErrorContains(t, apiError, "Resource not found")
})
t.Run("WorkspaceBuildNotFound", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
first := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err, "fetch me")
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
_, err = client.WorkspaceBuildByUsernameAndWorkspaceNameAndBuildNumber(
ctx,
user.Username,
workspace.Name,
"200",
)
var apiError *codersdk.Error
require.ErrorAs(t, err, &apiError)
require.Equal(t, http.StatusNotFound, apiError.StatusCode())
require.ErrorContains(t, apiError, fmt.Sprintf("Workspace %q Build 200 does not exist.", workspace.Name))
})
}
func TestWorkspaceBuilds(t *testing.T) {
t.Parallel()
t.Run("Single", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
first := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
user, err := client.User(ctx, codersdk.Me)
require.NoError(t, err, "fetch me")
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
builds, err := client.WorkspaceBuilds(ctx,
codersdk.WorkspaceBuildsRequest{WorkspaceID: workspace.ID})
require.Len(t, builds, 1)
require.Equal(t, int32(1), builds[0].BuildNumber)
require.Equal(t, user.Username, builds[0].InitiatorUsername)
require.NoError(t, err)
// Test since
builds, err = client.WorkspaceBuilds(ctx,
codersdk.WorkspaceBuildsRequest{WorkspaceID: workspace.ID, Since: database.Now().Add(time.Minute)},
)
require.NoError(t, err)
require.Len(t, builds, 0)
// Should never be nil for API consistency
require.NotNil(t, builds)
builds, err = client.WorkspaceBuilds(ctx,
codersdk.WorkspaceBuildsRequest{WorkspaceID: workspace.ID, Since: database.Now().Add(-time.Hour)},
)
require.NoError(t, err)
require.Len(t, builds, 1)
})
t.Run("DeletedInitiator", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
first := coderdtest.CreateFirstUser(t, client)
second, secondUser := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, "owner")
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace, err := second.CreateWorkspace(ctx, first.OrganizationID, first.UserID.String(), codersdk.CreateWorkspaceRequest{
TemplateID: template.ID,
Name: "example",
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
err = client.DeleteUser(ctx, secondUser.ID)
require.NoError(t, err)
builds, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{WorkspaceID: workspace.ID})
require.Len(t, builds, 1)
require.Equal(t, int32(1), builds[0].BuildNumber)
require.Equal(t, secondUser.Username, builds[0].InitiatorUsername)
require.NoError(t, err)
})
t.Run("PaginateNonExistentRow", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
_, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{
WorkspaceID: workspace.ID,
Pagination: codersdk.Pagination{
AfterID: uuid.New(),
},
})
var apiError *codersdk.Error
require.ErrorAs(t, err, &apiError)
require.Equal(t, http.StatusBadRequest, apiError.StatusCode())
require.Contains(t, apiError.Message, "does not exist")
})
t.Run("PaginateLimitOffset", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
var expectedBuilds []codersdk.WorkspaceBuild
extraBuilds := 4
for i := 0; i < extraBuilds; i++ {
b := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
expectedBuilds = append(expectedBuilds, b)
coderdtest.AwaitWorkspaceBuildJob(t, client, b.ID)
}
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
pageSize := 3
firstPage, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{
WorkspaceID: workspace.ID,
Pagination: codersdk.Pagination{Limit: pageSize, Offset: 0},
})
require.NoError(t, err)
require.Len(t, firstPage, pageSize)
for i := 0; i < pageSize; i++ {
require.Equal(t, expectedBuilds[extraBuilds-i-1].ID, firstPage[i].ID)
}
secondPage, err := client.WorkspaceBuilds(ctx, codersdk.WorkspaceBuildsRequest{
WorkspaceID: workspace.ID,
Pagination: codersdk.Pagination{Limit: pageSize, Offset: pageSize},
})
require.NoError(t, err)
require.Len(t, secondPage, 2)
require.Equal(t, expectedBuilds[0].ID, secondPage[0].ID)
require.Equal(t, workspace.LatestBuild.ID, secondPage[1].ID) // build created while creating workspace
})
}
func TestWorkspaceBuildsProvisionerState(t *testing.T) {
t.Parallel()
t.Run("Permissions", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
first := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
Transition: codersdk.WorkspaceTransitionDelete,
ProvisionerState: []byte(" "),
})
require.Nil(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
// A regular user on the very same template must not be able to modify the
// state.
regularUser, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
workspace = coderdtest.CreateWorkspace(t, regularUser, first.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, regularUser, workspace.LatestBuild.ID)
_, err = regularUser.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
Transition: workspace.LatestBuild.Transition,
ProvisionerState: []byte(" "),
})
require.Error(t, err)
var cerr *codersdk.Error
require.True(t, errors.As(err, &cerr))
code := cerr.StatusCode()
require.Equal(t, http.StatusForbidden, code, "unexpected status %s", http.StatusText(code))
})
t.Run("Orphan", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
first := coderdtest.CreateFirstUser(t, client)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, first.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
// Providing both state and orphan fails.
_, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
Transition: codersdk.WorkspaceTransitionDelete,
ProvisionerState: []byte(" "),
Orphan: true,
})
require.Error(t, err)
cerr := coderdtest.SDKError(t, err)
require.Equal(t, http.StatusBadRequest, cerr.StatusCode())
// Regular orphan operation succeeds.
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
Transition: codersdk.WorkspaceTransitionDelete,
Orphan: true,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
_, err = client.Workspace(ctx, workspace.ID)
require.Error(t, err)
require.Equal(t, http.StatusGone, coderdtest.SDKError(t, err).StatusCode())
})
}
func TestPatchCancelWorkspaceBuild(t *testing.T) {
t.Parallel()
t.Run("User is allowed to cancel", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{},
},
}},
ProvisionPlan: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
var build codersdk.WorkspaceBuild
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
require.Eventually(t, func() bool {
var err error
build, err = client.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
return assert.NoError(t, err) && build.Job.Status == codersdk.ProvisionerJobRunning
}, testutil.WaitShort, testutil.IntervalFast)
err := client.CancelWorkspaceBuild(ctx, build.ID)
require.NoError(t, err)
require.Eventually(t, func() bool {
var err error
build, err = client.WorkspaceBuild(ctx, build.ID)
return assert.NoError(t, err) &&
// The job will never actually cancel successfully because it will never send a
// provision complete response.
assert.Empty(t, build.Job.Error) &&
build.Job.Status == codersdk.ProvisionerJobCanceling
}, testutil.WaitShort, testutil.IntervalFast)
})
t.Run("User is not allowed to cancel", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
owner := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{},
},
}},
ProvisionPlan: echo.ProvisionComplete,
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
userClient, _ := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID)
workspace := coderdtest.CreateWorkspace(t, userClient, owner.OrganizationID, template.ID)
var build codersdk.WorkspaceBuild
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
require.Eventually(t, func() bool {
var err error
build, err = userClient.WorkspaceBuild(ctx, workspace.LatestBuild.ID)
return assert.NoError(t, err) && build.Job.Status == codersdk.ProvisionerJobRunning
}, testutil.WaitShort, testutil.IntervalFast)
err := userClient.CancelWorkspaceBuild(ctx, build.ID)
var apiErr *codersdk.Error
require.ErrorAs(t, err, &apiErr)
require.Equal(t, http.StatusForbidden, apiErr.StatusCode())
})
}
func TestWorkspaceBuildResources(t *testing.T) {
t.Parallel()
t.Run("List", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
Agents: []*proto.Agent{{
Id: "something",
Auth: &proto.Agent_Token{},
}},
}, {
Name: "another",
Type: "example",
}},
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
workspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.NotNil(t, workspace.LatestBuild.Resources)
require.Len(t, workspace.LatestBuild.Resources, 2)
require.Equal(t, "some", workspace.LatestBuild.Resources[0].Name)
require.Equal(t, "example", workspace.LatestBuild.Resources[1].Type)
require.Len(t, workspace.LatestBuild.Resources[0].Agents, 1)
})
}
func TestWorkspaceBuildLogs(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Log{
Log: &proto.Log{
Level: proto.LogLevel_INFO,
Output: "example",
},
},
}, {
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Resources: []*proto.Resource{{
Name: "some",
Type: "example",
Agents: []*proto.Agent{{
Id: "something",
Auth: &proto.Agent_Token{},
}},
}, {
Name: "another",
Type: "example",
}},
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
logs, closer, err := client.WorkspaceBuildLogsAfter(ctx, workspace.LatestBuild.ID, 0)
require.NoError(t, err)
defer closer.Close()
for {
log, ok := <-logs
if !ok {
break
}
if log.Output == "example" {
return
}
}
require.Fail(t, "example message never happened")
}
func TestWorkspaceBuildState(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
wantState := []byte("some kinda state")
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: echo.ProvisionComplete,
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
State: wantState,
},
},
}},
})
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
gotState, err := client.WorkspaceBuildState(ctx, workspace.LatestBuild.ID)
require.NoError(t, err)
require.Equal(t, wantState, gotState)
}
func TestWorkspaceBuildStatus(t *testing.T) {
t.Parallel()
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
auditor := audit.NewMock()
numLogs := len(auditor.AuditLogs)
client, closeDaemon, api := coderdtest.NewWithAPI(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
user := coderdtest.CreateFirstUser(t, client)
numLogs++ // add an audit log for login
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
numLogs++ // add an audit log for template version creation
numLogs++ // add an audit log for template version update
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
closeDaemon.Close()
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
numLogs++ // add an audit log for template creation
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID)
numLogs++ // add an audit log for workspace creation
// initial returned state is "pending"
require.EqualValues(t, codersdk.WorkspaceStatusPending, workspace.LatestBuild.Status)
closeDaemon = coderdtest.NewProvisionerDaemon(t, api)
// after successful build is "running"
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
workspace, err := client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceStatusRunning, workspace.LatestBuild.Status)
numLogs++ // add an audit log for workspace_build starting
// after successful stop is "stopped"
build := coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStop)
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceStatusStopped, workspace.LatestBuild.Status)
// assert an audit log has been created for workspace stopping
numLogs++ // add an audit log for workspace_build stop
require.Len(t, auditor.AuditLogs, numLogs)
require.Equal(t, database.AuditActionStop, auditor.AuditLogs[numLogs-1].Action)
_ = closeDaemon.Close()
// after successful cancel is "canceled"
build = coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionStart)
err = client.CancelWorkspaceBuild(ctx, build.ID)
require.NoError(t, err)
workspace, err = client.Workspace(ctx, workspace.ID)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceStatusCanceled, workspace.LatestBuild.Status)
_ = coderdtest.NewProvisionerDaemon(t, api)
// after successful delete is "deleted"
build = coderdtest.CreateWorkspaceBuild(t, client, workspace, database.WorkspaceTransitionDelete)
_ = coderdtest.AwaitWorkspaceBuildJob(t, client, build.ID)
workspace, err = client.DeletedWorkspace(ctx, workspace.ID)
require.NoError(t, err)
require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status)
}
func TestWorkspaceBuildWithRichParameters(t *testing.T) {
t.Parallel()
const (
firstParameterName = "first_parameter"
firstParameterDescription = "This is first parameter"
firstParameterValue = "1"
secondParameterName = "second_parameter"
secondParameterDescription = "This is second parameter"
secondParameterValue = "2"
immutableParameterName = "immutable_parameter"
immutableParameterDescription = "This is immutable parameter"
immutableParameterValue = "3"
)
initialBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},
{Name: secondParameterName, Value: secondParameterValue},
{Name: immutableParameterName, Value: immutableParameterValue},
}
echoResponses := &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: []*proto.RichParameter{
{Name: firstParameterName, Description: firstParameterDescription, Mutable: true},
{Name: secondParameterName, Description: secondParameterDescription, Mutable: true},
{Name: immutableParameterName, Description: immutableParameterDescription, Mutable: false},
},
},
},
},
},
ProvisionApply: []*proto.Provision_Response{{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
}},
}
t.Run("UpdateParameterValues", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
// Update build parameters
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
const updatedParameterValue = "3"
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: firstParameterName, Value: firstParameterValue},
{Name: secondParameterName, Value: updatedParameterValue},
}
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID)
require.NoError(t, err)
expected := append(nextBuildParameters, codersdk.WorkspaceBuildParameter{
Name: immutableParameterName,
Value: immutableParameterValue,
})
require.ElementsMatch(t, expected, workspaceBuildParameters)
})
t.Run("UsePreviousParameterValues", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
firstWorkspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, firstWorkspaceBuild.Status)
// Start new workspace build
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
})
require.NoError(t, err)
require.NotEqual(t, firstWorkspaceBuild, nextWorkspaceBuild)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
workspaceBuildParameters, err := client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID)
require.NoError(t, err)
require.ElementsMatch(t, initialBuildParameters, workspaceBuildParameters)
})
t.Run("DoNotModifyImmutables", func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, echoResponses)
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
// Update build parameters
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: immutableParameterName, Value: "BAD"},
}
_, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
require.Error(t, err)
})
}
func TestWorkspaceBuildValidateRichParameters(t *testing.T) {
t.Parallel()
const (
stringParameterName = "string_parameter"
stringParameterValue = "abc"
numberParameterName = "number_parameter"
numberParameterValue = "7"
boolParameterName = "bool_parameter"
boolParameterValue = "true"
)
initialBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: stringParameterName, Value: stringParameterValue},
{Name: numberParameterName, Value: numberParameterValue},
{Name: boolParameterName, Value: boolParameterValue},
}
prepareEchoResponses := func(richParameters []*proto.RichParameter) *echo.Responses {
return &echo.Responses{
Parse: echo.ParseComplete,
ProvisionPlan: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{
Parameters: richParameters,
},
},
},
},
ProvisionApply: []*proto.Provision_Response{
{
Type: &proto.Provision_Response_Complete{
Complete: &proto.Provision_Complete{},
},
},
},
}
}
t.Run("NoValidation", func(t *testing.T) {
t.Parallel()
richParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(richParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
// Update build parameters
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: numberParameterName, Value: "42"},
}
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
_, err = client.WorkspaceBuildParameters(ctx, nextWorkspaceBuild.ID)
require.NoError(t, err)
})
t.Run("Validation", func(t *testing.T) {
t.Parallel()
numberRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
monotonicIncreasingNumberRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10, ValidationMonotonic: "increasing"},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
monotonicDecreasingNumberRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true, ValidationMin: 3, ValidationMax: 10, ValidationMonotonic: "decreasing"},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
stringRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
boolRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
regexRichParameters := []*proto.RichParameter{
{Name: stringParameterName, Type: "string", Mutable: true, ValidationRegex: "^[a-z]+$", ValidationError: "this is error"},
{Name: numberParameterName, Type: "number", Mutable: true},
{Name: boolParameterName, Type: "bool", Mutable: true},
}
tests := []struct {
parameterName string
value string
valid bool
richParameters []*proto.RichParameter
}{
{numberParameterName, "2", false, numberRichParameters},
{numberParameterName, "3", true, numberRichParameters},
{numberParameterName, "10", true, numberRichParameters},
{numberParameterName, "11", false, numberRichParameters},
{numberParameterName, "6", false, monotonicIncreasingNumberRichParameters},
{numberParameterName, "7", true, monotonicIncreasingNumberRichParameters},
{numberParameterName, "8", true, monotonicIncreasingNumberRichParameters},
{numberParameterName, "6", true, monotonicDecreasingNumberRichParameters},
{numberParameterName, "7", true, monotonicDecreasingNumberRichParameters},
{numberParameterName, "8", false, monotonicDecreasingNumberRichParameters},
{stringParameterName, "", true, stringRichParameters},
{stringParameterName, "foobar", true, stringRichParameters},
{stringParameterName, "abcd", true, regexRichParameters},
{stringParameterName, "abcd1", false, regexRichParameters},
{boolParameterName, "true", true, boolRichParameters},
{boolParameterName, "false", true, boolRichParameters},
{boolParameterName, "cat", false, boolRichParameters},
}
for _, tc := range tests {
tc := tc
t.Run(tc.parameterName+"-"+tc.value, func(t *testing.T) {
t.Parallel()
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, prepareEchoResponses(tc.richParameters))
coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, user.OrganizationID, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
cwr.RichParameterValues = initialBuildParameters
})
workspaceBuild := coderdtest.AwaitWorkspaceBuildJob(t, client, workspace.LatestBuild.ID)
require.Equal(t, codersdk.WorkspaceStatusRunning, workspaceBuild.Status)
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
defer cancel()
nextBuildParameters := []codersdk.WorkspaceBuildParameter{
{Name: tc.parameterName, Value: tc.value},
}
nextWorkspaceBuild, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
Transition: codersdk.WorkspaceTransitionStart,
RichParameterValues: nextBuildParameters,
})
if tc.valid {
require.NoError(t, err)
coderdtest.AwaitWorkspaceBuildJob(t, client, nextWorkspaceBuild.ID)
} else {
require.Error(t, err)
}
})
}
})
}