Add Periodic config to job
This commit is contained in:
parent
b039f963f0
commit
a9b95bca2d
|
@ -90,6 +90,7 @@ func parseJob(result *structs.Job, list *ast.ObjectList) error {
|
|||
delete(m, "constraint")
|
||||
delete(m, "meta")
|
||||
delete(m, "update")
|
||||
delete(m, "periodic")
|
||||
|
||||
// Set the ID and name to the object key
|
||||
result.ID = obj.Keys[0].Token.Value().(string)
|
||||
|
@ -127,6 +128,13 @@ func parseJob(result *structs.Job, list *ast.ObjectList) error {
|
|||
}
|
||||
}
|
||||
|
||||
// If we have a periodic definition, then parse that
|
||||
if o := listVal.Filter("periodic"); len(o.Items) > 0 {
|
||||
if err := parsePeriodic(&result.Periodic, o); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Parse out meta fields. These are in HCL as a list so we need
|
||||
// to iterate over them and merge them.
|
||||
if metaO := listVal.Filter("meta"); len(metaO.Items) > 0 {
|
||||
|
@ -666,3 +674,43 @@ func parseUpdate(result *structs.UpdateStrategy, list *ast.ObjectList) error {
|
|||
}
|
||||
return dec.Decode(m)
|
||||
}
|
||||
|
||||
func parsePeriodic(result *structs.PeriodicConfig, list *ast.ObjectList) error {
|
||||
list = list.Elem()
|
||||
if len(list.Items) > 1 {
|
||||
return fmt.Errorf("only one 'periodic' block allowed per job")
|
||||
}
|
||||
|
||||
// Get our resource object
|
||||
o := list.Items[0]
|
||||
|
||||
var m map[string]interface{}
|
||||
if err := hcl.DecodeObject(&m, o.Val); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Enabled by default if the periodic block exists.
|
||||
if value, ok := m["enabled"]; !ok {
|
||||
m["Enabled"] = true
|
||||
} else {
|
||||
enabled, err := parseBool(value)
|
||||
if err != nil {
|
||||
return fmt.Errorf("periodic.enabled should be set to true or false; %v", err)
|
||||
}
|
||||
m["Enabled"] = enabled
|
||||
}
|
||||
|
||||
// If "cron_spec" is provided, set the type to "cron" and store the spec.
|
||||
if cron, ok := m["cron_spec"]; ok {
|
||||
m["SpecType"] = structs.PeriodicSpecCron
|
||||
m["Spec"] = cron
|
||||
}
|
||||
|
||||
// Build the constraint
|
||||
var p structs.PeriodicConfig
|
||||
if err := mapstructure.WeakDecode(m, &p); err != nil {
|
||||
return err
|
||||
}
|
||||
*result = p
|
||||
return nil
|
||||
}
|
||||
|
|
|
@ -231,6 +231,23 @@ func TestParse(t *testing.T) {
|
|||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"periodic-cron.hcl",
|
||||
&structs.Job{
|
||||
ID: "foo",
|
||||
Name: "foo",
|
||||
Priority: 50,
|
||||
Region: "global",
|
||||
Type: "service",
|
||||
Periodic: structs.PeriodicConfig{
|
||||
Enabled: true,
|
||||
SpecType: structs.PeriodicSpecCron,
|
||||
Spec: "*/5 * * *",
|
||||
},
|
||||
},
|
||||
false,
|
||||
},
|
||||
|
||||
{
|
||||
"specify-job.hcl",
|
||||
&structs.Job{
|
||||
|
|
5
jobspec/test-fixtures/periodic-cron.hcl
Normal file
5
jobspec/test-fixtures/periodic-cron.hcl
Normal file
|
@ -0,0 +1,5 @@
|
|||
job "foo" {
|
||||
periodic {
|
||||
cron_spec = "*/5 * * *"
|
||||
}
|
||||
}
|
|
@ -11,6 +11,7 @@ import (
|
|||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gorhill/cronexpr"
|
||||
"github.com/hashicorp/go-msgpack/codec"
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"github.com/hashicorp/go-version"
|
||||
|
@ -760,6 +761,9 @@ type Job struct {
|
|||
// Update is used to control the update strategy
|
||||
Update UpdateStrategy
|
||||
|
||||
// Periodic is used to define the interval the job is run at.
|
||||
Periodic PeriodicConfig
|
||||
|
||||
// Meta is used to associate arbitrary metadata with this
|
||||
// job. This is opaque to Nomad.
|
||||
Meta map[string]string
|
||||
|
@ -841,6 +845,13 @@ func (j *Job) Validate() error {
|
|||
mErr.Errors = append(mErr.Errors, outer)
|
||||
}
|
||||
}
|
||||
|
||||
// Validate periodic is only used with batch jobs.
|
||||
if j.Periodic.Enabled && j.Type != JobTypeBatch {
|
||||
mErr.Errors = append(mErr.Errors,
|
||||
fmt.Errorf("Periodic can only be used with %q scheduler", JobTypeBatch))
|
||||
}
|
||||
|
||||
return mErr.ErrorOrNil()
|
||||
}
|
||||
|
||||
|
@ -895,6 +906,61 @@ func (u *UpdateStrategy) Rolling() bool {
|
|||
return u.Stagger > 0 && u.MaxParallel > 0
|
||||
}
|
||||
|
||||
const (
|
||||
// PeriodicSpecCron is used for a cron spec.
|
||||
PeriodicSpecCron = "cron"
|
||||
)
|
||||
|
||||
// Periodic defines the interval a job should be run at.
|
||||
type PeriodicConfig struct {
|
||||
// Enabled determines if the job should be run periodically.
|
||||
Enabled bool
|
||||
|
||||
// Spec specifies the interval the job should be run as. It is parsed based
|
||||
// on the SpecType.
|
||||
Spec string
|
||||
|
||||
// SpecType defines the format of the spec.
|
||||
SpecType string
|
||||
}
|
||||
|
||||
func (p *PeriodicConfig) Validate() error {
|
||||
if !p.Enabled {
|
||||
return nil
|
||||
}
|
||||
|
||||
if p.SpecType == "" {
|
||||
return fmt.Errorf("Must specify a spec type to be able to parse the interval")
|
||||
}
|
||||
|
||||
switch p.SpecType {
|
||||
case PeriodicSpecCron:
|
||||
// Validate the cron spec
|
||||
if _, err := cronexpr.Parse(p.Spec); err != nil {
|
||||
return fmt.Errorf("Invalid cron spec %q: %v", p.Spec, err)
|
||||
}
|
||||
default:
|
||||
return fmt.Errorf("Unknown specification type %q", p.SpecType)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Next returns the closest time instant matching the spec that is after the
|
||||
// passed time. If no matching instance exists, the zero value of time.Time is
|
||||
// returned. The `time.Location` of the returned value matches that of the
|
||||
// passed time.
|
||||
func (p *PeriodicConfig) Next(fromTime time.Time) time.Time {
|
||||
switch p.SpecType {
|
||||
case PeriodicSpecCron:
|
||||
if e, err := cronexpr.Parse(p.Spec); err == nil {
|
||||
return e.Next(fromTime)
|
||||
}
|
||||
}
|
||||
|
||||
return time.Time{}
|
||||
}
|
||||
|
||||
// RestartPolicy influences how Nomad restarts Tasks when they
|
||||
// crash or fail.
|
||||
type RestartPolicy struct {
|
||||
|
|
|
@ -1,11 +1,12 @@
|
|||
package structs
|
||||
|
||||
import (
|
||||
"github.com/hashicorp/go-multierror"
|
||||
"reflect"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-multierror"
|
||||
)
|
||||
|
||||
func TestJob_Validate(t *testing.T) {
|
||||
|
@ -34,6 +35,18 @@ func TestJob_Validate(t *testing.T) {
|
|||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
j = &Job{
|
||||
Type: JobTypeService,
|
||||
Periodic: PeriodicConfig{
|
||||
Enabled: true,
|
||||
},
|
||||
}
|
||||
err = j.Validate()
|
||||
mErr = err.(*multierror.Error)
|
||||
if !strings.Contains(mErr.Error(), "Periodic") {
|
||||
t.Fatalf("err: %s", err)
|
||||
}
|
||||
|
||||
j = &Job{
|
||||
Region: "global",
|
||||
ID: GenerateUUID(),
|
||||
|
@ -488,3 +501,56 @@ func TestJob_ExpandServiceNames(t *testing.T) {
|
|||
}
|
||||
|
||||
}
|
||||
|
||||
func TestPeriodicConfig_EnabledInvalid(t *testing.T) {
|
||||
// Create a config that is enabled but with no interval specified.
|
||||
p := &PeriodicConfig{Enabled: true}
|
||||
if err := p.Validate(); err == nil {
|
||||
t.Fatal("Enabled PeriodicConfig with no spec or type shouldn't be valid")
|
||||
}
|
||||
|
||||
// Create a config that is enabled, with a spec but no type specified.
|
||||
p = &PeriodicConfig{Enabled: true, Spec: "foo"}
|
||||
if err := p.Validate(); err == nil {
|
||||
t.Fatal("Enabled PeriodicConfig with no spec type shouldn't be valid")
|
||||
}
|
||||
|
||||
// Create a config that is enabled, with a spec type but no spec specified.
|
||||
p = &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron}
|
||||
if err := p.Validate(); err == nil {
|
||||
t.Fatal("Enabled PeriodicConfig with no spec shouldn't be valid")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeriodicConfig_InvalidCron(t *testing.T) {
|
||||
specs := []string{"foo", "* *", "@foo"}
|
||||
for _, spec := range specs {
|
||||
p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
|
||||
if err := p.Validate(); err == nil {
|
||||
t.Fatal("Invalid cron spec")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeriodicConfig_ValidCron(t *testing.T) {
|
||||
specs := []string{"0 0 29 2 *", "@hourly", "0 0-15 * * *"}
|
||||
for _, spec := range specs {
|
||||
p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
|
||||
if err := p.Validate(); err != nil {
|
||||
t.Fatal("Passed valid cron")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeriodicConfig_NextCron(t *testing.T) {
|
||||
from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC)
|
||||
specs := []string{"0 0 29 2 * 1980", "*/5 * * * *"}
|
||||
expected := []time.Time{time.Time{}, time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC)}
|
||||
for i, spec := range specs {
|
||||
p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec}
|
||||
n := p.Next(from)
|
||||
if expected[i] != n {
|
||||
t.Fatalf("Next(%v) returned %v; want %v", from, n, expected[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue