mirror of
https://github.com/coder/coder.git
synced 2025-07-15 22:20:27 +00:00
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:
@ -46,7 +46,7 @@ When enabling scheduled stop, enter a duration in one of the following formats:
|
||||
* 2m (2 minutes)
|
||||
* 2 (2 minutes)
|
||||
`
|
||||
scheduleOverrideDescriptionLong = `
|
||||
scheduleExtendDescriptionLong = `
|
||||
* The new stop time is calculated from *now*.
|
||||
* The new stop time must be at least 30 minutes in the future.
|
||||
* 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 {
|
||||
scheduleCmd := &serpent.Command{
|
||||
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",
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
return inv.Command.HelpHandler(inv)
|
||||
@ -65,7 +65,7 @@ func (r *RootCmd) schedules() *serpent.Command {
|
||||
r.scheduleShow(),
|
||||
r.scheduleStart(),
|
||||
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)
|
||||
overrideCmd := &serpent.Command{
|
||||
Use: "override-stop <workspace-name> <duration from now>",
|
||||
Short: "Override the stop time of a currently running workspace instance.",
|
||||
Long: scheduleOverrideDescriptionLong + "\n" + FormatExamples(
|
||||
extendCmd := &serpent.Command{
|
||||
Use: "extend <workspace-name> <duration from now>",
|
||||
Aliases: []string{"override-stop"},
|
||||
Short: "Extend the stop time of a currently running workspace instance.",
|
||||
Long: scheduleExtendDescriptionLong + "\n" + FormatExamples(
|
||||
Example{
|
||||
Command: "coder schedule override-stop my-workspace 90m",
|
||||
Command: "coder schedule extend my-workspace 90m",
|
||||
},
|
||||
),
|
||||
Middleware: serpent.Chain(
|
||||
@ -244,7 +245,7 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
|
||||
r.InitClient(client),
|
||||
),
|
||||
Handler: func(inv *serpent.Invocation) error {
|
||||
overrideDuration, err := parseDuration(inv.Args[1])
|
||||
extendDuration, err := parseDuration(inv.Args[1])
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -259,7 +260,7 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
|
||||
loc = time.UTC // best effort
|
||||
}
|
||||
|
||||
if overrideDuration < 29*time.Minute {
|
||||
if extendDuration < 29*time.Minute {
|
||||
_, _ = fmt.Fprintf(
|
||||
inv.Stdout,
|
||||
"Please specify a duration of at least 30 minutes.\n",
|
||||
@ -267,7 +268,7 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
|
||||
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{
|
||||
Deadline: newDeadline,
|
||||
}); err != nil {
|
||||
@ -281,7 +282,7 @@ func (r *RootCmd) scheduleOverride() *serpent.Command {
|
||||
return displaySchedule(updated, inv.Stdout)
|
||||
},
|
||||
}
|
||||
return overrideCmd
|
||||
return extendCmd
|
||||
}
|
||||
|
||||
func displaySchedule(ws codersdk.Workspace, out io.Writer) error {
|
||||
|
@ -332,6 +332,18 @@ func TestScheduleModify(t *testing.T) {
|
||||
|
||||
//nolint:paralleltest // t.Setenv
|
||||
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
|
||||
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
|
||||
t.Setenv("TZ", "Asia/Kolkata")
|
||||
@ -347,7 +359,7 @@ func TestScheduleOverride(t *testing.T) {
|
||||
|
||||
// When: we override the stop schedule
|
||||
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)
|
||||
@ -360,4 +372,6 @@ func TestScheduleOverride(t *testing.T) {
|
||||
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
|
||||
pty.ExpectMatch("8h")
|
||||
pty.ExpectMatch(expectedDeadline)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
5
cli/testdata/coder_schedule_--help.golden
vendored
5
cli/testdata/coder_schedule_--help.golden
vendored
@ -1,13 +1,12 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
USAGE:
|
||||
coder schedule { show | start | stop | override } <workspace>
|
||||
coder schedule { show | start | stop | extend } <workspace>
|
||||
|
||||
Schedule automated start and stop times for workspaces
|
||||
|
||||
SUBCOMMANDS:
|
||||
override-stop Override the stop time of a currently running workspace
|
||||
instance.
|
||||
extend Extend the stop time of a currently running workspace instance.
|
||||
show Show workspace schedules
|
||||
start Edit workspace start schedule
|
||||
stop Edit workspace stop schedule
|
||||
|
@ -1,15 +1,17 @@
|
||||
coder v0.0.0-devel
|
||||
|
||||
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 must be at least 30 minutes in the future.
|
||||
* 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.
|
@ -1200,18 +1200,6 @@ func (api *API) putExtendWorkspace(rw http.ResponseWriter, r *http.Request) {
|
||||
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()
|
||||
if err := validWorkspaceDeadline(job.CompletedAt.Time, newDeadline); err != nil {
|
||||
// NOTE(Cian): Putting the error in the Message field on request from the FE folks.
|
||||
|
@ -1194,9 +1194,9 @@
|
||||
"path": "reference/cli/schedule.md"
|
||||
},
|
||||
{
|
||||
"title": "schedule override-stop",
|
||||
"description": "Override the stop time of a currently running workspace instance.",
|
||||
"path": "reference/cli/schedule_override-stop.md"
|
||||
"title": "schedule extend",
|
||||
"description": "Extend the stop time of a currently running workspace instance.",
|
||||
"path": "reference/cli/schedule_extend.md"
|
||||
},
|
||||
{
|
||||
"title": "schedule show",
|
||||
|
6
docs/reference/cli/schedule.md
generated
6
docs/reference/cli/schedule.md
generated
@ -7,14 +7,14 @@ Schedule automated start and stop times for workspaces
|
||||
## Usage
|
||||
|
||||
```console
|
||||
coder schedule { show | start | stop | override } <workspace>
|
||||
coder schedule { show | start | stop | extend } <workspace>
|
||||
```
|
||||
|
||||
## Subcommands
|
||||
|
||||
| Name | Purpose |
|
||||
| --------------------------------------------------------- | ----------------------------------------------------------------- |
|
||||
| ------------------------------------------- | --------------------------------------------------------------- |
|
||||
| [<code>show</code>](./schedule_show.md) | Show workspace schedules |
|
||||
| [<code>start</code>](./schedule_start.md) | Edit workspace start 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. |
|
||||
|
@ -1,13 +1,17 @@
|
||||
<!-- 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
|
||||
|
||||
```console
|
||||
coder schedule override-stop <workspace-name> <duration from now>
|
||||
coder schedule extend <workspace-name> <duration from now>
|
||||
```
|
||||
|
||||
## 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 workspace template may restrict the maximum workspace runtime.
|
||||
|
||||
$ coder schedule override-stop my-workspace 90m
|
||||
$ coder schedule extend my-workspace 90m
|
||||
```
|
@ -1391,35 +1391,6 @@ func TestTemplateDoesNotAllowUserAutostop(t *testing.T) {
|
||||
require.Equal(t, templateTTL, template.DefaultTTLMillis)
|
||||
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.
|
||||
|
@ -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 = {
|
||||
play: async ({ canvasElement, step }) => {
|
||||
const canvas = within(canvasElement);
|
||||
|
@ -234,9 +234,7 @@ export const WorkspaceTopbar: FC<WorkspaceProps> = ({
|
||||
<WorkspaceScheduleControls
|
||||
workspace={workspace}
|
||||
template={template}
|
||||
canUpdateSchedule={
|
||||
canUpdateWorkspace && template.allow_user_autostop
|
||||
}
|
||||
canUpdateSchedule={canUpdateWorkspace}
|
||||
/>
|
||||
<WorkspaceNotifications
|
||||
workspace={workspace}
|
||||
|
Reference in New Issue
Block a user