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
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 61 additions & 0 deletions coderd/crontab/crontab.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
// package crontab provides utilities for parsing and deserializing
// cron-style expressions.
package crontab

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 parserFormat = cron.Minute | cron.Hour | cron.Dow

var defaultParser = cron.NewParser(parserFormat)

// Parse parses a WeeklySchedule from spec.
//
// Example Usage:
// local_sched, _ := cron.Parse("59 23 *")
// fmt.Println(sched.Next(time.Now().Format(time.RFC3339)))
// // Output: 2022-04-04T23:59:00Z
// us_sched, _ := cron.Parse("CRON_TZ=US/Central 30 9 1-5")
// fmt.Println(sched.Next(time.Now()).Format(time.RFC3339))
// // Output: 2022-04-04T14:30:00Z
func Parse(spec string) (*WeeklySchedule, 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 := &WeeklySchedule{
sched: schedule,
spec: spec,
}
return cronSched, nil
}

// WeeklySchedule represents a weekly cron schedule.
// It's essentially a thin wrapper for robfig/cron/v3 that implements Stringer.
type WeeklySchedule struct {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This weekly schedule feels pretty specified to autostart/stop. Could we add this to the autostart/stop package once that's in (assuming it'll be inside it's own package)?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Also, I'm not sure this needs to have Weekly as a prefix. That seems like part of the parsing logic, not the struct functionality.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can move this to coderd/autostart/schedule if that works?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on what's here, I'd say that makes sense. I'm uncertain what structurally makes sense until we have more of the implementation worked out, but as long as we're not opposed to moving this afterwards, I'm good with it!

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 WeeklySchedule) String() string {
return s.spec
}

// Next returns the next time in the schedule relative to t.
func (s WeeklySchedule) Next(t time.Time) time.Time {
return s.sched.Next(t)
}
67 changes: 67 additions & 0 deletions coderd/crontab/crontab_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package crontab_test

import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/coder/coder/coderd/crontab"
)

func Test_Parse(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 := crontab.Parse(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)
}
})
}
}
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