Skip to content

Commit 91875c2

Browse files
fix: allow users to extend their running workspace's deadline (coder#15895)
Fixes coder#15515 This change effectively reverts the changes introduced by coder#13182 (for coder#13078). We also rename the `override-stop` command name to `extend` to match the API endpoint's name (keeping an alias to allow `override-stop` to be used).
1 parent 4c5b737 commit 91875c2

11 files changed

+85
-126
lines changed

cli/schedule.go

Lines changed: 14 additions & 13 deletions
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-
scheduleOverrideDescriptionLong = `
49+
scheduleExtendDescriptionLong = `
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.
@@ -56,7 +56,7 @@ When enabling scheduled stop, enter a duration in one of the following formats:
5656
func (r *RootCmd) schedules() *serpent.Command {
5757
scheduleCmd := &serpent.Command{
5858
Annotations: workspaceCommand,
59-
Use: "schedule { show | start | stop | override } <workspace>",
59+
Use: "schedule { show | start | stop | extend } <workspace>",
6060
Short: "Schedule automated start and stop times for workspaces",
6161
Handler: func(inv *serpent.Invocation) error {
6262
return inv.Command.HelpHandler(inv)
@@ -65,7 +65,7 @@ func (r *RootCmd) schedules() *serpent.Command {
6565
r.scheduleShow(),
6666
r.scheduleStart(),
6767
r.scheduleStop(),
68-
r.scheduleOverride(),
68+
r.scheduleExtend(),
6969
},
7070
}
7171

@@ -229,22 +229,23 @@ func (r *RootCmd) scheduleStop() *serpent.Command {
229229
}
230230
}
231231

232-
func (r *RootCmd) scheduleOverride() *serpent.Command {
232+
func (r *RootCmd) scheduleExtend() *serpent.Command {
233233
client := new(codersdk.Client)
234-
overrideCmd := &serpent.Command{
235-
Use: "override-stop <workspace-name> <duration from now>",
236-
Short: "Override the stop time of a currently running workspace instance.",
237-
Long: scheduleOverrideDescriptionLong + "\n" + FormatExamples(
234+
extendCmd := &serpent.Command{
235+
Use: "extend <workspace-name> <duration from now>",
236+
Aliases: []string{"override-stop"},
237+
Short: "Extend the stop time of a currently running workspace instance.",
238+
Long: scheduleExtendDescriptionLong + "\n" + FormatExamples(
238239
Example{
239-
Command: "coder schedule override-stop my-workspace 90m",
240+
Command: "coder schedule extend my-workspace 90m",
240241
},
241242
),
242243
Middleware: serpent.Chain(
243244
serpent.RequireNArgs(2),
244245
r.InitClient(client),
245246
),
246247
Handler: func(inv *serpent.Invocation) error {
247-
overrideDuration, err := parseDuration(inv.Args[1])
248+
extendDuration, err := parseDuration(inv.Args[1])
248249
if err != nil {
249250
return err
250251
}
@@ -259,15 +260,15 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
259260
loc = time.UTC // best effort
260261
}
261262

262-
if overrideDuration < 29*time.Minute {
263+
if extendDuration < 29*time.Minute {
263264
_, _ = fmt.Fprintf(
264265
inv.Stdout,
265266
"Please specify a duration of at least 30 minutes.\n",
266267
)
267268
return nil
268269
}
269270

270-
newDeadline := time.Now().In(loc).Add(overrideDuration)
271+
newDeadline := time.Now().In(loc).Add(extendDuration)
271272
if err := client.PutExtendWorkspace(inv.Context(), workspace.ID, codersdk.PutExtendWorkspaceRequest{
272273
Deadline: newDeadline,
273274
}); err != nil {
@@ -281,7 +282,7 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
281282
return displaySchedule(updated, inv.Stdout)
282283
},
283284
}
284-
return overrideCmd
285+
return extendCmd
285286
}
286287

287288
func displaySchedule(ws codersdk.Workspace, out io.Writer) error {

cli/schedule_test.go

Lines changed: 42 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -332,32 +332,46 @@ func TestScheduleModify(t *testing.T) {
332332

333333
//nolint:paralleltest // t.Setenv
334334
func TestScheduleOverride(t *testing.T) {
335-
// Given
336-
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
337-
t.Setenv("TZ", "Asia/Kolkata")
338-
loc, err := tz.TimezoneIANA()
339-
require.NoError(t, err)
340-
require.Equal(t, "Asia/Kolkata", loc.String())
341-
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
342-
require.NoError(t, err, "invalid schedule")
343-
ownerClient, _, _, ws := setupTestSchedule(t, sched)
344-
now := time.Now()
345-
// To avoid the likelihood of time-related flakes, only matching up to the hour.
346-
expectedDeadline := time.Now().In(loc).Add(10 * time.Hour).Format("2006-01-02T15:")
347-
348-
// When: we override the stop schedule
349-
inv, root := clitest.New(t,
350-
"schedule", "override-stop", ws[0].OwnerName+"/"+ws[0].Name, "10h",
351-
)
352-
353-
clitest.SetupConfig(t, ownerClient, root)
354-
pty := ptytest.New(t).Attach(inv)
355-
require.NoError(t, inv.Run())
356-
357-
// Then: the updated schedule should be shown
358-
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
359-
pty.ExpectMatch(sched.Humanize())
360-
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
361-
pty.ExpectMatch("8h")
362-
pty.ExpectMatch(expectedDeadline)
335+
tests := []struct {
336+
command string
337+
}{
338+
{command: "extend"},
339+
// test for backwards compatibility
340+
{command: "override-stop"},
341+
}
342+
343+
for _, tt := range tests {
344+
tt := tt
345+
346+
t.Run(tt.command, func(t *testing.T) {
347+
// Given
348+
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
349+
t.Setenv("TZ", "Asia/Kolkata")
350+
loc, err := tz.TimezoneIANA()
351+
require.NoError(t, err)
352+
require.Equal(t, "Asia/Kolkata", loc.String())
353+
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
354+
require.NoError(t, err, "invalid schedule")
355+
ownerClient, _, _, ws := setupTestSchedule(t, sched)
356+
now := time.Now()
357+
// To avoid the likelihood of time-related flakes, only matching up to the hour.
358+
expectedDeadline := time.Now().In(loc).Add(10 * time.Hour).Format("2006-01-02T15:")
359+
360+
// When: we override the stop schedule
361+
inv, root := clitest.New(t,
362+
"schedule", tt.command, ws[0].OwnerName+"/"+ws[0].Name, "10h",
363+
)
364+
365+
clitest.SetupConfig(t, ownerClient, root)
366+
pty := ptytest.New(t).Attach(inv)
367+
require.NoError(t, inv.Run())
368+
369+
// Then: the updated schedule should be shown
370+
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
371+
pty.ExpectMatch(sched.Humanize())
372+
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
373+
pty.ExpectMatch("8h")
374+
pty.ExpectMatch(expectedDeadline)
375+
})
376+
}
363377
}
Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,15 @@
11
coder v0.0.0-devel
22

33
USAGE:
4-
coder schedule { show | start | stop | override } <workspace>
4+
coder schedule { show | start | stop | extend } <workspace>
55

66
Schedule automated start and stop times for workspaces
77

88
SUBCOMMANDS:
9-
override-stop Override the stop time of a currently running workspace
10-
instance.
11-
show Show workspace schedules
12-
start Edit workspace start schedule
13-
stop Edit workspace stop schedule
9+
extend Extend the stop time of a currently running workspace instance.
10+
show Show workspace schedules
11+
start Edit workspace start schedule
12+
stop Edit workspace stop schedule
1413

1514
———
1615
Run `coder --help` for a list of global options.
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,17 @@
11
coder v0.0.0-devel
22

33
USAGE:
4-
coder schedule override-stop <workspace-name> <duration from now>
4+
coder schedule extend <workspace-name> <duration from now>
55

6-
Override the stop time of a currently running workspace instance.
6+
Extend the stop time of a currently running workspace instance.
7+
8+
Aliases: override-stop
79

810
* The new stop time is calculated from *now*.
911
* The new stop time must be at least 30 minutes in the future.
1012
* The workspace template may restrict the maximum workspace runtime.
1113

12-
$ coder schedule override-stop my-workspace 90m
14+
$ coder schedule extend my-workspace 90m
1315

1416
———
1517
Run `coder --help` for a list of global options.

coderd/workspaces.go

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1200,18 +1200,6 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
12001200
return xerrors.Errorf("workspace shutdown is manual")
12011201
}
12021202

1203-
tmpl, err := s.GetTemplateByID(ctx, workspace.TemplateID)
1204-
if err != nil {
1205-
code = http.StatusInternalServerError
1206-
resp.Message = "Error fetching template."
1207-
return xerrors.Errorf("get template: %w", err)
1208-
}
1209-
if !tmpl.AllowUserAutostop {
1210-
code = http.StatusBadRequest
1211-
resp.Message = "Cannot extend workspace: template does not allow user autostop."
1212-
return xerrors.New("cannot extend workspace: template does not allow user autostop")
1213-
}
1214-
12151203
newDeadline := req.Deadline.UTC()
12161204
if err := validWorkspaceDeadline(job.CompletedAt.Time, newDeadline); err != nil {
12171205
// NOTE(Cian): Putting the error in the Message field on request from the FE folks.

docs/manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1194,9 +1194,9 @@
11941194
"path": "reference/cli/schedule.md"
11951195
},
11961196
{
1197-
"title": "schedule override-stop",
1198-
"description": "Override the stop time of a currently running workspace instance.",
1199-
"path": "reference/cli/schedule_override-stop.md"
1197+
"title": "schedule extend",
1198+
"description": "Extend the stop time of a currently running workspace instance.",
1199+
"path": "reference/cli/schedule_extend.md"
12001200
},
12011201
{
12021202
"title": "schedule show",

docs/reference/cli/schedule.md

Lines changed: 7 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/reference/cli/schedule_override-stop.md renamed to docs/reference/cli/schedule_extend.md

Lines changed: 8 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

enterprise/coderd/workspaces_test.go

Lines changed: 0 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1391,35 +1391,6 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
13911391
require.Equal(t, templateTTL, template.DefaultTTLMillis)
13921392
require.Equal(t, templateTTL, *workspace.TTLMillis)
13931393
})
1394-
1395-
t.Run("ExtendIsNotEnabledByTemplate", func(t *testing.T) {
1396-
t.Parallel()
1397-
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
1398-
client := coderdtest.New(t, &coderdtest.Options{
1399-
IncludeProvisionerDaemon: true,
1400-
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
1401-
})
1402-
user := coderdtest.CreateFirstUser(t, client)
1403-
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
1404-
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
1405-
ctr.AllowUserAutostop = ptr.Ref(false)
1406-
})
1407-
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
1408-
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
1409-
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
1410-
1411-
require.Equal(t, false, template.AllowUserAutostop, "template should have AllowUserAutostop as false")
1412-
1413-
ctx := testutil.Context(t, testutil.WaitShort)
1414-
ttl := 8 * time.Hour
1415-
newDeadline := time.Now().Add(ttl + time.Hour).UTC()
1416-
1417-
err := client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
1418-
Deadline: newDeadline,
1419-
})
1420-
1421-
require.ErrorContains(t, err, "template does not allow user autostop")
1422-
})
14231394
}
14241395

14251396
// TestWorkspaceTagsTerraform tests that a workspace can be created with tags.

site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx

Lines changed: 0 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -303,24 +303,6 @@ export const WithQuotaWithOrgs: Story = {
303303
},
304304
};
305305

306-
export const TemplateDoesNotAllowAutostop: Story = {
307-
args: {
308-
workspace: {
309-
...MockWorkspace,
310-
latest_build: {
311-
...MockWorkspace.latest_build,
312-
get deadline() {
313-
return addHours(new Date(), 8).toISOString();
314-
},
315-
},
316-
},
317-
template: {
318-
...MockTemplate,
319-
allow_user_autostop: false,
320-
},
321-
},
322-
};
323-
324306
export const TemplateInfoPopover: Story = {
325307
play: async ({ canvasElement, step }) => {
326308
const canvas = within(canvasElement);

site/src/pages/WorkspacePage/WorkspaceTopbar.tsx

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -234,9 +234,7 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
234234
<WorkspaceScheduleControls
235235
workspace={workspace}
236236
template={template}
237-
canUpdateSchedule={
238-
canUpdateWorkspace && template.allow_user_autostop
239-
}
237+
canUpdateSchedule={canUpdateWorkspace}
240238
/>
241239
<WorkspaceNotifications
242240
workspace={workspace}

0 commit comments

Comments
 (0)