mirror of
https://github.com/coder/coder.git
synced 2025-07-03 16:13:58 +00:00
Relates to https://github.com/coder/coder/issues/15845 Rather than sending the notification to the user, we send it to the template admins. We also do not send it to the person that created the request.
1684 lines
65 KiB
Go
1684 lines
65 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"
|
|
"go.opentelemetry.io/otel"
|
|
"go.opentelemetry.io/otel/propagation"
|
|
"golang.org/x/xerrors"
|
|
|
|
"cdr.dev/slog"
|
|
"cdr.dev/slog/sloggers/slogtest"
|
|
"github.com/coder/coder/v2/coderd/audit"
|
|
"github.com/coder/coder/v2/coderd/coderdtest"
|
|
"github.com/coder/coder/v2/coderd/coderdtest/oidctest"
|
|
"github.com/coder/coder/v2/coderd/database"
|
|
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
|
"github.com/coder/coder/v2/coderd/database/dbgen"
|
|
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
|
"github.com/coder/coder/v2/coderd/database/dbtime"
|
|
"github.com/coder/coder/v2/coderd/externalauth"
|
|
"github.com/coder/coder/v2/coderd/notifications"
|
|
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
|
"github.com/coder/coder/v2/coderd/rbac"
|
|
"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 TestWorkspaceBuild(t *testing.T) {
|
|
t.Parallel()
|
|
otel.SetTextMapPropagator(
|
|
propagation.NewCompositeTextMapPropagator(
|
|
propagation.TraceContext{},
|
|
propagation.Baggage{},
|
|
),
|
|
)
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
auditor := audit.NewMock()
|
|
client, db := coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
Auditor: auditor,
|
|
})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
//nolint:gocritic // testing
|
|
up, err := db.UpdateUserProfile(dbauthz.AsSystemRestricted(ctx), database.UpdateUserProfileParams{
|
|
ID: user.UserID,
|
|
Email: coderdtest.FirstUserParams.Email,
|
|
Username: coderdtest.FirstUserParams.Username,
|
|
Name: "Admin",
|
|
AvatarURL: client.URL.String(),
|
|
UpdatedAt: dbtime.Now(),
|
|
})
|
|
require.NoError(t, err)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
auditor.ResetLogs()
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
// Create workspace will also start a build, so we need to wait for
|
|
// it to ensure all events are recorded.
|
|
require.Eventually(t, func() bool {
|
|
logs := auditor.AuditLogs()
|
|
return len(logs) == 2 &&
|
|
assert.Equal(t, logs[0].Ip.IPNet.IP.String(), "127.0.0.1") &&
|
|
assert.Equal(t, logs[1].Ip.IPNet.IP.String(), "127.0.0.1")
|
|
}, testutil.WaitShort, testutil.IntervalFast)
|
|
wb, err := client.WorkspaceBuild(testutil.Context(t, testutil.WaitShort), workspace.LatestBuild.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, up.Username, wb.WorkspaceOwnerName)
|
|
require.Equal(t, up.AvatarURL, wb.WorkspaceOwnerAvatarURL)
|
|
}
|
|
|
|
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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, 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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, 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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, 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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, 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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, 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: dbtime.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: dbtime.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, rbac.RoleOwner())
|
|
|
|
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.AwaitTemplateVersionJobCompleted(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.AwaitWorkspaceBuildJobCompleted(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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(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.AwaitWorkspaceBuildJobCompleted(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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(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.AwaitWorkspaceBuildJobCompleted(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, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(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.AwaitWorkspaceBuildJobCompleted(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.Response{{
|
|
Type: &proto.Response_Log{
|
|
Log: &proto.Log{},
|
|
},
|
|
}},
|
|
ProvisionPlan: echo.PlanComplete,
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, 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)
|
|
// job gets marked Failed when there is an Error; in practice we never get to Status = Canceled
|
|
// because provisioners report an Error when canceled. We check the Error string to ensure we don't mask
|
|
// other errors in this test.
|
|
return assert.NoError(t, err) &&
|
|
build.Job.Error == "canceled" &&
|
|
build.Job.Status == codersdk.ProvisionerJobFailed
|
|
}, testutil.WaitShort, testutil.IntervalFast)
|
|
})
|
|
t.Run("User is not allowed to cancel", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// need to include our own logger because the provisioner (rightly) drops error logs when we shut down the
|
|
// test with a build in progress.
|
|
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Logger: &logger})
|
|
owner := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Log{
|
|
Log: &proto.Log{},
|
|
},
|
|
}},
|
|
ProvisionPlan: echo.PlanComplete,
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(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, 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.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "first_resource",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: "something-1",
|
|
Name: "something-1",
|
|
Auth: &proto.Agent_Token{},
|
|
Order: 3,
|
|
}},
|
|
}, {
|
|
Name: "second_resource",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: "something-2",
|
|
Name: "something-2",
|
|
Auth: &proto.Agent_Token{},
|
|
Order: 1,
|
|
}, {
|
|
Id: "something-3",
|
|
Name: "something-3",
|
|
Auth: &proto.Agent_Token{},
|
|
Order: 2,
|
|
}},
|
|
}, {
|
|
Name: "third_resource",
|
|
Type: "example",
|
|
}, {
|
|
Name: "fourth_resource",
|
|
Type: "example",
|
|
}, {
|
|
Name: "fifth_resource",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: "something-4",
|
|
Name: "something-4",
|
|
Auth: &proto.Agent_Token{},
|
|
}},
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(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, 5)
|
|
assertWorkspaceResource(t, workspace.LatestBuild.Resources[0], "fifth_resource", "example", 1) // resource has agent with implicit order = 0
|
|
assertWorkspaceResource(t, workspace.LatestBuild.Resources[1], "second_resource", "example", 2) // resource has 2 agents, one with low order value (2)
|
|
assertWorkspaceResource(t, workspace.LatestBuild.Resources[2], "first_resource", "example", 1) // resource has 1 agent with explicit order
|
|
assertWorkspaceResource(t, workspace.LatestBuild.Resources[3], "fourth_resource", "example", 0) // resource has no agents, sorted by name
|
|
assertWorkspaceResource(t, workspace.LatestBuild.Resources[4], "third_resource", "example", 0) // resource is the last one
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceBuildWithUpdatedTemplateVersionSendsNotification(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("NoRepeatedNotifications", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
notify := ¬ificationstest.FakeEnqueuer{}
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: notify})
|
|
first := coderdtest.CreateFirstUser(t, client)
|
|
templateAdminClient, templateAdmin := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleTemplateAdmin())
|
|
userClient, user := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
|
|
|
// Create a template with an initial version
|
|
version := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version.ID)
|
|
template := coderdtest.CreateTemplate(t, templateAdminClient, first.OrganizationID, version.ID)
|
|
|
|
// Create a workspace using this template
|
|
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// Create a new version of the template
|
|
newVersion := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
|
ctvr.TemplateID = template.ID
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, newVersion.ID)
|
|
|
|
// Create a workspace build using this new template version
|
|
build := coderdtest.CreateWorkspaceBuild(t, userClient, workspace, database.WorkspaceTransitionStart, func(cwbr *codersdk.CreateWorkspaceBuildRequest) {
|
|
cwbr.TemplateVersionID = newVersion.ID
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID)
|
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// Create the workspace build _again_. We are doing this to
|
|
// ensure we do not create _another_ notification. This is
|
|
// separate to the notifications subsystem dedupe mechanism
|
|
// as this build shouldn't create a notification. It shouldn't
|
|
// create another notification as this new build isn't changing
|
|
// the template version.
|
|
build = coderdtest.CreateWorkspaceBuild(t, userClient, workspace, database.WorkspaceTransitionStart, func(cwbr *codersdk.CreateWorkspaceBuildRequest) {
|
|
cwbr.TemplateVersionID = newVersion.ID
|
|
})
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID)
|
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// We're going to have two notifications (one for the first user and one for the template admin)
|
|
// By ensuring we only have these two, we are sure the second build didn't trigger more
|
|
// notifications.
|
|
sent := notify.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceManuallyUpdated))
|
|
require.Len(t, sent, 2)
|
|
|
|
receivers := make([]uuid.UUID, len(sent))
|
|
for idx, notif := range sent {
|
|
receivers[idx] = notif.UserID
|
|
}
|
|
|
|
// Check the notification was sent to the first user and template admin
|
|
// (both of whom have the "template admin" role), and explicitly not the
|
|
// workspace owner (since they initiated the workspace build).
|
|
require.Contains(t, receivers, templateAdmin.ID)
|
|
require.Contains(t, receivers, first.UserID)
|
|
require.NotContains(t, receivers, user.ID)
|
|
|
|
require.Contains(t, sent[0].Targets, template.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
|
|
|
require.Contains(t, sent[1].Targets, template.ID)
|
|
require.Contains(t, sent[1].Targets, workspace.ID)
|
|
require.Contains(t, sent[1].Targets, workspace.OrganizationID)
|
|
require.Contains(t, sent[1].Targets, workspace.OwnerID)
|
|
})
|
|
|
|
t.Run("ToCorrectUser", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
notify := ¬ificationstest.FakeEnqueuer{}
|
|
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, NotificationsEnqueuer: notify})
|
|
first := coderdtest.CreateFirstUser(t, client)
|
|
templateAdminClient, templateAdmin := coderdtest.CreateAnotherUser(t, client, first.OrganizationID, rbac.RoleTemplateAdmin())
|
|
userClient, _ := coderdtest.CreateAnotherUser(t, client, first.OrganizationID)
|
|
|
|
// Create a template with an initial version
|
|
version := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, version.ID)
|
|
template := coderdtest.CreateTemplate(t, templateAdminClient, first.OrganizationID, version.ID)
|
|
|
|
// Create a workspace using this template
|
|
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// Create a new version of the template
|
|
newVersion := coderdtest.CreateTemplateVersion(t, templateAdminClient, first.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
|
ctvr.TemplateID = template.ID
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAdminClient, newVersion.ID)
|
|
|
|
// Create a workspace build using this new template version from a different user
|
|
ctx := testutil.Context(t, testutil.WaitShort)
|
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
TemplateVersionID: newVersion.ID,
|
|
})
|
|
require.NoError(t, err)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, build.ID)
|
|
coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, database.WorkspaceTransitionStart, database.WorkspaceTransitionStop)
|
|
|
|
// Ensure we receive only 1 workspace manually updated notification and to the right user
|
|
sent := notify.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceManuallyUpdated))
|
|
require.Len(t, sent, 1)
|
|
require.Equal(t, templateAdmin.ID, sent[0].UserID)
|
|
require.Contains(t, sent[0].Targets, template.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.ID)
|
|
require.Contains(t, sent[0].Targets, workspace.OrganizationID)
|
|
require.Contains(t, sent[0].Targets, workspace.OwnerID)
|
|
})
|
|
}
|
|
|
|
func assertWorkspaceResource(t *testing.T, actual codersdk.WorkspaceResource, name, aType string, numAgents int) {
|
|
assert.Equal(t, name, actual.Name)
|
|
assert.Equal(t, aType, actual.Type)
|
|
assert.Len(t, actual.Agents, numAgents)
|
|
}
|
|
|
|
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.Response{{
|
|
Type: &proto.Response_Log{
|
|
Log: &proto.Log{
|
|
Level: proto.LogLevel_INFO,
|
|
Output: "example",
|
|
},
|
|
},
|
|
}, {
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{
|
|
Resources: []*proto.Resource{{
|
|
Name: "some",
|
|
Type: "example",
|
|
Agents: []*proto.Agent{{
|
|
Id: "something",
|
|
Auth: &proto.Agent_Token{},
|
|
}},
|
|
}, {
|
|
Name: "another",
|
|
Type: "example",
|
|
}},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, 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.PlanComplete,
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{
|
|
State: wantState,
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(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()
|
|
|
|
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.AwaitTemplateVersionJobCompleted(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, 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.AwaitWorkspaceBuildJobCompleted(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.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.AwaitWorkspaceBuildJobCompleted(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.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
|
workspace, err = client.DeletedWorkspace(ctx, workspace.ID)
|
|
require.NoError(t, err)
|
|
require.EqualValues(t, codersdk.WorkspaceStatusDeleted, workspace.LatestBuild.Status)
|
|
}
|
|
|
|
func TestWorkspaceDeleteSuspendedUser(t *testing.T) {
|
|
t.Parallel()
|
|
const providerID = "fake-github"
|
|
fake := oidctest.NewFakeIDP(t, oidctest.WithServing())
|
|
|
|
validateCalls := 0
|
|
userSuspended := false
|
|
owner := coderdtest.New(t, &coderdtest.Options{
|
|
IncludeProvisionerDaemon: true,
|
|
ExternalAuthConfigs: []*externalauth.Config{
|
|
fake.ExternalAuthConfig(t, providerID, &oidctest.ExternalAuthConfigOptions{
|
|
ValidatePayload: func(email string) (interface{}, int, error) {
|
|
validateCalls++
|
|
if userSuspended {
|
|
// Simulate the user being suspended from the IDP too.
|
|
return "", http.StatusForbidden, xerrors.New("user is suspended")
|
|
}
|
|
return "OK", 0, nil
|
|
},
|
|
}),
|
|
},
|
|
})
|
|
|
|
first := coderdtest.CreateFirstUser(t, owner)
|
|
|
|
// New user that we will suspend when we try to delete the workspace.
|
|
client, user := coderdtest.CreateAnotherUser(t, owner, first.OrganizationID, rbac.RoleTemplateAdmin())
|
|
fake.ExternalLogin(t, client)
|
|
|
|
version := coderdtest.CreateTemplateVersion(t, client, first.OrganizationID, &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionApply: echo.ApplyComplete,
|
|
ProvisionPlan: []*proto.Response{{
|
|
Type: &proto.Response_Plan{
|
|
Plan: &proto.PlanComplete{
|
|
Error: "",
|
|
Resources: nil,
|
|
Parameters: nil,
|
|
ExternalAuthProviders: []*proto.ExternalAuthProviderResource{
|
|
{
|
|
Id: providerID,
|
|
Optional: false,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}},
|
|
})
|
|
|
|
validateCalls = 0 // Reset
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, first.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
require.Equal(t, 1, validateCalls) // Ensure the external link is working
|
|
|
|
// Suspend the user
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
_, err := owner.UpdateUserStatus(ctx, user.ID.String(), codersdk.UserStatusSuspended)
|
|
require.NoError(t, err, "suspend user")
|
|
|
|
// Now delete the workspace build
|
|
userSuspended = true
|
|
build, err := owner.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionDelete,
|
|
})
|
|
require.NoError(t, err)
|
|
build = coderdtest.AwaitWorkspaceBuildJobCompleted(t, owner, build.ID)
|
|
require.Equal(t, 2, validateCalls)
|
|
require.Equal(t, codersdk.WorkspaceStatusDeleted, build.Status)
|
|
}
|
|
|
|
func TestWorkspaceBuildDebugMode(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
t.Run("DebugModeDisabled", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create user
|
|
deploymentValues := coderdtest.DeploymentValues(t)
|
|
err := deploymentValues.EnableTerraformDebugMode.Set("false")
|
|
require.NoError(t, err)
|
|
|
|
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: deploymentValues})
|
|
owner := coderdtest.CreateFirstUser(t, adminClient)
|
|
|
|
// Template author: create a template
|
|
version := coderdtest.CreateTemplateVersion(t, adminClient, owner.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, adminClient, owner.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
|
|
|
|
// Template author: create a workspace
|
|
workspace := coderdtest.CreateWorkspace(t, adminClient, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, adminClient, workspace.LatestBuild.ID)
|
|
|
|
// Template author: try to start a workspace build in debug mode
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err = adminClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
LogLevel: "debug",
|
|
})
|
|
|
|
// Template author: expect an error as the debug mode is disabled
|
|
require.NotNil(t, err)
|
|
var sdkError *codersdk.Error
|
|
isSdkError := xerrors.As(err, &sdkError)
|
|
require.True(t, isSdkError)
|
|
require.Contains(t, sdkError.Message, "Terraform debug mode is disabled in the deployment configuration.")
|
|
})
|
|
t.Run("AsRegularUser", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create users
|
|
deploymentValues := coderdtest.DeploymentValues(t)
|
|
deploymentValues.EnableTerraformDebugMode = true
|
|
|
|
templateAuthorClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: deploymentValues})
|
|
templateAuthor := coderdtest.CreateFirstUser(t, templateAuthorClient)
|
|
regularUserClient, _ := coderdtest.CreateAnotherUser(t, templateAuthorClient, templateAuthor.OrganizationID)
|
|
|
|
// Template owner: create a template
|
|
version := coderdtest.CreateTemplateVersion(t, templateAuthorClient, templateAuthor.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, templateAuthorClient, templateAuthor.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAuthorClient, version.ID)
|
|
|
|
// Regular user: create a workspace
|
|
workspace := coderdtest.CreateWorkspace(t, regularUserClient, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, regularUserClient, workspace.LatestBuild.ID)
|
|
|
|
// Regular user: try to start a workspace build in debug mode
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := regularUserClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
LogLevel: "debug",
|
|
})
|
|
|
|
// Regular user: expect an error
|
|
require.NotNil(t, err)
|
|
var sdkError *codersdk.Error
|
|
isSdkError := xerrors.As(err, &sdkError)
|
|
require.True(t, isSdkError)
|
|
require.Contains(t, sdkError.Message, "Workspace builds with a custom log level are restricted to administrators only.")
|
|
})
|
|
t.Run("AsTemplateAuthor", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create users
|
|
deploymentValues := coderdtest.DeploymentValues(t)
|
|
deploymentValues.EnableTerraformDebugMode = true
|
|
|
|
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: deploymentValues})
|
|
owner := coderdtest.CreateFirstUser(t, adminClient)
|
|
templateAuthorClient, _ := coderdtest.CreateAnotherUser(t, adminClient, owner.OrganizationID, rbac.RoleTemplateAdmin())
|
|
|
|
// Template author: create a template
|
|
version := coderdtest.CreateTemplateVersion(t, templateAuthorClient, owner.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, templateAuthorClient, owner.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, templateAuthorClient, version.ID)
|
|
|
|
// Template author: create a workspace
|
|
workspace := coderdtest.CreateWorkspace(t, templateAuthorClient, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, templateAuthorClient, workspace.LatestBuild.ID)
|
|
|
|
// Template author: try to start a workspace build in debug mode
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := templateAuthorClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
LogLevel: "debug",
|
|
})
|
|
|
|
// Template author: expect an error as the debug mode is disabled
|
|
require.NotNil(t, err)
|
|
var sdkError *codersdk.Error
|
|
isSdkError := xerrors.As(err, &sdkError)
|
|
require.True(t, isSdkError)
|
|
require.Contains(t, sdkError.Message, "Workspace builds with a custom log level are restricted to administrators only.")
|
|
})
|
|
t.Run("AsAdmin", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Create users
|
|
deploymentValues := coderdtest.DeploymentValues(t)
|
|
deploymentValues.EnableTerraformDebugMode = true
|
|
|
|
adminClient := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, DeploymentValues: deploymentValues})
|
|
owner := coderdtest.CreateFirstUser(t, adminClient)
|
|
|
|
// Interact as template admin
|
|
echoResponses := &echo.Responses{
|
|
Parse: echo.ParseComplete,
|
|
ProvisionPlan: echo.PlanComplete,
|
|
ProvisionApply: []*proto.Response{{
|
|
Type: &proto.Response_Log{
|
|
Log: &proto.Log{
|
|
Level: proto.LogLevel_DEBUG,
|
|
Output: "want-it",
|
|
},
|
|
},
|
|
}, {
|
|
Type: &proto.Response_Log{
|
|
Log: &proto.Log{
|
|
Level: proto.LogLevel_TRACE,
|
|
Output: "dont-want-it",
|
|
},
|
|
},
|
|
}, {
|
|
Type: &proto.Response_Log{
|
|
Log: &proto.Log{
|
|
Level: proto.LogLevel_DEBUG,
|
|
Output: "done",
|
|
},
|
|
},
|
|
}, {
|
|
Type: &proto.Response_Apply{
|
|
Apply: &proto.ApplyComplete{},
|
|
},
|
|
}},
|
|
}
|
|
version := coderdtest.CreateTemplateVersion(t, adminClient, owner.OrganizationID, echoResponses)
|
|
template := coderdtest.CreateTemplate(t, adminClient, owner.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, adminClient, version.ID)
|
|
|
|
// Create workspace
|
|
workspace := coderdtest.CreateWorkspace(t, adminClient, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, adminClient, workspace.LatestBuild.ID)
|
|
|
|
// Create workspace build
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
build, err := adminClient.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: workspace.LatestBuild.TemplateVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
ProvisionerState: []byte(" "),
|
|
LogLevel: "debug",
|
|
})
|
|
require.Nil(t, err)
|
|
|
|
build = coderdtest.AwaitWorkspaceBuildJobCompleted(t, adminClient, build.ID)
|
|
|
|
// Watch for incoming logs
|
|
logs, closer, err := adminClient.WorkspaceBuildLogsAfter(ctx, build.ID, 0)
|
|
require.NoError(t, err)
|
|
defer closer.Close()
|
|
|
|
var logsProcessed int
|
|
|
|
processingLogs:
|
|
for {
|
|
select {
|
|
case <-ctx.Done():
|
|
require.Fail(t, "timeout occurred while processing logs")
|
|
return
|
|
case log, ok := <-logs:
|
|
if !ok {
|
|
break processingLogs
|
|
}
|
|
t.Logf("got log: %s -- %s | %s | %s", log.Level, log.Stage, log.Source, log.Output)
|
|
if log.Source != "provisioner" {
|
|
continue
|
|
}
|
|
logsProcessed++
|
|
|
|
require.NotEqual(t, "dont-want-it", log.Output, "unexpected log message", "%s log message shouldn't be logged: %s")
|
|
|
|
if log.Output == "done" {
|
|
break processingLogs
|
|
}
|
|
}
|
|
}
|
|
require.Equal(t, 2, logsProcessed)
|
|
})
|
|
}
|
|
|
|
func TestPostWorkspaceBuild(t *testing.T) {
|
|
t.Parallel()
|
|
t.Run("NoTemplateVersion", 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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: uuid.New(),
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
})
|
|
require.Error(t, err)
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
|
})
|
|
|
|
t.Run("TemplateVersionFailedImport", 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{
|
|
ProvisionApply: []*proto.Response{{}},
|
|
})
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.CreateWorkspace(ctx, user.OrganizationID, codersdk.Me, codersdk.CreateWorkspaceRequest{
|
|
TemplateID: template.ID,
|
|
Name: "workspace",
|
|
})
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusBadRequest, apiErr.StatusCode())
|
|
})
|
|
|
|
t.Run("AlreadyActive", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, closer := coderdtest.NewWithProvisionerCloser(t, nil)
|
|
defer closer.Close()
|
|
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
closer.Close()
|
|
// Close here so workspace build doesn't process!
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
_, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: template.ActiveVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
})
|
|
require.Error(t, err)
|
|
var apiErr *codersdk.Error
|
|
require.ErrorAs(t, err, &apiErr)
|
|
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
|
|
})
|
|
|
|
t.Run("Audit", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
otel.SetTextMapPropagator(
|
|
propagation.NewCompositeTextMapPropagator(
|
|
propagation.TraceContext{},
|
|
propagation.Baggage{},
|
|
),
|
|
)
|
|
auditor := audit.NewMock()
|
|
client := coderdtest.New(t, &coderdtest.Options{IncludeProvisionerDaemon: true, Auditor: auditor})
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
auditor.ResetLogs()
|
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: template.ActiveVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
})
|
|
require.NoError(t, err)
|
|
if assert.NotNil(t, build.MatchedProvisioners) {
|
|
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
|
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
|
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
|
}
|
|
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
|
|
|
require.Eventually(t, func() bool {
|
|
logs := auditor.AuditLogs()
|
|
return len(logs) > 0 &&
|
|
assert.Equal(t, logs[0].Ip.IPNet.IP.String(), "127.0.0.1")
|
|
}, testutil.WaitShort, testutil.IntervalFast)
|
|
})
|
|
|
|
t.Run("IncrementBuildNumber", 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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: template.ActiveVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
})
|
|
require.NoError(t, err)
|
|
if assert.NotNil(t, build.MatchedProvisioners) {
|
|
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
|
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
|
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
|
}
|
|
|
|
require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber)
|
|
})
|
|
|
|
t.Run("WithState", func(t *testing.T) {
|
|
t.Parallel()
|
|
client, closeDaemon := coderdtest.NewWithProvisionerCloser(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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
wantState := []byte("something")
|
|
_ = closeDaemon.Close()
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: template.ActiveVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
ProvisionerState: wantState,
|
|
})
|
|
require.NoError(t, err)
|
|
if assert.NotNil(t, build.MatchedProvisioners) {
|
|
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
|
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
|
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
|
}
|
|
|
|
gotState, err := client.WorkspaceBuildState(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
require.Equal(t, wantState, gotState)
|
|
})
|
|
|
|
t.Run("Delete", 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.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
defer cancel()
|
|
|
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
Transition: codersdk.WorkspaceTransitionDelete,
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, workspace.LatestBuild.BuildNumber+1, build.BuildNumber)
|
|
if assert.NotNil(t, build.MatchedProvisioners) {
|
|
require.Equal(t, 1, build.MatchedProvisioners.Count)
|
|
require.Equal(t, 1, build.MatchedProvisioners.Available)
|
|
require.NotZero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
|
}
|
|
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, build.ID)
|
|
|
|
res, err := client.Workspaces(ctx, codersdk.WorkspaceFilter{
|
|
Owner: user.UserID.String(),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, res.Workspaces, 0)
|
|
})
|
|
|
|
t.Run("NoProvisionersAvailable", func(t *testing.T) {
|
|
t.Parallel()
|
|
if !dbtestutil.WillUsePostgres() {
|
|
t.Skip("this test requires postgres")
|
|
}
|
|
// Given: a coderd instance with a provisioner daemon
|
|
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
|
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
|
Database: store,
|
|
Pubsub: ps,
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
defer closeDaemon.Close()
|
|
// Given: a user, template, and workspace
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
// Stop the provisioner daemon.
|
|
require.NoError(t, closeDaemon.Close())
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
// Given: no provisioner daemons exist.
|
|
_, err := db.ExecContext(ctx, `DELETE FROM provisioner_daemons;`)
|
|
require.NoError(t, err)
|
|
|
|
// When: a new workspace build is created
|
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: template.ActiveVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
})
|
|
// Then: the request should succeed.
|
|
require.NoError(t, err)
|
|
// Then: the provisioner job should remain pending.
|
|
require.Equal(t, codersdk.ProvisionerJobPending, build.Job.Status)
|
|
// Then: the response should indicate no provisioners are available.
|
|
if assert.NotNil(t, build.MatchedProvisioners) {
|
|
assert.Zero(t, build.MatchedProvisioners.Count)
|
|
assert.Zero(t, build.MatchedProvisioners.Available)
|
|
assert.Zero(t, build.MatchedProvisioners.MostRecentlySeen.Time)
|
|
assert.False(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
|
|
}
|
|
})
|
|
|
|
t.Run("AllProvisionersStale", func(t *testing.T) {
|
|
t.Parallel()
|
|
if !dbtestutil.WillUsePostgres() {
|
|
t.Skip("this test requires postgres")
|
|
}
|
|
// Given: a coderd instance with a provisioner daemon
|
|
store, ps, db := dbtestutil.NewDBWithSQLDB(t)
|
|
client, closeDaemon := coderdtest.NewWithProvisionerCloser(t, &coderdtest.Options{
|
|
Database: store,
|
|
Pubsub: ps,
|
|
IncludeProvisionerDaemon: true,
|
|
})
|
|
defer closeDaemon.Close()
|
|
// Given: a user, template, and workspace
|
|
user := coderdtest.CreateFirstUser(t, client)
|
|
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
|
|
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
|
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
|
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
|
|
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
|
|
|
ctx := testutil.Context(t, testutil.WaitLong)
|
|
// Given: all provisioner daemons are stale
|
|
// First stop the provisioner
|
|
require.NoError(t, closeDaemon.Close())
|
|
newLastSeenAt := dbtime.Now().Add(-time.Hour)
|
|
// Update the last seen at for all provisioner daemons. We have to use the
|
|
// SQL db directly because store.UpdateProvisionerDaemonLastSeenAt has a
|
|
// built-in check to prevent updating the last seen at to a time in the past.
|
|
_, err := db.ExecContext(ctx, `UPDATE provisioner_daemons SET last_seen_at = $1;`, newLastSeenAt)
|
|
require.NoError(t, err)
|
|
|
|
// When: a new workspace build is created
|
|
build, err := client.CreateWorkspaceBuild(ctx, workspace.ID, codersdk.CreateWorkspaceBuildRequest{
|
|
TemplateVersionID: template.ActiveVersionID,
|
|
Transition: codersdk.WorkspaceTransitionStart,
|
|
})
|
|
// Then: the request should succeed
|
|
require.NoError(t, err)
|
|
// Then: the provisioner job should remain pending
|
|
require.Equal(t, codersdk.ProvisionerJobPending, build.Job.Status)
|
|
// Then: the response should indicate no provisioners are available
|
|
if assert.NotNil(t, build.MatchedProvisioners) {
|
|
assert.Zero(t, build.MatchedProvisioners.Available)
|
|
assert.Equal(t, 1, build.MatchedProvisioners.Count)
|
|
assert.Equal(t, newLastSeenAt.UTC(), build.MatchedProvisioners.MostRecentlySeen.Time.UTC())
|
|
assert.True(t, build.MatchedProvisioners.MostRecentlySeen.Valid)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestWorkspaceBuildTimings(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Setup the test environment with a template and version
|
|
db, pubsub := dbtestutil.NewDB(t)
|
|
ownerClient := coderdtest.New(t, &coderdtest.Options{
|
|
Database: db,
|
|
Pubsub: pubsub,
|
|
})
|
|
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
|
client, user := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
|
|
|
file := dbgen.File(t, db, database.File{
|
|
CreatedBy: owner.UserID,
|
|
})
|
|
versionJob := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
|
OrganizationID: owner.OrganizationID,
|
|
InitiatorID: user.ID,
|
|
FileID: file.ID,
|
|
Tags: database.StringMap{
|
|
"custom": "true",
|
|
},
|
|
})
|
|
version := dbgen.TemplateVersion(t, db, database.TemplateVersion{
|
|
OrganizationID: owner.OrganizationID,
|
|
JobID: versionJob.ID,
|
|
CreatedBy: owner.UserID,
|
|
})
|
|
template := dbgen.Template(t, db, database.Template{
|
|
OrganizationID: owner.OrganizationID,
|
|
ActiveVersionID: version.ID,
|
|
CreatedBy: owner.UserID,
|
|
})
|
|
|
|
// Tests will run in parallel. To avoid conflicts and race conditions on the
|
|
// build number, each test will have its own workspace and build.
|
|
makeBuild := func(t *testing.T) database.WorkspaceBuild {
|
|
ws := dbgen.Workspace(t, db, database.WorkspaceTable{
|
|
OwnerID: user.ID,
|
|
OrganizationID: owner.OrganizationID,
|
|
TemplateID: template.ID,
|
|
})
|
|
jobID := uuid.New()
|
|
job := dbgen.ProvisionerJob(t, db, pubsub, database.ProvisionerJob{
|
|
ID: jobID,
|
|
OrganizationID: owner.OrganizationID,
|
|
Tags: database.StringMap{jobID.String(): "true"},
|
|
})
|
|
return dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
|
WorkspaceID: ws.ID,
|
|
TemplateVersionID: version.ID,
|
|
InitiatorID: owner.UserID,
|
|
JobID: job.ID,
|
|
BuildNumber: 1,
|
|
})
|
|
}
|
|
|
|
t.Run("NonExistentBuild", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a non-existent build
|
|
buildID := uuid.New()
|
|
|
|
// When: fetching timings for the build
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
_, err := client.WorkspaceBuildTimings(ctx, buildID)
|
|
|
|
// Then: expect a not found error
|
|
require.Error(t, err)
|
|
require.Contains(t, err.Error(), "not found")
|
|
})
|
|
|
|
t.Run("EmptyTimings", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a build with no timings
|
|
build := makeBuild(t)
|
|
|
|
// When: fetching timings for the build
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
|
|
|
// Then: return a response with empty timings
|
|
require.NoError(t, err)
|
|
require.Empty(t, res.ProvisionerTimings)
|
|
require.Empty(t, res.AgentScriptTimings)
|
|
})
|
|
|
|
t.Run("ProvisionerTimings", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a build with provisioner timings
|
|
build := makeBuild(t)
|
|
provisionerTimings := dbgen.ProvisionerJobTimings(t, db, build, 5)
|
|
|
|
// When: fetching timings for the build
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Then: return a response with the expected timings
|
|
require.Len(t, res.ProvisionerTimings, 5)
|
|
for i := range res.ProvisionerTimings {
|
|
timingRes := res.ProvisionerTimings[i]
|
|
genTiming := provisionerTimings[i]
|
|
require.Equal(t, genTiming.Resource, timingRes.Resource)
|
|
require.Equal(t, genTiming.Action, timingRes.Action)
|
|
require.Equal(t, string(genTiming.Stage), string(timingRes.Stage))
|
|
require.Equal(t, genTiming.JobID.String(), timingRes.JobID.String())
|
|
require.Equal(t, genTiming.Source, timingRes.Source)
|
|
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
|
|
require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli())
|
|
}
|
|
})
|
|
|
|
t.Run("AgentScriptTimings", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a build with agent script timings
|
|
build := makeBuild(t)
|
|
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
|
JobID: build.JobID,
|
|
})
|
|
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
|
ResourceID: resource.ID,
|
|
})
|
|
script := dbgen.WorkspaceAgentScript(t, db, database.WorkspaceAgentScript{
|
|
WorkspaceAgentID: agent.ID,
|
|
})
|
|
agentScriptTimings := dbgen.WorkspaceAgentScriptTimings(t, db, script, 5)
|
|
|
|
// When: fetching timings for the build
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Then: return a response with the expected timings
|
|
require.Len(t, res.AgentScriptTimings, 5)
|
|
for i := range res.AgentScriptTimings {
|
|
timingRes := res.AgentScriptTimings[i]
|
|
genTiming := agentScriptTimings[i]
|
|
require.Equal(t, genTiming.ExitCode, timingRes.ExitCode)
|
|
require.Equal(t, string(genTiming.Status), timingRes.Status)
|
|
require.Equal(t, string(genTiming.Stage), string(timingRes.Stage))
|
|
require.Equal(t, genTiming.StartedAt.UnixMilli(), timingRes.StartedAt.UnixMilli())
|
|
require.Equal(t, genTiming.EndedAt.UnixMilli(), timingRes.EndedAt.UnixMilli())
|
|
require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID)
|
|
require.Equal(t, agent.Name, timingRes.WorkspaceAgentName)
|
|
}
|
|
})
|
|
|
|
t.Run("NoAgentScripts", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a build with no agent scripts
|
|
build := makeBuild(t)
|
|
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
|
JobID: build.JobID,
|
|
})
|
|
dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
|
ResourceID: resource.ID,
|
|
})
|
|
|
|
// When: fetching timings for the build
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Then: return a response with empty agent script timings
|
|
require.Empty(t, res.AgentScriptTimings)
|
|
})
|
|
|
|
// Some workspaces might not have agents. It is improbable, but possible.
|
|
t.Run("NoAgents", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a build with no agents
|
|
build := makeBuild(t)
|
|
dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
|
JobID: build.JobID,
|
|
})
|
|
|
|
// When: fetching timings for the build
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Then: return a response with empty agent script timings
|
|
require.Empty(t, res.AgentScriptTimings)
|
|
require.Empty(t, res.AgentConnectionTimings)
|
|
})
|
|
|
|
t.Run("AgentConnectionTimings", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a build with an agent
|
|
build := makeBuild(t)
|
|
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
|
JobID: build.JobID,
|
|
})
|
|
agent := dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
|
ResourceID: resource.ID,
|
|
})
|
|
|
|
// When: fetching timings for the build
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Then: return a response with the expected timings
|
|
require.Len(t, res.AgentConnectionTimings, 1)
|
|
for i := range res.ProvisionerTimings {
|
|
timingRes := res.AgentConnectionTimings[i]
|
|
require.Equal(t, agent.ID.String(), timingRes.WorkspaceAgentID)
|
|
require.Equal(t, agent.Name, timingRes.WorkspaceAgentName)
|
|
require.NotEmpty(t, timingRes.StartedAt)
|
|
require.NotEmpty(t, timingRes.EndedAt)
|
|
}
|
|
})
|
|
|
|
t.Run("MultipleAgents", func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// Given: a build with multiple agents
|
|
build := makeBuild(t)
|
|
resource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
|
JobID: build.JobID,
|
|
})
|
|
agents := make([]database.WorkspaceAgent, 5)
|
|
for i := range agents {
|
|
agents[i] = dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
|
ResourceID: resource.ID,
|
|
})
|
|
}
|
|
|
|
// When: fetching timings for the build
|
|
ctx, cancel := context.WithTimeout(context.Background(), testutil.WaitLong)
|
|
t.Cleanup(cancel)
|
|
res, err := client.WorkspaceBuildTimings(ctx, build.ID)
|
|
require.NoError(t, err)
|
|
|
|
// Then: return a response with the expected timings
|
|
require.Len(t, res.AgentConnectionTimings, 5)
|
|
})
|
|
}
|