mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
fix: set prebuilds lifecycle parameters on creation and claim (#19252)
## Description This PR ensures that prebuilt workspaces are properly excluded from the lifecycle executor and treated as a separate class of workspaces, fully managed by the prebuild reconciliation loop. It introduces two lifecycle guarantees: * When a prebuilt workspace is created (i.e., when the workspace build completes), all lifecycle-related fields are unset, ensuring the workspace does not participate in TTL, autostop, autostart, dormancy, or auto-deletion logic. * When a prebuilt workspace is claimed, it transitions into a regular user workspace. At this point, all lifecycle fields are correctly populated according to template-level configurations, allowing the workspace to be managed by the lifecycle executor as expected. ## Changes * Prebuilt workspaces now have all lifecycle-relevant fields unset during creation * When a prebuild is claimed: * Lifecycle fields are set based on template and workspace level configurations. This ensures a clean transition into the standard workspace lifecycle flow. * Updated lifecycle-related SQL update queries to explicitly exclude prebuilt workspaces. ## Relates Related issue: https://github.com/coder/coder/issues/18898 To reduce the scope of this PR and make the review process more manageable, the original implementation has been split into the following focused PRs: * https://github.com/coder/coder/pull/19259 * https://github.com/coder/coder/pull/19263 * https://github.com/coder/coder/pull/19264 * https://github.com/coder/coder/pull/19265 These PRs should be considered in conjunction with this one to understand the complete set of lifecycle separation changes for prebuilt workspaces.
This commit is contained in:
@@ -1722,7 +1722,7 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestExecutorPrebuilds(t *testing.T) {
|
||||
func TestPrebuildsAutobuild(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if !dbtestutil.WillUsePostgres() {
|
||||
@@ -1800,14 +1800,21 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
username string,
|
||||
version codersdk.TemplateVersion,
|
||||
presetID uuid.UUID,
|
||||
autostartSchedule ...string,
|
||||
) codersdk.Workspace {
|
||||
t.Helper()
|
||||
|
||||
var startSchedule string
|
||||
if len(autostartSchedule) > 0 {
|
||||
startSchedule = autostartSchedule[0]
|
||||
}
|
||||
|
||||
workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-")
|
||||
userWorkspace, err := userClient.CreateUserWorkspace(ctx, username, codersdk.CreateWorkspaceRequest{
|
||||
TemplateVersionID: version.ID,
|
||||
Name: workspaceName,
|
||||
TemplateVersionPresetID: presetID,
|
||||
AutostartSchedule: ptr.Ref(startSchedule),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
build := coderdtest.AwaitWorkspaceBuildJobCompleted(t, userClient, userWorkspace.LatestBuild.ID)
|
||||
@@ -1820,7 +1827,7 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
|
||||
// Prebuilt workspaces should not be autostopped based on the default TTL.
|
||||
// This test ensures that DefaultTTLMillis is ignored while the workspace is in a prebuild state.
|
||||
// Once the workspace is claimed, the default autostop timer should take effect.
|
||||
// Once the workspace is claimed, the default TTL should take effect.
|
||||
t.Run("DefaultTTLOnlyTriggersAfterClaim", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -1875,9 +1882,9 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(prebuildInstances))
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
// Set a template level TTL to trigger the autostop
|
||||
// Template level TTL can only be set if autostop is disabled for users
|
||||
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
// Set a template level TTL to trigger the autostop
|
||||
// Template level TTL can only be set if autostop is disabled for users
|
||||
ctr.AllowUserAutostop = ptr.Ref[bool](false)
|
||||
ctr.DefaultTTLMillis = ptr.Ref[int64](ttlTime.Milliseconds())
|
||||
})
|
||||
@@ -1890,43 +1897,48 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
runningPrebuilds := getRunningPrebuilds(t, ctx, db, int(prebuildInstances))
|
||||
require.Len(t, runningPrebuilds, int(prebuildInstances))
|
||||
|
||||
// Given: a running prebuilt workspace with a deadline, ready to be claimed
|
||||
// Given: a running prebuilt workspace, ready to be claimed
|
||||
prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID)
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition)
|
||||
require.NotZero(t, prebuild.LatestBuild.Deadline)
|
||||
// Prebuilt workspaces should have an empty Deadline and MaxDeadline
|
||||
// which is equivalent to 0001-01-01 00:00:00 +0000
|
||||
require.Zero(t, prebuild.LatestBuild.Deadline)
|
||||
require.Zero(t, prebuild.LatestBuild.MaxDeadline)
|
||||
|
||||
// When: the autobuild executor ticks *after* the deadline
|
||||
next := prebuild.LatestBuild.Deadline.Time.Add(time.Minute)
|
||||
clock.Set(next)
|
||||
// When: the autobuild executor ticks *after* the TTL time (10:00 AM UTC)
|
||||
next := clock.Now().Add(ttlTime).Add(time.Minute)
|
||||
clock.Set(next) // 10:01 AM UTC
|
||||
go func() {
|
||||
tickCh <- next
|
||||
}()
|
||||
|
||||
// Then: the prebuilt workspace should remain in a start transition
|
||||
prebuildStats := <-statsCh
|
||||
prebuildStats := testutil.RequireReceive(ctx, t, 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)
|
||||
require.Zero(t, prebuild.LatestBuild.Deadline)
|
||||
require.Zero(t, prebuild.LatestBuild.MaxDeadline)
|
||||
|
||||
// Given: a user claims the prebuilt workspace sometime later
|
||||
clock.Set(clock.Now().Add(ttlTime))
|
||||
clock.Set(clock.Now().Add(1 * time.Hour)) // 11:01 AM UTC
|
||||
workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID)
|
||||
require.Equal(t, prebuild.ID, workspace.ID)
|
||||
// Workspace deadline must be ttlTime from the time it is claimed
|
||||
// Workspace deadline must be ttlTime from the time it is claimed (1:01 PM UTC)
|
||||
require.True(t, workspace.LatestBuild.Deadline.Time.Equal(clock.Now().Add(ttlTime)))
|
||||
|
||||
// When: the autobuild executor ticks *after* the deadline
|
||||
// When: the autobuild executor ticks *after* the TTL time (1:01 PM UTC)
|
||||
next = workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||||
clock.Set(next)
|
||||
clock.Set(next) // 1:02 PM UTC
|
||||
go func() {
|
||||
tickCh <- next
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
// Then: the workspace should be stopped
|
||||
workspaceStats := <-statsCh
|
||||
workspaceStats := testutil.RequireReceive(ctx, t, statsCh)
|
||||
require.Len(t, workspaceStats.Errors, 0)
|
||||
require.Len(t, workspaceStats.Transitions, 1)
|
||||
require.Contains(t, workspaceStats.Transitions, workspace.ID)
|
||||
@@ -1941,158 +1953,125 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
t.Run("AutostopScheduleOnlyTriggersAfterClaim", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
isClaimedBeforeDeadline bool
|
||||
}{
|
||||
// If the prebuild is claimed before the scheduled deadline,
|
||||
// the claimed workspace should inherit and respect that same deadline.
|
||||
{
|
||||
name: "ClaimedBeforeDeadline_UsesSameDeadline",
|
||||
isClaimedBeforeDeadline: true,
|
||||
},
|
||||
// If the prebuild is claimed after the scheduled deadline,
|
||||
// the workspace should not stop immediately, but instead respect the next
|
||||
// valid scheduled deadline (the next day).
|
||||
{
|
||||
name: "ClaimedAfterDeadline_SchedulesForNextDay",
|
||||
isClaimedBeforeDeadline: false,
|
||||
},
|
||||
}
|
||||
// Set the clock to Monday, January 1st, 2024 at 8:00 AM UTC to keep the test deterministic
|
||||
clock := quartz.NewMock(t)
|
||||
clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC))
|
||||
|
||||
for _, tc := range cases {
|
||||
tc := tc
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Set the clock to Monday, January 1st, 2024 at 8:00 AM UTC to keep the test deterministic
|
||||
clock := quartz.NewMock(t)
|
||||
clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC))
|
||||
|
||||
// Setup
|
||||
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||
db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
|
||||
logger := testutil.Logger(t)
|
||||
tickCh := make(chan time.Time)
|
||||
statsCh := make(chan autobuild.Stats)
|
||||
notificationsNoop := notifications.NewNoopEnqueuer()
|
||||
client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pb,
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
Clock: clock,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(
|
||||
agplUserQuietHoursScheduleStore(),
|
||||
notificationsNoop,
|
||||
logger,
|
||||
clock,
|
||||
),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
},
|
||||
})
|
||||
|
||||
// Setup Prebuild reconciler
|
||||
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
|
||||
reconciler := prebuilds.NewStoreReconciler(
|
||||
db, pb, cache,
|
||||
codersdk.PrebuildsConfig{},
|
||||
// Setup
|
||||
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||
db, pb := dbtestutil.NewDB(t, dbtestutil.WithDumpOnFailure())
|
||||
logger := testutil.Logger(t)
|
||||
tickCh := make(chan time.Time)
|
||||
statsCh := make(chan autobuild.Stats)
|
||||
notificationsNoop := notifications.NewNoopEnqueuer()
|
||||
client, _, api, owner := coderdenttest.NewWithAPI(t, &coderdenttest.Options{
|
||||
Options: &coderdtest.Options{
|
||||
Database: db,
|
||||
Pubsub: pb,
|
||||
AutobuildTicker: tickCh,
|
||||
IncludeProvisionerDaemon: true,
|
||||
AutobuildStats: statsCh,
|
||||
Clock: clock,
|
||||
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(
|
||||
agplUserQuietHoursScheduleStore(),
|
||||
notificationsNoop,
|
||||
logger,
|
||||
clock,
|
||||
prometheus.NewRegistry(),
|
||||
notificationsNoop,
|
||||
api.AGPL.BuildUsageChecker,
|
||||
)
|
||||
var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(db)
|
||||
api.AGPL.PrebuildsClaimer.Store(&claimer)
|
||||
),
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{codersdk.FeatureAdvancedTemplateScheduling: 1},
|
||||
},
|
||||
})
|
||||
|
||||
// Setup user, template and template version with a preset with 1 prebuild instance
|
||||
prebuildInstances := int32(1)
|
||||
userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(prebuildInstances))
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
// Set a template level Autostop schedule to trigger the autostop daily
|
||||
ctr.AutostopRequirement = ptr.Ref[codersdk.TemplateAutostopRequirement](
|
||||
codersdk.TemplateAutostopRequirement{
|
||||
DaysOfWeek: []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"},
|
||||
Weeks: 1,
|
||||
})
|
||||
// Setup Prebuild reconciler
|
||||
cache := files.New(prometheus.NewRegistry(), &coderdtest.FakeAuthorizer{})
|
||||
reconciler := prebuilds.NewStoreReconciler(
|
||||
db, pb, cache,
|
||||
codersdk.PrebuildsConfig{},
|
||||
logger,
|
||||
clock,
|
||||
prometheus.NewRegistry(),
|
||||
notificationsNoop,
|
||||
api.AGPL.BuildUsageChecker,
|
||||
)
|
||||
var claimer agplprebuilds.Claimer = prebuilds.NewEnterpriseClaimer(db)
|
||||
api.AGPL.PrebuildsClaimer.Store(&claimer)
|
||||
|
||||
// Setup user, template and template version with a preset with 1 prebuild instance
|
||||
prebuildInstances := int32(1)
|
||||
userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(prebuildInstances))
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
// Set a template level Autostop schedule to trigger the autostop daily
|
||||
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
ctr.AutostopRequirement = ptr.Ref[codersdk.TemplateAutostopRequirement](
|
||||
codersdk.TemplateAutostopRequirement{
|
||||
DaysOfWeek: []string{"monday", "tuesday", "wednesday", "thursday", "friday", "saturday", "sunday"},
|
||||
Weeks: 1,
|
||||
})
|
||||
presets, err := client.TemplateVersionPresets(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, presets, 1)
|
||||
})
|
||||
presets, err := client.TemplateVersionPresets(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, presets, 1)
|
||||
|
||||
// Given: Reconciliation loop runs and starts prebuilt workspace
|
||||
runReconciliationLoop(t, ctx, db, reconciler, presets)
|
||||
runningPrebuilds := getRunningPrebuilds(t, ctx, db, int(prebuildInstances))
|
||||
require.Len(t, runningPrebuilds, int(prebuildInstances))
|
||||
// Given: Reconciliation loop runs and starts prebuilt workspace
|
||||
runReconciliationLoop(t, ctx, db, reconciler, presets)
|
||||
runningPrebuilds := getRunningPrebuilds(t, ctx, db, int(prebuildInstances))
|
||||
require.Len(t, runningPrebuilds, int(prebuildInstances))
|
||||
|
||||
// Given: a running prebuilt workspace with a deadline, ready to be claimed
|
||||
prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID)
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition)
|
||||
require.NotZero(t, prebuild.LatestBuild.Deadline)
|
||||
// Given: a running prebuilt workspace, ready to be claimed
|
||||
prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID)
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition)
|
||||
// Prebuilt workspaces should have an empty Deadline and MaxDeadline
|
||||
// which is equivalent to 0001-01-01 00:00:00 +0000
|
||||
require.Zero(t, prebuild.LatestBuild.Deadline)
|
||||
require.Zero(t, prebuild.LatestBuild.MaxDeadline)
|
||||
|
||||
next := clock.Now()
|
||||
if tc.isClaimedBeforeDeadline {
|
||||
// When: the autobuild executor ticks *before* the deadline:
|
||||
next = next.Add(time.Minute)
|
||||
} else {
|
||||
// When: the autobuild executor ticks *after* the deadline:
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
// When: the autobuild executor ticks *after* the deadline (2024-01-02 0:00 UTC)
|
||||
next := clock.Now().Truncate(24 * time.Hour).Add(24 * time.Hour).Add(time.Minute)
|
||||
clock.Set(next) // 2024-01-02 0:01 UTC
|
||||
go func() {
|
||||
tickCh <- next
|
||||
}()
|
||||
|
||||
clock.Set(next)
|
||||
go func() {
|
||||
tickCh <- next
|
||||
}()
|
||||
// Then: the prebuilt workspace should remain in a start transition
|
||||
prebuildStats := testutil.RequireReceive(ctx, t, 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)
|
||||
require.Zero(t, prebuild.LatestBuild.Deadline)
|
||||
require.Zero(t, prebuild.LatestBuild.MaxDeadline)
|
||||
|
||||
// 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
|
||||
workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID)
|
||||
require.Equal(t, prebuild.ID, workspace.ID)
|
||||
// Then: the claimed workspace should respect the next valid scheduled deadline (2024-01-03 0:00 UTC)
|
||||
require.True(t, workspace.LatestBuild.Deadline.Time.Equal(clock.Now().Truncate(24*time.Hour).Add(24*time.Hour)))
|
||||
|
||||
// Given: a user claims the prebuilt workspace
|
||||
workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID)
|
||||
require.Equal(t, prebuild.ID, workspace.ID)
|
||||
// When: the autobuild executor ticks *after* the deadline (2024-01-03 0:00 UTC)
|
||||
next = workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||||
clock.Set(next) // 2024-01-03 0:01 UTC
|
||||
go func() {
|
||||
tickCh <- next
|
||||
close(tickCh)
|
||||
}()
|
||||
|
||||
if tc.isClaimedBeforeDeadline {
|
||||
// Then: the claimed workspace should inherit and respect that same deadline.
|
||||
require.True(t, workspace.LatestBuild.Deadline.Time.Equal(prebuild.LatestBuild.Deadline.Time))
|
||||
} else {
|
||||
// Then: the claimed workspace should respect the next valid scheduled deadline (next day).
|
||||
require.True(t, workspace.LatestBuild.Deadline.Time.Equal(clock.Now().Truncate(24*time.Hour).Add(24*time.Hour)))
|
||||
}
|
||||
|
||||
// When: the autobuild executor ticks *after* the deadline:
|
||||
next = workspace.LatestBuild.Deadline.Time.Add(time.Minute)
|
||||
clock.Set(next)
|
||||
go func() {
|
||||
tickCh <- next
|
||||
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)
|
||||
})
|
||||
}
|
||||
// Then: the workspace should be stopped
|
||||
workspaceStats := testutil.RequireReceive(ctx, t, 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 follow the autostart schedule.
|
||||
// This test verifies that AutostartRequirement (autostart schedule) is ignored while the workspace is a prebuild.
|
||||
// After being claimed, the workspace should be started according to the autostart schedule.
|
||||
t.Run("AutostartScheduleOnlyTriggersAfterClaim", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -2146,8 +2125,11 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(prebuildInstances))
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
// Template-level autostart config only defines allowed days for workspaces to autostart
|
||||
// The actual autostart schedule is set at the workspace level
|
||||
sched, err := cron.Weekly("CRON_TZ=UTC 0 0 * * *")
|
||||
require.NoError(t, err)
|
||||
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
// Set a template level Autostart schedule to trigger the autostart daily
|
||||
ctr.AllowUserAutostart = ptr.Ref[bool](true)
|
||||
ctr.AutostartRequirement = &codersdk.TemplateAutostartRequirement{DaysOfWeek: codersdk.AllDaysOfWeek}
|
||||
})
|
||||
@@ -2160,14 +2142,11 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
runningPrebuilds := getRunningPrebuilds(t, ctx, db, int(prebuildInstances))
|
||||
require.Len(t, runningPrebuilds, int(prebuildInstances))
|
||||
|
||||
// Given: prebuilt workspace has autostart schedule daily at midnight
|
||||
// Given: a running prebuilt workspace
|
||||
prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID)
|
||||
sched, err := cron.Weekly("CRON_TZ=UTC 0 0 * * *")
|
||||
require.NoError(t, err)
|
||||
err = client.UpdateWorkspaceAutostart(ctx, prebuild.ID, codersdk.UpdateWorkspaceAutostartRequest{
|
||||
Schedule: ptr.Ref(sched.String()),
|
||||
})
|
||||
require.NoError(t, err)
|
||||
// Prebuilt workspaces should have an empty Autostart Schedule
|
||||
require.Nil(t, prebuild.AutostartSchedule)
|
||||
require.Nil(t, prebuild.NextStartAt)
|
||||
|
||||
// Given: prebuilt workspace is stopped
|
||||
prebuild = coderdtest.MustTransitionWorkspace(t, client, prebuild.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||||
@@ -2181,32 +2160,32 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
}()
|
||||
|
||||
// Then: the prebuilt workspace should remain in a stop transition
|
||||
prebuildStats := <-statsCh
|
||||
prebuildStats := testutil.RequireReceive(ctx, t, 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)
|
||||
require.Nil(t, prebuild.AutostartSchedule)
|
||||
require.Nil(t, prebuild.NextStartAt)
|
||||
|
||||
// Given: a prebuilt workspace that is running and ready to be claimed
|
||||
prebuild = coderdtest.MustTransitionWorkspace(t, client, prebuild.ID, codersdk.WorkspaceTransitionStop, codersdk.WorkspaceTransitionStart)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, prebuild.LatestBuild.ID)
|
||||
|
||||
// Make sure the workspace's agent is again ready
|
||||
getRunningPrebuilds(t, ctx, db, int(prebuildInstances))
|
||||
|
||||
// Given: a user claims the prebuilt workspace
|
||||
workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID)
|
||||
// Given: a user claims the prebuilt workspace with an Autostart schedule request
|
||||
workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID, sched.String())
|
||||
require.Equal(t, prebuild.ID, workspace.ID)
|
||||
// Then: newly claimed workspace's AutostartSchedule and NextStartAt should be set
|
||||
require.NotNil(t, workspace.AutostartSchedule)
|
||||
require.NotNil(t, workspace.NextStartAt)
|
||||
|
||||
// Given: workspace is stopped
|
||||
workspace = coderdtest.MustTransitionWorkspace(t, client, workspace.ID, codersdk.WorkspaceTransitionStart, codersdk.WorkspaceTransitionStop)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
|
||||
// Then: the claimed workspace should inherit and respect that same NextStartAt
|
||||
require.True(t, workspace.NextStartAt.Equal(*prebuild.NextStartAt))
|
||||
|
||||
// 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
|
||||
@@ -2215,17 +2194,19 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
}()
|
||||
|
||||
// Then: the workspace should have a NextStartAt equal to the next autostart schedule
|
||||
workspaceStats := <-statsCh
|
||||
workspaceStats := testutil.RequireReceive(ctx, t, statsCh)
|
||||
require.Len(t, workspaceStats.Errors, 0)
|
||||
require.Len(t, workspaceStats.Transitions, 1)
|
||||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||||
require.NotNil(t, workspace.AutostartSchedule)
|
||||
require.NotNil(t, workspace.NextStartAt)
|
||||
require.Equal(t, sched.Next(clock.Now()), workspace.NextStartAt.UTC())
|
||||
})
|
||||
|
||||
// Prebuild workspaces should not transition to dormant when the inactive TTL is reached.
|
||||
// This test verifies that TimeTilDormantMillis is ignored while the workspace is a prebuild.
|
||||
// After being claimed, the workspace should become dormant according to the configured inactivity period.
|
||||
// Prebuild workspaces should not transition to dormant or be deleted due to inactivity.
|
||||
// This test verifies that both TimeTilDormantMillis and TimeTilDormantAutoDeleteMillis
|
||||
// are ignored while the workspace is a prebuild. After the workspace is claimed,
|
||||
// it should respect these inactivity thresholds accordingly.
|
||||
t.Run("DormantOnlyAfterClaimed", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@@ -2276,13 +2257,15 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
|
||||
// Setup user, template and template version with a preset with 1 prebuild instance
|
||||
prebuildInstances := int32(1)
|
||||
inactiveTTL := 2 * time.Hour
|
||||
dormantTTL := 2 * time.Hour
|
||||
deletionTTL := 2 * time.Hour
|
||||
userClient, user := coderdtest.CreateAnotherUser(t, client, owner.OrganizationID, rbac.RoleMember())
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithAgentAndPresetsWithPrebuilds(prebuildInstances))
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
// Set a template level dormant TTL to trigger dormancy
|
||||
coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
// Set a template level inactive TTL to trigger dormancy
|
||||
ctr.TimeTilDormantMillis = ptr.Ref[int64](inactiveTTL.Milliseconds())
|
||||
ctr.TimeTilDormantMillis = ptr.Ref[int64](dormantTTL.Milliseconds())
|
||||
ctr.TimeTilDormantAutoDeleteMillis = ptr.Ref[int64](deletionTTL.Milliseconds())
|
||||
})
|
||||
presets, err := client.TemplateVersionPresets(ctx, version.ID)
|
||||
require.NoError(t, err)
|
||||
@@ -2296,41 +2279,68 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
// Given: a running prebuilt workspace, ready to be claimed
|
||||
prebuild := coderdtest.MustWorkspace(t, client, runningPrebuilds[0].ID)
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition)
|
||||
require.Nil(t, prebuild.DormantAt)
|
||||
require.Nil(t, prebuild.DeletingAt)
|
||||
|
||||
// When: the autobuild executor ticks *after* the inactive TTL
|
||||
// When: the autobuild executor ticks *after* the dormant TTL (10:00 AM UTC)
|
||||
next := clock.Now().Add(dormantTTL).Add(time.Minute)
|
||||
clock.Set(next) // 10:01 AM UTC
|
||||
go func() {
|
||||
tickCh <- prebuild.LastUsedAt.Add(inactiveTTL).Add(time.Minute)
|
||||
tickCh <- next
|
||||
}()
|
||||
|
||||
// Then: the prebuilt workspace should remain in a start transition
|
||||
prebuildStats := <-statsCh
|
||||
prebuildStats := testutil.RequireReceive(ctx, t, 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)
|
||||
require.Nil(t, prebuild.DormantAt)
|
||||
require.Nil(t, prebuild.DeletingAt)
|
||||
|
||||
// Given: a user claims the prebuilt workspace sometime later
|
||||
clock.Set(clock.Now().Add(inactiveTTL))
|
||||
clock.Set(clock.Now().Add(1 * time.Hour)) // 11:01 AM UTC
|
||||
workspace := claimPrebuild(t, ctx, client, userClient, user.Username, version, presets[0].ID)
|
||||
require.Equal(t, prebuild.ID, workspace.ID)
|
||||
require.Nil(t, prebuild.DormantAt)
|
||||
// Then: the claimed workspace should have DormantAt and DeletingAt unset (nil),
|
||||
// and LastUsedAt updated
|
||||
require.Nil(t, workspace.DormantAt)
|
||||
require.Nil(t, workspace.DeletingAt)
|
||||
require.True(t, workspace.LastUsedAt.After(prebuild.LastUsedAt))
|
||||
|
||||
// When: the autobuild executor ticks *after* the inactive TTL
|
||||
// When: the autobuild executor ticks *after* the dormant TTL (1:01 PM UTC)
|
||||
next = clock.Now().Add(dormantTTL).Add(time.Minute)
|
||||
clock.Set(next) // 1:02 PM UTC
|
||||
go func() {
|
||||
tickCh <- prebuild.LastUsedAt.Add(inactiveTTL).Add(time.Minute)
|
||||
close(tickCh)
|
||||
tickCh <- next
|
||||
}()
|
||||
|
||||
// Then: the workspace should transition to stopped state for breaching failure TTL
|
||||
workspaceStats := <-statsCh
|
||||
// Then: the workspace should transition to stopped state for breaching dormant TTL
|
||||
workspaceStats := testutil.RequireReceive(ctx, t, 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)
|
||||
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
|
||||
workspace = coderdtest.MustWorkspace(t, client, workspace.ID)
|
||||
require.Equal(t, codersdk.BuildReasonDormancy, workspace.LatestBuild.Reason)
|
||||
require.Equal(t, codersdk.WorkspaceStatusStopped, workspace.LatestBuild.Status)
|
||||
require.NotNil(t, workspace.DormantAt)
|
||||
require.NotNil(t, workspace.DeletingAt)
|
||||
|
||||
// When: the autobuild executor ticks *after* the deletion TTL
|
||||
go func() {
|
||||
tickCh <- workspace.DeletingAt.Add(time.Minute)
|
||||
}()
|
||||
|
||||
// Then: the workspace should be deleted
|
||||
dormantWorkspaceStats := testutil.RequireReceive(ctx, t, statsCh)
|
||||
require.Len(t, dormantWorkspaceStats.Errors, 0)
|
||||
require.Len(t, dormantWorkspaceStats.Transitions, 1)
|
||||
require.Contains(t, dormantWorkspaceStats.Transitions, workspace.ID)
|
||||
require.Equal(t, database.WorkspaceTransitionDelete, dormantWorkspaceStats.Transitions[workspace.ID])
|
||||
})
|
||||
|
||||
// Prebuild workspaces should not be deleted when the failure TTL is reached.
|
||||
@@ -2390,8 +2400,8 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
failureTTL := 2 * time.Hour
|
||||
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, templateWithFailedResponseAndPresetsWithPrebuilds(prebuildInstances))
|
||||
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
|
||||
// Set a template level Failure TTL to trigger workspace deletion
|
||||
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
|
||||
// Set a template level Failure TTL to trigger workspace deletion
|
||||
ctr.FailureTTLMillis = ptr.Ref[int64](failureTTL.Milliseconds())
|
||||
})
|
||||
presets, err := client.TemplateVersionPresets(ctx, version.ID)
|
||||
@@ -2400,7 +2410,6 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
|
||||
// Given: reconciliation loop runs and starts prebuilt workspace in failed state
|
||||
runReconciliationLoop(t, ctx, db, reconciler, presets)
|
||||
|
||||
var failedWorkspaceBuilds []database.GetFailedWorkspaceBuildsByTemplateIDRow
|
||||
require.Eventually(t, func() bool {
|
||||
rows, err := db.GetFailedWorkspaceBuildsByTemplateID(ctx, database.GetFailedWorkspaceBuildsByTemplateIDParams{
|
||||
@@ -2427,7 +2436,7 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
}()
|
||||
|
||||
// Then: the prebuilt workspace should remain in a start transition
|
||||
prebuildStats := <-statsCh
|
||||
prebuildStats := testutil.RequireReceive(ctx, t, statsCh)
|
||||
require.Len(t, prebuildStats.Errors, 0)
|
||||
require.Len(t, prebuildStats.Transitions, 0)
|
||||
require.Equal(t, codersdk.WorkspaceTransitionStart, prebuild.LatestBuild.Transition)
|
||||
@@ -2437,50 +2446,46 @@ func TestExecutorPrebuilds(t *testing.T) {
|
||||
}
|
||||
|
||||
func templateWithAgentAndPresetsWithPrebuilds(desiredInstances int32) *echo.Responses {
|
||||
agent := &proto.Agent{
|
||||
Name: "smith",
|
||||
OperatingSystem: "linux",
|
||||
Architecture: "i386",
|
||||
}
|
||||
|
||||
resource := func(withAgent bool) *proto.Resource {
|
||||
r := &proto.Resource{Type: "compute", Name: "main"}
|
||||
if withAgent {
|
||||
r.Agents = []*proto.Agent{agent}
|
||||
}
|
||||
return r
|
||||
}
|
||||
|
||||
applyResponse := func(withAgent bool) *proto.Response {
|
||||
return &proto.Response{
|
||||
Type: &proto.Response_Apply{
|
||||
Apply: &proto.ApplyComplete{
|
||||
Resources: []*proto.Resource{resource(withAgent)},
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return &echo.Responses{
|
||||
Parse: echo.ParseComplete,
|
||||
ProvisionPlan: []*proto.Response{
|
||||
{
|
||||
Type: &proto.Response_Plan{
|
||||
Plan: &proto.PlanComplete{
|
||||
Presets: []*proto.Preset{
|
||||
{
|
||||
Name: "preset-test",
|
||||
Parameters: []*proto.PresetParameter{
|
||||
{
|
||||
Name: "k1",
|
||||
Value: "v1",
|
||||
},
|
||||
},
|
||||
Prebuild: &proto.Prebuild{
|
||||
Instances: desiredInstances,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ProvisionApply: []*proto.Response{
|
||||
{
|
||||
Type: &proto.Response_Apply{
|
||||
Apply: &proto.ApplyComplete{
|
||||
Resources: []*proto.Resource{
|
||||
{
|
||||
Type: "compute",
|
||||
Name: "main",
|
||||
Agents: []*proto.Agent{
|
||||
{
|
||||
Name: "smith",
|
||||
OperatingSystem: "linux",
|
||||
Architecture: "i386",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
ProvisionPlan: []*proto.Response{{
|
||||
Type: &proto.Response_Plan{
|
||||
Plan: &proto.PlanComplete{
|
||||
Presets: []*proto.Preset{{
|
||||
Name: "preset-test",
|
||||
Parameters: []*proto.PresetParameter{{Name: "k1", Value: "v1"}},
|
||||
Prebuild: &proto.Prebuild{Instances: desiredInstances},
|
||||
}},
|
||||
},
|
||||
},
|
||||
}},
|
||||
ProvisionApplyMap: map[proto.WorkspaceTransition][]*proto.Response{
|
||||
proto.WorkspaceTransition_START: {applyResponse(true)},
|
||||
proto.WorkspaceTransition_STOP: {applyResponse(false)},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user