Add cron schedules and sync docs
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
||||
"unicode/utf8"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
var idPattern = regexp.MustCompile(`^[A-Za-z0-9._:-]+$`)
|
||||
@@ -58,6 +59,7 @@ type CheckConfig struct {
|
||||
Name string `toml:"name"`
|
||||
Command string `toml:"command"`
|
||||
Interval Duration `toml:"interval"`
|
||||
Cron string `toml:"cron"`
|
||||
Timeout Duration `toml:"timeout"`
|
||||
NotificationsEnabled *bool `toml:"notifications_enabled"`
|
||||
DedupeKey string `toml:"dedupe_key"`
|
||||
@@ -211,14 +213,24 @@ func (c *Config) Validate() error {
|
||||
if strings.TrimSpace(ch.Command) == "" {
|
||||
return fmt.Errorf("check %q: command is required", ch.ID)
|
||||
}
|
||||
if ch.Interval.Duration <= 0 {
|
||||
return fmt.Errorf("check %q: interval must be > 0", ch.ID)
|
||||
}
|
||||
if ch.Timeout.Duration <= 0 {
|
||||
return fmt.Errorf("check %q: timeout must be > 0", ch.ID)
|
||||
}
|
||||
if ch.Timeout.Duration > ch.Interval.Duration {
|
||||
if ch.Interval.Duration < 0 {
|
||||
return fmt.Errorf("check %q: interval must be > 0", ch.ID)
|
||||
}
|
||||
ch.Cron = strings.TrimSpace(ch.Cron)
|
||||
hasInterval := ch.Interval.Duration > 0
|
||||
hasCron := ch.Cron != ""
|
||||
switch {
|
||||
case hasInterval == hasCron:
|
||||
return fmt.Errorf("check %q: exactly one of interval or cron is required", ch.ID)
|
||||
case hasInterval && ch.Timeout.Duration > ch.Interval.Duration:
|
||||
return fmt.Errorf("check %q: timeout must be <= interval", ch.ID)
|
||||
case hasCron:
|
||||
if _, err := ParseCronSchedule(ch.Cron); err != nil {
|
||||
return fmt.Errorf("check %q: invalid cron: %w", ch.ID, err)
|
||||
}
|
||||
}
|
||||
if len(ch.DedupeKey) > 256 {
|
||||
return fmt.Errorf("check %q: dedupe_key too long", ch.ID)
|
||||
@@ -253,6 +265,42 @@ func (c CheckConfig) NotificationsOn() bool {
|
||||
return c.NotificationsEnabled == nil || *c.NotificationsEnabled
|
||||
}
|
||||
|
||||
func (c CheckConfig) UsesInterval() bool {
|
||||
return c.Interval.Duration > 0
|
||||
}
|
||||
|
||||
func (c CheckConfig) UsesCron() bool {
|
||||
return strings.TrimSpace(c.Cron) != ""
|
||||
}
|
||||
|
||||
func ParseCronSchedule(spec string) (cron.Schedule, error) {
|
||||
spec = strings.TrimSpace(spec)
|
||||
if spec == "" {
|
||||
return nil, fmt.Errorf("cron is empty")
|
||||
}
|
||||
withoutTZ := specWithoutTZ(spec)
|
||||
lower := strings.ToLower(withoutTZ)
|
||||
if strings.HasPrefix(lower, "@every") {
|
||||
return nil, fmt.Errorf("@every is not supported; use interval")
|
||||
}
|
||||
if lower == "@reboot" {
|
||||
return nil, fmt.Errorf("@reboot is not supported")
|
||||
}
|
||||
return cron.ParseStandard(spec)
|
||||
}
|
||||
|
||||
func specWithoutTZ(spec string) string {
|
||||
fields := strings.Fields(spec)
|
||||
if len(fields) == 0 {
|
||||
return ""
|
||||
}
|
||||
first := fields[0]
|
||||
if strings.HasPrefix(first, "CRON_TZ=") || strings.HasPrefix(first, "TZ=") {
|
||||
return strings.ToLower(strings.Join(fields[1:], " "))
|
||||
}
|
||||
return strings.ToLower(strings.Join(fields, " "))
|
||||
}
|
||||
|
||||
func validateResourceLimits(checkID string, limits ResourceLimits) error {
|
||||
if limits.CPUTime.Duration < 0 {
|
||||
return fmt.Errorf("check %q: resource_limits.cpu_time must be > 0", checkID)
|
||||
|
||||
@@ -48,9 +48,84 @@ func TestLoadMinimal(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadCronCheck(t *testing.T) {
|
||||
c, err := Load(writeTmp(t, `
|
||||
agent_id = "host-1"
|
||||
state_dir = "/tmp/x"
|
||||
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = "true"
|
||||
cron = "CRON_TZ=UTC 0 6 * * *"
|
||||
timeout = "5m"
|
||||
`))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !c.Checks[0].UsesCron() || c.Checks[0].UsesInterval() {
|
||||
t.Fatalf("unexpected schedule mode: %+v", c.Checks[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateCheckScheduleChoice(t *testing.T) {
|
||||
for name, body := range map[string]string{
|
||||
"both_interval_and_cron": `
|
||||
interval = "10s"
|
||||
cron = "0 * * * *"
|
||||
timeout = "5s"
|
||||
`,
|
||||
"neither_interval_nor_cron": `
|
||||
timeout = "5s"
|
||||
`,
|
||||
"invalid_cron": `
|
||||
cron = "not a cron"
|
||||
timeout = "5s"
|
||||
`,
|
||||
"seconds_cron": `
|
||||
cron = "0 0 * * * *"
|
||||
timeout = "5s"
|
||||
`,
|
||||
"every_cron": `
|
||||
cron = "@every 10s"
|
||||
timeout = "5s"
|
||||
`,
|
||||
"tz_every_cron": `
|
||||
cron = "CRON_TZ=UTC @every 10s"
|
||||
timeout = "5s"
|
||||
`,
|
||||
"reboot_cron": `
|
||||
cron = "@reboot"
|
||||
timeout = "5s"
|
||||
`,
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
_, err := Load(writeTmp(t, `
|
||||
agent_id = "host-1"
|
||||
state_dir = "/tmp/x"
|
||||
|
||||
[server]
|
||||
enabled = true
|
||||
url = "http://localhost"
|
||||
|
||||
[[checks]]
|
||||
id = "c1"
|
||||
command = "true"
|
||||
`+body+`
|
||||
`))
|
||||
if err == nil {
|
||||
t.Fatal("expected schedule validation error")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadResourceLimits(t *testing.T) {
|
||||
c, err := Load(writeTmp(t, `
|
||||
agent_id = "host-1"
|
||||
agent_id = "host-1"
|
||||
state_dir = "/tmp/x"
|
||||
|
||||
[server]
|
||||
|
||||
Reference in New Issue
Block a user