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)
+18 -4
View File
@@ -1436,11 +1436,25 @@ func UserSecret(t testing.TB, db database.Store, seed database.UserSecret) datab
return userSecret
}
func ClaimPrebuild(t testing.TB, db database.Store, newUserID uuid.UUID, newName string, presetID uuid.UUID) database.ClaimPrebuiltWorkspaceRow {
func ClaimPrebuild(
t testing.TB,
db database.Store,
now time.Time,
newUserID uuid.UUID,
newName string,
presetID uuid.UUID,
autostartSchedule sql.NullString,
nextStartAt sql.NullTime,
ttl sql.NullInt64,
) database.ClaimPrebuiltWorkspaceRow {
claimedWorkspace, err := db.ClaimPrebuiltWorkspace(genCtx, database.ClaimPrebuiltWorkspaceParams{
NewUserID: newUserID,
NewName: newName,
PresetID: presetID,
NewUserID: newUserID,
NewName: newName,
Now: now,
PresetID: presetID,
AutostartSchedule: autostartSchedule,
NextStartAt: nextStartAt,
WorkspaceTtl: ttl,
})
require.NoError(t, err, "claim prebuilt workspace")
+59 -10
View File
@@ -7122,7 +7122,20 @@ const claimPrebuiltWorkspace = `-- name: ClaimPrebuiltWorkspace :one
UPDATE workspaces w
SET owner_id = $1::uuid,
name = $2::text,
updated_at = NOW()
updated_at = $3::timestamptz,
-- Update autostart_schedule, next_start_at and ttl according to template and workspace-level
-- configurations, allowing the workspace to be managed by the lifecycle executor as expected.
autostart_schedule = $4,
next_start_at = $5,
ttl = $6,
-- Update last_used_at during claim to ensure the claimed workspace is treated as recently used.
-- This avoids unintended dormancy caused by prebuilds having stale usage timestamps.
last_used_at = $3::timestamptz,
-- Clear dormant and deletion timestamps as a safeguard to ensure a clean lifecycle state after claim.
-- These fields should not be set on prebuilds, but we defensively reset them here to prevent
-- accidental dormancy or deletion by the lifecycle executor.
dormant_at = NULL,
deleting_at = NULL
WHERE w.id IN (
SELECT p.id
FROM workspace_prebuilds p
@@ -7133,7 +7146,7 @@ WHERE w.id IN (
-- The prebuilds system should never try to claim a prebuild for an inactive template version.
-- Nevertheless, this filter is here as a defensive measure:
AND b.template_version_id = t.active_version_id
AND p.current_preset_id = $3::uuid
AND p.current_preset_id = $7::uuid
AND p.ready
AND NOT t.deleted
LIMIT 1 FOR UPDATE OF p SKIP LOCKED -- Ensure that a concurrent request will not select the same prebuild.
@@ -7142,9 +7155,13 @@ RETURNING w.id, w.name
`
type ClaimPrebuiltWorkspaceParams struct {
NewUserID uuid.UUID `db:"new_user_id" json:"new_user_id"`
NewName string `db:"new_name" json:"new_name"`
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
NewUserID uuid.UUID `db:"new_user_id" json:"new_user_id"`
NewName string `db:"new_name" json:"new_name"`
Now time.Time `db:"now" json:"now"`
AutostartSchedule sql.NullString `db:"autostart_schedule" json:"autostart_schedule"`
NextStartAt sql.NullTime `db:"next_start_at" json:"next_start_at"`
WorkspaceTtl sql.NullInt64 `db:"workspace_ttl" json:"workspace_ttl"`
PresetID uuid.UUID `db:"preset_id" json:"preset_id"`
}
type ClaimPrebuiltWorkspaceRow struct {
@@ -7153,7 +7170,15 @@ type ClaimPrebuiltWorkspaceRow struct {
}
func (q *sqlQuerier) ClaimPrebuiltWorkspace(ctx context.Context, arg ClaimPrebuiltWorkspaceParams) (ClaimPrebuiltWorkspaceRow, error) {
row := q.db.QueryRowContext(ctx, claimPrebuiltWorkspace, arg.NewUserID, arg.NewName, arg.PresetID)
row := q.db.QueryRowContext(ctx, claimPrebuiltWorkspace,
arg.NewUserID,
arg.NewName,
arg.Now,
arg.AutostartSchedule,
arg.NextStartAt,
arg.WorkspaceTtl,
arg.PresetID,
)
var i ClaimPrebuiltWorkspaceRow
err := row.Scan(&i.ID, &i.Name)
return i, err
@@ -19180,7 +19205,15 @@ SET
deadline = $1::timestamptz,
max_deadline = $2::timestamptz,
updated_at = $3::timestamptz
WHERE id = $4::uuid
FROM
workspaces
WHERE
workspace_builds.id = $4::uuid
AND workspace_builds.workspace_id = workspaces.id
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- deadline and max_deadline
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type UpdateWorkspaceBuildDeadlineByIDParams struct {
@@ -21135,6 +21168,10 @@ SET
next_start_at = $3
WHERE
id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- autostart_schedule and next_start_at
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type UpdateWorkspaceAutostartParams struct {
@@ -21191,6 +21228,10 @@ FROM
WHERE
workspaces.id = $1
AND templates.id = workspaces.template_id
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- dormant_at and deleting_at
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
RETURNING
workspaces.id, workspaces.created_at, workspaces.updated_at, workspaces.owner_id, workspaces.organization_id, workspaces.template_id, workspaces.deleted, workspaces.name, workspaces.autostart_schedule, workspaces.ttl, workspaces.last_used_at, workspaces.dormant_at, workspaces.deleting_at, workspaces.automatic_updates, workspaces.favorite, workspaces.next_start_at, workspaces.group_acl, workspaces.user_acl
`
@@ -21252,6 +21293,10 @@ SET
next_start_at = $2
WHERE
id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- next_start_at
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type UpdateWorkspaceNextStartAtParams struct {
@@ -21271,6 +21316,10 @@ SET
ttl = $2
WHERE
id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- ttl
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
`
type UpdateWorkspaceTTLParams struct {
@@ -21349,11 +21398,11 @@ func (q *sqlQuerier) UpdateWorkspacesDormantDeletingAtByTemplateID(ctx context.C
const updateWorkspacesTTLByTemplateID = `-- name: UpdateWorkspacesTTLByTemplateID :exec
UPDATE
workspaces
workspaces
SET
ttl = $2
ttl = $2
WHERE
template_id = $1
template_id = $1
`
type UpdateWorkspacesTTLByTemplateIDParams struct {
+14 -1
View File
@@ -2,7 +2,20 @@
UPDATE workspaces w
SET owner_id = @new_user_id::uuid,
name = @new_name::text,
updated_at = NOW()
updated_at = @now::timestamptz,
-- Update autostart_schedule, next_start_at and ttl according to template and workspace-level
-- configurations, allowing the workspace to be managed by the lifecycle executor as expected.
autostart_schedule = @autostart_schedule,
next_start_at = @next_start_at,
ttl = @workspace_ttl,
-- Update last_used_at during claim to ensure the claimed workspace is treated as recently used.
-- This avoids unintended dormancy caused by prebuilds having stale usage timestamps.
last_used_at = @now::timestamptz,
-- Clear dormant and deletion timestamps as a safeguard to ensure a clean lifecycle state after claim.
-- These fields should not be set on prebuilds, but we defensively reset them here to prevent
-- accidental dormancy or deletion by the lifecycle executor.
dormant_at = NULL,
deleting_at = NULL
WHERE w.id IN (
SELECT p.id
FROM workspace_prebuilds p
+9 -1
View File
@@ -127,7 +127,15 @@ SET
deadline = @deadline::timestamptz,
max_deadline = @max_deadline::timestamptz,
updated_at = @updated_at::timestamptz
WHERE id = @id::uuid;
FROM
workspaces
WHERE
workspace_builds.id = @id::uuid
AND workspace_builds.workspace_id = workspaces.id
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- deadline and max_deadline
AND workspaces.owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID;
-- name: UpdateWorkspaceBuildProvisionerStateByID :exec
UPDATE
+22 -6
View File
@@ -518,7 +518,11 @@ SET
autostart_schedule = $2,
next_start_at = $3
WHERE
id = $1;
id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- autostart_schedule and next_start_at
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID;
-- name: UpdateWorkspaceNextStartAt :exec
UPDATE
@@ -526,7 +530,11 @@ UPDATE
SET
next_start_at = $2
WHERE
id = $1;
id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- next_start_at
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID;
-- name: BatchUpdateWorkspaceNextStartAt :exec
UPDATE
@@ -550,15 +558,19 @@ UPDATE
SET
ttl = $2
WHERE
id = $1;
id = $1
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- ttl
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID;
-- name: UpdateWorkspacesTTLByTemplateID :exec
UPDATE
workspaces
workspaces
SET
ttl = $2
ttl = $2
WHERE
template_id = $1;
template_id = $1;
-- name: UpdateWorkspaceLastUsedAt :exec
UPDATE
@@ -791,6 +803,10 @@ FROM
WHERE
workspaces.id = $1
AND templates.id = workspaces.template_id
-- Prebuilt workspaces (identified by having the prebuilds system user as owner_id)
-- are managed by the reconciliation loop, not the lifecycle executor which handles
-- dormant_at and deleting_at
AND owner_id != 'c42fdf75-3097-471c-8c33-fb52454d81c0'::UUID
RETURNING
workspaces.*;
+12 -1
View File
@@ -2,6 +2,8 @@ package prebuilds
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
"golang.org/x/xerrors"
@@ -54,6 +56,15 @@ type StateSnapshotter interface {
}
type Claimer interface {
Claim(ctx context.Context, userID uuid.UUID, name string, presetID uuid.UUID) (*uuid.UUID, error)
Claim(
ctx context.Context,
now time.Time,
userID uuid.UUID,
name string,
presetID uuid.UUID,
autostartSchedule sql.NullString,
nextStartAt sql.NullTime,
ttl sql.NullInt64,
) (*uuid.UUID, error)
Initiator() uuid.UUID
}
+3 -1
View File
@@ -2,6 +2,8 @@ package prebuilds
import (
"context"
"database/sql"
"time"
"github.com/google/uuid"
@@ -28,7 +30,7 @@ var DefaultReconciler ReconciliationOrchestrator = NoopReconciler{}
type NoopClaimer struct{}
func (NoopClaimer) Claim(context.Context, uuid.UUID, string, uuid.UUID) (*uuid.UUID, error) {
func (NoopClaimer) Claim(context.Context, time.Time, uuid.UUID, string, uuid.UUID, sql.NullString, sql.NullTime, sql.NullInt64) (*uuid.UUID, error) {
// Not entitled to claim prebuilds in AGPL version.
return nil, ErrAGPLDoesNotSupportPrebuiltWorkspaces
}
+44 -28
View File
@@ -1183,11 +1183,18 @@ func (s *server) FailJob(ctx context.Context, failJob *proto.FailedJob) (*proto.
if err != nil {
return xerrors.Errorf("update workspace build state: %w", err)
}
deadline := build.Deadline
maxDeadline := build.MaxDeadline
if workspace.IsPrebuild() {
deadline = time.Time{}
maxDeadline = time.Time{}
}
err = db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
ID: input.WorkspaceBuildID,
UpdatedAt: s.timeNow(),
Deadline: build.Deadline,
MaxDeadline: build.MaxDeadline,
Deadline: deadline,
MaxDeadline: maxDeadline,
})
if err != nil {
return xerrors.Errorf("update workspace build deadline: %w", err)
@@ -1860,38 +1867,47 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
return getWorkspaceError
}
templateScheduleStore := *s.TemplateScheduleStore.Load()
// Prebuilt workspaces must not have Deadline or MaxDeadline set,
// as they are managed by the prebuild reconciliation loop, not the lifecycle executor
deadline := time.Time{}
maxDeadline := time.Time{}
autoStop, err := schedule.CalculateAutostop(ctx, schedule.CalculateAutostopParams{
Database: db,
TemplateScheduleStore: templateScheduleStore,
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
// `now` is used below to set the build completion time.
WorkspaceBuildCompletedAt: now,
Workspace: workspace.WorkspaceTable(),
// Allowed to be the empty string.
WorkspaceAutostart: workspace.AutostartSchedule.String,
})
if err != nil {
return xerrors.Errorf("calculate auto stop: %w", err)
}
if !workspace.IsPrebuild() {
templateScheduleStore := *s.TemplateScheduleStore.Load()
if workspace.AutostartSchedule.Valid {
templateScheduleOptions, err := templateScheduleStore.Get(ctx, db, workspace.TemplateID)
autoStop, err := schedule.CalculateAutostop(ctx, schedule.CalculateAutostopParams{
Database: db,
TemplateScheduleStore: templateScheduleStore,
UserQuietHoursScheduleStore: *s.UserQuietHoursScheduleStore.Load(),
// `now` is used below to set the build completion time.
WorkspaceBuildCompletedAt: now,
Workspace: workspace.WorkspaceTable(),
// Allowed to be the empty string.
WorkspaceAutostart: workspace.AutostartSchedule.String,
})
if err != nil {
return xerrors.Errorf("get template schedule options: %w", err)
return xerrors.Errorf("calculate auto stop: %w", err)
}
nextStartAt, err := schedule.NextAllowedAutostart(now, workspace.AutostartSchedule.String, templateScheduleOptions)
if err == nil {
err = db.UpdateWorkspaceNextStartAt(ctx, database.UpdateWorkspaceNextStartAtParams{
ID: workspace.ID,
NextStartAt: sql.NullTime{Valid: true, Time: nextStartAt.UTC()},
})
if workspace.AutostartSchedule.Valid {
templateScheduleOptions, err := templateScheduleStore.Get(ctx, db, workspace.TemplateID)
if err != nil {
return xerrors.Errorf("update workspace next start at: %w", err)
return xerrors.Errorf("get template schedule options: %w", err)
}
nextStartAt, err := schedule.NextAllowedAutostart(now, workspace.AutostartSchedule.String, templateScheduleOptions)
if err == nil {
err = db.UpdateWorkspaceNextStartAt(ctx, database.UpdateWorkspaceNextStartAtParams{
ID: workspace.ID,
NextStartAt: sql.NullTime{Valid: true, Time: nextStartAt.UTC()},
})
if err != nil {
return xerrors.Errorf("update workspace next start at: %w", err)
}
}
}
deadline = autoStop.Deadline
maxDeadline = autoStop.MaxDeadline
}
err = db.UpdateProvisionerJobWithCompleteByID(ctx, database.UpdateProvisionerJobWithCompleteByIDParams{
@@ -1917,8 +1933,8 @@ func (s *server) completeWorkspaceBuildJob(ctx context.Context, job database.Pro
}
err = db.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
ID: workspaceBuild.ID,
Deadline: autoStop.Deadline,
MaxDeadline: autoStop.MaxDeadline,
Deadline: deadline,
MaxDeadline: maxDeadline,
UpdatedAt: now,
})
if err != nil {
+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)