Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 14 additions & 13 deletions cli/schedule.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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)
Expand All @@ -65,7 +65,7 @@ func (r *RootCmd) schedules() *serpent.Command {
r.scheduleShow(),
r.scheduleStart(),
r.scheduleStop(),
r.scheduleOverride(),
r.scheduleExtend(),
},
}

Expand Down Expand Up @@ -229,22 +229,23 @@ 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(
serpent.RequireNArgs(2),
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
}
Expand All @@ -259,15 +260,15 @@ 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",
)
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 {
Expand All @@ -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 {
Expand Down
70 changes: 42 additions & 28 deletions cli/schedule_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -332,32 +332,46 @@ func TestScheduleModify(t *testing.T) {

//nolint:paralleltest // t.Setenv
func TestScheduleOverride(t *testing.T) {
// Given
// Set timezone to Asia/Kolkata to surface any timezone-related bugs.
t.Setenv("TZ", "Asia/Kolkata")
loc, err := tz.TimezoneIANA()
require.NoError(t, err)
require.Equal(t, "Asia/Kolkata", loc.String())
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
require.NoError(t, err, "invalid schedule")
ownerClient, _, _, ws := setupTestSchedule(t, sched)
now := time.Now()
// To avoid the likelihood of time-related flakes, only matching up to the hour.
expectedDeadline := time.Now().In(loc).Add(10 * time.Hour).Format("2006-01-02T15:")

// When: we override the stop schedule
inv, root := clitest.New(t,
"schedule", "override-stop", ws[0].OwnerName+"/"+ws[0].Name, "10h",
)

clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())

// Then: the updated schedule should be shown
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(expectedDeadline)
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")
loc, err := tz.TimezoneIANA()
require.NoError(t, err)
require.Equal(t, "Asia/Kolkata", loc.String())
sched, err := cron.Weekly("CRON_TZ=Europe/Dublin 30 7 * * Mon-Fri")
require.NoError(t, err, "invalid schedule")
ownerClient, _, _, ws := setupTestSchedule(t, sched)
now := time.Now()
// To avoid the likelihood of time-related flakes, only matching up to the hour.
expectedDeadline := time.Now().In(loc).Add(10 * time.Hour).Format("2006-01-02T15:")

// When: we override the stop schedule
inv, root := clitest.New(t,
"schedule", tt.command, ws[0].OwnerName+"/"+ws[0].Name, "10h",
)

clitest.SetupConfig(t, ownerClient, root)
pty := ptytest.New(t).Attach(inv)
require.NoError(t, inv.Run())

// Then: the updated schedule should be shown
pty.ExpectMatch(ws[0].OwnerName + "/" + ws[0].Name)
pty.ExpectMatch(sched.Humanize())
pty.ExpectMatch(sched.Next(now).In(loc).Format(time.RFC3339))
pty.ExpectMatch("8h")
pty.ExpectMatch(expectedDeadline)
})
}
}
11 changes: 5 additions & 6 deletions cli/testdata/coder_schedule_--help.golden
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
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.
show Show workspace schedules
start Edit workspace start schedule
stop Edit workspace stop schedule
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

———
Run `coder --help` for a list of global options.
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 0 additions & 12 deletions coderd/workspaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
6 changes: 3 additions & 3 deletions docs/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -1189,9 +1189,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",
Expand Down
14 changes: 7 additions & 7 deletions docs/reference/cli/schedule.md

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

29 changes: 0 additions & 29 deletions enterprise/coderd/workspaces_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1389,35 +1389,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")
})
}

// Blocked by autostart requirements
Expand Down
18 changes: 0 additions & 18 deletions site/src/pages/WorkspacePage/WorkspaceTopbar.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
4 changes: 1 addition & 3 deletions site/src/pages/WorkspacePage/WorkspaceTopbar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand Down
Loading