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
+22 -5
View File
@@ -635,10 +635,17 @@ func createWorkspace(
claimedWorkspace *database.Workspace
)
// Use injected Clock to allow time mocking in tests
now := api.Clock.Now()
// If a template preset was chosen, try claim a prebuilt workspace.
if req.TemplateVersionPresetID != uuid.Nil {
// Try and claim an eligible prebuild, if available.
claimedWorkspace, err = claimPrebuild(ctx, prebuildsClaimer, db, api.Logger, req, owner)
// On successful claim, initialize all lifecycle fields from template and workspace-level config
// so the newly claimed workspace is properly managed by the lifecycle executor.
claimedWorkspace, err = claimPrebuild(
ctx, prebuildsClaimer, db, api.Logger, now, req, owner,
dbAutostartSchedule, nextStartAt, dbTTL)
// If claiming fails with an expected error (no claimable prebuilds or AGPL does not support prebuilds),
// we fall back to creating a new workspace. Otherwise, propagate the unexpected error.
if err != nil {
@@ -666,7 +673,6 @@ func createWorkspace(
// No prebuild found; regular flow.
if claimedWorkspace == nil {
now := dbtime.Now()
// Workspaces are created without any versions.
minimumWorkspace, err := db.InsertWorkspace(ctx, database.InsertWorkspaceParams{
ID: uuid.New(),
@@ -681,7 +687,7 @@ func createWorkspace(
Ttl: dbTTL,
// The workspaces page will sort by last used at, and it's useful to
// have the newly created workspace at the top of the list!
LastUsedAt: dbtime.Now(),
LastUsedAt: now,
AutomaticUpdates: dbAU,
})
if err != nil {
@@ -872,8 +878,19 @@ func requestTemplate(ctx context.Context, rw http.ResponseWriter, req codersdk.C
return template, true
}
func claimPrebuild(ctx context.Context, claimer prebuilds.Claimer, db database.Store, logger slog.Logger, req codersdk.CreateWorkspaceRequest, owner workspaceOwner) (*database.Workspace, error) {
claimedID, err := claimer.Claim(ctx, owner.ID, req.Name, req.TemplateVersionPresetID)
func claimPrebuild(
ctx context.Context,
claimer prebuilds.Claimer,
db database.Store,
logger slog.Logger,
now time.Time,
req codersdk.CreateWorkspaceRequest,
owner workspaceOwner,
autostartSchedule sql.NullString,
nextStartAt sql.NullTime,
ttl sql.NullInt64,
) (*database.Workspace, error) {
claimedID, err := claimer.Claim(ctx, now, owner.ID, req.Name, req.TemplateVersionPresetID, autostartSchedule, nextStartAt, ttl)
if err != nil {
// TODO: enhance this by clarifying whether this *specific* prebuild failed or whether there are none to claim.
return nil, xerrors.Errorf("claim prebuild: %w", err)