Skip to content

Commit 7bbb045

Browse files
committed
test: add tests for lifecycle endpoints with prebuilds
1 parent 951cbca commit 7bbb045

File tree

2 files changed

+209
-11
lines changed

2 files changed

+209
-11
lines changed

coderd/workspaces.go

Lines changed: 28 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1126,12 +1126,20 @@ func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
11261126
return
11271127
}
11281128

1129+
// Use injected Clock to allow time mocking in tests
1130+
now := api.Clock.Now()
1131+
11291132
nextStartAt := sql.NullTime{}
11301133
if dbSched.Valid {
1131-
next, err := schedule.NextAllowedAutostart(dbtime.Now(), dbSched.String, templateSchedule)
1132-
if err == nil {
1133-
nextStartAt = sql.NullTime{Valid: true, Time: dbtime.Time(next.UTC())}
1134+
next, err := schedule.NextAllowedAutostart(now, dbSched.String, templateSchedule)
1135+
if err != nil {
1136+
httpapi.Write(ctx, rw, http.StatusInternalServerError, codersdk.Response{
1137+
Message: "Internal error calculating workspace autostart schedule.",
1138+
Detail: err.Error(),
1139+
})
1140+
return
11341141
}
1142+
nextStartAt = sql.NullTime{Valid: true, Time: dbtime.Time(next.UTC())}
11351143
}
11361144

11371145
err = api.Database.UpdateWorkspaceAutostart(ctx, database.UpdateWorkspaceAutostartParams{
@@ -1220,6 +1228,9 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
12201228
return xerrors.Errorf("update workspace time until shutdown: %w", err)
12211229
}
12221230

1231+
// Use injected Clock to allow time mocking in tests
1232+
now := api.Clock.Now()
1233+
12231234
// If autostop has been disabled, we want to remove the deadline from the
12241235
// existing workspace build (if there is one).
12251236
if !dbTTL.Valid {
@@ -1247,7 +1258,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
12471258
// more information.
12481259
Deadline: build.MaxDeadline,
12491260
MaxDeadline: build.MaxDeadline,
1250-
UpdatedAt: dbtime.Time(api.Clock.Now()),
1261+
UpdatedAt: dbtime.Time(now),
12511262
}); err != nil {
12521263
return xerrors.Errorf("update workspace build deadline: %w", err)
12531264
}
@@ -1315,7 +1326,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
13151326
// Prebuilds are managed by the reconciliation loop and are not subject to dormancy.
13161327
if oldWorkspace.IsPrebuild() {
13171328
httpapi.Write(ctx, rw, http.StatusConflict, codersdk.Response{
1318-
Message: "Dormancy configuration is not supported for prebuilt workspaces",
1329+
Message: "Dormancy updates are not supported for prebuilt workspaces",
13191330
Detail: "Prebuilt workspaces are not subject to dormancy. Dormancy behavior is only applicable to regular workspaces",
13201331
})
13211332
return
@@ -1327,11 +1338,14 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
13271338
return
13281339
}
13291340

1341+
// Use injected Clock to allow time mocking in tests
1342+
now := api.Clock.Now()
1343+
13301344
dormantAt := sql.NullTime{
13311345
Valid: req.Dormant,
13321346
}
13331347
if req.Dormant {
1334-
dormantAt.Time = dbtime.Now()
1348+
dormantAt.Time = dbtime.Time(now)
13351349
}
13361350

13371351
newWorkspace, err := api.Database.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
@@ -1371,7 +1385,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
13711385
}
13721386

13731387
if initiatorErr == nil && tmplErr == nil {
1374-
dormantTime := dbtime.Now().Add(time.Duration(tmpl.TimeTilDormant))
1388+
dormantTime := dbtime.Time(now).Add(time.Duration(tmpl.TimeTilDormant))
13751389
_, err = api.NotificationsEnqueuer.Enqueue(
13761390
// nolint:gocritic // Need notifier actor to enqueue notifications
13771391
dbauthz.AsNotifier(ctx),
@@ -1512,8 +1526,11 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
15121526
return xerrors.Errorf("workspace shutdown is manual")
15131527
}
15141528

1529+
// Use injected Clock to allow time mocking in tests
1530+
now := api.Clock.Now()
1531+
15151532
newDeadline := req.Deadline.UTC()
1516-
if err := validWorkspaceDeadline(job.CompletedAt.Time, newDeadline); err != nil {
1533+
if err := validWorkspaceDeadline(now, job.CompletedAt.Time, newDeadline); err != nil {
15171534
// NOTE(Cian): Putting the error in the Message field on request from the FE folks.
15181535
// Normally, we would put the validation error in Validations, but this endpoint is
15191536
// not tied to a form or specific named user input on the FE.
@@ -1529,7 +1546,7 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
15291546

15301547
if err := s.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
15311548
ID: build.ID,
1532-
UpdatedAt: dbtime.Now(),
1549+
UpdatedAt: dbtime.Time(now),
15331550
Deadline: newDeadline,
15341551
MaxDeadline: build.MaxDeadline,
15351552
}); err != nil {
@@ -2484,8 +2501,8 @@ func validWorkspaceAutomaticUpdates(updates codersdk.AutomaticUpdates) (database
24842501
return dbAU, nil
24852502
}
24862503

2487-
func validWorkspaceDeadline(startedAt, newDeadline time.Time) error {
2488-
soon := time.Now().Add(29 * time.Minute)
2504+
func validWorkspaceDeadline(now, startedAt, newDeadline time.Time) error {
2505+
soon := now.Add(29 * time.Minute)
24892506
if newDeadline.Before(soon) {
24902507
return errDeadlineTooSoon
24912508
}

enterprise/coderd/workspaces_test.go

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717

1818
"github.com/prometheus/client_golang/prometheus"
1919

20+
"github.com/coder/coder/v2/coderd/database/dbgen"
21+
2022
"github.com/coder/coder/v2/coderd/files"
2123
agplprebuilds "github.com/coder/coder/v2/coderd/prebuilds"
2224
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
@@ -2519,6 +2521,185 @@ func templateWithFailedResponseAndPresetsWithPrebuilds(desiredInstances int32) *
25192521
}
25202522
}
25212523

2524+
func TestPrebuildUpdateLifecycleParams(t *testing.T) {
2525+
t.Parallel()
2526+
2527+
// Set the clock to Monday, January 1st, 2024 at 8:00 AM UTC to keep the test deterministic
2528+
clock := quartz.NewMock(t)
2529+
clock.Set(time.Date(2024, 1, 1, 8, 0, 0, 0, time.UTC))
2530+
2531+
// Autostart schedule configuration set to weekly at 9:30 AM UTC
2532+
autostartSchedule, err := cron.Weekly("CRON_TZ=UTC 30 9 * * 1-5")
2533+
require.NoError(t, err)
2534+
2535+
// TTL configuration set to 8 hours
2536+
ttlMillis := ptr.Ref((8 * time.Hour).Milliseconds())
2537+
2538+
// Deadline configuration set to 10:00 AM UTC
2539+
deadline := clock.Now().Add(2 * time.Hour)
2540+
2541+
cases := []struct {
2542+
name string
2543+
endpoint func(*testing.T, context.Context, *codersdk.Client, uuid.UUID) error
2544+
apiErrorMsg string
2545+
assertUpdate func(*testing.T, *quartz.Mock, *codersdk.Client, uuid.UUID)
2546+
}{
2547+
{
2548+
name: "AutostartUpdatePrebuildAfterClaim",
2549+
endpoint: func(t *testing.T, ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID) error {
2550+
err = client.UpdateWorkspaceAutostart(ctx, workspaceID, codersdk.UpdateWorkspaceAutostartRequest{
2551+
Schedule: ptr.Ref(autostartSchedule.String()),
2552+
})
2553+
return err
2554+
},
2555+
apiErrorMsg: "Autostart is not supported for prebuilt workspaces",
2556+
assertUpdate: func(t *testing.T, clock *quartz.Mock, client *codersdk.Client, workspaceID uuid.UUID) {
2557+
// The workspace's autostart schedule should be updated to the given schedule,
2558+
// and its next start time should be set to 2024-01-01 09:30 AM UTC
2559+
updatedWorkspace := coderdtest.MustWorkspace(t, client, workspaceID)
2560+
require.Equal(t, autostartSchedule.String(), *updatedWorkspace.AutostartSchedule)
2561+
require.Equal(t, autostartSchedule.Next(clock.Now()), updatedWorkspace.NextStartAt.UTC())
2562+
expectedNext := time.Date(2024, 1, 1, 9, 30, 0, 0, time.UTC)
2563+
require.Equal(t, expectedNext, updatedWorkspace.NextStartAt.UTC())
2564+
},
2565+
},
2566+
{
2567+
name: "TTLUpdatePrebuildAfterClaim",
2568+
endpoint: func(t *testing.T, ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID) error {
2569+
err := client.UpdateWorkspaceTTL(ctx, workspaceID, codersdk.UpdateWorkspaceTTLRequest{
2570+
TTLMillis: ttlMillis,
2571+
})
2572+
return err
2573+
},
2574+
apiErrorMsg: "TTL updates are not supported for prebuilt workspaces",
2575+
assertUpdate: func(t *testing.T, clock *quartz.Mock, client *codersdk.Client, workspaceID uuid.UUID) {
2576+
// The workspace's TTL should be updated accordingly
2577+
updatedWorkspace := coderdtest.MustWorkspace(t, client, workspaceID)
2578+
require.Equal(t, ttlMillis, updatedWorkspace.TTLMillis)
2579+
},
2580+
},
2581+
{
2582+
name: "DormantUpdatePrebuildAfterClaim",
2583+
endpoint: func(t *testing.T, ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID) error {
2584+
err := client.UpdateWorkspaceDormancy(ctx, workspaceID, codersdk.UpdateWorkspaceDormancy{
2585+
Dormant: true,
2586+
})
2587+
return err
2588+
},
2589+
apiErrorMsg: "Dormancy updates are not supported for prebuilt workspaces",
2590+
assertUpdate: func(t *testing.T, clock *quartz.Mock, client *codersdk.Client, workspaceID uuid.UUID) {
2591+
// The workspace's dormantAt should be updated accordingly
2592+
updatedWorkspace := coderdtest.MustWorkspace(t, client, workspaceID)
2593+
require.Equal(t, clock.Now(), updatedWorkspace.DormantAt.UTC())
2594+
},
2595+
},
2596+
{
2597+
name: "DeadlineUpdatePrebuildAfterClaim",
2598+
endpoint: func(t *testing.T, ctx context.Context, client *codersdk.Client, workspaceID uuid.UUID) error {
2599+
err := client.PutExtendWorkspace(ctx, workspaceID, codersdk.PutExtendWorkspaceRequest{
2600+
Deadline: deadline,
2601+
})
2602+
return err
2603+
},
2604+
apiErrorMsg: "Deadline extension is not supported for prebuilt workspaces",
2605+
assertUpdate: func(t *testing.T, clock *quartz.Mock, client *codersdk.Client, workspaceID uuid.UUID) {
2606+
// The workspace build's deadline should be updated accordingly
2607+
updatedWorkspace := coderdtest.MustWorkspace(t, client, workspaceID)
2608+
require.Equal(t, deadline, updatedWorkspace.LatestBuild.Deadline.Time.UTC())
2609+
},
2610+
},
2611+
}
2612+
2613+
for _, tc := range cases {
2614+
tc := tc
2615+
t.Run(tc.name, func(t *testing.T) {
2616+
t.Parallel()
2617+
2618+
// Setup
2619+
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
2620+
Options: &coderdtest.Options{
2621+
IncludeProvisionerDaemon: true,
2622+
Clock: clock,
2623+
},
2624+
LicenseOptions: &coderdenttest.LicenseOptions{
2625+
Features: license.Features{
2626+
codersdk.FeatureWorkspacePrebuilds: 1,
2627+
},
2628+
},
2629+
})
2630+
2631+
// Given: a template and a template version with preset and a prebuilt workspace
2632+
presetID := uuid.New()
2633+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
2634+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
2635+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
2636+
dbgen.Preset(t, db, database.InsertPresetParams{
2637+
ID: presetID,
2638+
TemplateVersionID: version.ID,
2639+
DesiredInstances: sql.NullInt32{Int32: 1, Valid: true},
2640+
})
2641+
workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
2642+
OwnerID: database.PrebuildsSystemUserID,
2643+
TemplateID: template.ID,
2644+
}).Seed(database.WorkspaceBuild{
2645+
TemplateVersionID: version.ID,
2646+
TemplateVersionPresetID: uuid.NullUUID{
2647+
UUID: presetID,
2648+
Valid: true,
2649+
},
2650+
}).WithAgent(func(agent []*proto.Agent) []*proto.Agent {
2651+
return agent
2652+
}).Do()
2653+
2654+
// Mark the prebuilt workspace's agent as ready so the prebuild can be claimed
2655+
// nolint:gocritic
2656+
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitLong))
2657+
agent, err := db.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, uuid.MustParse(workspaceBuild.AgentToken))
2658+
require.NoError(t, err)
2659+
err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{
2660+
ID: agent.WorkspaceAgent.ID,
2661+
LifecycleState: database.WorkspaceAgentLifecycleStateReady,
2662+
})
2663+
require.NoError(t, err)
2664+
2665+
// Given: a prebuilt workspace
2666+
prebuild := coderdtest.MustWorkspace(t, client, workspaceBuild.Workspace.ID)
2667+
2668+
// When: the lifecycle-update endpoint is called for the prebuilt workspace
2669+
err = tc.endpoint(t, ctx, client, prebuild.ID)
2670+
2671+
// Then: a 409 Conflict should be returned, with an error message specific to the lifecycle parameter
2672+
var apiErr *codersdk.Error
2673+
require.ErrorAs(t, err, &apiErr)
2674+
require.Equal(t, http.StatusConflict, apiErr.StatusCode())
2675+
require.Equal(t, tc.apiErrorMsg, apiErr.Response.Message)
2676+
2677+
// Given: the prebuilt workspace is claimed by a user
2678+
user, err := client.User(ctx, "testUser")
2679+
require.NoError(t, err)
2680+
claimedWorkspace, err := client.CreateUserWorkspace(ctx, user.ID.String(), codersdk.CreateWorkspaceRequest{
2681+
TemplateVersionID: version.ID,
2682+
TemplateVersionPresetID: presetID,
2683+
Name: coderdtest.RandomUsername(t),
2684+
// The 'extend' endpoint requires the workspace to have an existing deadline.
2685+
// To ensure this, we set the workspace's TTL to 1 hour.
2686+
TTLMillis: ptr.Ref[int64](time.Hour.Milliseconds()),
2687+
})
2688+
require.NoError(t, err)
2689+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, claimedWorkspace.LatestBuild.ID)
2690+
workspace := coderdtest.MustWorkspace(t, client, claimedWorkspace.ID)
2691+
require.Equal(t, prebuild.ID, workspace.ID)
2692+
2693+
// When: the same lifecycle-update endpoint is called for the claimed workspace
2694+
err = tc.endpoint(t, ctx, client, workspace.ID)
2695+
require.NoError(t, err)
2696+
2697+
// Then: the workspace's lifecycle parameter should be updated accordingly
2698+
tc.assertUpdate(t, clock, client, claimedWorkspace.ID)
2699+
})
2700+
}
2701+
}
2702+
25222703
// TestWorkspaceTemplateParamsChange tests a workspace with a parameter that
25232704
// validation changes on apply. The params used in create workspace are invalid
25242705
// according to the static params on import.

0 commit comments

Comments
 (0)