mirror of
https://github.com/coder/coder.git
synced 2026-09-21 20:51:01 +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:
@@ -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)
|
||||
|
||||
|
||||
@@ -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")
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"golang.org/x/xerrors"
|
||||
@@ -24,14 +25,22 @@ func NewEnterpriseClaimer(store database.Store) *EnterpriseClaimer {
|
||||
|
||||
func (c EnterpriseClaimer) 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) {
|
||||
result, err := c.store.ClaimPrebuiltWorkspace(ctx, database.ClaimPrebuiltWorkspaceParams{
|
||||
NewUserID: userID,
|
||||
NewName: name,
|
||||
PresetID: presetID,
|
||||
NewUserID: userID,
|
||||
NewName: name,
|
||||
Now: now,
|
||||
PresetID: presetID,
|
||||
AutostartSchedule: autostartSchedule,
|
||||
NextStartAt: nextStartAt,
|
||||
WorkspaceTtl: ttl,
|
||||
})
|
||||
if err != nil {
|
||||
switch {
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/xerrors"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/database/dbtime"
|
||||
|
||||
"github.com/coder/coder/v2/coderd/files"
|
||||
"github.com/coder/quartz"
|
||||
|
||||
@@ -132,7 +134,9 @@ func TestClaimPrebuild(t *testing.T) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Setup.
|
||||
// Setup
|
||||
clock := quartz.NewMock(t)
|
||||
clock.Set(dbtime.Now())
|
||||
ctx := testutil.Context(t, testutil.WaitSuperLong)
|
||||
db, pubsub := dbtestutil.NewDB(t)
|
||||
|
||||
@@ -144,6 +148,7 @@ func TestClaimPrebuild(t *testing.T) {
|
||||
Options: &coderdtest.Options{
|
||||
Database: spy,
|
||||
Pubsub: pubsub,
|
||||
Clock: clock,
|
||||
},
|
||||
LicenseOptions: &coderdenttest.LicenseOptions{
|
||||
Features: license.Features{
|
||||
@@ -238,6 +243,7 @@ func TestClaimPrebuild(t *testing.T) {
|
||||
// When: a user creates a new workspace with a preset for which prebuilds are configured.
|
||||
workspaceName := strings.ReplaceAll(testutil.GetRandomName(t), "_", "-")
|
||||
params := database.ClaimPrebuiltWorkspaceParams{
|
||||
Now: clock.Now(),
|
||||
NewUserID: user.ID,
|
||||
NewName: workspaceName,
|
||||
PresetID: presets[0].ID,
|
||||
|
||||
@@ -205,7 +205,6 @@ func (s *EnterpriseTemplateScheduleStore) Set(ctx context.Context, db database.S
|
||||
if opts.DefaultTTL != 0 {
|
||||
ttl = sql.NullInt64{Valid: true, Int64: int64(opts.DefaultTTL)}
|
||||
}
|
||||
|
||||
if err = tx.UpdateWorkspacesTTLByTemplateID(ctx, database.UpdateWorkspacesTTLByTemplateIDParams{
|
||||
TemplateID: template.ID,
|
||||
Ttl: ttl,
|
||||
|
||||
@@ -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