Skip to content

Commit af67280

Browse files
authored
autostart/autostop: move to traditional 5-valued cron string for compatibility (coder#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 coder#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

+26-13
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

+16-34
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

+27-14
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

+15-33
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)