Skip to content

Commit 92d505c

Browse files
authored
feat(cli): prevent coder schedule command on prebuilt workspaces (#19259)
## Description This PR adds CLI-side validation to prevent the use of the `coder schedule` command (including both `start` and `stop` subcommands) on prebuilt workspaces. Prebuilt workspaces are scheduled independently by the reconciliation loop, based on template and preset-level configuration. They do not participate in the regular user workspace lifecycle, and cannot be configured via the `coder schedule` CLI command. This change ensures that attempting to configure scheduling on a prebuilt workspace results in a clear CLI error. ## Changes - `coder schedule start` — now returns an error if the target workspace is a prebuild - `coder schedule stop` — now returns an error if the target workspace is a prebuild Related with: * Issue: #18898 * **Depends on PR**: #19252
1 parent e10f29c commit 92d505c

File tree

4 files changed

+174
-3
lines changed

4 files changed

+174
-3
lines changed

cli/schedule.go

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ When enabling scheduled stop, enter a duration in one of the following formats:
4646
* 2m (2 minutes)
4747
* 2 (2 minutes)
4848
`
49-
scheduleExtendDescriptionLong = `
49+
scheduleExtendDescriptionLong = `Extends the workspace deadline.
5050
* The new stop time is calculated from *now*.
5151
* The new stop time must be at least 30 minutes in the future.
5252
* The workspace template may restrict the maximum workspace runtime.
@@ -157,6 +157,13 @@ func (r *RootCmd) scheduleStart() *serpent.Command {
157157
return err
158158
}
159159

160+
// Autostart configuration is not supported for prebuilt workspaces.
161+
// Prebuild lifecycle is managed by the reconciliation loop, with scheduling behavior
162+
// defined per preset at the template level, not per workspace.
163+
if workspace.IsPrebuild {
164+
return xerrors.Errorf("autostart configuration is not supported for prebuilt workspaces")
165+
}
166+
160167
var schedStr *string
161168
if inv.Args[1] != "manual" {
162169
sched, err := parseCLISchedule(inv.Args[1:]...)
@@ -205,6 +212,13 @@ func (r *RootCmd) scheduleStop() *serpent.Command {
205212
return err
206213
}
207214

215+
// Autostop configuration is not supported for prebuilt workspaces.
216+
// Prebuild lifecycle is managed by the reconciliation loop, with scheduling behavior
217+
// defined per preset at the template level, not per workspace.
218+
if workspace.IsPrebuild {
219+
return xerrors.Errorf("autostop configuration is not supported for prebuilt workspaces")
220+
}
221+
208222
var durMillis *int64
209223
if inv.Args[1] != "manual" {
210224
dur, err := parseDuration(inv.Args[1])
@@ -255,6 +269,13 @@ func (r *RootCmd) scheduleExtend() *serpent.Command {
255269
return xerrors.Errorf("get workspace: %w", err)
256270
}
257271

272+
// Deadline extensions are not supported for prebuilt workspaces.
273+
// Prebuild lifecycle is managed by the reconciliation loop, with TTL behavior
274+
// defined per preset at the template level, not per workspace.
275+
if workspace.IsPrebuild {
276+
return xerrors.Errorf("extend configuration is not supported for prebuilt workspaces")
277+
}
278+
258279
loc, err := tz.TimezoneIANA()
259280
if err != nil {
260281
loc = time.UTC // best effort

cli/testdata/coder_schedule_extend_--help.golden

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ USAGE:
77

88
Aliases: override-stop
99

10-
* The new stop time is calculated from *now*.
10+
Extends the workspace deadline.
11+
* The new stop time is calculated from *now*.
1112
* The new stop time must be at least 30 minutes in the future.
1213
* The workspace template may restrict the maximum workspace runtime.
1314

docs/reference/cli/schedule_extend.md

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/cli/prebuilds_test.go

Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,30 @@ package cli_test
22

33
import (
44
"bytes"
5+
"database/sql"
56
"net/http"
67
"testing"
8+
"time"
79

10+
"github.com/google/uuid"
811
"github.com/stretchr/testify/assert"
912
"github.com/stretchr/testify/require"
1013

1114
"github.com/coder/coder/v2/cli/clitest"
1215
"github.com/coder/coder/v2/coderd/coderdtest"
16+
"github.com/coder/coder/v2/coderd/database"
17+
"github.com/coder/coder/v2/coderd/database/dbauthz"
18+
"github.com/coder/coder/v2/coderd/database/dbfake"
19+
"github.com/coder/coder/v2/coderd/database/dbgen"
20+
"github.com/coder/coder/v2/coderd/database/dbtime"
21+
"github.com/coder/coder/v2/coderd/util/ptr"
1322
"github.com/coder/coder/v2/codersdk"
1423
"github.com/coder/coder/v2/enterprise/coderd/coderdenttest"
1524
"github.com/coder/coder/v2/enterprise/coderd/license"
25+
"github.com/coder/coder/v2/provisionersdk/proto"
26+
"github.com/coder/coder/v2/pty/ptytest"
27+
"github.com/coder/coder/v2/testutil"
28+
"github.com/coder/quartz"
1629
)
1730

1831
func TestPrebuildsPause(t *testing.T) {
@@ -341,3 +354,139 @@ func TestPrebuildsSettingsAPI(t *testing.T) {
341354
assert.False(t, settings.ReconciliationPaused)
342355
})
343356
}
357+
358+
// TestSchedulePrebuilds verifies the CLI schedule command when used with prebuilds.
359+
// Running the command on an unclaimed prebuild fails, but after the prebuild is
360+
// claimed (becoming a regular workspace) it succeeds as expected.
361+
func TestSchedulePrebuilds(t *testing.T) {
362+
t.Parallel()
363+
364+
cases := []struct {
365+
name string
366+
cliErrorMsg string
367+
cmdArgs func(string) []string
368+
}{
369+
{
370+
name: "AutostartPrebuildError",
371+
cliErrorMsg: "autostart configuration is not supported for prebuilt workspaces",
372+
cmdArgs: func(workspaceName string) []string {
373+
return []string{"schedule", "start", workspaceName, "7:30AM", "Mon-Fri", "Europe/Lisbon"}
374+
},
375+
},
376+
{
377+
name: "AutostopPrebuildError",
378+
cliErrorMsg: "autostop configuration is not supported for prebuilt workspaces",
379+
cmdArgs: func(workspaceName string) []string {
380+
return []string{"schedule", "stop", workspaceName, "8h30m"}
381+
},
382+
},
383+
{
384+
name: "ExtendPrebuildError",
385+
cliErrorMsg: "extend configuration is not supported for prebuilt workspaces",
386+
cmdArgs: func(workspaceName string) []string {
387+
return []string{"schedule", "extend", workspaceName, "90m"}
388+
},
389+
},
390+
}
391+
392+
for _, tc := range cases {
393+
tc := tc
394+
t.Run(tc.name, func(t *testing.T) {
395+
t.Parallel()
396+
397+
clock := quartz.NewMock(t)
398+
clock.Set(dbtime.Now())
399+
400+
// Setup
401+
client, db, owner := coderdenttest.NewWithDatabase(t, &coderdenttest.Options{
402+
Options: &coderdtest.Options{
403+
IncludeProvisionerDaemon: true,
404+
Clock: clock,
405+
},
406+
LicenseOptions: &coderdenttest.LicenseOptions{
407+
Features: license.Features{
408+
codersdk.FeatureWorkspacePrebuilds: 1,
409+
},
410+
},
411+
})
412+
413+
// Given: a template and a template version with preset and a prebuilt workspace
414+
presetID := uuid.New()
415+
version := coderdtest.CreateTemplateVersion(t, client, owner.OrganizationID, nil)
416+
_ = coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
417+
template := coderdtest.CreateTemplate(t, client, owner.OrganizationID, version.ID)
418+
dbgen.Preset(t, db, database.InsertPresetParams{
419+
ID: presetID,
420+
TemplateVersionID: version.ID,
421+
DesiredInstances: sql.NullInt32{Int32: 1, Valid: true},
422+
})
423+
workspaceBuild := dbfake.WorkspaceBuild(t, db, database.WorkspaceTable{
424+
OwnerID: database.PrebuildsSystemUserID,
425+
TemplateID: template.ID,
426+
}).Seed(database.WorkspaceBuild{
427+
TemplateVersionID: version.ID,
428+
TemplateVersionPresetID: uuid.NullUUID{
429+
UUID: presetID,
430+
Valid: true,
431+
},
432+
}).WithAgent(func(agent []*proto.Agent) []*proto.Agent {
433+
return agent
434+
}).Do()
435+
436+
// Mark the prebuilt workspace's agent as ready so the prebuild can be claimed
437+
// nolint:gocritic
438+
ctx := dbauthz.AsSystemRestricted(testutil.Context(t, testutil.WaitLong))
439+
agent, err := db.GetWorkspaceAgentAndLatestBuildByAuthToken(ctx, uuid.MustParse(workspaceBuild.AgentToken))
440+
require.NoError(t, err)
441+
err = db.UpdateWorkspaceAgentLifecycleStateByID(ctx, database.UpdateWorkspaceAgentLifecycleStateByIDParams{
442+
ID: agent.WorkspaceAgent.ID,
443+
LifecycleState: database.WorkspaceAgentLifecycleStateReady,
444+
})
445+
require.NoError(t, err)
446+
447+
// Given: a prebuilt workspace
448+
prebuild := coderdtest.MustWorkspace(t, client, workspaceBuild.Workspace.ID)
449+
450+
// When: running the schedule command over a prebuilt workspace
451+
inv, root := clitest.New(t, tc.cmdArgs(prebuild.OwnerName+"/"+prebuild.Name)...)
452+
clitest.SetupConfig(t, client, root)
453+
ptytest.New(t).Attach(inv)
454+
doneChan := make(chan struct{})
455+
var runErr error
456+
go func() {
457+
defer close(doneChan)
458+
runErr = inv.Run()
459+
}()
460+
<-doneChan
461+
462+
// Then: an error should be returned, with an error message specific to the lifecycle parameter
463+
require.Error(t, runErr)
464+
require.Contains(t, runErr.Error(), tc.cliErrorMsg)
465+
466+
// Given: the prebuilt workspace is claimed by a user
467+
user, err := client.User(ctx, "testUser")
468+
require.NoError(t, err)
469+
claimedWorkspace, err := client.CreateUserWorkspace(ctx, user.ID.String(), codersdk.CreateWorkspaceRequest{
470+
TemplateVersionID: version.ID,
471+
TemplateVersionPresetID: presetID,
472+
Name: coderdtest.RandomUsername(t),
473+
// The 'extend' command requires the workspace to have an existing deadline.
474+
// To ensure this, we set the workspace's TTL to 1 hour.
475+
TTLMillis: ptr.Ref[int64](time.Hour.Milliseconds()),
476+
})
477+
require.NoError(t, err)
478+
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, claimedWorkspace.LatestBuild.ID)
479+
workspace := coderdtest.MustWorkspace(t, client, claimedWorkspace.ID)
480+
require.Equal(t, prebuild.ID, workspace.ID)
481+
482+
// When: running the schedule command over the claimed workspace
483+
inv, root = clitest.New(t, tc.cmdArgs(workspace.OwnerName+"/"+workspace.Name)...)
484+
clitest.SetupConfig(t, client, root)
485+
pty := ptytest.New(t).Attach(inv)
486+
require.NoError(t, inv.Run())
487+
488+
// Then: the updated schedule should be shown
489+
pty.ExpectMatch(workspace.OwnerName + "/" + workspace.Name)
490+
})
491+
}
492+
}

0 commit comments

Comments
 (0)
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy