@@ -2,32 +2,41 @@ 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: <start-time> [day-of-week] [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 {
20
29
autostartCmd := & cobra.Command {
21
30
Annotations : workspaceCommand ,
22
- Use : "autostart enable <workspace>" ,
31
+ Use : "autostart set <workspace> <start-time> [day-of-week] [location] " ,
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 9:30AM Mon-Fri 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
}
@@ -60,13 +69,12 @@ func autostartShow() *cobra.Command {
60
69
}
61
70
62
71
next := validSchedule .Next (time .Now ())
63
- loc , _ := time .LoadLocation (validSchedule .Timezone ())
64
72
65
73
_ , _ = fmt .Fprintf (cmd .OutOrStdout (),
66
74
"schedule: %s\n timezone: %s\n next: %s\n " ,
67
75
validSchedule .Cron (),
68
- validSchedule .Timezone (),
69
- next .In (loc ),
76
+ validSchedule .Location (),
77
+ next .In (validSchedule . Location () ),
70
78
)
71
79
72
80
return nil
@@ -75,23 +83,17 @@ func autostartShow() *cobra.Command {
75
83
return cmd
76
84
}
77
85
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
86
+ func autostartSet () * cobra.Command {
84
87
cmd := & cobra.Command {
85
- Use : "enable <workspace_name> <schedule> " ,
86
- Args : cobra .ExactArgs ( 1 ),
88
+ Use : "set <workspace_name> <start-time> [day-of-week] [location] " ,
89
+ Args : cobra .RangeArgs ( 2 , 4 ),
87
90
RunE : func (cmd * cobra.Command , args []string ) error {
88
91
client , err := createClient (cmd )
89
92
if err != nil {
90
93
return err
91
94
}
92
95
93
- spec := fmt .Sprintf ("CRON_TZ=%s %s %s * * %s" , autostartTimezone , autostartMinute , autostartHour , autostartDayOfWeek )
94
- validSchedule , err := schedule .Weekly (spec )
96
+ sched , err := parseCLISchedule (args [1 :]... )
95
97
if err != nil {
96
98
return err
97
99
}
@@ -102,32 +104,30 @@ func autostartEnable() *cobra.Command {
102
104
}
103
105
104
106
err = client .UpdateWorkspaceAutostart (cmd .Context (), workspace .ID , codersdk.UpdateWorkspaceAutostartRequest {
105
- Schedule : & spec ,
107
+ Schedule : ptr . Ref ( sched . String ()) ,
106
108
})
107
109
if err != nil {
108
110
return err
109
111
}
110
112
111
- _ , _ = fmt .Fprintf (cmd .OutOrStdout (), "\n The %s workspace will automatically start at %s.\n \n " , workspace .Name , validSchedule .Next (time .Now ()))
112
-
113
+ schedNext := sched .Next (time .Now ())
114
+ _ , _ = fmt .Fprintf (cmd .OutOrStdout (),
115
+ "%s will automatically start at %s %s (%s)\n " ,
116
+ workspace .Name ,
117
+ schedNext .In (sched .Location ()).Format (time .Kitchen ),
118
+ sched .DaysOfWeek (),
119
+ sched .Location ().String (),
120
+ )
113
121
return nil
114
122
},
115
123
}
116
124
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
125
return cmd
126
126
}
127
127
128
- func autostartDisable () * cobra.Command {
128
+ func autostartUnset () * cobra.Command {
129
129
return & cobra.Command {
130
- Use : "disable <workspace_name>" ,
130
+ Use : "unset <workspace_name>" ,
131
131
Args : cobra .ExactArgs (1 ),
132
132
RunE : func (cmd * cobra.Command , args []string ) error {
133
133
client , err := createClient (cmd )
@@ -147,9 +147,98 @@ func autostartDisable() *cobra.Command {
147
147
return err
148
148
}
149
149
150
- _ , _ = fmt .Fprintf (cmd .OutOrStdout (), "\n The %s workspace will no longer automatically start.\n \n " , workspace .Name )
150
+ _ , _ = fmt .Fprintf (cmd .OutOrStdout (), "%s will no longer automatically start.\n " , workspace .Name )
151
151
152
152
return nil
153
153
},
154
154
}
155
155
}
156
+
157
+ var errInvalidScheduleFormat = xerrors .New ("Schedule must be in the format Mon-Fri 09:00AM America/Chicago" )
158
+ var errInvalidTimeFormat = xerrors .New ("Start time must be in the format hh:mm[am|pm] or HH:MM" )
159
+ var errUnsupportedTimezone = xerrors .New ("The location you provided looks like a timezone. Check https://ipinfo.io for your location." )
160
+
161
+ // parseCLISchedule parses a schedule in the format HH:MM{AM|PM} [DOW] [LOCATION]
162
+ func parseCLISchedule (parts ... string ) (* schedule.Schedule , error ) {
163
+ // If the user was careful and quoted the schedule, un-quote it.
164
+ // In the case that only time was specified, this will be a no-op.
165
+ if len (parts ) == 1 {
166
+ parts = strings .Fields (parts [0 ])
167
+ }
168
+ var loc * time.Location
169
+ dayOfWeek := "*"
170
+ t , err := parseTime (parts [0 ])
171
+ if err != nil {
172
+ return nil , err
173
+ }
174
+ hour , minute := t .Hour (), t .Minute ()
175
+
176
+ // Any additional parts get ignored.
177
+ switch len (parts ) {
178
+ case 3 :
179
+ dayOfWeek = parts [1 ]
180
+ loc , err = time .LoadLocation (parts [2 ])
181
+ if err != nil {
182
+ _ , err = time .Parse ("MST" , parts [2 ])
183
+ if err == nil {
184
+ return nil , errUnsupportedTimezone
185
+ }
186
+ return nil , xerrors .Errorf ("Invalid timezone %q specified: a valid IANA timezone is required" , parts [2 ])
187
+ }
188
+ case 2 :
189
+ // Did they provide day-of-week or location?
190
+ if maybeLoc , err := time .LoadLocation (parts [1 ]); err != nil {
191
+ // Assume day-of-week.
192
+ dayOfWeek = parts [1 ]
193
+ } else {
194
+ loc = maybeLoc
195
+ }
196
+ case 1 : // already handled
197
+ default :
198
+ return nil , errInvalidScheduleFormat
199
+ }
200
+
201
+ // If location was not specified, attempt to automatically determine it as a last resort.
202
+ if loc == nil {
203
+ loc , err = tz .TimezoneIANA ()
204
+ if err != nil {
205
+ return nil , xerrors .Errorf ("Could not automatically determine your timezone" )
206
+ }
207
+ }
208
+
209
+ sched , err := schedule .Weekly (fmt .Sprintf (
210
+ "CRON_TZ=%s %d %d * * %s" ,
211
+ loc .String (),
212
+ minute ,
213
+ hour ,
214
+ dayOfWeek ,
215
+ ))
216
+ if err != nil {
217
+ // This will either be an invalid dayOfWeek or an invalid timezone.
218
+ return nil , xerrors .Errorf ("Invalid schedule: %w" , err )
219
+ }
220
+
221
+ return sched , nil
222
+ }
223
+
224
+ func parseTime (s string ) (time.Time , error ) {
225
+ // Try a number of possible layouts.
226
+ for _ , layout := range []string {
227
+ time .Kitchen , // 03:04PM
228
+ "03:04pm" ,
229
+ "3:04PM" ,
230
+ "3:04pm" ,
231
+ "15:04" ,
232
+ "1504" ,
233
+ "03PM" ,
234
+ "03pm" ,
235
+ "3PM" ,
236
+ "3pm" ,
237
+ } {
238
+ t , err := time .Parse (layout , s )
239
+ if err == nil {
240
+ return t , nil
241
+ }
242
+ }
243
+ return time.Time {}, errInvalidTimeFormat
244
+ }
0 commit comments