Skip to content

Commit b704903

Browse files
authored
fix: cli: prettify schedule when printing output (#1440)
* Adds methods to schedule.Schedule to show the raw cron string and timezone * Uses these methods to clean up output of auto(start|stop) show or ls * Defaults CRON_TZ=UTC if not provided
1 parent 2a278b8 commit b704903

File tree

7 files changed

+77
-16
lines changed

7 files changed

+77
-16
lines changed

cli/autostart.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,15 @@ func autostartShow() *cobra.Command {
6363
return nil
6464
}
6565

66-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "schedule: %s\nnext: %s\n", workspace.AutostartSchedule, validSchedule.Next(time.Now()))
66+
next := validSchedule.Next(time.Now())
67+
loc, _ := time.LoadLocation(validSchedule.Timezone())
68+
69+
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
70+
"schedule: %s\ntimezone: %s\nnext: %s\n",
71+
validSchedule.Cron(),
72+
validSchedule.Timezone(),
73+
next.In(loc),
74+
)
6775

6876
return nil
6977
},

cli/autostart_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ func TestAutostart(t *testing.T) {
4545

4646
err = cmd.Execute()
4747
require.NoError(t, err, "unexpected error")
48-
require.Contains(t, stdoutBuf.String(), "schedule: "+sched)
48+
// CRON_TZ gets stripped
49+
require.Contains(t, stdoutBuf.String(), "schedule: 30 17 * * 1-5")
4950
})
5051

5152
t.Run("EnableDisableOK", func(t *testing.T) {

cli/autostop.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,15 @@ func autostopShow() *cobra.Command {
6363
return nil
6464
}
6565

66-
_, _ = fmt.Fprintf(cmd.OutOrStdout(), "schedule: %s\nnext: %s\n", workspace.AutostopSchedule, validSchedule.Next(time.Now()))
66+
next := validSchedule.Next(time.Now())
67+
loc, _ := time.LoadLocation(validSchedule.Timezone())
68+
69+
_, _ = fmt.Fprintf(cmd.OutOrStdout(),
70+
"schedule: %s\ntimezone: %s\nnext: %s\n",
71+
validSchedule.Cron(),
72+
validSchedule.Timezone(),
73+
next.In(loc),
74+
)
6775

6876
return nil
6977
},

cli/autostop_test.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,8 @@ func TestAutostop(t *testing.T) {
4545

4646
err = cmd.Execute()
4747
require.NoError(t, err, "unexpected error")
48-
require.Contains(t, stdoutBuf.String(), "schedule: "+sched)
48+
// CRON_TZ gets stripped
49+
require.Contains(t, stdoutBuf.String(), "schedule: 30 17 * * 1-5")
4950
})
5051

5152
t.Run("EnableDisableOK", func(t *testing.T) {

cli/list.go

+9-4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"github.com/spf13/cobra"
1111

1212
"github.com/coder/coder/cli/cliui"
13+
"github.com/coder/coder/coderd/autobuild/schedule"
1314
"github.com/coder/coder/coderd/database"
1415
"github.com/coder/coder/codersdk"
1516
)
@@ -108,14 +109,18 @@ func list() *cobra.Command {
108109
durationDisplay = durationDisplay[:len(durationDisplay)-2]
109110
}
110111

111-
autostartDisplay := "not enabled"
112+
autostartDisplay := "-"
112113
if workspace.AutostartSchedule != "" {
113-
autostartDisplay = workspace.AutostartSchedule
114+
if sched, err := schedule.Weekly(workspace.AutostartSchedule); err == nil {
115+
autostartDisplay = sched.Cron()
116+
}
114117
}
115118

116-
autostopDisplay := "not enabled"
119+
autostopDisplay := "-"
117120
if workspace.AutostopSchedule != "" {
118-
autostopDisplay = workspace.AutostopSchedule
121+
if sched, err := schedule.Weekly(workspace.AutostopSchedule); err == nil {
122+
autostopDisplay = sched.Cron()
123+
}
119124
}
120125

121126
user := usersByID[workspace.OwnerID]

coderd/autobuild/schedule/schedule.go

+37-7
Original file line numberDiff line numberDiff line change
@@ -34,12 +34,18 @@ var defaultParser = cron.NewParser(parserFormat)
3434
// us_sched, _ := schedule.Weekly("CRON_TZ=US/Central 30 9 1-5")
3535
// fmt.Println(sched.Next(time.Now()).Format(time.RFC3339))
3636
// // Output: 2022-04-04T14:30:00Z
37-
func Weekly(spec string) (*Schedule, error) {
38-
if err := validateWeeklySpec(spec); err != nil {
37+
func Weekly(raw string) (*Schedule, error) {
38+
if err := validateWeeklySpec(raw); err != nil {
3939
return nil, xerrors.Errorf("validate weekly schedule: %w", err)
4040
}
4141

42-
specSched, err := defaultParser.Parse(spec)
42+
// If schedule does not specify a timezone, default to UTC. Otherwise,
43+
// the library will default to time.Local which we want to avoid.
44+
if !strings.HasPrefix(raw, "CRON_TZ=") {
45+
raw = "CRON_TZ=UTC " + raw
46+
}
47+
48+
specSched, err := defaultParser.Parse(raw)
4349
if err != nil {
4450
return nil, xerrors.Errorf("parse schedule: %w", err)
4551
}
@@ -49,9 +55,16 @@ func Weekly(spec string) (*Schedule, error) {
4955
return nil, xerrors.Errorf("expected *cron.SpecSchedule but got %T", specSched)
5056
}
5157

58+
// Strip the leading CRON_TZ prefix so we just store the cron string.
59+
// The timezone info is available in SpecSchedule.
60+
cronStr := raw
61+
if strings.HasPrefix(raw, "CRON_TZ=") {
62+
cronStr = strings.Join(strings.Fields(raw)[1:], " ")
63+
}
64+
5265
cronSched := &Schedule{
53-
sched: schedule,
54-
spec: spec,
66+
sched: schedule,
67+
cronStr: cronStr,
5568
}
5669
return cronSched, nil
5770
}
@@ -61,12 +74,29 @@ func Weekly(spec string) (*Schedule, error) {
6174
type Schedule struct {
6275
sched *cron.SpecSchedule
6376
// XXX: there isn't any nice way for robfig/cron to serialize
64-
spec string
77+
cronStr string
6578
}
6679

6780
// String serializes the schedule to its original human-friendly format.
81+
// The leading CRON_TZ is maintained.
6882
func (s Schedule) String() string {
69-
return s.spec
83+
var sb strings.Builder
84+
_, _ = sb.WriteString("CRON_TZ=")
85+
_, _ = sb.WriteString(s.sched.Location.String())
86+
_, _ = sb.WriteString(" ")
87+
_, _ = sb.WriteString(s.cronStr)
88+
return sb.String()
89+
}
90+
91+
// Timezone returns the timezone for the schedule.
92+
func (s Schedule) Timezone() string {
93+
return s.sched.Location.String()
94+
}
95+
96+
// Cron returns the cron spec for the schedule with the leading CRON_TZ
97+
// stripped, if present.
98+
func (s Schedule) Cron() string {
99+
return s.cronStr
70100
}
71101

72102
// Next returns the next time in the schedule relative to t.

coderd/autobuild/schedule/schedule_test.go

+9-1
Original file line numberDiff line numberDiff line change
@@ -17,20 +17,26 @@ func Test_Weekly(t *testing.T) {
1717
at time.Time
1818
expectedNext time.Time
1919
expectedError string
20+
expectedCron string
21+
expectedTz string
2022
}{
2123
{
2224
name: "with timezone",
2325
spec: "CRON_TZ=US/Central 30 9 * * 1-5",
2426
at: time.Date(2022, 4, 1, 14, 29, 0, 0, time.UTC),
2527
expectedNext: time.Date(2022, 4, 1, 14, 30, 0, 0, time.UTC),
2628
expectedError: "",
29+
expectedCron: "30 9 * * 1-5",
30+
expectedTz: "US/Central",
2731
},
2832
{
2933
name: "without timezone",
30-
spec: "30 9 * * 1-5",
34+
spec: "CRON_TZ=UTC 30 9 * * 1-5",
3135
at: time.Date(2022, 4, 1, 9, 29, 0, 0, time.Local),
3236
expectedNext: time.Date(2022, 4, 1, 9, 30, 0, 0, time.Local),
3337
expectedError: "",
38+
expectedCron: "30 9 * * 1-5",
39+
expectedTz: "UTC",
3440
},
3541
{
3642
name: "invalid schedule",
@@ -86,6 +92,8 @@ func Test_Weekly(t *testing.T) {
8692
require.NoError(t, err)
8793
require.Equal(t, testCase.expectedNext, nextTime)
8894
require.Equal(t, testCase.spec, actual.String())
95+
require.Equal(t, testCase.expectedCron, actual.Cron())
96+
require.Equal(t, testCase.expectedTz, actual.Timezone())
8997
} else {
9098
require.EqualError(t, err, testCase.expectedError)
9199
require.Nil(t, actual)

0 commit comments

Comments
 (0)