Skip to content

feat: add crontab package for supporting autostart/stop. #844

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
Apr 4, 2022
67 changes: 67 additions & 0 deletions coderd/autostart/schedule/crontab_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package schedule_test

import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd/autostart/schedule"
)

func Test_Weekly(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
spec string
at time.Time
expectedNext time.Time
expectedError string
}{
{
name: "with timezone",
spec: "CRON_TZ=US/Central 30 9 1-5",
at: time.Date(2022, 4, 1, 14, 29, 0, 0, time.UTC),
expectedNext: time.Date(2022, 4, 1, 14, 30, 0, 0, time.UTC),
expectedError: "",
},
{
name: "without timezone",
spec: "30 9 1-5",
at: time.Date(2022, 4, 1, 9, 29, 0, 0, time.Local),
expectedNext: time.Date(2022, 4, 1, 9, 30, 0, 0, time.Local),
expectedError: "",
},
{
name: "invalid schedule",
spec: "asdfasdfasdfsd",
at: time.Time{},
expectedNext: time.Time{},
expectedError: "parse schedule: expected exactly 3 fields, found 1: [asdfasdfasdfsd]",
},
{
name: "invalid location",
spec: "CRON_TZ=Fictional/Country 30 9 1-5",
at: time.Time{},
expectedNext: time.Time{},
expectedError: "parse schedule: provided bad location Fictional/Country: unknown time zone Fictional/Country",
},
}

for _, testCase := range testCases {
testCase := testCase
t.Run(testCase.name, func(t *testing.T) {
t.Parallel()
actual, err := schedule.Weekly(testCase.spec)
if testCase.expectedError == "" {
nextTime := actual.Next(testCase.at)
require.NoError(t, err)
require.Equal(t, testCase.expectedNext, nextTime)
require.Equal(t, testCase.spec, actual.String())
} else {
require.EqualError(t, err, testCase.expectedError)
require.Nil(t, actual)
}
})
}
}
66 changes: 66 additions & 0 deletions coderd/autostart/schedule/schedule.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
// package schedule provides utilities for parsing and deserializing
// cron-style expressions.
package schedule

import (
"time"

"github.com/robfig/cron/v3"
"golang.org/x/xerrors"
)

// For the purposes of this library, we only need minute, hour, and
// day-of-week.
const parserFormatWeekly = cron.Minute | cron.Hour | cron.Dow

var defaultParser = cron.NewParser(parserFormatWeekly)

// Weekly parses a Schedule from spec scoped to a recurring weekly event.
// Spec consists of the following space-delimited fields, in the following order:
// - timezone e.g. CRON_TZ=US/Central (optional)
// - minutes of hour e.g. 30 (required)
// - hour of day e.g. 9 (required)
// - day of week e.g. 1 (required)
//
// Example Usage:
// local_sched, _ := schedule.Weekly("59 23 *")
// fmt.Println(sched.Next(time.Now().Format(time.RFC3339)))
// // Output: 2022-04-04T23:59:00Z
// us_sched, _ := schedule.Weekly("CRON_TZ=US/Central 30 9 1-5")
// fmt.Println(sched.Next(time.Now()).Format(time.RFC3339))
// // Output: 2022-04-04T14:30:00Z
func Weekly(spec string) (*Schedule, error) {
specSched, err := defaultParser.Parse(spec)
if err != nil {
return nil, xerrors.Errorf("parse schedule: %w", err)
}

schedule, ok := specSched.(*cron.SpecSchedule)
if !ok {
return nil, xerrors.Errorf("expected *cron.SpecSchedule but got %T", specSched)
}

cronSched := &Schedule{
sched: schedule,
spec: spec,
}
return cronSched, nil
}

// Schedule represents a cron schedule.
// It's essentially a thin wrapper for robfig/cron/v3 that implements Stringer.
type Schedule struct {
sched *cron.SpecSchedule
// XXX: there isn't any nice way for robfig/cron to serialize
spec string
}

// String serializes the schedule to its original human-friendly format.
func (s Schedule) String() string {
return s.spec
}

// Next returns the next time in the schedule relative to t.
func (s Schedule) Next(t time.Time) time.Time {
return s.sched.Next(t)
}
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,7 @@ require (
github.com/prometheus/procfs v0.7.3 // indirect
github.com/rivo/tview v0.0.0-20200712113419-c65badfc3d92 // indirect
github.com/rivo/uniseg v0.2.0 // indirect
github.com/robfig/cron/v3 v3.0.1
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/sirupsen/logrus v1.8.1 // indirect
github.com/spf13/afero v1.8.1 // indirect
Expand Down
2 changes: 2 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -1501,6 +1501,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qq
github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg=
github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ=
github.com/rogpeppe/go-internal v1.1.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=
Expand Down