Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Commit0a949aa

Browse files
authored
cli: streamline autostart ux (#2251)
This commit adds the following changes:- autostart enable|disable => autostart set|unset- autostart enable now accepts a more natual schedule format: <time> <days-of-week> <location>- autostart show now shows configured timezone- 🎉 automatic timezone detection across mac, windows, linux 🎉Fixes#1647
1 parent9d15584 commit0a949aa

File tree

10 files changed

+584
-148
lines changed

10 files changed

+584
-148
lines changed

‎cli/autostart.go‎

Lines changed: 123 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,41 @@ package cli
22

33
import (
44
"fmt"
5-
"os"
5+
"strings"
66
"time"
77

88
"github.com/spf13/cobra"
9+
"golang.org/x/xerrors"
910

1011
"github.com/coder/coder/coderd/autobuild/schedule"
12+
"github.com/coder/coder/coderd/util/ptr"
13+
"github.com/coder/coder/coderd/util/tz"
1114
"github.com/coder/coder/codersdk"
1215
)
1316

1417
constautostartDescriptionLong=`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.
1726
`
1827

1928
funcautostart()*cobra.Command {
2029
autostartCmd:=&cobra.Command{
2130
Annotations:workspaceCommand,
22-
Use:"autostartenable <workspace>",
31+
Use:"autostartset <workspace> <start-time> [day-of-week] [location]",
2332
Short:"schedule a workspace to automatically start at a regular time",
2433
Long:autostartDescriptionLong,
25-
Example:"coder autostartenable my-workspace--minute 30 --hour 9 --days 1-5 --tz Europe/Dublin",
34+
Example:"coder autostartset my-workspace9:30AM Mon-Fri Europe/Dublin",
2635
}
2736

2837
autostartCmd.AddCommand(autostartShow())
29-
autostartCmd.AddCommand(autostartEnable())
30-
autostartCmd.AddCommand(autostartDisable())
38+
autostartCmd.AddCommand(autostartSet())
39+
autostartCmd.AddCommand(autostartUnset())
3140

3241
returnautostartCmd
3342
}
@@ -60,13 +69,12 @@ func autostartShow() *cobra.Command {
6069
}
6170

6271
next:=validSchedule.Next(time.Now())
63-
loc,_:=time.LoadLocation(validSchedule.Timezone())
6472

6573
_,_=fmt.Fprintf(cmd.OutOrStdout(),
6674
"schedule: %s\ntimezone: %s\nnext: %s\n",
6775
validSchedule.Cron(),
68-
validSchedule.Timezone(),
69-
next.In(loc),
76+
validSchedule.Location(),
77+
next.In(validSchedule.Location()),
7078
)
7179

7280
returnnil
@@ -75,23 +83,17 @@ func autostartShow() *cobra.Command {
7583
returncmd
7684
}
7785

78-
funcautostartEnable()*cobra.Command {
79-
// yes some of these are technically numbers but the cron library will do that work
80-
varautostartMinutestring
81-
varautostartHourstring
82-
varautostartDayOfWeekstring
83-
varautostartTimezonestring
86+
funcautostartSet()*cobra.Command {
8487
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),
8790
RunE:func(cmd*cobra.Command,args []string)error {
8891
client,err:=createClient(cmd)
8992
iferr!=nil {
9093
returnerr
9194
}
9295

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:]...)
9597
iferr!=nil {
9698
returnerr
9799
}
@@ -102,32 +104,30 @@ func autostartEnable() *cobra.Command {
102104
}
103105

104106
err=client.UpdateWorkspaceAutostart(cmd.Context(),workspace.ID, codersdk.UpdateWorkspaceAutostartRequest{
105-
Schedule:&spec,
107+
Schedule:ptr.Ref(sched.String()),
106108
})
107109
iferr!=nil {
108110
returnerr
109111
}
110112

111-
_,_=fmt.Fprintf(cmd.OutOrStdout(),"\nThe %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+
)
113121
returnnil
114122
},
115123
}
116124

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-
iftzEnv=="" {
122-
tzEnv="UTC"
123-
}
124-
cmd.Flags().StringVar(&autostartTimezone,"tz",tzEnv,"autostart timezone")
125125
returncmd
126126
}
127127

128-
funcautostartDisable()*cobra.Command {
128+
funcautostartUnset()*cobra.Command {
129129
return&cobra.Command{
130-
Use:"disable <workspace_name>",
130+
Use:"unset <workspace_name>",
131131
Args:cobra.ExactArgs(1),
132132
RunE:func(cmd*cobra.Command,args []string)error {
133133
client,err:=createClient(cmd)
@@ -147,9 +147,98 @@ func autostartDisable() *cobra.Command {
147147
returnerr
148148
}
149149

150-
_,_=fmt.Fprintf(cmd.OutOrStdout(),"\nThe%sworkspacewill no longer automatically start.\n\n",workspace.Name)
150+
_,_=fmt.Fprintf(cmd.OutOrStdout(),"%s will no longer automatically start.\n",workspace.Name)
151151

152152
returnnil
153153
},
154154
}
155155
}
156+
157+
varerrInvalidScheduleFormat=xerrors.New("Schedule must be in the format Mon-Fri 09:00AM America/Chicago")
158+
varerrInvalidTimeFormat=xerrors.New("Start time must be in the format hh:mm[am|pm] or HH:MM")
159+
varerrUnsupportedTimezone=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+
funcparseCLISchedule(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+
iflen(parts)==1 {
166+
parts=strings.Fields(parts[0])
167+
}
168+
varloc*time.Location
169+
dayOfWeek:="*"
170+
t,err:=parseTime(parts[0])
171+
iferr!=nil {
172+
returnnil,err
173+
}
174+
hour,minute:=t.Hour(),t.Minute()
175+
176+
// Any additional parts get ignored.
177+
switchlen(parts) {
178+
case3:
179+
dayOfWeek=parts[1]
180+
loc,err=time.LoadLocation(parts[2])
181+
iferr!=nil {
182+
_,err=time.Parse("MST",parts[2])
183+
iferr==nil {
184+
returnnil,errUnsupportedTimezone
185+
}
186+
returnnil,xerrors.Errorf("Invalid timezone %q specified: a valid IANA timezone is required",parts[2])
187+
}
188+
case2:
189+
// Did they provide day-of-week or location?
190+
ifmaybeLoc,err:=time.LoadLocation(parts[1]);err!=nil {
191+
// Assume day-of-week.
192+
dayOfWeek=parts[1]
193+
}else {
194+
loc=maybeLoc
195+
}
196+
case1:// already handled
197+
default:
198+
returnnil,errInvalidScheduleFormat
199+
}
200+
201+
// If location was not specified, attempt to automatically determine it as a last resort.
202+
ifloc==nil {
203+
loc,err=tz.TimezoneIANA()
204+
iferr!=nil {
205+
returnnil,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+
iferr!=nil {
217+
// This will either be an invalid dayOfWeek or an invalid timezone.
218+
returnnil,xerrors.Errorf("Invalid schedule: %w",err)
219+
}
220+
221+
returnsched,nil
222+
}
223+
224+
funcparseTime(sstring) (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+
iferr==nil {
240+
returnt,nil
241+
}
242+
}
243+
return time.Time{},errInvalidTimeFormat
244+
}

‎cli/autostart_internal_test.go‎

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
package cli
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
)
8+
9+
//nolint:paralleltest // t.Setenv
10+
funcTestParseCLISchedule(t*testing.T) {
11+
for_,testCase:=range []struct {
12+
namestring
13+
input []string
14+
expectedSchedulestring
15+
expectedErrorstring
16+
tzEnvstring
17+
}{
18+
{
19+
name:"TimeAndDayOfWeekAndLocation",
20+
input: []string{"09:00AM","Sun-Sat","America/Chicago"},
21+
expectedSchedule:"CRON_TZ=America/Chicago 0 9 * * Sun-Sat",
22+
tzEnv:"UTC",
23+
},
24+
{
25+
name:"TimeOfDay24HourAndDayOfWeekAndLocation",
26+
input: []string{"09:00","Sun-Sat","America/Chicago"},
27+
expectedSchedule:"CRON_TZ=America/Chicago 0 9 * * Sun-Sat",
28+
tzEnv:"UTC",
29+
},
30+
{
31+
name:"TimeOfDay24HourAndDayOfWeekAndLocationButItsAllQuoted",
32+
input: []string{"09:00 Sun-Sat America/Chicago"},
33+
expectedSchedule:"CRON_TZ=America/Chicago 0 9 * * Sun-Sat",
34+
tzEnv:"UTC",
35+
},
36+
{
37+
name:"TimeOfDayOnly",
38+
input: []string{"09:00AM"},
39+
expectedSchedule:"CRON_TZ=America/Chicago 0 9 * * *",
40+
tzEnv:"America/Chicago",
41+
},
42+
{
43+
name:"Time24Military",
44+
input: []string{"0900"},
45+
expectedSchedule:"CRON_TZ=America/Chicago 0 9 * * *",
46+
tzEnv:"America/Chicago",
47+
},
48+
{
49+
name:"DayOfWeekAndTime",
50+
input: []string{"09:00AM","Sun-Sat"},
51+
expectedSchedule:"CRON_TZ=America/Chicago 0 9 * * Sun-Sat",
52+
tzEnv:"America/Chicago",
53+
},
54+
{
55+
name:"TimeAndLocation",
56+
input: []string{"09:00AM","America/Chicago"},
57+
expectedSchedule:"CRON_TZ=America/Chicago 0 9 * * *",
58+
tzEnv:"UTC",
59+
},
60+
{
61+
name:"LazyTime",
62+
input: []string{"9am","America/Chicago"},
63+
expectedSchedule:"CRON_TZ=America/Chicago 0 9 * * *",
64+
tzEnv:"UTC",
65+
},
66+
{
67+
name:"ZeroPrefixedLazyTime",
68+
input: []string{"09am","America/Chicago"},
69+
expectedSchedule:"CRON_TZ=America/Chicago 0 9 * * *",
70+
tzEnv:"UTC",
71+
},
72+
{
73+
name:"InvalidTime",
74+
input: []string{"nine"},
75+
expectedError:errInvalidTimeFormat.Error(),
76+
},
77+
{
78+
name:"DayOfWeekAndInvalidTime",
79+
input: []string{"nine","Sun-Sat"},
80+
expectedError:errInvalidTimeFormat.Error(),
81+
},
82+
{
83+
name:"InvalidTimeAndLocation",
84+
input: []string{"nine","America/Chicago"},
85+
expectedError:errInvalidTimeFormat.Error(),
86+
},
87+
{
88+
name:"DayOfWeekAndInvalidTimeAndLocation",
89+
input: []string{"nine","Sun-Sat","America/Chicago"},
90+
expectedError:errInvalidTimeFormat.Error(),
91+
},
92+
{
93+
name:"TimezoneProvidedInsteadOfLocation",
94+
input: []string{"09:00AM","Sun-Sat","CST"},
95+
expectedError:errUnsupportedTimezone.Error(),
96+
},
97+
{
98+
name:"WhoKnows",
99+
input: []string{"Time","is","a","human","construct"},
100+
expectedError:errInvalidTimeFormat.Error(),
101+
},
102+
} {
103+
testCase:=testCase
104+
//nolint:paralleltest // t.Setenv
105+
t.Run(testCase.name,func(t*testing.T) {
106+
t.Setenv("TZ",testCase.tzEnv)
107+
actualSchedule,actualError:=parseCLISchedule(testCase.input...)
108+
iftestCase.expectedError!="" {
109+
assert.Nil(t,actualSchedule)
110+
assert.ErrorContains(t,actualError,testCase.expectedError)
111+
return
112+
}
113+
assert.NoError(t,actualError)
114+
ifassert.NotEmpty(t,actualSchedule) {
115+
assert.Equal(t,testCase.expectedSchedule,actualSchedule.String())
116+
}
117+
})
118+
}
119+
}

0 commit comments

Comments
 (0)

[8]ページ先頭

©2009-2025 Movatter.jp