Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit734299d

Browse files
authored
fix: disallow lifecycle endpoints for prebuilt workspaces (#19264)
## DescriptionThis PR updates the API to prevent lifecycle configuration endpointsfrom being used on prebuilt workspaces. Since prebuilds are managed bythe reconciliation loop and do not participate in the regular workspacelifecycle, they must not support per-workspace overrides for fields likedeadline, TTL, autostart, or dormancy.Attempting to use these endpoints on a prebuilt workspace will nowreturn a clear validation error (`409 Conflict`) with an appropriateexplanation. This prevents accidental misconfiguration and preserves thelifecycle separation between prebuilds and regular workspaces.## ChangesThe following endpoints now return an error if the target workspace is aprebuild:* `PUT /workspaces/{workspace}/extend`* `PUT /workspaces/{workspace}/ttl`* `PUT /workspaces/{workspace}/autostart`* `PUT /workspaces/{workspace}/dormant`Update endpoints logic to use the API clock in order to allow timemocking in tests.Related with: * Issue:#18898* PR:#19252
1 parentaf97b78 commit734299d

File tree

2 files changed

+254
-17
lines changed

2 files changed

+254
-17
lines changed

‎coderd/workspaces.go‎

Lines changed: 70 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1089,6 +1089,17 @@ func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
10891089
return
10901090
}
10911091

1092+
// Autostart configuration is not supported for prebuilt workspaces.
1093+
// Prebuild lifecycle is managed by the reconciliation loop, with scheduling behavior
1094+
// defined per preset at the template level, not per workspace.
1095+
ifworkspace.IsPrebuild() {
1096+
httpapi.Write(ctx,rw,http.StatusConflict, codersdk.Response{
1097+
Message:"Autostart is not supported for prebuilt workspaces",
1098+
Detail:"Prebuilt workspace scheduling is configured per preset at the template level. Workspace-level overrides are not supported.",
1099+
})
1100+
return
1101+
}
1102+
10921103
dbSched,err:=validWorkspaceSchedule(req.Schedule)
10931104
iferr!=nil {
10941105
httpapi.Write(ctx,rw,http.StatusBadRequest, codersdk.Response{
@@ -1115,12 +1126,20 @@ func (api *API) putWorkspaceAutostart(rw http.ResponseWriter, r *http.Request) {
11151126
return
11161127
}
11171128

1129+
// Use injected Clock to allow time mocking in tests
1130+
now:=api.Clock.Now()
1131+
11181132
nextStartAt:= sql.NullTime{}
11191133
ifdbSched.Valid {
1120-
next,err:=schedule.NextAllowedAutostart(dbtime.Now(),dbSched.String,templateSchedule)
1121-
iferr==nil {
1122-
nextStartAt= sql.NullTime{Valid:true,Time:dbtime.Time(next.UTC())}
1134+
next,err:=schedule.NextAllowedAutostart(now,dbSched.String,templateSchedule)
1135+
iferr!=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
11231141
}
1142+
nextStartAt= sql.NullTime{Valid:true,Time:dbtime.Time(next.UTC())}
11241143
}
11251144

11261145
err=api.Database.UpdateWorkspaceAutostart(ctx, database.UpdateWorkspaceAutostartParams{
@@ -1173,6 +1192,17 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
11731192
return
11741193
}
11751194

1195+
// TTL updates are not supported for prebuilt workspaces.
1196+
// Prebuild lifecycle is managed by the reconciliation loop, with TTL behavior
1197+
// defined per preset at the template level, not per workspace.
1198+
ifworkspace.IsPrebuild() {
1199+
httpapi.Write(ctx,rw,http.StatusConflict, codersdk.Response{
1200+
Message:"TTL updates are not supported for prebuilt workspaces",
1201+
Detail:"Prebuilt workspace TTL is configured per preset at the template level. Workspace-level overrides are not supported.",
1202+
})
1203+
return
1204+
}
1205+
11761206
vardbTTL sql.NullInt64
11771207

11781208
err:=api.Database.InTx(func(s database.Store)error {
@@ -1198,6 +1228,9 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
11981228
returnxerrors.Errorf("update workspace time until shutdown: %w",err)
11991229
}
12001230

1231+
// Use injected Clock to allow time mocking in tests
1232+
now:=api.Clock.Now()
1233+
12011234
// If autostop has been disabled, we want to remove the deadline from the
12021235
// existing workspace build (if there is one).
12031236
if!dbTTL.Valid {
@@ -1225,7 +1258,7 @@ func (api *API) putWorkspaceTTL(rw http.ResponseWriter, r *http.Request) {
12251258
// more information.
12261259
Deadline:build.MaxDeadline,
12271260
MaxDeadline:build.MaxDeadline,
1228-
UpdatedAt:dbtime.Time(api.Clock.Now()),
1261+
UpdatedAt:dbtime.Time(now),
12291262
});err!=nil {
12301263
returnxerrors.Errorf("update workspace build deadline: %w",err)
12311264
}
@@ -1289,17 +1322,30 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
12891322
return
12901323
}
12911324

1325+
// Dormancy configuration is not supported for prebuilt workspaces.
1326+
// Prebuilds are managed by the reconciliation loop and are not subject to dormancy.
1327+
ifoldWorkspace.IsPrebuild() {
1328+
httpapi.Write(ctx,rw,http.StatusConflict, codersdk.Response{
1329+
Message:"Dormancy updates are not supported for prebuilt workspaces",
1330+
Detail:"Prebuilt workspaces are not subject to dormancy. Dormancy behavior is only applicable to regular workspaces",
1331+
})
1332+
return
1333+
}
1334+
12921335
// If the workspace is already in the desired state do nothing!
12931336
ifoldWorkspace.DormantAt.Valid==req.Dormant {
12941337
rw.WriteHeader(http.StatusNotModified)
12951338
return
12961339
}
12971340

1341+
// Use injected Clock to allow time mocking in tests
1342+
now:=api.Clock.Now()
1343+
12981344
dormantAt:= sql.NullTime{
12991345
Valid:req.Dormant,
13001346
}
13011347
ifreq.Dormant {
1302-
dormantAt.Time=dbtime.Now()
1348+
dormantAt.Time=dbtime.Time(now)
13031349
}
13041350

13051351
newWorkspace,err:=api.Database.UpdateWorkspaceDormantDeletingAt(ctx, database.UpdateWorkspaceDormantDeletingAtParams{
@@ -1339,7 +1385,7 @@ func (api *API) putWorkspaceDormant(rw http.ResponseWriter, r *http.Request) {
13391385
}
13401386

13411387
ifinitiatorErr==nil&&tmplErr==nil {
1342-
dormantTime:=dbtime.Now().Add(time.Duration(tmpl.TimeTilDormant))
1388+
dormantTime:=dbtime.Time(now).Add(time.Duration(tmpl.TimeTilDormant))
13431389
_,err=api.NotificationsEnqueuer.Enqueue(
13441390
// nolint:gocritic // Need notifier actor to enqueue notifications
13451391
dbauthz.AsNotifier(ctx),
@@ -1433,6 +1479,17 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
14331479
return
14341480
}
14351481

1482+
// Deadline extensions are not supported for prebuilt workspaces.
1483+
// Prebuilds are managed by the reconciliation loop and must always have
1484+
// Deadline and MaxDeadline unset.
1485+
ifworkspace.IsPrebuild() {
1486+
httpapi.Write(ctx,rw,http.StatusConflict, codersdk.Response{
1487+
Message:"Deadline extension is not supported for prebuilt workspaces",
1488+
Detail:"Prebuilt workspaces do not support user deadline modifications. Deadline extension is only applicable to regular workspaces",
1489+
})
1490+
return
1491+
}
1492+
14361493
code:=http.StatusOK
14371494
resp:= codersdk.Response{}
14381495

@@ -1469,8 +1526,11 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
14691526
returnxerrors.Errorf("workspace shutdown is manual")
14701527
}
14711528

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

14871547
iferr:=s.UpdateWorkspaceBuildDeadlineByID(ctx, database.UpdateWorkspaceBuildDeadlineByIDParams{
14881548
ID:build.ID,
1489-
UpdatedAt:dbtime.Now(),
1549+
UpdatedAt:dbtime.Time(now),
14901550
Deadline:newDeadline,
14911551
MaxDeadline:build.MaxDeadline,
14921552
});err!=nil {
@@ -2441,8 +2501,8 @@ func validWorkspaceAutomaticUpdates(updates codersdk.AutomaticUpdates) (database
24412501
returndbAU,nil
24422502
}
24432503

2444-
funcvalidWorkspaceDeadline(startedAt,newDeadline time.Time)error {
2445-
soon:=time.Now().Add(29*time.Minute)
2504+
funcvalidWorkspaceDeadline(now,startedAt,newDeadline time.Time)error {
2505+
soon:=now.Add(29*time.Minute)
24462506
ifnewDeadline.Before(soon) {
24472507
returnerrDeadlineTooSoon
24482508
}

‎enterprise/coderd/workspaces_test.go‎

Lines changed: 184 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,18 +15,12 @@ import (
1515
"testing"
1616
"time"
1717

18-
"github.com/prometheus/client_golang/prometheus"
19-
20-
"github.com/coder/coder/v2/coderd/files"
21-
agplprebuilds"github.com/coder/coder/v2/coderd/prebuilds"
22-
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
23-
2418
"github.com/google/uuid"
19+
"github.com/prometheus/client_golang/prometheus"
2520
"github.com/stretchr/testify/assert"
2621
"github.com/stretchr/testify/require"
2722

2823
"cdr.dev/slog"
29-
3024
"cdr.dev/slog/sloggers/slogtest"
3125

3226
"github.com/coder/coder/v2/coderd/audit"
@@ -35,10 +29,13 @@ import (
3529
"github.com/coder/coder/v2/coderd/database"
3630
"github.com/coder/coder/v2/coderd/database/dbauthz"
3731
"github.com/coder/coder/v2/coderd/database/dbfake"
32+
"github.com/coder/coder/v2/coderd/database/dbgen"
3833
"github.com/coder/coder/v2/coderd/database/dbtestutil"
3934
"github.com/coder/coder/v2/coderd/database/dbtime"
35+
"github.com/coder/coder/v2/coderd/files"
4036
"github.com/coder/coder/v2/coderd/httpmw"
4137
"github.com/coder/coder/v2/coderd/notifications"
38+
agplprebuilds"github.com/coder/coder/v2/coderd/prebuilds"
4239
"github.com/coder/coder/v2/coderd/provisionerdserver"
4340
"github.com/coder/coder/v2/coderd/rbac"
4441
"github.com/coder/coder/v2/coderd/rbac/policy"
@@ -50,6 +47,7 @@ import (
5047
"github.com/coder/coder/v2/enterprise/audit/backends"
5148
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
5249
"github.com/coder/coder/v2/enterprise/coderd/license"
50+
"github.com/coder/coder/v2/enterprise/coderd/prebuilds"
5351
"github.com/coder/coder/v2/enterprise/coderd/schedule"
5452
"github.com/coder/coder/v2/provisioner/echo"
5553
"github.com/coder/coder/v2/provisionersdk"
@@ -2519,6 +2517,185 @@ func templateWithFailedResponseAndPresetsWithPrebuilds(desiredInstances int32) *
25192517
}
25202518
}
25212519

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

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp