mirror of
https://github.com/coder/coder.git
synced 2026-06-03 13:08:25 +00:00
211393a69c
## Description This PR updates the lifecycle executor to explicitly exclude prebuilt workspaces from being considered for lifecycle operations such as `autostart`, `autostop`, `dormancy`, `default TTL` and `failure TTL`. Prebuilt workspaces (i.e., those owned by the prebuild system user) are handled separately by the prebuild reconciliation loop. Including them in the lifecycle executor could lead to unintended behavior such as incorrect scheduling or state transitions. ## Changes * Updated the lifecycle executor query `GetWorkspacesEligibleForTransition` to exclude workspaces with `owner_id = 'c42fdf75-3097-471c-8c33-fb52454d81c0'` (prebuilds). * Added tests to verify prebuilt workspaces are not considered in: * Autostop * Autostart * Default TTL * Dormancy * Failure TTL Fixes: https://github.com/coder/coder/issues/18740 Related to: https://github.com/coder/coder/issues/18658
1586 lines
55 KiB
Go
1586 lines
55 KiB
Go
package autobuild_test
|
||
|
||
import (
|
||
"context"
|
||
"database/sql"
|
||
"errors"
|
||
"testing"
|
||
"time"
|
||
|
||
"github.com/coder/coder/v2/coderd/database/dbgen"
|
||
"github.com/coder/coder/v2/coderd/database/pubsub"
|
||
"github.com/coder/coder/v2/coderd/rbac"
|
||
"github.com/coder/quartz"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/stretchr/testify/assert"
|
||
"github.com/stretchr/testify/require"
|
||
"go.uber.org/goleak"
|
||
|
||
"cdr.dev/slog"
|
||
"cdr.dev/slog/sloggers/slogtest"
|
||
|
||
"github.com/coder/coder/v2/coderd/autobuild"
|
||
"github.com/coder/coder/v2/coderd/coderdtest"
|
||
"github.com/coder/coder/v2/coderd/database"
|
||
"github.com/coder/coder/v2/coderd/database/dbauthz"
|
||
"github.com/coder/coder/v2/coderd/database/dbtestutil"
|
||
"github.com/coder/coder/v2/coderd/notifications"
|
||
"github.com/coder/coder/v2/coderd/notifications/notificationstest"
|
||
"github.com/coder/coder/v2/coderd/schedule"
|
||
"github.com/coder/coder/v2/coderd/schedule/cron"
|
||
"github.com/coder/coder/v2/coderd/util/ptr"
|
||
"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 TestExecutorAutostartOK(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace that has autostart enabled
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
})
|
||
)
|
||
// Given: workspace is stopped
|
||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
|
||
// When: the autobuild executor ticks after the scheduled time
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should eventually be started
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 1)
|
||
assert.Contains(t, stats.Transitions, workspace.ID)
|
||
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
|
||
|
||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
assert.Equal(t, codersdk.BuildReasonAutostart, workspace.LatestBuild.Reason)
|
||
// Assert some template props. If this is not set correctly, the test
|
||
// will fail.
|
||
ctx := testutil.Context(t, testutil.WaitShort)
|
||
template, err := client.Template(ctx, workspace.TemplateID)
|
||
require.NoError(t, err)
|
||
require.Equal(t, template.AutostartRequirement.DaysOfWeek, []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"})
|
||
}
|
||
|
||
func TestMultipleLifecycleExecutors(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
db, ps := dbtestutil.NewDB(t)
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
// Create our first client
|
||
tickCh = make(chan time.Time, 2)
|
||
statsChA = make(chan autobuild.Stats)
|
||
clientA = coderdtest.New(t, &coderdtest.Options{
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildTicker: tickCh,
|
||
AutobuildStats: statsChA,
|
||
Database: db,
|
||
Pubsub: ps,
|
||
})
|
||
// ... And then our second client
|
||
statsChB = make(chan autobuild.Stats)
|
||
_ = coderdtest.New(t, &coderdtest.Options{
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildTicker: tickCh,
|
||
AutobuildStats: statsChB,
|
||
Database: db,
|
||
Pubsub: ps,
|
||
})
|
||
// Now create a workspace (we can use either client, it doesn't matter)
|
||
workspace = mustProvisionWorkspace(t, clientA, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
})
|
||
)
|
||
|
||
// Have the workspace stopped so we can perform an autostart
|
||
workspace = coderdtest.MustTransitionWorkspace(t, clientA, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
|
||
// Get both clients to perform a lifecycle execution tick
|
||
next := sched.Next(workspace.LatestBuild.CreatedAt)
|
||
|
||
startCh := make(chan struct{})
|
||
go func() {
|
||
<-startCh
|
||
tickCh <- next
|
||
}()
|
||
go func() {
|
||
<-startCh
|
||
tickCh <- next
|
||
}()
|
||
close(startCh)
|
||
|
||
// Now we want to check the stats for both clients
|
||
statsA := <-statsChA
|
||
statsB := <-statsChB
|
||
|
||
// We expect there to be no errors
|
||
assert.Len(t, statsA.Errors, 0)
|
||
assert.Len(t, statsB.Errors, 0)
|
||
|
||
// We also expect there to have been only one transition
|
||
require.Equal(t, 1, len(statsA.Transitions)+len(statsB.Transitions))
|
||
|
||
stats := statsA
|
||
if len(statsB.Transitions) == 1 {
|
||
stats = statsB
|
||
}
|
||
|
||
// And we expect this transition to have been a start transition
|
||
assert.Contains(t, stats.Transitions, workspace.ID)
|
||
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
|
||
}
|
||
|
||
func TestExecutorAutostartTemplateUpdated(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
testCases := []struct {
|
||
name string
|
||
automaticUpdates codersdk.AutomaticUpdates
|
||
compatibleParameters bool
|
||
expectStart bool
|
||
expectUpdate bool
|
||
expectNotification bool
|
||
}{
|
||
{
|
||
name: "Never",
|
||
automaticUpdates: codersdk.AutomaticUpdatesNever,
|
||
compatibleParameters: true,
|
||
expectStart: true,
|
||
expectUpdate: false,
|
||
},
|
||
{
|
||
name: "Always_Compatible",
|
||
automaticUpdates: codersdk.AutomaticUpdatesAlways,
|
||
compatibleParameters: true,
|
||
expectStart: true,
|
||
expectUpdate: true,
|
||
expectNotification: true,
|
||
},
|
||
{
|
||
name: "Always_Incompatible",
|
||
automaticUpdates: codersdk.AutomaticUpdatesAlways,
|
||
compatibleParameters: false,
|
||
expectStart: false,
|
||
expectUpdate: false,
|
||
},
|
||
}
|
||
for _, tc := range testCases {
|
||
t.Run(tc.name, func(t *testing.T) {
|
||
t.Parallel()
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
ctx = context.Background()
|
||
err error
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
logger = slogtest.Make(t, &slogtest.Options{IgnoreErrors: !tc.expectStart}).Leveled(slog.LevelDebug)
|
||
enqueuer = notificationstest.FakeEnqueuer{}
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
Logger: &logger,
|
||
NotificationsEnqueuer: &enqueuer,
|
||
})
|
||
// Given: we have a user with a workspace that has autostart enabled
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
// Given: automatic updates from the test case
|
||
cwr.AutomaticUpdates = tc.automaticUpdates
|
||
})
|
||
)
|
||
// Given: workspace is stopped
|
||
workspace = coderdtest.MustTransitionWorkspace(
|
||
t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
|
||
orgs, err := client.OrganizationsByUser(ctx, workspace.OwnerID.String())
|
||
require.NoError(t, err)
|
||
require.Len(t, orgs, 1)
|
||
|
||
var res *echo.Responses
|
||
if !tc.compatibleParameters {
|
||
// Given, parameters of the new version are not compatible.
|
||
// Since initial version has no parameters, any parameters in the new version will be incompatible
|
||
res = &echo.Responses{
|
||
Parse: echo.ParseComplete,
|
||
ProvisionApply: []*proto.Response{{
|
||
Type: &proto.Response_Apply{
|
||
Apply: &proto.ApplyComplete{
|
||
Parameters: []*proto.RichParameter{
|
||
{
|
||
Name: "new",
|
||
Mutable: false,
|
||
Required: true,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
}},
|
||
}
|
||
}
|
||
|
||
// Given: the workspace template has been updated
|
||
newVersion := coderdtest.UpdateTemplateVersion(t, client, orgs[0].ID, res, workspace.TemplateID)
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, newVersion.ID)
|
||
require.NoError(t, client.UpdateActiveTemplateVersion(
|
||
ctx, workspace.TemplateID, codersdk.UpdateActiveTemplateVersion{
|
||
ID: newVersion.ID,
|
||
},
|
||
))
|
||
|
||
t.Log("sending autobuild tick")
|
||
// When: the autobuild executor ticks after the scheduled time
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||
close(tickCh)
|
||
}()
|
||
|
||
stats := <-statsCh
|
||
if !tc.expectStart {
|
||
// Then: the workspace should not be started
|
||
assert.Len(t, stats.Transitions, 0)
|
||
assert.Len(t, stats.Errors, 1)
|
||
return
|
||
}
|
||
|
||
assert.Len(t, stats.Errors, 0)
|
||
// Then: the workspace should be started
|
||
assert.Len(t, stats.Transitions, 1)
|
||
assert.Contains(t, stats.Transitions, workspace.ID)
|
||
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
|
||
ws := coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
if tc.expectUpdate {
|
||
// Then: uses the updated version
|
||
assert.Equal(t, newVersion.ID, ws.LatestBuild.TemplateVersionID,
|
||
"expected workspace build to be using the updated template version")
|
||
} else {
|
||
// Then: uses the previous template version
|
||
assert.Equal(t, workspace.LatestBuild.TemplateVersionID, ws.LatestBuild.TemplateVersionID,
|
||
"expected workspace build to be using the old template version")
|
||
}
|
||
|
||
if tc.expectNotification {
|
||
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceAutoUpdated))
|
||
require.Len(t, sent, 1)
|
||
require.Equal(t, sent[0].UserID, workspace.OwnerID)
|
||
require.Contains(t, sent[0].Targets, workspace.TemplateID)
|
||
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.Equal(t, newVersion.Name, sent[0].Labels["template_version_name"])
|
||
require.Equal(t, "autobuild", sent[0].Labels["initiator"])
|
||
require.Equal(t, "autostart", sent[0].Labels["reason"])
|
||
} else {
|
||
sent := enqueuer.Sent(notificationstest.WithTemplateID(notifications.TemplateWorkspaceAutoUpdated))
|
||
require.Empty(t, sent)
|
||
}
|
||
})
|
||
}
|
||
}
|
||
|
||
func TestExecutorAutostartAlreadyRunning(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace that has autostart enabled
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
})
|
||
)
|
||
|
||
// Given: we ensure the workspace is running
|
||
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
||
|
||
// When: the autobuild executor ticks
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should not be started.
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
require.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorAutostartNotEnabled(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace that does not have autostart enabled
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = nil
|
||
})
|
||
)
|
||
|
||
// Given: workspace does not have autostart enabled
|
||
require.Empty(t, workspace.AutostartSchedule)
|
||
|
||
// Given: workspace is stopped
|
||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
|
||
// When: the autobuild executor ticks way into the future
|
||
go func() {
|
||
tickCh <- workspace.LatestBuild.CreatedAt.Add(24 * time.Hour)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should not be started.
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
require.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorAutostartUserSuspended(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
)
|
||
|
||
admin := coderdtest.CreateFirstUser(t, client)
|
||
version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil)
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
|
||
userClient, user := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
})
|
||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||
workspace = coderdtest.MustWorkspace(t, userClient, workspace.ID)
|
||
|
||
// Given: workspace is stopped, and the user is suspended.
|
||
workspace = coderdtest.MustTransitionWorkspace(t, userClient, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
|
||
ctx := testutil.Context(t, testutil.WaitShort)
|
||
|
||
_, err := client.UpdateUserStatus(ctx, user.ID.String(), codersdk.UserStatusSuspended)
|
||
require.NoError(t, err, "update user status")
|
||
|
||
// When: the autobuild executor ticks after the scheduled time
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: nothing should happen
|
||
stats := testutil.TryReceive(ctx, t, statsCh)
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorAutostopOK(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace
|
||
workspace = mustProvisionWorkspace(t, client)
|
||
)
|
||
// Given: workspace is running
|
||
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
||
require.NotZero(t, workspace.LatestBuild.Deadline)
|
||
|
||
// When: the autobuild executor ticks *after* the deadline:
|
||
go func() {
|
||
tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should be stopped
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 1)
|
||
assert.Contains(t, stats.Transitions, workspace.ID)
|
||
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID])
|
||
|
||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
assert.Equal(t, codersdk.BuildReasonAutostop, workspace.LatestBuild.Reason)
|
||
}
|
||
|
||
func TestExecutorAutostopExtend(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
ctx = context.Background()
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace
|
||
workspace = mustProvisionWorkspace(t, client)
|
||
originalDeadline = workspace.LatestBuild.Deadline
|
||
)
|
||
// Given: workspace is running
|
||
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
||
require.NotZero(t, originalDeadline)
|
||
|
||
// Given: we extend the workspace deadline
|
||
newDeadline := originalDeadline.Time.Add(30 * time.Minute)
|
||
err := client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
|
||
Deadline: newDeadline,
|
||
})
|
||
require.NoError(t, err, "extend workspace deadline")
|
||
|
||
// When: the autobuild executor ticks *after* the original deadline:
|
||
go func() {
|
||
tickCh <- originalDeadline.Time.Add(time.Minute)
|
||
}()
|
||
|
||
// Then: nothing should happen and the workspace should stay running
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
|
||
// When: the autobuild executor ticks after the *new* deadline:
|
||
go func() {
|
||
tickCh <- newDeadline.Add(time.Minute)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should be stopped
|
||
stats = <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 1)
|
||
assert.Contains(t, stats.Transitions, workspace.ID)
|
||
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID])
|
||
}
|
||
|
||
func TestExecutorAutostopAlreadyStopped(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace (disabling autostart)
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = nil
|
||
})
|
||
)
|
||
|
||
// Given: workspace is stopped
|
||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
|
||
// When: the autobuild executor ticks past the TTL
|
||
go func() {
|
||
tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should remain stopped and no build should happen.
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorAutostopNotEnabled(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.TTLMillis = nil
|
||
})
|
||
)
|
||
|
||
// Given: workspace has no TTL set
|
||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
require.Nil(t, workspace.TTLMillis)
|
||
require.Zero(t, workspace.LatestBuild.Deadline)
|
||
require.NotZero(t, workspace.LatestBuild.Job.CompletedAt)
|
||
|
||
// Given: workspace is running
|
||
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
||
|
||
// When: the autobuild executor ticks a year in the future
|
||
go func() {
|
||
tickCh <- workspace.LatestBuild.Job.CompletedAt.AddDate(1, 0, 0)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should not be stopped.
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorWorkspaceDeleted(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace that has autostart enabled
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
})
|
||
)
|
||
|
||
// Given: workspace is deleted
|
||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionDelete)
|
||
|
||
// When: the autobuild executor ticks
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: nothing should happen
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorWorkspaceAutostartTooEarly(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// futureTime = time.Now().Add(time.Hour)
|
||
// futureTimeCron = fmt.Sprintf("%d %d * * *", futureTime.Minute(), futureTime.Hour())
|
||
// Given: we have a user with a workspace configured to autostart some time in the future
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
})
|
||
)
|
||
|
||
// When: the autobuild executor ticks before the next scheduled time
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt).Add(-time.Minute)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: nothing should happen
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorWorkspaceAutostopBeforeDeadline(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace
|
||
workspace = mustProvisionWorkspace(t, client)
|
||
)
|
||
|
||
// Given: workspace is running and has a non-zero deadline
|
||
require.Equal(t, codersdk.WorkspaceTransitionStart, workspace.LatestBuild.Transition)
|
||
require.NotZero(t, workspace.LatestBuild.Deadline)
|
||
|
||
// When: the autobuild executor ticks before the TTL
|
||
go func() {
|
||
tickCh <- workspace.LatestBuild.Deadline.Time.Add(-1 * time.Minute)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: nothing should happen
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecuteAutostopSuspendedUser(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
)
|
||
|
||
admin := coderdtest.CreateFirstUser(t, client)
|
||
version := coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil)
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID)
|
||
userClient, user := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
|
||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||
|
||
// Given: workspace is running, and the user is suspended.
|
||
workspace = coderdtest.MustWorkspace(t, userClient, workspace.ID)
|
||
require.Equal(t, codersdk.WorkspaceStatusRunning, workspace.LatestBuild.Status)
|
||
|
||
ctx := testutil.Context(t, testutil.WaitShort)
|
||
|
||
_, err := client.UpdateUserStatus(ctx, user.ID.String(), codersdk.UserStatusSuspended)
|
||
require.NoError(t, err, "update user status")
|
||
|
||
// When: the autobuild executor ticks after the scheduled time
|
||
go func() {
|
||
tickCh <- time.Unix(0, 0) // the exact time is not important
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should be stopped
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 1)
|
||
assert.Equal(t, stats.Transitions[workspace.ID], database.WorkspaceTransitionStop)
|
||
|
||
// Wait for stop to complete
|
||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
workspaceBuild := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||
assert.Equal(t, codersdk.WorkspaceStatusStopped, workspaceBuild.Status)
|
||
}
|
||
|
||
func TestExecutorWorkspaceAutostopNoWaitChangedMyMind(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
ctx = context.Background()
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
// Given: we have a user with a workspace
|
||
workspace = mustProvisionWorkspace(t, client)
|
||
)
|
||
|
||
// Given: the user changes their mind and decides their workspace should not autostop
|
||
err := client.UpdateWorkspaceTTL(ctx, workspace.ID, codersdk.UpdateWorkspaceTTLRequest{TTLMillis: nil})
|
||
require.NoError(t, err)
|
||
|
||
// Then: the deadline should be set to zero
|
||
updated := coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
assert.True(t, !updated.LatestBuild.Deadline.Valid)
|
||
|
||
// When: the autobuild executor ticks after the original deadline
|
||
go func() {
|
||
tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||
}()
|
||
|
||
// Then: the workspace should not stop
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorAutostartMultipleOK(t *testing.T) {
|
||
if !dbtestutil.WillUsePostgres() {
|
||
t.Skip(`This test only really works when using a "real" database, similar to a HA setup`)
|
||
}
|
||
|
||
t.Parallel()
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
tickCh = make(chan time.Time)
|
||
tickCh2 = make(chan time.Time)
|
||
statsCh1 = make(chan autobuild.Stats)
|
||
statsCh2 = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh1,
|
||
})
|
||
_ = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh2,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh2,
|
||
})
|
||
// Given: we have a user with a workspace that has autostart enabled (default)
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
})
|
||
)
|
||
// Given: workspace is stopped
|
||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
|
||
// When: the autobuild executor ticks past the scheduled time
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||
tickCh2 <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||
close(tickCh)
|
||
close(tickCh2)
|
||
}()
|
||
|
||
// Then: the workspace should eventually be started
|
||
stats1 := <-statsCh1
|
||
assert.Len(t, stats1.Errors, 0)
|
||
assert.Len(t, stats1.Transitions, 1)
|
||
assert.Contains(t, stats1.Transitions, workspace.ID)
|
||
assert.Equal(t, database.WorkspaceTransitionStart, stats1.Transitions[workspace.ID])
|
||
|
||
// Then: the other executor should not have done anything
|
||
stats2 := <-statsCh2
|
||
assert.Len(t, stats2.Errors, 0)
|
||
assert.Len(t, stats2.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorAutostartWithParameters(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
const (
|
||
stringParameterName = "string_parameter"
|
||
stringParameterValue = "abc"
|
||
|
||
numberParameterName = "number_parameter"
|
||
numberParameterValue = "7"
|
||
)
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
|
||
richParameters = []*proto.RichParameter{
|
||
{Name: stringParameterName, Type: "string", Mutable: true},
|
||
{Name: numberParameterName, Type: "number", Mutable: true},
|
||
}
|
||
|
||
// Given: we have a user with a workspace that has autostart enabled
|
||
workspace = mustProvisionWorkspaceWithParameters(t, client, richParameters, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
cwr.RichParameterValues = []codersdk.WorkspaceBuildParameter{
|
||
{
|
||
Name: stringParameterName,
|
||
Value: stringParameterValue,
|
||
},
|
||
{
|
||
Name: numberParameterName,
|
||
Value: numberParameterValue,
|
||
},
|
||
}
|
||
})
|
||
)
|
||
// Given: workspace is stopped
|
||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
|
||
// When: the autobuild executor ticks after the scheduled time
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace with parameters should eventually be started
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 1)
|
||
assert.Contains(t, stats.Transitions, workspace.ID)
|
||
assert.Equal(t, database.WorkspaceTransitionStart, stats.Transitions[workspace.ID])
|
||
|
||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
mustWorkspaceParameters(t, client, workspace.LatestBuild.ID)
|
||
}
|
||
|
||
func TestExecutorAutostartTemplateDisabled(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
|
||
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
||
return schedule.TemplateScheduleOptions{
|
||
UserAutostartEnabled: false,
|
||
UserAutostopEnabled: true,
|
||
DefaultTTL: 0,
|
||
AutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||
}, nil
|
||
},
|
||
},
|
||
})
|
||
// futureTime = time.Now().Add(time.Hour)
|
||
// futureTimeCron = fmt.Sprintf("%d %d * * *", futureTime.Minute(), futureTime.Hour())
|
||
// Given: we have a user with a workspace configured to autostart some time in the future
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
})
|
||
)
|
||
// Given: workspace is stopped
|
||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
|
||
// When: the autobuild executor ticks before the next scheduled time
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt).Add(time.Minute)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: nothing should happen
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
}
|
||
|
||
func TestExecutorAutostopTemplateDisabled(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// Given: we have a workspace built from a template that disallows user autostop
|
||
var (
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
// We are using a mock store here as the AGPL store does not implement this.
|
||
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
|
||
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
||
return schedule.TemplateScheduleOptions{
|
||
UserAutostopEnabled: false,
|
||
DefaultTTL: time.Hour,
|
||
}, nil
|
||
},
|
||
},
|
||
})
|
||
// Given: we have a user with a workspace configured to autostop 30 minutes in the future
|
||
workspace = mustProvisionWorkspace(t, client, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.TTLMillis = ptr.Ref(30 * time.Minute.Milliseconds())
|
||
})
|
||
)
|
||
|
||
// When: we create the workspace
|
||
// Then: the deadline should be set to the template default TTL
|
||
assert.WithinDuration(t, workspace.LatestBuild.CreatedAt.Add(time.Hour), workspace.LatestBuild.Deadline.Time, time.Minute)
|
||
|
||
// When: the autobuild executor ticks after the workspace setting, but before the template setting:
|
||
go func() {
|
||
tickCh <- workspace.LatestBuild.Job.CompletedAt.Add(45 * time.Minute)
|
||
}()
|
||
|
||
// Then: nothing should happen
|
||
stats := <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 0)
|
||
|
||
// When: the autobuild executor ticks after the template setting:
|
||
go func() {
|
||
tickCh <- workspace.LatestBuild.Job.CompletedAt.Add(61 * time.Minute)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should be stopped
|
||
stats = <-statsCh
|
||
assert.Len(t, stats.Errors, 0)
|
||
assert.Len(t, stats.Transitions, 1)
|
||
assert.Contains(t, stats.Transitions, workspace.ID)
|
||
assert.Equal(t, database.WorkspaceTransitionStop, stats.Transitions[workspace.ID])
|
||
}
|
||
|
||
// Test that an AGPL AccessControlStore properly disables
|
||
// functionality.
|
||
func TestExecutorRequireActiveVersion(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
sched = mustSchedule(t, "CRON_TZ=UTC 0 * * * *")
|
||
ticker = make(chan time.Time)
|
||
statCh = make(chan autobuild.Stats)
|
||
|
||
ownerClient, db = coderdtest.NewWithDatabase(t, &coderdtest.Options{
|
||
AutobuildTicker: ticker,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statCh,
|
||
TemplateScheduleStore: schedule.NewAGPLTemplateScheduleStore(),
|
||
})
|
||
)
|
||
ctx := testutil.Context(t, testutil.WaitShort)
|
||
owner := coderdtest.CreateFirstUser(t, ownerClient)
|
||
me, err := ownerClient.User(ctx, codersdk.Me)
|
||
require.NoError(t, err)
|
||
|
||
// Create an active and inactive template version. We'll
|
||
// build a regular member's workspace using a non-active
|
||
// template version and assert that the field is not abided
|
||
// since there is no enterprise license.
|
||
activeVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil)
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, activeVersion.ID)
|
||
template := coderdtest.CreateTemplate(t, ownerClient, owner.OrganizationID, activeVersion.ID)
|
||
|
||
ctx = testutil.Context(t, testutil.WaitShort) // Reset context after setting up the template.
|
||
|
||
//nolint We need to set this in the database directly, because the API will return an error
|
||
// letting you know that this feature requires an enterprise license.
|
||
err = db.UpdateTemplateAccessControlByID(dbauthz.As(ctx, coderdtest.AuthzUserSubject(me, owner.OrganizationID)), database.UpdateTemplateAccessControlByIDParams{
|
||
ID: template.ID,
|
||
RequireActiveVersion: true,
|
||
})
|
||
require.NoError(t, err)
|
||
inactiveVersion := coderdtest.CreateTemplateVersion(t, ownerClient, owner.OrganizationID, nil, func(ctvr *codersdk.CreateTemplateVersionRequest) {
|
||
ctvr.TemplateID = template.ID
|
||
})
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, ownerClient, inactiveVersion.ID)
|
||
memberClient, _ := coderdtest.CreateAnotherUser(t, ownerClient, owner.OrganizationID)
|
||
ws := coderdtest.CreateWorkspace(t, memberClient, uuid.Nil, func(cwr *codersdk.CreateWorkspaceRequest) {
|
||
cwr.TemplateVersionID = inactiveVersion.ID
|
||
cwr.AutostartSchedule = ptr.Ref(sched.String())
|
||
})
|
||
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, ownerClient, ws.LatestBuild.ID)
|
||
ws = coderdtest.MustTransitionWorkspace(t, memberClient, ws.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop, func(req *codersdk.CreateWorkspaceBuildRequest) {
|
||
req.TemplateVersionID = inactiveVersion.ID
|
||
})
|
||
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
|
||
ticker <- sched.Next(ws.LatestBuild.CreatedAt)
|
||
stats := <-statCh
|
||
require.Len(t, stats.Transitions, 1)
|
||
|
||
ws = coderdtest.MustWorkspace(t, memberClient, ws.ID)
|
||
require.Equal(t, inactiveVersion.ID, ws.LatestBuild.TemplateVersionID)
|
||
}
|
||
|
||
// TestExecutorFailedWorkspace test AGPL functionality which mainly
|
||
// ensures that autostop actions as a result of a failed workspace
|
||
// build do not trigger.
|
||
// For enterprise functionality see enterprise/coderd/workspaces_test.go
|
||
func TestExecutorFailedWorkspace(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// Test that an AGPL TemplateScheduleStore properly disables
|
||
// functionality.
|
||
t.Run("OK", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
ticker = make(chan time.Time)
|
||
statCh = make(chan autobuild.Stats)
|
||
logger = slogtest.Make(t, &slogtest.Options{
|
||
// We ignore errors here since we expect to fail
|
||
// builds.
|
||
IgnoreErrors: true,
|
||
})
|
||
failureTTL = time.Millisecond
|
||
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
Logger: &logger,
|
||
AutobuildTicker: ticker,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statCh,
|
||
TemplateScheduleStore: schedule.NewAGPLTemplateScheduleStore(),
|
||
})
|
||
)
|
||
user := coderdtest.CreateFirstUser(t, client)
|
||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||
Parse: echo.ParseComplete,
|
||
ProvisionPlan: echo.PlanComplete,
|
||
ProvisionApply: echo.ApplyFailed,
|
||
})
|
||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
|
||
})
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||
ws := coderdtest.CreateWorkspace(t, client, template.ID)
|
||
build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||
require.Equal(t, codersdk.WorkspaceStatusFailed, build.Status)
|
||
ticker <- build.Job.CompletedAt.Add(failureTTL * 2)
|
||
stats := <-statCh
|
||
// Expect no transitions since we're using AGPL.
|
||
require.Len(t, stats.Transitions, 0)
|
||
})
|
||
}
|
||
|
||
// TestExecutorInactiveWorkspace test AGPL functionality which mainly
|
||
// ensures that autostop actions as a result of an inactive workspace
|
||
// do not trigger.
|
||
// For enterprise functionality see enterprise/coderd/workspaces_test.go
|
||
func TestExecutorInactiveWorkspace(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// Test that an AGPL TemplateScheduleStore properly disables
|
||
// functionality.
|
||
t.Run("OK", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
var (
|
||
ticker = make(chan time.Time)
|
||
statCh = make(chan autobuild.Stats)
|
||
logger = slogtest.Make(t, &slogtest.Options{
|
||
// We ignore errors here since we expect to fail
|
||
// builds.
|
||
IgnoreErrors: true,
|
||
})
|
||
inactiveTTL = time.Millisecond
|
||
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
Logger: &logger,
|
||
AutobuildTicker: ticker,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statCh,
|
||
TemplateScheduleStore: schedule.NewAGPLTemplateScheduleStore(),
|
||
})
|
||
)
|
||
user := coderdtest.CreateFirstUser(t, client)
|
||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||
Parse: echo.ParseComplete,
|
||
ProvisionPlan: echo.PlanComplete,
|
||
ProvisionApply: echo.ApplyComplete,
|
||
})
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||
ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
|
||
})
|
||
ws := coderdtest.CreateWorkspace(t, client, template.ID)
|
||
build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||
require.Equal(t, codersdk.WorkspaceStatusRunning, build.Status)
|
||
ticker <- ws.LastUsedAt.Add(inactiveTTL * 2)
|
||
stats := <-statCh
|
||
// Expect no transitions since we're using AGPL.
|
||
require.Len(t, stats.Transitions, 0)
|
||
})
|
||
}
|
||
|
||
func TestNotifications(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
t.Run("Dormancy", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// Setup template with dormancy and create a workspace with it
|
||
var (
|
||
ticker = make(chan time.Time)
|
||
statCh = make(chan autobuild.Stats)
|
||
notifyEnq = notificationstest.FakeEnqueuer{}
|
||
timeTilDormant = time.Minute
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
AutobuildTicker: ticker,
|
||
AutobuildStats: statCh,
|
||
IncludeProvisionerDaemon: true,
|
||
NotificationsEnqueuer: ¬ifyEnq,
|
||
TemplateScheduleStore: schedule.MockTemplateScheduleStore{
|
||
SetFn: func(ctx context.Context, db database.Store, template database.Template, options schedule.TemplateScheduleOptions) (database.Template, error) {
|
||
template.TimeTilDormant = int64(options.TimeTilDormant)
|
||
return schedule.NewAGPLTemplateScheduleStore().Set(ctx, db, template, options)
|
||
},
|
||
GetFn: func(_ context.Context, _ database.Store, _ uuid.UUID) (schedule.TemplateScheduleOptions, error) {
|
||
return schedule.TemplateScheduleOptions{
|
||
UserAutostartEnabled: false,
|
||
UserAutostopEnabled: true,
|
||
DefaultTTL: 0,
|
||
AutostopRequirement: schedule.TemplateAutostopRequirement{},
|
||
TimeTilDormant: timeTilDormant,
|
||
}, nil
|
||
},
|
||
},
|
||
})
|
||
admin = coderdtest.CreateFirstUser(t, client)
|
||
version = coderdtest.CreateTemplateVersion(t, client, admin.OrganizationID, nil)
|
||
)
|
||
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||
template := coderdtest.CreateTemplate(t, client, admin.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||
ctr.TimeTilDormantMillis = ptr.Ref(timeTilDormant.Milliseconds())
|
||
})
|
||
userClient, _ := coderdtest.CreateAnotherUser(t, client, admin.OrganizationID)
|
||
workspace := coderdtest.CreateWorkspace(t, userClient, template.ID)
|
||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||
|
||
// Stop workspace
|
||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||
_ = coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, workspace.LatestBuild.ID)
|
||
|
||
// Wait for workspace to become dormant
|
||
notifyEnq.Clear()
|
||
ticker <- workspace.LastUsedAt.Add(timeTilDormant * 3)
|
||
_ = testutil.TryReceive(testutil.Context(t, testutil.WaitShort), t, statCh)
|
||
|
||
// Check that the workspace is dormant
|
||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
require.NotNil(t, workspace.DormantAt)
|
||
|
||
// Check that a notification was enqueued
|
||
sent := notifyEnq.Sent()
|
||
require.Len(t, sent, 1)
|
||
require.Equal(t, sent[0].UserID, workspace.OwnerID)
|
||
require.Equal(t, sent[0].TemplateID, notifications.TemplateWorkspaceDormant)
|
||
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)
|
||
})
|
||
}
|
||
|
||
// TestExecutorPrebuilds verifies AGPL behavior for prebuilt workspaces.
|
||
// It ensures that workspace schedules do not trigger while the workspace
|
||
// is still in a prebuilt state. Scheduling behavior only applies after the
|
||
// workspace has been claimed and becomes a regular user workspace.
|
||
// For enterprise-related functionality, see enterprise/coderd/workspaces_test.go.
|
||
func TestExecutorPrebuilds(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
if !dbtestutil.WillUsePostgres() {
|
||
t.Skip("this test requires postgres")
|
||
}
|
||
|
||
// Prebuild workspaces should not be autostopped when the deadline is reached.
|
||
// After being claimed, the workspace should stop at the deadline.
|
||
t.Run("OnlyStopsAfterClaimed", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// Setup
|
||
ctx := testutil.Context(t, testutil.WaitShort)
|
||
clock := quartz.NewMock(t)
|
||
db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
|
||
var (
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
Database: db,
|
||
Pubsub: pb,
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
)
|
||
|
||
// Setup user, template and template version
|
||
owner := coderdtest.CreateFirstUser(t, client)
|
||
_, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())
|
||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||
|
||
// Database setup of a preset with a prebuild instance
|
||
preset := setupTestDBPreset(t, db, version.ID, int32(1))
|
||
|
||
// Given: a running prebuilt workspace with a deadline and ready to be claimed
|
||
dbPrebuild := setupTestDBPrebuiltWorkspace(
|
||
ctx, t, clock, db, pb,
|
||
owner.OrganizationID,
|
||
template.ID,
|
||
version.ID,
|
||
preset.ID,
|
||
)
|
||
prebuild := coderdtest.MustWorkspace(t, client, dbPrebuild.ID)
|
||
require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition)
|
||
require.NotZero(t, prebuild.LatestBuild.Deadline)
|
||
|
||
// When: the autobuild executor ticks *after* the deadline:
|
||
go func() {
|
||
tickCh <- prebuild.LatestBuild.Deadline.Time.Add(time.Minute)
|
||
}()
|
||
|
||
// Then: the prebuilt workspace should remain in a start transition
|
||
prebuildStats := <-statsCh
|
||
require.Len(t, prebuildStats.Errors, 0)
|
||
require.Len(t, prebuildStats.Transitions, 0)
|
||
require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition)
|
||
prebuild = coderdtest.MustWorkspace(t, client, prebuild.ID)
|
||
require.Equal(t, codersdk.BuildReasonInitiator, prebuild.LatestBuild.Reason)
|
||
|
||
// Given: a user claims the prebuilt workspace
|
||
dbWorkspace := dbgen.ClaimPrebuild(t, db, user.ID, "claimedWorkspace-autostop", preset.ID)
|
||
workspace := coderdtest.MustWorkspace(t, client, dbWorkspace.ID)
|
||
|
||
// When: the autobuild executor ticks *after* the deadline:
|
||
go func() {
|
||
tickCh <- workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should be stopped
|
||
workspaceStats := <-statsCh
|
||
require.Len(t, workspaceStats.Errors, 0)
|
||
require.Len(t, workspaceStats.Transitions, 1)
|
||
require.Contains(t, workspaceStats.Transitions, workspace.ID)
|
||
require.Equal(t, database.WorkspaceTransitionStop, workspaceStats.Transitions[workspace.ID])
|
||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
require.Equal(t, codersdk.BuildReasonAutostop, workspace.LatestBuild.Reason)
|
||
})
|
||
|
||
// Prebuild workspaces should not be autostarted when the autostart scheduled is reached.
|
||
// After being claimed, the workspace should autostart at the schedule.
|
||
t.Run("OnlyStartsAfterClaimed", func(t *testing.T) {
|
||
t.Parallel()
|
||
|
||
// Setup
|
||
ctx := testutil.Context(t, testutil.WaitShort)
|
||
clock := quartz.NewMock(t)
|
||
db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
|
||
var (
|
||
tickCh = make(chan time.Time)
|
||
statsCh = make(chan autobuild.Stats)
|
||
client = coderdtest.New(t, &coderdtest.Options{
|
||
Database: db,
|
||
Pubsub: pb,
|
||
AutobuildTicker: tickCh,
|
||
IncludeProvisionerDaemon: true,
|
||
AutobuildStats: statsCh,
|
||
})
|
||
)
|
||
|
||
// Setup user, template and template version
|
||
owner := coderdtest.CreateFirstUser(t, client)
|
||
_, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())
|
||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
|
||
|
||
// Database setup of a preset with a prebuild instance
|
||
preset := setupTestDBPreset(t, db, version.ID, int32(1))
|
||
|
||
// Given: prebuilt workspace is stopped and set to autostart daily at midnight
|
||
sched := mustSchedule(t, "CRON_TZ=UTC 0 0 * * *")
|
||
autostartSched := sql.NullString{
|
||
String: sched.String(),
|
||
Valid: true,
|
||
}
|
||
dbPrebuild := setupTestDBPrebuiltWorkspace(
|
||
ctx, t, clock, db, pb,
|
||
owner.OrganizationID,
|
||
template.ID,
|
||
version.ID,
|
||
preset.ID,
|
||
WithAutostartSchedule(autostartSched),
|
||
WithIsStopped(true),
|
||
)
|
||
prebuild := coderdtest.MustWorkspace(t, client, dbPrebuild.ID)
|
||
require.Equal(t, codersdk.WorkspaceTransitionStop, prebuild.LatestBuild.Transition)
|
||
require.NotNil(t, prebuild.AutostartSchedule)
|
||
|
||
// Tick at the next scheduled time after the prebuild’s LatestBuild.CreatedAt,
|
||
// since the next allowed autostart is calculated starting from that point.
|
||
// When: the autobuild executor ticks after the scheduled time
|
||
go func() {
|
||
tickCh <- sched.Next(prebuild.LatestBuild.CreatedAt).Add(time.Minute)
|
||
}()
|
||
|
||
// Then: the prebuilt workspace should remain in a stop transition
|
||
prebuildStats := <-statsCh
|
||
require.Len(t, prebuildStats.Errors, 0)
|
||
require.Len(t, prebuildStats.Transitions, 0)
|
||
require.Equal(t, codersdk.WorkspaceTransitionStop, prebuild.LatestBuild.Transition)
|
||
prebuild = coderdtest.MustWorkspace(t, client, prebuild.ID)
|
||
require.Equal(t, codersdk.BuildReasonInitiator, prebuild.LatestBuild.Reason)
|
||
|
||
// Given: prebuilt workspace is in a start status
|
||
setupTestDBWorkspaceBuild(
|
||
ctx, t, clock, db, pb,
|
||
owner.OrganizationID,
|
||
prebuild.ID,
|
||
version.ID,
|
||
preset.ID,
|
||
database.WorkspaceTransitionStart)
|
||
|
||
// Given: a user claims the prebuilt workspace
|
||
dbWorkspace := dbgen.ClaimPrebuild(t, db, user.ID, "claimedWorkspace-autostart", preset.ID)
|
||
workspace := coderdtest.MustWorkspace(t, client, dbWorkspace.ID)
|
||
|
||
// Given: the prebuilt workspace goes to a stop status
|
||
setupTestDBWorkspaceBuild(
|
||
ctx, t, clock, db, pb,
|
||
owner.OrganizationID,
|
||
prebuild.ID,
|
||
version.ID,
|
||
preset.ID,
|
||
database.WorkspaceTransitionStop)
|
||
|
||
// Tick at the next scheduled time after the prebuild’s LatestBuild.CreatedAt,
|
||
// since the next allowed autostart is calculated starting from that point.
|
||
// When: the autobuild executor ticks after the scheduled time
|
||
go func() {
|
||
tickCh <- sched.Next(workspace.LatestBuild.CreatedAt).Add(time.Minute)
|
||
close(tickCh)
|
||
}()
|
||
|
||
// Then: the workspace should eventually be started
|
||
workspaceStats := <-statsCh
|
||
require.Len(t, workspaceStats.Errors, 0)
|
||
require.Len(t, workspaceStats.Transitions, 1)
|
||
require.Contains(t, workspaceStats.Transitions, workspace.ID)
|
||
require.Equal(t, database.WorkspaceTransitionStart, workspaceStats.Transitions[workspace.ID])
|
||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||
require.Equal(t, codersdk.BuildReasonAutostart, workspace.LatestBuild.Reason)
|
||
})
|
||
}
|
||
|
||
func setupTestDBPreset(
|
||
t *testing.T,
|
||
db database.Store,
|
||
templateVersionID uuid.UUID,
|
||
desiredInstances int32,
|
||
) database.TemplateVersionPreset {
|
||
t.Helper()
|
||
|
||
preset := dbgen.Preset(t, db, database.InsertPresetParams{
|
||
TemplateVersionID: templateVersionID,
|
||
Name: "preset-test",
|
||
DesiredInstances: sql.NullInt32{
|
||
Valid: true,
|
||
Int32: desiredInstances,
|
||
},
|
||
})
|
||
dbgen.PresetParameter(t, db, database.InsertPresetParametersParams{
|
||
TemplateVersionPresetID: preset.ID,
|
||
Names: []string{"test-name"},
|
||
Values: []string{"test-value"},
|
||
})
|
||
|
||
return preset
|
||
}
|
||
|
||
type SetupPrebuiltOptions struct {
|
||
AutostartSchedule sql.NullString
|
||
IsStopped bool
|
||
}
|
||
|
||
func WithAutostartSchedule(sched sql.NullString) func(*SetupPrebuiltOptions) {
|
||
return func(o *SetupPrebuiltOptions) {
|
||
o.AutostartSchedule = sched
|
||
}
|
||
}
|
||
|
||
func WithIsStopped(isStopped bool) func(*SetupPrebuiltOptions) {
|
||
return func(o *SetupPrebuiltOptions) {
|
||
o.IsStopped = isStopped
|
||
}
|
||
}
|
||
|
||
func setupTestDBWorkspaceBuild(
|
||
ctx context.Context,
|
||
t *testing.T,
|
||
clock quartz.Clock,
|
||
db database.Store,
|
||
ps pubsub.Pubsub,
|
||
orgID uuid.UUID,
|
||
workspaceID uuid.UUID,
|
||
templateVersionID uuid.UUID,
|
||
presetID uuid.UUID,
|
||
transition database.WorkspaceTransition,
|
||
) (database.ProvisionerJob, database.WorkspaceBuild) {
|
||
t.Helper()
|
||
|
||
var buildNumber int32 = 1
|
||
latestWorkspaceBuild, err := db.GetLatestWorkspaceBuildByWorkspaceID(ctx, workspaceID)
|
||
if !errors.Is(err, sql.ErrNoRows) {
|
||
buildNumber = latestWorkspaceBuild.BuildNumber + 1
|
||
}
|
||
|
||
job := dbgen.ProvisionerJob(t, db, ps, database.ProvisionerJob{
|
||
InitiatorID: database.PrebuildsSystemUserID,
|
||
CreatedAt: clock.Now().Add(-time.Hour * 2),
|
||
StartedAt: sql.NullTime{Time: clock.Now().Add(-time.Hour * 2), Valid: true},
|
||
CompletedAt: sql.NullTime{Time: clock.Now().Add(-time.Hour), Valid: true},
|
||
OrganizationID: orgID,
|
||
JobStatus: database.ProvisionerJobStatusSucceeded,
|
||
})
|
||
workspaceBuild := dbgen.WorkspaceBuild(t, db, database.WorkspaceBuild{
|
||
WorkspaceID: workspaceID,
|
||
InitiatorID: database.PrebuildsSystemUserID,
|
||
TemplateVersionID: templateVersionID,
|
||
BuildNumber: buildNumber,
|
||
JobID: job.ID,
|
||
TemplateVersionPresetID: uuid.NullUUID{UUID: presetID, Valid: true},
|
||
Transition: transition,
|
||
CreatedAt: clock.Now(),
|
||
})
|
||
dbgen.WorkspaceBuildParameters(t, db, []database.WorkspaceBuildParameter{
|
||
{
|
||
WorkspaceBuildID: workspaceBuild.ID,
|
||
Name: "test",
|
||
Value: "test",
|
||
},
|
||
})
|
||
|
||
workspaceResource := dbgen.WorkspaceResource(t, db, database.WorkspaceResource{
|
||
JobID: job.ID,
|
||
Transition: database.WorkspaceTransitionStart,
|
||
Type: "compute",
|
||
Name: "main",
|
||
})
|
||
|
||
// Workspaces are eligible to be claimed once their agent is marked "ready"
|
||
dbgen.WorkspaceAgent(t, db, database.WorkspaceAgent{
|
||
Name: "test",
|
||
ResourceID: workspaceResource.ID,
|
||
Architecture: "i386",
|
||
OperatingSystem: "linux",
|
||
LifecycleState: database.WorkspaceAgentLifecycleStateReady,
|
||
StartedAt: sql.NullTime{Time: time.Now().Add(time.Hour), Valid: true},
|
||
ReadyAt: sql.NullTime{Time: time.Now().Add(-1 * time.Hour), Valid: true},
|
||
APIKeyScope: database.AgentKeyScopeEnumAll,
|
||
})
|
||
|
||
return job, workspaceBuild
|
||
}
|
||
|
||
func setupTestDBPrebuiltWorkspace(
|
||
ctx context.Context,
|
||
t *testing.T,
|
||
clock quartz.Clock,
|
||
db database.Store,
|
||
ps pubsub.Pubsub,
|
||
orgID uuid.UUID,
|
||
templateID uuid.UUID,
|
||
templateVersionID uuid.UUID,
|
||
presetID uuid.UUID,
|
||
opts ...func(*SetupPrebuiltOptions),
|
||
) database.WorkspaceTable {
|
||
t.Helper()
|
||
|
||
// Optional parameters
|
||
options := &SetupPrebuiltOptions{}
|
||
for _, opt := range opts {
|
||
opt(options)
|
||
}
|
||
|
||
buildTransition := database.WorkspaceTransitionStart
|
||
if options.IsStopped {
|
||
buildTransition = database.WorkspaceTransitionStop
|
||
}
|
||
|
||
workspace := dbgen.Workspace(t, db, database.WorkspaceTable{
|
||
TemplateID: templateID,
|
||
OrganizationID: orgID,
|
||
OwnerID: database.PrebuildsSystemUserID,
|
||
Deleted: false,
|
||
CreatedAt: time.Now().Add(-time.Hour * 2),
|
||
AutostartSchedule: options.AutostartSchedule,
|
||
})
|
||
setupTestDBWorkspaceBuild(ctx, t, clock, db, ps, orgID, workspace.ID, templateVersionID, presetID, buildTransition)
|
||
|
||
return workspace
|
||
}
|
||
|
||
func mustProvisionWorkspace(t *testing.T, client *codersdk.Client, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace {
|
||
t.Helper()
|
||
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)
|
||
ws := coderdtest.CreateWorkspace(t, client, template.ID, mut...)
|
||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||
return coderdtest.MustWorkspace(t, client, ws.ID)
|
||
}
|
||
|
||
func mustProvisionWorkspaceWithParameters(t *testing.T, client *codersdk.Client, richParameters []*proto.RichParameter, mut ...func(*codersdk.CreateWorkspaceRequest)) codersdk.Workspace {
|
||
t.Helper()
|
||
user := coderdtest.CreateFirstUser(t, client)
|
||
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, &echo.Responses{
|
||
Parse: echo.ParseComplete,
|
||
ProvisionPlan: []*proto.Response{
|
||
{
|
||
Type: &proto.Response_Plan{
|
||
Plan: &proto.PlanComplete{
|
||
Parameters: richParameters,
|
||
},
|
||
},
|
||
},
|
||
},
|
||
ProvisionApply: echo.ApplyComplete,
|
||
})
|
||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
|
||
ws := coderdtest.CreateWorkspace(t, client, template.ID, mut...)
|
||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, ws.LatestBuild.ID)
|
||
return coderdtest.MustWorkspace(t, client, ws.ID)
|
||
}
|
||
|
||
func mustSchedule(t *testing.T, s string) *cron.Schedule {
|
||
t.Helper()
|
||
sched, err := cron.Weekly(s)
|
||
require.NoError(t, err)
|
||
return sched
|
||
}
|
||
|
||
func mustWorkspaceParameters(t *testing.T, client *codersdk.Client, workspaceID uuid.UUID) {
|
||
ctx := testutil.Context(t, testutil.WaitShort)
|
||
buildParameters, err := client.WorkspaceBuildParameters(ctx, workspaceID)
|
||
require.NoError(t, err)
|
||
require.NotEmpty(t, buildParameters)
|
||
}
|
||
|
||
func TestMain(m *testing.M) {
|
||
goleak.VerifyTestMain(m, testutil.GoleakOptions...)
|
||
}
|