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:
Susana Ferreira
2025-08-13 12:45:46 +01:00
committed by GitHub
parent f17ab92798
commit 8567ecbe52
14 changed files with 479 additions and 297 deletions
+26 -9
View File
@@ -1251,7 +1251,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)
@@ -1259,7 +1259,15 @@ func TestExecutorPrebuilds(t *testing.T) {
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)
dbWorkspace := dbgen.ClaimPrebuild(
t, db,
clock.Now(),
user.ID,
"claimedWorkspace-autostop",
preset.ID,
sql.NullString{},
sql.NullTime{},
sql.NullInt64{})
workspace := coderdtest.MustWorkspace(t, client, dbWorkspace.ID)
// When: the autobuild executor ticks *after* the deadline:
@@ -1269,7 +1277,7 @@ func TestExecutorPrebuilds(t *testing.T) {
}()
// 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)
@@ -1336,7 +1344,7 @@ 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)
@@ -1353,7 +1361,15 @@ func TestExecutorPrebuilds(t *testing.T) {
database.WorkspaceTransitionStart)
// Given: a user claims the prebuilt workspace
dbWorkspace := dbgen.ClaimPrebuild(t, db, user.ID, "claimedWorkspace-autostart", preset.ID)
dbWorkspace := dbgen.ClaimPrebuild(
t, db,
clock.Now(),
user.ID,
"claimedWorkspace-autostart",
preset.ID,
autostartSched,
sql.NullTime{},
sql.NullInt64{})
workspace := coderdtest.MustWorkspace(t, client, dbWorkspace.ID)
// Given: the prebuilt workspace goes to a stop status
@@ -1374,7 +1390,7 @@ func TestExecutorPrebuilds(t *testing.T) {
}()
// Then: the workspace should eventually be started
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)
@@ -1486,8 +1502,8 @@ func setupTestDBWorkspaceBuild(
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},
StartedAt: sql.NullTime{Time: clock.Now().Add(time.Hour), Valid: true},
ReadyAt: sql.NullTime{Time: clock.Now().Add(-1 * time.Hour), Valid: true},
APIKeyScope: database.AgentKeyScopeEnumAll,
})
@@ -1524,8 +1540,9 @@ func setupTestDBPrebuiltWorkspace(
OrganizationID: orgID,
OwnerID: database.PrebuildsSystemUserID,
Deleted: false,
CreatedAt: time.Now().Add(-time.Hour * 2),
CreatedAt: clock.Now().Add(-time.Hour * 2),
AutostartSchedule: options.AutostartSchedule,
LastUsedAt: clock.Now(),
})
setupTestDBWorkspaceBuild(ctx, t, clock, db, ps, orgID, workspace.ID, templateVersionID, presetID, buildTransition)