Skip to content

Commit af67280

Browse files
authored
autostart/autostop: move to traditional 5-valued cron string for compatibility (#1049)
This PR modfies the original 3-valued cron strings used in package schedule to be traditional 5-valued cron strings. - schedule.Weekly will validate that the month and dom fields are equal to * - cli autostart/autostop will attempt to detect local timezone using TZ env var, defaulting to UTC - cli autostart/autostop no longer accepts a raw schedule -- instead use the --minute, --hour, --dow, and --tz arguments. - Default schedules are provided that should suffice for most users. Fixes #993
1 parent 3311c2f commit af67280

File tree

11 files changed

+170
-150
lines changed

11 files changed

+170
-150
lines changed

cli/workspaceautostart.go

Lines changed: 26 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"fmt"
5+
"os"
56
"time"
67

78
"github.com/spf13/cobra"
@@ -11,20 +12,16 @@ import (
1112
)
1213

1314
const autostartDescriptionLong = `To have your workspace build automatically at a regular time you can enable autostart.
14-
When enabling autostart, provide a schedule. This schedule is in cron format except only
15-
the following fields are allowed:
16-
- minute
17-
- hour
18-
- day of week
19-
20-
For example, to start your workspace every weekday at 9.30 am, provide the schedule '30 9 1-5'.`
15+
When enabling autostart, provide the minute, hour, and day(s) of week.
16+
The default schedule is at 09:00 in your local timezone (TZ env, UTC by default).
17+
`
2118

2219
func workspaceAutostart() *cobra.Command {
2320
autostartCmd := &cobra.Command{
24-
Use: "autostart enable <workspace> <schedule>",
21+
Use: "autostart enable <workspace>",
2522
Short: "schedule a workspace to automatically start at a regular time",
2623
Long: autostartDescriptionLong,
27-
Example: "coder workspaces autostart enable my-workspace '30 9 1-5'",
24+
Example: "coder workspaces autostart enable my-workspace --minute 30 --hour 9 --days 1-5 --tz Europe/Dublin",
2825
Hidden: true, // TODO(cian): un-hide when autostart scheduling implemented
2926
}
3027

@@ -35,22 +32,28 @@ func workspaceAutostart() *cobra.Command {
3532
}
3633

3734
func workspaceAutostartEnable() *cobra.Command {
38-
return &cobra.Command{
35+
// yes some of these are technically numbers but the cron library will do that work
36+
var autostartMinute string
37+
var autostartHour string
38+
var autostartDayOfWeek string
39+
var autostartTimezone string
40+
cmd := &cobra.Command{
3941
Use: "enable <workspace_name> <schedule>",
4042
ValidArgsFunction: validArgsWorkspaceName,
41-
Args: cobra.ExactArgs(2),
43+
Args: cobra.ExactArgs(1),
4244
RunE: func(cmd *cobra.Command, args []string) error {
4345
client, err := createClient(cmd)
4446
if err != nil {
4547
return err
4648
}
4749

48-
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
50+
spec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", autostartTimezone, autostartMinute, autostartHour, autostartDayOfWeek)
51+
validSchedule, err := schedule.Weekly(spec)
4952
if err != nil {
5053
return err
5154
}
5255

53-
validSchedule, err := schedule.Weekly(args[1])
56+
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
5457
if err != nil {
5558
return err
5659
}
@@ -67,6 +70,16 @@ func workspaceAutostartEnable() *cobra.Command {
6770
return nil
6871
},
6972
}
73+
74+
cmd.Flags().StringVar(&autostartMinute, "minute", "0", "autostart minute")
75+
cmd.Flags().StringVar(&autostartHour, "hour", "9", "autostart hour")
76+
cmd.Flags().StringVar(&autostartDayOfWeek, "days", "1-5", "autostart day(s) of week")
77+
tzEnv := os.Getenv("TZ")
78+
if tzEnv == "" {
79+
tzEnv = "UTC"
80+
}
81+
cmd.Flags().StringVar(&autostartTimezone, "tz", tzEnv, "autostart timezone")
82+
return cmd
7083
}
7184

7285
func workspaceAutostartDisable() *cobra.Command {

cli/workspaceautostart_test.go

Lines changed: 16 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package cli_test
33
import (
44
"bytes"
55
"context"
6+
"fmt"
7+
"os"
68
"testing"
79

810
"github.com/stretchr/testify/require"
@@ -27,11 +29,13 @@ func TestWorkspaceAutostart(t *testing.T) {
2729
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
2830
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
2931
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
30-
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
32+
tz = "Europe/Dublin"
33+
cmdArgs = []string{"workspaces", "autostart", "enable", workspace.Name, "--minute", "30", "--hour", "9", "--days", "1-5", "--tz", tz}
34+
sched = "CRON_TZ=Europe/Dublin 30 9 * * 1-5"
3135
stdoutBuf = &bytes.Buffer{}
3236
)
3337

34-
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name, sched)
38+
cmd, root := clitest.New(t, cmdArgs...)
3539
clitest.SetupConfig(t, client, root)
3640
cmd.SetOut(stdoutBuf)
3741

@@ -68,10 +72,9 @@ func TestWorkspaceAutostart(t *testing.T) {
6872
user = coderdtest.CreateFirstUser(t, client)
6973
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
7074
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
71-
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
7275
)
7376

74-
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", "doesnotexist", sched)
77+
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", "doesnotexist")
7578
clitest.SetupConfig(t, client, root)
7679

7780
err := cmd.Execute()
@@ -96,34 +99,7 @@ func TestWorkspaceAutostart(t *testing.T) {
9699
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
97100
})
98101

99-
t.Run("Enable_InvalidSchedule", func(t *testing.T) {
100-
t.Parallel()
101-
102-
var (
103-
ctx = context.Background()
104-
client = coderdtest.New(t, nil)
105-
_ = coderdtest.NewProvisionerDaemon(t, client)
106-
user = coderdtest.CreateFirstUser(t, client)
107-
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
108-
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
109-
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
110-
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
111-
sched = "sdfasdfasdf asdf asdf"
112-
)
113-
114-
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name, sched)
115-
clitest.SetupConfig(t, client, root)
116-
117-
err := cmd.Execute()
118-
require.ErrorContains(t, err, "failed to parse int from sdfasdfasdf: strconv.Atoi:", "unexpected error")
119-
120-
// Ensure nothing happened
121-
updated, err := client.Workspace(ctx, workspace.ID)
122-
require.NoError(t, err, "fetch updated workspace")
123-
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to be empty")
124-
})
125-
126-
t.Run("Enable_NoSchedule", func(t *testing.T) {
102+
t.Run("Enable_DefaultSchedule", func(t *testing.T) {
127103
t.Parallel()
128104

129105
var (
@@ -137,15 +113,21 @@ func TestWorkspaceAutostart(t *testing.T) {
137113
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
138114
)
139115

116+
// check current TZ env var
117+
currTz := os.Getenv("TZ")
118+
if currTz == "" {
119+
currTz = "UTC"
120+
}
121+
expectedSchedule := fmt.Sprintf("CRON_TZ=%s 0 9 * * 1-5", currTz)
140122
cmd, root := clitest.New(t, "workspaces", "autostart", "enable", workspace.Name)
141123
clitest.SetupConfig(t, client, root)
142124

143125
err := cmd.Execute()
144-
require.ErrorContains(t, err, "accepts 2 arg(s), received 1", "unexpected error")
126+
require.NoError(t, err, "unexpected error")
145127

146128
// Ensure nothing happened
147129
updated, err := client.Workspace(ctx, workspace.ID)
148130
require.NoError(t, err, "fetch updated workspace")
149-
require.Empty(t, updated.AutostartSchedule, "expected autostart schedule to be empty")
131+
require.Equal(t, expectedSchedule, updated.AutostartSchedule, "expected default autostart schedule")
150132
})
151133
}

cli/workspaceautostop.go

Lines changed: 27 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cli
22

33
import (
44
"fmt"
5+
"os"
56
"time"
67

78
"github.com/spf13/cobra"
@@ -11,20 +12,16 @@ import (
1112
)
1213

1314
const autostopDescriptionLong = `To have your workspace stop automatically at a regular time you can enable autostop.
14-
When enabling autostop, provide a schedule. This schedule is in cron format except only
15-
the following fields are allowed:
16-
- minute
17-
- hour
18-
- day of week
19-
20-
For example, to stop your workspace every weekday at 5.30 pm, provide the schedule '30 17 1-5'.`
15+
When enabling autostop, provide the minute, hour, and day(s) of week.
16+
The default autostop schedule is at 18:00 in your local timezone (TZ env, UTC by default).
17+
`
2118

2219
func workspaceAutostop() *cobra.Command {
2320
autostopCmd := &cobra.Command{
24-
Use: "autostop enable <workspace> <schedule>",
25-
Short: "schedule a workspace to automatically start at a regular time",
21+
Use: "autostop enable <workspace>",
22+
Short: "schedule a workspace to automatically stop at a regular time",
2623
Long: autostopDescriptionLong,
27-
Example: "coder workspaces autostop enable my-workspace '30 17 1-5'",
24+
Example: "coder workspaces autostop enable my-workspace --minute 0 --hour 18 --days 1-5 -tz Europe/Dublin",
2825
Hidden: true, // TODO(cian): un-hide when autostop scheduling implemented
2926
}
3027

@@ -35,22 +32,28 @@ func workspaceAutostop() *cobra.Command {
3532
}
3633

3734
func workspaceAutostopEnable() *cobra.Command {
38-
return &cobra.Command{
35+
// yes some of these are technically numbers but the cron library will do that work
36+
var autostopMinute string
37+
var autostopHour string
38+
var autostopDayOfWeek string
39+
var autostopTimezone string
40+
cmd := &cobra.Command{
3941
Use: "enable <workspace_name> <schedule>",
4042
ValidArgsFunction: validArgsWorkspaceName,
41-
Args: cobra.ExactArgs(2),
43+
Args: cobra.ExactArgs(1),
4244
RunE: func(cmd *cobra.Command, args []string) error {
4345
client, err := createClient(cmd)
4446
if err != nil {
4547
return err
4648
}
4749

48-
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
50+
spec := fmt.Sprintf("CRON_TZ=%s %s %s * * %s", autostopTimezone, autostopMinute, autostopHour, autostopDayOfWeek)
51+
validSchedule, err := schedule.Weekly(spec)
4952
if err != nil {
5053
return err
5154
}
5255

53-
validSchedule, err := schedule.Weekly(args[1])
56+
workspace, err := client.WorkspaceByName(cmd.Context(), codersdk.Me, args[0])
5457
if err != nil {
5558
return err
5659
}
@@ -67,6 +70,16 @@ func workspaceAutostopEnable() *cobra.Command {
6770
return nil
6871
},
6972
}
73+
74+
cmd.Flags().StringVar(&autostopMinute, "minute", "0", "autostop minute")
75+
cmd.Flags().StringVar(&autostopHour, "hour", "18", "autostop hour")
76+
cmd.Flags().StringVar(&autostopDayOfWeek, "days", "1-5", "autostop day(s) of week")
77+
tzEnv := os.Getenv("TZ")
78+
if tzEnv == "" {
79+
tzEnv = "UTC"
80+
}
81+
cmd.Flags().StringVar(&autostopTimezone, "tz", tzEnv, "autostop timezone")
82+
return cmd
7083
}
7184

7285
func workspaceAutostopDisable() *cobra.Command {

cli/workspaceautostop_test.go

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ package cli_test
33
import (
44
"bytes"
55
"context"
6+
"fmt"
7+
"os"
68
"testing"
79

810
"github.com/stretchr/testify/require"
@@ -27,11 +29,12 @@ func TestWorkspaceAutostop(t *testing.T) {
2729
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
2830
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
2931
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
30-
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
32+
cmdArgs = []string{"workspaces", "autostop", "enable", workspace.Name, "--minute", "30", "--hour", "17", "--days", "1-5", "--tz", "Europe/Dublin"}
33+
sched = "CRON_TZ=Europe/Dublin 30 17 * * 1-5"
3134
stdoutBuf = &bytes.Buffer{}
3235
)
3336

34-
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", workspace.Name, sched)
37+
cmd, root := clitest.New(t, cmdArgs...)
3538
clitest.SetupConfig(t, client, root)
3639
cmd.SetOut(stdoutBuf)
3740

@@ -68,10 +71,9 @@ func TestWorkspaceAutostop(t *testing.T) {
6871
user = coderdtest.CreateFirstUser(t, client)
6972
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
7073
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
71-
sched = "CRON_TZ=Europe/Dublin 30 9 1-5"
7274
)
7375

74-
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", "doesnotexist", sched)
76+
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", "doesnotexist")
7577
clitest.SetupConfig(t, client, root)
7678

7779
err := cmd.Execute()
@@ -96,7 +98,7 @@ func TestWorkspaceAutostop(t *testing.T) {
9698
require.ErrorContains(t, err, "status code 404: no workspace found by name", "unexpected error")
9799
})
98100

99-
t.Run("Enable_InvalidSchedule", func(t *testing.T) {
101+
t.Run("Enable_DefaultSchedule", func(t *testing.T) {
100102
t.Parallel()
101103

102104
var (
@@ -108,44 +110,24 @@ func TestWorkspaceAutostop(t *testing.T) {
108110
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
109111
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
110112
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
111-
sched = "sdfasdfasdf asdf asdf"
112113
)
113114

114-
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", workspace.Name, sched)
115-
clitest.SetupConfig(t, client, root)
116-
117-
err := cmd.Execute()
118-
require.ErrorContains(t, err, "failed to parse int from sdfasdfasdf: strconv.Atoi:", "unexpected error")
119-
120-
// Ensure nothing happened
121-
updated, err := client.Workspace(ctx, workspace.ID)
122-
require.NoError(t, err, "fetch updated workspace")
123-
require.Empty(t, updated.AutostopSchedule, "expected autostop schedule to be empty")
124-
})
125-
126-
t.Run("Enable_NoSchedule", func(t *testing.T) {
127-
t.Parallel()
128-
129-
var (
130-
ctx = context.Background()
131-
client = coderdtest.New(t, nil)
132-
_ = coderdtest.NewProvisionerDaemon(t, client)
133-
user = coderdtest.CreateFirstUser(t, client)
134-
version = coderdtest.CreateTemplateVersion(t, client, user.OrganizationID, nil)
135-
_ = coderdtest.AwaitTemplateVersionJob(t, client, version.ID)
136-
project = coderdtest.CreateTemplate(t, client, user.OrganizationID, version.ID)
137-
workspace = coderdtest.CreateWorkspace(t, client, codersdk.Me, project.ID)
138-
)
115+
// check current TZ env var
116+
currTz := os.Getenv("TZ")
117+
if currTz == "" {
118+
currTz = "UTC"
119+
}
120+
expectedSchedule := fmt.Sprintf("CRON_TZ=%s 0 18 * * 1-5", currTz)
139121

140122
cmd, root := clitest.New(t, "workspaces", "autostop", "enable", workspace.Name)
141123
clitest.SetupConfig(t, client, root)
142124

143125
err := cmd.Execute()
144-
require.ErrorContains(t, err, "accepts 2 arg(s), received 1", "unexpected error")
126+
require.NoError(t, err, "unexpected error")
145127

146128
// Ensure nothing happened
147129
updated, err := client.Workspace(ctx, workspace.ID)
148130
require.NoError(t, err, "fetch updated workspace")
149-
require.Empty(t, updated.AutostopSchedule, "expected autostop schedule to be empty")
131+
require.Equal(t, expectedSchedule, updated.AutostopSchedule, "expected default autostop schedule")
150132
})
151133
}

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