fix: allow users to extend their running workspace's deadline (#15895)

Fixes https://github.com/coder/coder/issues/15515

This change effectively reverts the changes introduced by
https://github.com/coder/coder/pull/13182 (for
https://github.com/coder/coder/issues/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).
This commit is contained in:
Danielle Maywood
2024-12-18 10:58:33 +00:00
committed by GitHub
parent 4c5b737368
commit 91875c2b47
11 changed files with 82 additions and 123 deletions

View File

@ -46,7 +46,7 @@ When enabling scheduled stop, enter a duration in one of the following formats:
* 2m (2 minutes) * 2m (2 minutes)
* 2 (2 minutes) * 2 (2 minutes)
` `
scheduleOverrideDescriptionLong = ` scheduleExtendDescriptionLong = `
* The new stop time is calculated from *now*. * The new stop time is calculated from *now*.
* The new stop time must be at least 30 minutes in the future. * The new stop time must be at least 30 minutes in the future.
* The workspace template may restrict the maximum workspace runtime. * 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:
func (r *RootCmd) schedules() *serpent.Command { func (r *RootCmd) schedules() *serpent.Command {
scheduleCmd := &serpent.Command{ scheduleCmd := &serpent.Command{
Annotations: workspaceCommand, Annotations: workspaceCommand,
Use: "schedule { show | start | stop | override } <workspace>", Use: "schedule { show | start | stop | extend } <workspace>",
Short: "Schedule automated start and stop times for workspaces", Short: "Schedule automated start and stop times for workspaces",
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
return inv.Command.HelpHandler(inv) return inv.Command.HelpHandler(inv)
@ -65,7 +65,7 @@ func (r *RootCmd) schedules() *serpent.Command {
r.scheduleShow(), r.scheduleShow(),
r.scheduleStart(), r.scheduleStart(),
r.scheduleStop(), r.scheduleStop(),
r.scheduleOverride(), r.scheduleExtend(),
}, },
} }
@ -229,14 +229,15 @@ func (r *RootCmd) scheduleStop() *serpent.Command {
} }
} }
func (r *RootCmd) scheduleOverride() *serpent.Command { func (r *RootCmd) scheduleExtend() *serpent.Command {
client := new(codersdk.Client) client := new(codersdk.Client)
overrideCmd := &serpent.Command{ extendCmd := &serpent.Command{
Use: "override-stop <workspace-name> <duration from now>", Use: "extend <workspace-name> <duration from now>",
Short: "Override the stop time of a currently running workspace instance.", Aliases: []string{"override-stop"},
Long: scheduleOverrideDescriptionLong + "\n" + FormatExamples( Short: "Extend the stop time of a currently running workspace instance.",
Long: scheduleExtendDescriptionLong + "\n" + FormatExamples(
Example{ Example{
Command: "coder schedule override-stop my-workspace 90m", Command: "coder schedule extend my-workspace 90m",
}, },
), ),
Middleware: serpent.Chain( Middleware: serpent.Chain(
@ -244,7 +245,7 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
r.InitClient(client), r.InitClient(client),
), ),
Handler: func(inv *serpent.Invocation) error { Handler: func(inv *serpent.Invocation) error {
overrideDuration, err := parseDuration(inv.Args[1]) extendDuration, err := parseDuration(inv.Args[1])
if err != nil { if err != nil {
return err return err
} }
@ -259,7 +260,7 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
loc = time.UTC // best effort loc = time.UTC // best effort
} }
if overrideDuration < 29*time.Minute { if extendDuration < 29*time.Minute {
_, _ = fmt.Fprintf( _, _ = fmt.Fprintf(
inv.Stdout, inv.Stdout,
"Please specify a duration of at least 30 minutes.\n", "Please specify a duration of at least 30 minutes.\n",
@ -267,7 +268,7 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
return nil return nil
} }
newDeadline := time.Now().In(loc).Add(overrideDuration) newDeadline := time.Now().In(loc).Add(extendDuration)
if err := client.PutExtendWorkspace(inv.Context(), workspace.ID, codersdk.PutExtendWorkspaceRequest{ if err := client.PutExtendWorkspace(inv.Context(), workspace.ID, codersdk.PutExtendWorkspaceRequest{
Deadline: newDeadline, Deadline: newDeadline,
}); err != nil { }); err != nil {
@ -281,7 +282,7 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
return displaySchedule(updated, inv.Stdout) return displaySchedule(updated, inv.Stdout)
}, },
} }
return overrideCmd return extendCmd
} }
func displaySchedule(ws codersdk.Workspace, out io.Writer) error { func displaySchedule(ws codersdk.Workspace, out io.Writer) error {

View File

@ -332,6 +332,18 @@ func TestScheduleModify(t *testing.T) {
//nolint:paralleltest // t.Setenv //nolint:paralleltest // t.Setenv
func TestScheduleOverride(t *testing.T) { func TestScheduleOverride(t *testing.T) {
tests := []struct {
command string
}{
{command: "extend"},
// test for backwards compatibility
{command: "override-stop"},
}
for _, tt := range tests {
tt := tt
t.Run(tt.command, func(t *testing.T) {
// Given // Given
// Set timezone to Asia/Kolkata to surface any timezone-related bugs. // Set timezone to Asia/Kolkata to surface any timezone-related bugs.
t.Setenv("TZ", "Asia/Kolkata") t.Setenv("TZ", "Asia/Kolkata")
@ -347,7 +359,7 @@ func TestScheduleOverride(t *testing.T) {
// When: we override the stop schedule // When: we override the stop schedule
inv, root := clitest.New(t, inv, root := clitest.New(t,
"schedule", "override-stop", ws[0].OwnerName+"/"+ws[0].Name, "10h", "schedule", tt.command, ws[0].OwnerName+"/"+ws[0].Name, "10h",
) )
clitest.SetupConfig(t, ownerClient, root) clitest.SetupConfig(t, ownerClient, root)
@ -360,4 +372,6 @@ func TestScheduleOverride(t *testing.T) {
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339)) pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h") pty.ExpectMatch("8h")
pty.ExpectMatch(expectedDeadline) pty.ExpectMatch(expectedDeadline)
})
}
} }

View File

@ -1,13 +1,12 @@
coder v0.0.0-devel coder v0.0.0-devel
USAGE: USAGE:
coder schedule { show | start | stop | override } <workspace> coder schedule { show | start | stop | extend } <workspace>
Schedule automated start and stop times for workspaces Schedule automated start and stop times for workspaces
SUBCOMMANDS: SUBCOMMANDS:
override-stop Override the stop time of a currently running workspace extend Extend the stop time of a currently running workspace instance.
instance.
show Show workspace schedules show Show workspace schedules
start Edit workspace start schedule start Edit workspace start schedule
stop Edit workspace stop schedule stop Edit workspace stop schedule

View File

@ -1,15 +1,17 @@
coder v0.0.0-devel coder v0.0.0-devel
USAGE: USAGE:
coder schedule override-stop <workspace-name> <duration from now> coder schedule extend <workspace-name> <duration from now>
Override the stop time of a currently running workspace instance. Extend the stop time of a currently running workspace instance.
Aliases: override-stop
* The new stop time is calculated from *now*. * The new stop time is calculated from *now*.
* The new stop time must be at least 30 minutes in the future. * The new stop time must be at least 30 minutes in the future.
* The workspace template may restrict the maximum workspace runtime. * The workspace template may restrict the maximum workspace runtime.
$ coder schedule override-stop my-workspace 90m $ coder schedule extend my-workspace 90m
——— ———
Run `coder --help` for a list of global options. Run `coder --help` for a list of global options.

View File

@ -1200,18 +1200,6 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
return xerrors.Errorf("workspace shutdown is manual") return xerrors.Errorf("workspace shutdown is manual")
} }
tmpl, err := s.GetTemplateByID(ctx, workspace.TemplateID)
if err != nil {
code = http.StatusInternalServerError
resp.Message = "Error fetching template."
return xerrors.Errorf("get template: %w", err)
}
if !tmpl.AllowUserAutostop {
code = http.StatusBadRequest
resp.Message = "Cannot extend workspace: template does not allow user autostop."
return xerrors.New("cannot extend workspace: template does not allow user autostop")
}
newDeadline := req.Deadline.UTC() newDeadline := req.Deadline.UTC()
if err := validWorkspaceDeadline(job.CompletedAt.Time, newDeadline); err != nil { if err := validWorkspaceDeadline(job.CompletedAt.Time, newDeadline); err != nil {
// NOTE(Cian): Putting the error in the Message field on request from the FE folks. // NOTE(Cian): Putting the error in the Message field on request from the FE folks.

View File

@ -1194,9 +1194,9 @@
"path": "reference/cli/schedule.md" "path": "reference/cli/schedule.md"
}, },
{ {
"title": "schedule override-stop", "title": "schedule extend",
"description": "Override the stop time of a currently running workspace instance.", "description": "Extend the stop time of a currently running workspace instance.",
"path": "reference/cli/schedule_override-stop.md" "path": "reference/cli/schedule_extend.md"
}, },
{ {
"title": "schedule show", "title": "schedule show",

View File

@ -7,14 +7,14 @@ Schedule automated start and stop times for workspaces
## Usage ## Usage
```console ```console
coder schedule { show | start | stop | override } <workspace> coder schedule { show | start | stop | extend } <workspace>
``` ```
## Subcommands ## Subcommands
| Name | Purpose | | Name | Purpose |
| --------------------------------------------------------- | ----------------------------------------------------------------- | | ------------------------------------------- | --------------------------------------------------------------- |
| [<code>show</code>](./schedule_show.md) | Show workspace schedules | | [<code>show</code>](./schedule_show.md) | Show workspace schedules |
| [<code>start</code>](./schedule_start.md) | Edit workspace start schedule | | [<code>start</code>](./schedule_start.md) | Edit workspace start schedule |
| [<code>stop</code>](./schedule_stop.md) | Edit workspace stop schedule | | [<code>stop</code>](./schedule_stop.md) | Edit workspace stop schedule |
| [<code>override-stop</code>](./schedule_override-stop.md) | Override the stop time of a currently running workspace instance. | | [<code>extend</code>](./schedule_extend.md) | Extend the stop time of a currently running workspace instance. |

View File

@ -1,13 +1,17 @@
<!-- DO NOT EDIT | GENERATED CONTENT --> <!-- DO NOT EDIT | GENERATED CONTENT -->
# schedule override-stop # schedule extend
Override the stop time of a currently running workspace instance. Extend the stop time of a currently running workspace instance.
Aliases:
- override-stop
## Usage ## Usage
```console ```console
coder schedule override-stop <workspace-name> <duration from now> coder schedule extend <workspace-name> <duration from now>
``` ```
## Description ## Description
@ -18,5 +22,5 @@ coder schedule override-stop <workspace-name> <duration from now>
* The new stop time must be at least 30 minutes in the future. * The new stop time must be at least 30 minutes in the future.
* The workspace template may restrict the maximum workspace runtime. * The workspace template may restrict the maximum workspace runtime.
$ coder schedule override-stop my-workspace 90m $ coder schedule extend my-workspace 90m
``` ```

View File

@ -1391,35 +1391,6 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
require.Equal(t, templateTTL, template.DefaultTTLMillis) require.Equal(t, templateTTL, template.DefaultTTLMillis)
require.Equal(t, templateTTL, *workspace.TTLMillis) require.Equal(t, templateTTL, *workspace.TTLMillis)
}) })
t.Run("ExtendIsNotEnabledByTemplate", func(t *testing.T) {
t.Parallel()
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
client := coderdtest.New(t, &coderdtest.Options{
IncludeProvisionerDaemon: true,
TemplateScheduleStore: schedule.NewEnterpriseTemplateScheduleStore(agplUserQuietHoursScheduleStore(), notifications.NewNoopEnqueuer(), logger, nil),
})
user := coderdtest.CreateFirstUser(t, client)
version := coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
template := coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID, func(ctr *codersdk.CreateTemplateRequest) {
ctr.AllowUserAutostop = ptr.Ref(false)
})
coderdtest.AwaitTemplateVersionJobCompleted(t, client, version.ID)
workspace := coderdtest.CreateWorkspace(t, client, template.ID)
coderdtest.AwaitWorkspaceBuildJobCompleted(t, client, workspace.LatestBuild.ID)
require.Equal(t, false, template.AllowUserAutostop, "template should have AllowUserAutostop as false")
ctx := testutil.Context(t, testutil.WaitShort)
ttl := 8 * time.Hour
newDeadline := time.Now().Add(ttl + time.Hour).UTC()
err := client.PutExtendWorkspace(ctx, workspace.ID, codersdk.PutExtendWorkspaceRequest{
Deadline: newDeadline,
})
require.ErrorContains(t, err, "template does not allow user autostop")
})
} }
// TestWorkspaceTagsTerraform tests that a workspace can be created with tags. // TestWorkspaceTagsTerraform tests that a workspace can be created with tags.

View File

@ -303,24 +303,6 @@ export const WithQuotaWithOrgs: Story = {
}, },
}; };
export const TemplateDoesNotAllowAutostop: Story = {
args: {
workspace: {
...MockWorkspace,
latest_build: {
...MockWorkspace.latest_build,
get deadline() {
return addHours(new Date(), 8).toISOString();
},
},
},
template: {
...MockTemplate,
allow_user_autostop: false,
},
},
};
export const TemplateInfoPopover: Story = { export const TemplateInfoPopover: Story = {
play: async ({ canvasElement, step }) => { play: async ({ canvasElement, step }) => {
const canvas = within(canvasElement); const canvas = within(canvasElement);

View File

@ -234,9 +234,7 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
<WorkspaceScheduleControls <WorkspaceScheduleControls
workspace={workspace} workspace={workspace}
template={template} template={template}
canUpdateSchedule={ canUpdateSchedule={canUpdateWorkspace}
canUpdateWorkspace && template.allow_user_autostop
}
/> />
<WorkspaceNotifications <WorkspaceNotifications
workspace={workspace} workspace={workspace}