@@ -2,18 +2,27 @@ package cli
2
2
3
3
import (
4
4
"fmt"
5
- "os "
5
+ "strings "
6
6
"time"
7
7
8
8
"github.com/spf13/cobra"
9
+ "golang.org/x/xerrors"
9
10
10
11
"github.com/coder/coder/coderd/autobuild/schedule"
12
+ "github.com/coder/coder/coderd/util/ptr"
13
+ "github.com/coder/coder/coderd/util/tz"
11
14
"github.com/coder/coder/codersdk"
12
15
)
13
16
14
17
const autostartDescriptionLong = `To have your workspace build automatically at a regular time you can enable autostart.
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).
18
+ When enabling autostart, enter a schedule in the format: [day-of-week] start-time [location].
19
+ * Start-time (required) is accepted either in 12-hour (hh:mm{am|pm}) format, or 24-hour format {hh:mm}.
20
+ * Day-of-week (optional) allows specifying in the cron format, e.g. 1,3,5 or Mon-Fri.
21
+ Aliases such as @daily are not supported.
22
+ Default: * (every day)
23
+ * Location (optional) must be a valid location in the IANA timezone database.
24
+ If omitted, we will fall back to either the TZ environment variable or /etc/localtime.
25
+ You can check your corresponding location by visiting https://ipinfo.io - it shows in the demo widget on the right.
17
26
`
18
27
19
28
func autostart () * cobra.Command {
@@ -22,12 +31,12 @@ func autostart() *cobra.Command {
22
31
Use : "autostart enable <workspace>" ,
23
32
Short : "schedule a workspace to automatically start at a regular time" ,
24
33
Long : autostartDescriptionLong ,
25
- Example : "coder autostart enable my-workspace --minute 30 --hour 9 --days 1-5 --tz Europe/Dublin" ,
34
+ Example : "coder autostart set my-workspace Mon-Fri 9:30AM Europe/Dublin" ,
26
35
}
27
36
28
37
autostartCmd .AddCommand (autostartShow ())
29
- autostartCmd .AddCommand (autostartEnable ())
30
- autostartCmd .AddCommand (autostartDisable ())
38
+ autostartCmd .AddCommand (autostartSet ())
39
+ autostartCmd .AddCommand (autostartUnset ())
31
40
32
41
return autostartCmd
33
42
}
@@ -75,23 +84,17 @@ func autostartShow() *cobra.Command {
75
84
return cmd
76
85
}
77
86
78
- func autostartEnable () * cobra.Command {
79
- // yes some of these are technically numbers but the cron library will do that work
80
- var autostartMinute string
81
- var autostartHour string
82
- var autostartDayOfWeek string
83
- var autostartTimezone string
87
+ func autostartSet () * cobra.Command {
84
88
cmd := & cobra.Command {
85
- Use : "enable <workspace_name> <schedule> " ,
86
- Args : cobra .ExactArgs ( 1 ),
89
+ Use : "set <workspace_name> " ,
90
+ Args : cobra .MinimumNArgs ( 2 ),
87
91
RunE : func (cmd * cobra.Command , args []string ) error {
88
92
client , err := createClient (cmd )
89
93
if err != nil {
90
94
return err
91
95
}
92
96
93
- spec := fmt .Sprintf ("CRON_TZ=%s %s %s * * %s" , autostartTimezone , autostartMinute , autostartHour , autostartDayOfWeek )
94
- validSchedule , err := schedule .Weekly (spec )
97
+ sched , err := parseCLISchedule (args [1 :]... )
95
98
if err != nil {
96
99
return err
97
100
}
@@ -102,32 +105,24 @@ func autostartEnable() *cobra.Command {
102
105
}
103
106
104
107
err = client .UpdateWorkspaceAutostart (cmd .Context (), workspace .ID , codersdk.UpdateWorkspaceAutostartRequest {
105
- Schedule : & spec ,
108
+ Schedule : ptr . Ref ( sched . String ()) ,
106
109
})
107
110
if err != nil {
108
111
return err
109
112
}
110
113
111
- _ , _ = fmt .Fprintf (cmd .OutOrStdout (), "\n The %s workspace will automatically start at %s.\n \n " , workspace .Name , validSchedule .Next (time .Now ()))
114
+ _ , _ = fmt .Fprintf (cmd .OutOrStdout (), "\n The %s workspace will automatically start at %s.\n \n " , workspace .Name , sched .Next (time .Now ()))
112
115
113
116
return nil
114
117
},
115
118
}
116
119
117
- cmd .Flags ().StringVar (& autostartMinute , "minute" , "0" , "autostart minute" )
118
- cmd .Flags ().StringVar (& autostartHour , "hour" , "9" , "autostart hour" )
119
- cmd .Flags ().StringVar (& autostartDayOfWeek , "days" , "1-5" , "autostart day(s) of week" )
120
- tzEnv := os .Getenv ("TZ" )
121
- if tzEnv == "" {
122
- tzEnv = "UTC"
123
- }
124
- cmd .Flags ().StringVar (& autostartTimezone , "tz" , tzEnv , "autostart timezone" )
125
120
return cmd
126
121
}
127
122
128
- func autostartDisable () * cobra.Command {
123
+ func autostartUnset () * cobra.Command {
129
124
return & cobra.Command {
130
- Use : "disable <workspace_name>" ,
125
+ Use : "unset <workspace_name>" ,
131
126
Args : cobra .ExactArgs (1 ),
132
127
RunE : func (cmd * cobra.Command , args []string ) error {
133
128
client , err := createClient (cmd )
@@ -153,3 +148,92 @@ func autostartDisable() *cobra.Command {
153
148
},
154
149
}
155
150
}
151
+
152
+ var errInvalidScheduleFormat = xerrors .New ("Schedule must be in the format Mon-Fri 09:00AM America/Chicago" )
153
+ var errInvalidTimeFormat = xerrors .New ("Start time must be in the format hh:mm[am|pm] or HH:MM" )
154
+
155
+ // parseCLISchedule parses a schedule in the format Mon-Fri 09:00AM America/Chicago.
156
+ func parseCLISchedule (parts ... string ) (* schedule.Schedule , error ) {
157
+ // If the user was careful and quoted the schedule, un-quote it.
158
+ // In the case that only time was specified, this will be a no-op.
159
+ if len (parts ) == 1 {
160
+ parts = strings .Fields (parts [0 ])
161
+ }
162
+ timezone := ""
163
+ dayOfWeek := "*"
164
+ var hour , minute int
165
+ switch len (parts ) {
166
+ case 1 :
167
+ t , err := parseTime (parts [0 ])
168
+ if err != nil {
169
+ return nil , err
170
+ }
171
+ hour , minute = t .Hour (), t .Minute ()
172
+ case 2 :
173
+ if ! strings .Contains (parts [0 ], ":" ) {
174
+ // DOW + Time
175
+ t , err := parseTime (parts [1 ])
176
+ if err != nil {
177
+ return nil , err
178
+ }
179
+ hour , minute = t .Hour (), t .Minute ()
180
+ dayOfWeek = parts [0 ]
181
+ } else {
182
+ // Time + TZ
183
+ t , err := parseTime (parts [0 ])
184
+ if err != nil {
185
+ return nil , err
186
+ }
187
+ hour , minute = t .Hour (), t .Minute ()
188
+ timezone = parts [1 ]
189
+ }
190
+ case 3 :
191
+ // DOW + Time + TZ
192
+ t , err := parseTime (parts [1 ])
193
+ if err != nil {
194
+ return nil , err
195
+ }
196
+ hour , minute = t .Hour (), t .Minute ()
197
+ dayOfWeek = parts [0 ]
198
+ timezone = parts [2 ]
199
+ default :
200
+ return nil , errInvalidScheduleFormat
201
+ }
202
+
203
+ // If timezone was not specified, attempt to automatically determine it as a last resort.
204
+ if timezone == "" {
205
+ loc , err := tz .TimezoneIANA ()
206
+ if err != nil {
207
+ return nil , xerrors .Errorf ("Could not automatically determine your timezone." )
208
+ }
209
+ timezone = loc .String ()
210
+ }
211
+
212
+ sched , err := schedule .Weekly (fmt .Sprintf (
213
+ "CRON_TZ=%s %d %d * * %s" ,
214
+ timezone ,
215
+ minute ,
216
+ hour ,
217
+ dayOfWeek ,
218
+ ))
219
+ if err != nil {
220
+ // This will either be an invalid dayOfWeek or an invalid timezone.
221
+ return nil , xerrors .Errorf ("Invalid schedule: %w" , err )
222
+ }
223
+
224
+ return sched , nil
225
+ }
226
+
227
+ func parseTime (s string ) (time.Time , error ) {
228
+ // Assume only time provided, HH:MM[AM|PM]
229
+ t , err := time .Parse (time .Kitchen , s )
230
+ if err == nil {
231
+ return t , nil
232
+ }
233
+ // Try 24-hour format without AM/PM suffix.
234
+ t , err = time .Parse ("15:04" , s )
235
+ if err != nil {
236
+ return time.Time {}, errInvalidTimeFormat
237
+ }
238
+ return t , nil
239
+ }
0 commit comments