From fc2e6717deadcff039d082c72043fefc43b99f55 Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Thu, 7 May 2020 17:36:59 -0400 Subject: [PATCH 1/4] tests for periodic job scheduling and DST --- nomad/structs/structs_periodic_test.go | 284 +++++++++++++++++++++++++ 1 file changed, 284 insertions(+) create mode 100644 nomad/structs/structs_periodic_test.go diff --git a/nomad/structs/structs_periodic_test.go b/nomad/structs/structs_periodic_test.go new file mode 100644 index 000000000000..de795296c445 --- /dev/null +++ b/nomad/structs/structs_periodic_test.go @@ -0,0 +1,284 @@ +package structs + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestPeriodicConfig_DSTChange_Transitions(t *testing.T) { + locName := "America/Los_Angeles" + loc, err := time.LoadLocation(locName) + require.NoError(t, err) + + cases := []struct { + name string + pattern string + initTime time.Time + expected []time.Time + }{ + { + "normal time", + "0 2 * * * 2019", + time.Date(2019, time.February, 7, 1, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.February, 7, 2, 0, 0, 0, loc), + time.Date(2019, time.February, 8, 2, 0, 0, 0, loc), + time.Date(2019, time.February, 9, 2, 0, 0, 0, loc), + }, + }, + { + "Spring forward but not in switch time", + "0 4 * * * 2019", + time.Date(2019, time.March, 9, 1, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.March, 9, 4, 0, 0, 0, loc), + time.Date(2019, time.March, 10, 4, 0, 0, 0, loc), + time.Date(2019, time.March, 11, 4, 0, 0, 0, loc), + }, + }, + { + "Spring forward at a skipped time odd", + "2 2 * * * 2019", + time.Date(2019, time.March, 9, 1, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.March, 9, 2, 2, 0, 0, loc), + // no time in March 10! + time.Date(2019, time.March, 11, 2, 2, 0, 0, loc), + time.Date(2019, time.March, 12, 2, 2, 0, 0, loc), + }, + }, + { + "Spring forward at a skipped time", + "1 2 * * * 2019", + time.Date(2019, time.March, 9, 1, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.March, 9, 2, 1, 0, 0, loc), + // no time in March 8! + time.Date(2019, time.March, 11, 2, 1, 0, 0, loc), + time.Date(2019, time.March, 12, 2, 1, 0, 0, loc), + }, + }, + { + "Spring forward at a skipped time boundary", + "0 2 * * * 2019", + time.Date(2019, time.March, 9, 1, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.March, 9, 2, 0, 0, 0, loc), + // no time in March 8! + time.Date(2019, time.March, 11, 2, 0, 0, 0, loc), + time.Date(2019, time.March, 12, 2, 0, 0, 0, loc), + }, + }, + { + "Spring forward at a boundary of repeating time", + "0 1 * * * 2019", + time.Date(2019, time.March, 9, 0, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.March, 9, 1, 0, 0, 0, loc), + time.Date(2019, time.March, 10, 0, 0, 0, 0, loc).Add(1 * time.Hour), + time.Date(2019, time.March, 11, 1, 0, 0, 0, loc), + time.Date(2019, time.March, 12, 1, 0, 0, 0, loc), + }, + }, + { + "Fall back: before transition", + "30 0 * * * 2019", + time.Date(2019, time.November, 3, 0, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.November, 3, 0, 30, 0, 0, loc), + time.Date(2019, time.November, 4, 0, 30, 0, 0, loc), + time.Date(2019, time.November, 5, 0, 30, 0, 0, loc), + time.Date(2019, time.November, 6, 0, 30, 0, 0, loc), + }, + }, + { + "Fall back: after transition", + "30 3 * * * 2019", + time.Date(2019, time.November, 3, 0, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.November, 3, 3, 30, 0, 0, loc), + time.Date(2019, time.November, 4, 3, 30, 0, 0, loc), + time.Date(2019, time.November, 5, 3, 30, 0, 0, loc), + time.Date(2019, time.November, 6, 3, 30, 0, 0, loc), + }, + }, + { + "Fall back: after transition starting in repeated span before", + "30 3 * * * 2019", + time.Date(2019, time.November, 3, 0, 10, 0, 0, loc).Add(1 * time.Hour), + []time.Time{ + time.Date(2019, time.November, 3, 3, 30, 0, 0, loc), + time.Date(2019, time.November, 4, 3, 30, 0, 0, loc), + time.Date(2019, time.November, 5, 3, 30, 0, 0, loc), + time.Date(2019, time.November, 6, 3, 30, 0, 0, loc), + }, + }, + { + "Fall back: after transition starting in repeated span after", + "30 3 * * * 2019", + time.Date(2019, time.November, 3, 0, 10, 0, 0, loc).Add(2 * time.Hour), + []time.Time{ + time.Date(2019, time.November, 3, 3, 30, 0, 0, loc), + time.Date(2019, time.November, 4, 3, 30, 0, 0, loc), + time.Date(2019, time.November, 5, 3, 30, 0, 0, loc), + time.Date(2019, time.November, 6, 3, 30, 0, 0, loc), + }, + }, + { + "Fall back: in repeated region", + "30 1 * * * 2019", + time.Date(2019, time.November, 3, 0, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(1 * time.Hour), + time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(2 * time.Hour), + time.Date(2019, time.November, 4, 1, 30, 0, 0, loc), + time.Date(2019, time.November, 5, 1, 30, 0, 0, loc), + time.Date(2019, time.November, 6, 1, 30, 0, 0, loc), + }, + }, + { + "Fall back: in repeated region boundary", + "0 1 * * * 2019", + time.Date(2019, time.November, 3, 0, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.November, 3, 0, 0, 0, 0, loc).Add(1 * time.Hour), + time.Date(2019, time.November, 3, 0, 0, 0, 0, loc).Add(2 * time.Hour), + time.Date(2019, time.November, 4, 1, 0, 0, 0, loc), + time.Date(2019, time.November, 5, 1, 0, 0, 0, loc), + time.Date(2019, time.November, 6, 1, 0, 0, 0, loc), + }, + }, + { + "Fall back: in repeated region boundary 2", + "0 2 * * * 2019", + time.Date(2019, time.November, 3, 0, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.November, 3, 0, 0, 0, 0, loc).Add(3 * time.Hour), + time.Date(2019, time.November, 4, 2, 0, 0, 0, loc), + time.Date(2019, time.November, 5, 2, 0, 0, 0, loc), + time.Date(2019, time.November, 6, 2, 0, 0, 0, loc), + }, + }, + { + "Fall back: in repeated region, starting from within region", + "30 1 * * * 2019", + time.Date(2019, time.November, 3, 0, 40, 0, 0, loc).Add(1 * time.Hour), + []time.Time{ + time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(2 * time.Hour), + time.Date(2019, time.November, 4, 1, 30, 0, 0, loc), + time.Date(2019, time.November, 5, 1, 30, 0, 0, loc), + time.Date(2019, time.November, 6, 1, 30, 0, 0, loc), + }, + }, + { + "Fall back: in repeated region, starting from within region 2", + "30 1 * * * 2019", + time.Date(2019, time.November, 3, 0, 40, 0, 0, loc).Add(2 * time.Hour), + []time.Time{ + time.Date(2019, time.November, 4, 1, 30, 0, 0, loc), + time.Date(2019, time.November, 5, 1, 30, 0, 0, loc), + time.Date(2019, time.November, 6, 1, 30, 0, 0, loc), + }, + }, + { + "Fall back: wildcard", + "30 * * * * 2019", + time.Date(2019, time.November, 3, 0, 0, 0, 0, loc), + []time.Time{ + time.Date(2019, time.November, 3, 0, 30, 0, 0, loc), + time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(1 * time.Hour), + time.Date(2019, time.November, 3, 0, 30, 0, 0, loc).Add(2 * time.Hour), + time.Date(2019, time.November, 3, 2, 30, 0, 0, loc), + }, + }, + } + + for _, c := range cases { + t.Run(c.name, func(t *testing.T) { + p := &PeriodicConfig{ + Enabled: true, + SpecType: PeriodicSpecCron, + Spec: c.pattern, + TimeZone: locName, + } + p.Canonicalize() + + starting := c.initTime + for _, next := range c.expected { + n, err := p.Next(starting) + assert.NoError(t, err) + assert.Equalf(t, next, n, "next time of %v", starting) + + starting = next + } + }) + } +} + +func TestPeriodConfig_DSTSprintForward_Property(t *testing.T) { + locName := "America/Los_Angeles" + loc, err := time.LoadLocation(locName) + require.NoError(t, err) + + cronExprs := []string{ + "* * * * *", + "0 2 * * *", + "* 1 * * *", + } + + times := []time.Time{ + // spring forward + time.Date(2019, time.March, 11, 0, 0, 0, 0, loc), + time.Date(2019, time.March, 10, 0, 0, 0, 0, loc), + time.Date(2019, time.March, 11, 0, 0, 0, 0, loc), + + // leap backwards + time.Date(2019, time.November, 4, 0, 0, 0, 0, loc), + time.Date(2019, time.November, 5, 0, 0, 0, 0, loc), + time.Date(2019, time.November, 6, 0, 0, 0, 0, loc), + } + + testSpan := 4 * time.Hour + + testCase := func(t *testing.T, cronExpr string, init time.Time) { + p := &PeriodicConfig{ + Enabled: true, + SpecType: PeriodicSpecCron, + Spec: cronExpr, + TimeZone: "America/Los_Angeles", + } + p.Canonicalize() + + lastNext := init + for start := init; start.Before(init.Add(testSpan)); start = start.Add(1 * time.Minute) { + next, err := p.Next(start) + require.NoError(t, err) + require.Truef(t, next.After(start), + "next(%v) = %v is not after init time", start, next) + + if start.Before(lastNext) { + require.Equalf(t, lastNext, next, "next(%v) = %v is earlier than previously known next %v", + start, next, lastNext) + } + if strings.HasPrefix(cronExpr, "* * ") { + require.Equalf(t, next.Sub(start), 1*time.Minute, + "next(%v) = %v is the next minute", start, next) + } + + lastNext = next + } + } + + for _, cron := range cronExprs { + for _, startTime := range times { + t.Run(fmt.Sprintf("%v: %v", cron, startTime), func(t *testing.T) { + testCase(t, cron, startTime) + }) + } + } +} From 702d69ff3a1a5fb3bf181c8710d3f9c3900ae408 Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Thu, 7 May 2020 17:50:45 -0400 Subject: [PATCH 2/4] Update cronexpr to point to hashicorp/cronexpr --- api/go.mod | 2 +- api/go.sum | 4 +- api/jobs.go | 2 +- nomad/structs/structs.go | 2 +- .../{gorhill => hashicorp}/cronexpr/APLv2 | 0 .../{gorhill => hashicorp}/cronexpr/GPLv3 | 0 .../{gorhill => hashicorp}/cronexpr/README.md | 0 .../cronexpr/cronexpr.go | 158 ++++++++++------ .../cronexpr/cronexpr_next.go | 169 ++---------------- .../cronexpr/cronexpr_parse.go | 4 + vendor/vendor.json | 2 +- 11 files changed, 126 insertions(+), 217 deletions(-) rename vendor/github.com/{gorhill => hashicorp}/cronexpr/APLv2 (100%) rename vendor/github.com/{gorhill => hashicorp}/cronexpr/GPLv3 (100%) rename vendor/github.com/{gorhill => hashicorp}/cronexpr/README.md (100%) rename vendor/github.com/{gorhill => hashicorp}/cronexpr/cronexpr.go (59%) rename vendor/github.com/{gorhill => hashicorp}/cronexpr/cronexpr_next.go (50%) rename vendor/github.com/{gorhill => hashicorp}/cronexpr/cronexpr_parse.go (99%) diff --git a/api/go.mod b/api/go.mod index f05251050620..1a18369dd1c9 100644 --- a/api/go.mod +++ b/api/go.mod @@ -5,8 +5,8 @@ go 1.12 require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.3.3 - github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 github.com/gorilla/websocket v1.4.1 + github.com/hashicorp/cronexpr v0.0.0-20200507212857-921335d977b6 github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-rootcerts v1.0.2 github.com/kr/pretty v0.1.0 diff --git a/api/go.sum b/api/go.sum index 95acde21d500..6bc4dd258da1 100644 --- a/api/go.sum +++ b/api/go.sum @@ -4,10 +4,10 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/docker/go-units v0.3.3 h1:Xk8S3Xj5sLGlG5g67hJmYMmUgXv5N4PhkjJHHqrwnTk= github.com/docker/go-units v0.3.3/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75 h1:f0n1xnMSmBLzVfsMMvriDyA75NB/oBgILX2GcHXIQzY= -github.com/gorhill/cronexpr v0.0.0-20180427100037-88b0669f7d75/go.mod h1:g2644b03hfBX9Ov0ZBDgXXens4rxSxmqFBbhvKv2yVA= github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/cronexpr v0.0.0-20200507212857-921335d977b6 h1:hMI/9mZ+/xcLlMG1VjW/KwScPOJRDyY30b4aUzxfz0g= +github.com/hashicorp/cronexpr v0.0.0-20200507212857-921335d977b6/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= diff --git a/api/jobs.go b/api/jobs.go index acfa1252b4db..0052583f3699 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -7,7 +7,7 @@ import ( "strconv" "time" - "github.com/gorhill/cronexpr" + "github.com/hashicorp/cronexpr" ) const ( diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 0880cbef067e..658a0a9fca90 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -23,7 +23,7 @@ import ( "strings" "time" - "github.com/gorhill/cronexpr" + "github.com/hashicorp/cronexpr" "github.com/hashicorp/go-msgpack/codec" hcodec "github.com/hashicorp/go-msgpack/codec" "github.com/hashicorp/go-multierror" diff --git a/vendor/github.com/gorhill/cronexpr/APLv2 b/vendor/github.com/hashicorp/cronexpr/APLv2 similarity index 100% rename from vendor/github.com/gorhill/cronexpr/APLv2 rename to vendor/github.com/hashicorp/cronexpr/APLv2 diff --git a/vendor/github.com/gorhill/cronexpr/GPLv3 b/vendor/github.com/hashicorp/cronexpr/GPLv3 similarity index 100% rename from vendor/github.com/gorhill/cronexpr/GPLv3 rename to vendor/github.com/hashicorp/cronexpr/GPLv3 diff --git a/vendor/github.com/gorhill/cronexpr/README.md b/vendor/github.com/hashicorp/cronexpr/README.md similarity index 100% rename from vendor/github.com/gorhill/cronexpr/README.md rename to vendor/github.com/hashicorp/cronexpr/README.md diff --git a/vendor/github.com/gorhill/cronexpr/cronexpr.go b/vendor/github.com/hashicorp/cronexpr/cronexpr.go similarity index 59% rename from vendor/github.com/gorhill/cronexpr/cronexpr.go rename to vendor/github.com/hashicorp/cronexpr/cronexpr.go index 58b518fa58de..09572767081c 100644 --- a/vendor/github.com/gorhill/cronexpr/cronexpr.go +++ b/vendor/github.com/hashicorp/cronexpr/cronexpr.go @@ -160,78 +160,122 @@ func (expr *Expression) Next(fromTime time.Time) time.Time { return fromTime } - // Since expr.nextSecond()-expr.nextMonth() expects that the - // supplied time stamp is a perfect match to the underlying cron - // expression, and since this function is an entry point where `fromTime` - // does not necessarily matches the underlying cron expression, - // we first need to ensure supplied time stamp matches - // the cron expression. If not, this means the supplied time - // stamp falls in between matching time stamps, thus we move - // to closest future matching immediately upon encountering a mismatching - // time stamp. - - // year - v := fromTime.Year() - i := sort.SearchInts(expr.yearList, v) - if i == len(expr.yearList) { + loc := fromTime.Location() + t := fromTime.Add(time.Second - time.Duration(fromTime.Nanosecond())*time.Nanosecond) + +WRAP: + + // let's find the next date that satisfies condition + v := t.Year() + if i := sort.SearchInts(expr.yearList, v); i == len(expr.yearList) { return time.Time{} + } else if v != expr.yearList[i] { + t = time.Date(expr.yearList[i], time.Month(expr.monthList[0]), 1, 0, 0, 0, 0, loc) } - if v != expr.yearList[i] { - return expr.nextYear(fromTime) - } - // month - v = int(fromTime.Month()) - i = sort.SearchInts(expr.monthList, v) - if i == len(expr.monthList) { - return expr.nextYear(fromTime) - } - if v != expr.monthList[i] { - return expr.nextMonth(fromTime) + + v = int(t.Month()) + if i := sort.SearchInts(expr.monthList, v); i == len(expr.monthList) { + // try again with a new year + t = time.Date(t.Year()+1, time.Month(expr.monthList[0]), 1, 0, 0, 0, 0, loc) + goto WRAP + } else if v != expr.monthList[i] { + t = time.Date(t.Year(), time.Month(expr.monthList[i]), 1, 0, 0, 0, 0, loc) } - expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(fromTime.Year(), int(fromTime.Month())) + expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(t.Year(), int(t.Month())) if len(expr.actualDaysOfMonthList) == 0 { - return expr.nextMonth(fromTime) + t = time.Date(t.Year(), t.Month()+1, 1, 0, 0, 0, 0, loc) + goto WRAP } - // day of month - v = fromTime.Day() - i = sort.SearchInts(expr.actualDaysOfMonthList, v) - if i == len(expr.actualDaysOfMonthList) { - return expr.nextMonth(fromTime) + v = t.Day() + if i := sort.SearchInts(expr.actualDaysOfMonthList, v); i == len(expr.actualDaysOfMonthList) { + t = time.Date(t.Year(), t.Month()+1, 1, 0, 0, 0, 0, loc) + goto WRAP + } else if v != expr.actualDaysOfMonthList[i] { + t = time.Date(t.Year(), t.Month(), expr.actualDaysOfMonthList[i], 0, 0, 0, 0, loc) + + // in San Palo, before 2019, there may be no midnight (or multiple midnights) + // due to DST + if t.Hour() != 0 { + if t.Hour() > 12 { + t = t.Add(time.Duration(24-t.Hour()) * time.Hour) + } else { + t = t.Add(time.Duration(-t.Hour()) * time.Hour) + } + } } - if v != expr.actualDaysOfMonthList[i] { - return expr.nextDayOfMonth(fromTime) + + if timeZoneInDay(t) { + goto SLOW_CLOCK } - // hour - v = fromTime.Hour() - i = sort.SearchInts(expr.hourList, v) - if i == len(expr.hourList) { - return expr.nextDayOfMonth(fromTime) + + // Fast path where hours/minutes behave as expected trivially + v = t.Hour() + if i := sort.SearchInts(expr.hourList, v); i == len(expr.hourList) { + t = time.Date(t.Year(), t.Month(), t.Day()+1, 0, 0, 0, 0, loc) + goto WRAP + } else if v != expr.hourList[i] { + t = time.Date(t.Year(), t.Month(), t.Day(), expr.hourList[i], expr.minuteList[0], expr.secondList[0], 0, loc) } - if v != expr.hourList[i] { - return expr.nextHour(fromTime) + + v = t.Minute() + if i := sort.SearchInts(expr.minuteList, v); i == len(expr.minuteList) { + t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour()+1, 0, 0, 0, loc) + goto WRAP + } else if v != expr.minuteList[i] { + t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), expr.minuteList[i], expr.secondList[0], 0, loc) } - // minute - v = fromTime.Minute() - i = sort.SearchInts(expr.minuteList, v) - if i == len(expr.minuteList) { - return expr.nextHour(fromTime) + + v = t.Second() + if i := sort.SearchInts(expr.secondList, v); i == len(expr.secondList) { + t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute()+1, 0, 0, loc) + goto WRAP + } else if v != expr.secondList[i] { + t = time.Date(t.Year(), t.Month(), t.Day(), t.Hour(), t.Minute(), expr.secondList[i], 0, loc) } - if v != expr.minuteList[i] { - return expr.nextMinute(fromTime) + + return t + +SLOW_CLOCK: + // daylight saving effect is here, where odd things happen: + // An hour may have 60 minutes, 30 minutes or 90 minutes; + // partial hours may "repeat"! + for !sortContains(expr.hourList, t.Hour()) { + hourBefore := t.Hour() + t = t.Add(time.Hour) + if hourBefore == t.Hour() { + t = t.Add(time.Hour) + } + t = t.Truncate(time.Minute) + if t.Minute() != 0 { + t = t.Add(-1 * time.Minute * time.Duration(t.Minute())) + } + + if t.Hour() == 0 { + t = time.Date(t.Year(), t.Month(), t.Day(), 0, 0, 0, 0, loc) + goto WRAP + } } - // second - v = fromTime.Second() - i = sort.SearchInts(expr.secondList, v) - if i == len(expr.secondList) { - return expr.nextMinute(fromTime) + + for !sortContains(expr.minuteList, t.Minute()) { + hoursBefore := t.Hour() + t = t.Truncate(time.Minute).Add(time.Minute) + if hoursBefore != t.Hour() { + goto WRAP + } } - // If we reach this point, there is nothing better to do - // than to move to the next second + v = t.Second() + t = t.Truncate(time.Minute) + if i := sort.SearchInts(expr.secondList, v); i == len(expr.secondList) { + t = t.Add(time.Minute) + goto WRAP + } else { + t = t.Add(time.Duration(expr.secondList[i]) * time.Second) + } - return expr.nextSecond(fromTime) + return t } /******************************************************************************/ @@ -259,7 +303,7 @@ func (expr *Expression) NextN(fromTime time.Time, n uint) []time.Time { if n == 0 { break } - fromTime = expr.nextSecond(fromTime) + fromTime = expr.Next(fromTime) } } return nextTimes diff --git a/vendor/github.com/gorhill/cronexpr/cronexpr_next.go b/vendor/github.com/hashicorp/cronexpr/cronexpr_next.go similarity index 50% rename from vendor/github.com/gorhill/cronexpr/cronexpr_next.go rename to vendor/github.com/hashicorp/cronexpr/cronexpr_next.go index a0ebdb6b203b..d028a842e093 100644 --- a/vendor/github.com/gorhill/cronexpr/cronexpr_next.go +++ b/vendor/github.com/hashicorp/cronexpr/cronexpr_next.go @@ -33,160 +33,6 @@ var dowNormalizedOffsets = [][]int{ /******************************************************************************/ -func (expr *Expression) nextYear(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate year - i := sort.SearchInts(expr.yearList, t.Year()+1) - if i == len(expr.yearList) { - return time.Time{} - } - // Year changed, need to recalculate actual days of month - expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(expr.yearList[i], expr.monthList[0]) - if len(expr.actualDaysOfMonthList) == 0 { - return expr.nextMonth(time.Date( - expr.yearList[i], - time.Month(expr.monthList[0]), - 1, - expr.hourList[0], - expr.minuteList[0], - expr.secondList[0], - 0, - t.Location())) - } - return time.Date( - expr.yearList[i], - time.Month(expr.monthList[0]), - expr.actualDaysOfMonthList[0], - expr.hourList[0], - expr.minuteList[0], - expr.secondList[0], - 0, - t.Location()) -} - -/******************************************************************************/ - -func (expr *Expression) nextMonth(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate month - i := sort.SearchInts(expr.monthList, int(t.Month())+1) - if i == len(expr.monthList) { - return expr.nextYear(t) - } - // Month changed, need to recalculate actual days of month - expr.actualDaysOfMonthList = expr.calculateActualDaysOfMonth(t.Year(), expr.monthList[i]) - if len(expr.actualDaysOfMonthList) == 0 { - return expr.nextMonth(time.Date( - t.Year(), - time.Month(expr.monthList[i]), - 1, - expr.hourList[0], - expr.minuteList[0], - expr.secondList[0], - 0, - t.Location())) - } - - return time.Date( - t.Year(), - time.Month(expr.monthList[i]), - expr.actualDaysOfMonthList[0], - expr.hourList[0], - expr.minuteList[0], - expr.secondList[0], - 0, - t.Location()) -} - -/******************************************************************************/ - -func (expr *Expression) nextDayOfMonth(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate day of month - i := sort.SearchInts(expr.actualDaysOfMonthList, t.Day()+1) - if i == len(expr.actualDaysOfMonthList) { - return expr.nextMonth(t) - } - - return time.Date( - t.Year(), - t.Month(), - expr.actualDaysOfMonthList[i], - expr.hourList[0], - expr.minuteList[0], - expr.secondList[0], - 0, - t.Location()) -} - -/******************************************************************************/ - -func (expr *Expression) nextHour(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate hour - i := sort.SearchInts(expr.hourList, t.Hour()+1) - if i == len(expr.hourList) { - return expr.nextDayOfMonth(t) - } - - return time.Date( - t.Year(), - t.Month(), - t.Day(), - expr.hourList[i], - expr.minuteList[0], - expr.secondList[0], - 0, - t.Location()) -} - -/******************************************************************************/ - -func (expr *Expression) nextMinute(t time.Time) time.Time { - // Find index at which item in list is greater or equal to - // candidate minute - i := sort.SearchInts(expr.minuteList, t.Minute()+1) - if i == len(expr.minuteList) { - return expr.nextHour(t) - } - - return time.Date( - t.Year(), - t.Month(), - t.Day(), - t.Hour(), - expr.minuteList[i], - expr.secondList[0], - 0, - t.Location()) -} - -/******************************************************************************/ - -func (expr *Expression) nextSecond(t time.Time) time.Time { - // nextSecond() assumes all other fields are exactly matched - // to the cron expression - - // Find index at which item in list is greater or equal to - // candidate second - i := sort.SearchInts(expr.secondList, t.Second()+1) - if i == len(expr.secondList) { - return expr.nextMinute(t) - } - - return time.Date( - t.Year(), - t.Month(), - t.Day(), - t.Hour(), - t.Minute(), - expr.secondList[i], - 0, - t.Location()) -} - -/******************************************************************************/ - func (expr *Expression) calculateActualDaysOfMonth(year, month int) []int { actualDaysOfMonthMap := make(map[int]bool) firstDayOfMonth := time.Date(year, time.Month(month), 1, 0, 0, 0, 0, time.UTC) @@ -290,3 +136,18 @@ func workdayOfMonth(targetDom, lastDom time.Time) int { } return dom } + +func sortContains(a []int, x int) bool { + i := sort.SearchInts(a, x) + return i < len(a) && a[i] == x +} + +func timeZoneInDay(t time.Time) bool { + if t.Location() == time.UTC { + return false + } + + _, off := t.AddDate(0, 0, -1).Zone() + _, ndoff := t.AddDate(0, 0, 1).Zone() + return off != ndoff +} diff --git a/vendor/github.com/gorhill/cronexpr/cronexpr_parse.go b/vendor/github.com/hashicorp/cronexpr/cronexpr_parse.go similarity index 99% rename from vendor/github.com/gorhill/cronexpr/cronexpr_parse.go rename to vendor/github.com/hashicorp/cronexpr/cronexpr_parse.go index a9fe74646cf9..a7f8fe041a72 100644 --- a/vendor/github.com/gorhill/cronexpr/cronexpr_parse.go +++ b/vendor/github.com/hashicorp/cronexpr/cronexpr_parse.go @@ -319,6 +319,10 @@ func (expr *Expression) dowFieldHandler(s string) error { case one: populateOne(expr.daysOfWeek, directive.first) case span: + // To properly handle spans that end in 7 (Sunday) + if directive.last == 0 { + directive.last = 6 + } populateMany(expr.daysOfWeek, directive.first, directive.last, directive.step) case all: populateMany(expr.daysOfWeek, directive.first, directive.last, directive.step) diff --git a/vendor/vendor.json b/vendor/vendor.json index c0ef827100c2..f18005f7fe1e 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -183,7 +183,6 @@ {"path":"github.com/google/go-cmp/cmp/internal/function","checksumSHA1":"kYtvRhMjM0X4bvEjR3pqEHLw1qo=","revision":"d5735f74713c51f7450a43d0a98d41ce2c1db3cb","revisionTime":"2017-09-01T21:42:48Z"}, {"path":"github.com/google/go-cmp/cmp/internal/value","checksumSHA1":"f+mgZLvc4VITtMmBv0bmew4rL2Y=","revision":"d5735f74713c51f7450a43d0a98d41ce2c1db3cb","revisionTime":"2017-09-01T21:42:48Z"}, {"path":"github.com/googleapis/gax-go/v2","checksumSHA1":"WZoHSeTnVjnPIX2+U1Otst5MUKw=","revision":"bd5b16380fd03dc758d11cef74ba2e3bc8b0e8c2","revisionTime":"2019-05-13T18:38:25Z"}, - {"path":"github.com/gorhill/cronexpr","checksumSHA1":"m8B3L3qJ3tFfP6BI9pIFr9oal3w=","comment":"1.0.0","origin":"github.com/dadgar/cronexpr","revision":"675cac9b2d182dccb5ba8d5f8a0d5988df8a4394","revisionTime":"2017-09-15T18:30:32Z"}, {"path":"github.com/gorilla/context","checksumSHA1":"g/V4qrXjUGG9B+e3hB+4NAYJ5Gs=","revision":"08b5f424b9271eedf6f9f0ce86cb9396ed337a42","revisionTime":"2016-08-17T18:46:32Z"}, {"path":"github.com/gorilla/mux","checksumSHA1":"STQSdSj2FcpCf0NLfdsKhNutQT0=","revision":"e48e440e4c92e3251d812f8ce7858944dfa3331c","revisionTime":"2018-08-07T07:52:56Z"}, {"path":"github.com/gorilla/websocket","checksumSHA1":"gr0edNJuVv4+olNNZl5ZmwLgscA=","revision":"0ec3d1bd7fe50c503d6df98ee649d81f4857c564","revisionTime":"2019-03-06T00:42:57Z"}, @@ -210,6 +209,7 @@ {"path":"github.com/hashicorp/consul/sdk/testutil","checksumSHA1":"BdbalXv3cKiFTZpRCy4fgIzHBEU=","revision":"b137060630b463d7ad5360f0d8f32f9347ae3b7d","revisionTime":"2020-02-13T19:55:27Z"}, {"path":"github.com/hashicorp/consul/sdk/testutil/retry","checksumSHA1":"d3PJhffDKar25kzK0iEqssVMkck=","revision":"b137060630b463d7ad5360f0d8f32f9347ae3b7d","revisionTime":"2020-02-13T19:55:27Z"}, {"path":"github.com/hashicorp/consul/version","checksumSHA1":"fRbV3oycM2uY4oOkDoSXtP4o6Tc=","revision":"b137060630b463d7ad5360f0d8f32f9347ae3b7d","revisionTime":"2020-02-13T19:55:27Z"}, + {"path":"github.com/hashicorp/cronexpr","checksumSHA1":"WeBDLvdDp60EAfJO8kJKsQ0QPck=","revision":"921335d977b67fcda1de05136426559895b11ac4","revisionTime":"2020-05-07T21:28:57Z"}, {"path":"github.com/hashicorp/errwrap","checksumSHA1":"cdOCt0Yb+hdErz8NAQqayxPmRsY=","revision":"7554cd9344cec97297fa6649b055a8c98c2a1e55"}, {"path":"github.com/hashicorp/go-checkpoint","checksumSHA1":"D267IUMW2rcb+vNe3QU+xhfSrgY=","revision":"1545e56e46dec3bba264e41fde2c1e2aa65b5dd4","revisionTime":"2017-10-09T17:35:28Z"}, {"path":"github.com/hashicorp/go-cleanhttp","checksumSHA1":"6ihdHMkDfFx/rJ1A36com2F6bQk=","revision":"a45970658e51fea2c41445ff0f7e07106d007617","revisionTime":"2017-02-11T00:33:01Z"}, From 6319db4115131e0ee5505ecd96ee1304c1fa0489 Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Thu, 7 May 2020 18:33:48 -0400 Subject: [PATCH 3/4] Update current DST and some code style issues --- api/jobs.go | 7 ++-- nomad/periodic_test.go | 24 ++++++-------- nomad/structs/structs.go | 6 ++-- nomad/structs/structs_test.go | 61 +++++++++++++++++------------------ 4 files changed, 47 insertions(+), 51 deletions(-) diff --git a/api/jobs.go b/api/jobs.go index 0052583f3699..5759476f9190 100644 --- a/api/jobs.go +++ b/api/jobs.go @@ -648,9 +648,11 @@ func (p *PeriodicConfig) Canonicalize() { // passed time. func (p *PeriodicConfig) Next(fromTime time.Time) (time.Time, error) { if *p.SpecType == PeriodicSpecCron { - if e, err := cronexpr.Parse(*p.Spec); err == nil { - return cronParseNext(e, fromTime, *p.Spec) + e, err := cronexpr.Parse(*p.Spec) + if err != nil { + return time.Time{}, fmt.Errorf("failed parsing cron expression %q: %v", *p.Spec, err) } + return cronParseNext(e, fromTime, *p.Spec) } return time.Time{}, nil @@ -670,6 +672,7 @@ func cronParseNext(e *cronexpr.Expression, fromTime time.Time, spec string) (t t return e.Next(fromTime), nil } + func (p *PeriodicConfig) GetLocation() (*time.Location, error) { if p.TimeZone == nil || *p.TimeZone == "" { return time.UTC, nil diff --git a/nomad/periodic_test.go b/nomad/periodic_test.go index b1f57bb9fef0..7ecc902521bc 100644 --- a/nomad/periodic_test.go +++ b/nomad/periodic_test.go @@ -16,6 +16,7 @@ import ( "github.com/hashicorp/nomad/nomad/structs" "github.com/hashicorp/nomad/testutil" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) type MockJobEvalDispatcher struct { @@ -173,29 +174,22 @@ func TestPeriodicDispatch_Add_UpdateJob(t *testing.T) { t.Parallel() p, _ := testPeriodicDispatcher(t) job := mock.PeriodicJob() - if err := p.Add(job); err != nil { - t.Fatalf("Add failed %v", err) - } + err := p.Add(job) + require.NoError(t, err) tracked := p.Tracked() - if len(tracked) != 1 { - t.Fatalf("Add didn't track the job: %v", tracked) - } + require.Lenf(t, tracked, 1, "did not track the job") // Update the job and add it again. job.Periodic.Spec = "foo" - if err := p.Add(job); err != nil { - t.Fatalf("Add failed %v", err) - } + err = p.Add(job) + require.Error(t, err) + require.Contains(t, err.Error(), "failed parsing cron expression") tracked = p.Tracked() - if len(tracked) != 1 { - t.Fatalf("Add didn't update: %v", tracked) - } + require.Lenf(t, tracked, 1, "did not update") - if !reflect.DeepEqual(job, tracked[0]) { - t.Fatalf("Add didn't properly update: got %v; want %v", tracked[0], job) - } + require.Equalf(t, job, tracked[0], "add did not properly update") } func TestPeriodicDispatch_Add_Remove_Namespaced(t *testing.T) { diff --git a/nomad/structs/structs.go b/nomad/structs/structs.go index 658a0a9fca90..3f2548a41127 100644 --- a/nomad/structs/structs.go +++ b/nomad/structs/structs.go @@ -4425,9 +4425,11 @@ func CronParseNext(e *cronexpr.Expression, fromTime time.Time, spec string) (t t func (p *PeriodicConfig) Next(fromTime time.Time) (time.Time, error) { switch p.SpecType { case PeriodicSpecCron: - if e, err := cronexpr.Parse(p.Spec); err == nil { - return CronParseNext(e, fromTime, p.Spec) + e, err := cronexpr.Parse(p.Spec) + if err != nil { + return time.Time{}, fmt.Errorf("failed parsing cron expression: %q: %v", p.Spec, err) } + return CronParseNext(e, fromTime, p.Spec) case PeriodicSpecTest: split := strings.Split(p.Spec, ",") if len(split) == 1 && split[0] == "" { diff --git a/nomad/structs/structs_test.go b/nomad/structs/structs_test.go index b34269d4c814..96529be7c9eb 100644 --- a/nomad/structs/structs_test.go +++ b/nomad/structs/structs_test.go @@ -2903,45 +2903,42 @@ func TestPeriodicConfig_ValidCron(t *testing.T) { } func TestPeriodicConfig_NextCron(t *testing.T) { - require := require.New(t) - - type testExpectation struct { - Time time.Time - HasError bool - ErrorMsg string - } - from := time.Date(2009, time.November, 10, 23, 22, 30, 0, time.UTC) - specs := []string{"0 0 29 2 * 1980", - "*/5 * * * *", - "1 15-0 * * 1-5"} - expected := []*testExpectation{ + + cases := []struct { + spec string + nextTime time.Time + errorMsg string + }{ { - Time: time.Time{}, - HasError: false, + spec: "0 0 29 2 * 1980", + nextTime: time.Time{}, }, { - Time: time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC), - HasError: false, + spec: "*/5 * * * *", + nextTime: time.Date(2009, time.November, 10, 23, 25, 0, 0, time.UTC), }, { - Time: time.Time{}, - HasError: true, - ErrorMsg: "failed parsing cron expression", + spec: "1 15-0 *", + nextTime: time.Time{}, + errorMsg: "failed parsing cron expression", }, } - for i, spec := range specs { - p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: spec} - p.Canonicalize() - n, err := p.Next(from) - nextExpected := expected[i] - - require.Equal(nextExpected.Time, n) - require.Equal(err != nil, nextExpected.HasError) - if err != nil { - require.True(strings.Contains(err.Error(), nextExpected.ErrorMsg)) - } + for i, c := range cases { + t.Run(fmt.Sprintf("case: %d: %s", i, c.spec), func(t *testing.T) { + p := &PeriodicConfig{Enabled: true, SpecType: PeriodicSpecCron, Spec: c.spec} + p.Canonicalize() + n, err := p.Next(from) + + require.Equal(t, c.nextTime, n) + if c.errorMsg == "" { + require.NoError(t, err) + } else { + require.Error(t, err) + require.Contains(t, err.Error(), c.errorMsg) + } + }) } } @@ -2963,7 +2960,7 @@ func TestPeriodicConfig_DST(t *testing.T) { p := &PeriodicConfig{ Enabled: true, SpecType: PeriodicSpecCron, - Spec: "0 2 11-12 3 * 2017", + Spec: "0 2 11-13 3 * 2017", TimeZone: "America/Los_Angeles", } p.Canonicalize() @@ -2973,7 +2970,7 @@ func TestPeriodicConfig_DST(t *testing.T) { // E1 is an 8 hour adjustment, E2 is a 7 hour adjustment e1 := time.Date(2017, time.March, 11, 10, 0, 0, 0, time.UTC) - e2 := time.Date(2017, time.March, 12, 9, 0, 0, 0, time.UTC) + e2 := time.Date(2017, time.March, 13, 9, 0, 0, 0, time.UTC) n1, err := p.Next(t1) require.Nil(err) From 1e7ebf5f552c35db78c39a84788245152faff79f Mon Sep 17 00:00:00 2001 From: Mahmood Ali Date: Tue, 12 May 2020 16:20:00 -0400 Subject: [PATCH 4/4] vendor: use tagged cronexpr, v1.1.0 Also, update to the version with modification notice --- api/go.mod | 2 +- api/go.sum | 2 ++ vendor/github.com/hashicorp/cronexpr/cronexpr.go | 2 ++ vendor/github.com/hashicorp/cronexpr/cronexpr_next.go | 2 ++ vendor/github.com/hashicorp/cronexpr/cronexpr_parse.go | 2 ++ vendor/vendor.json | 2 +- 6 files changed, 10 insertions(+), 2 deletions(-) diff --git a/api/go.mod b/api/go.mod index 1a18369dd1c9..db05221804db 100644 --- a/api/go.mod +++ b/api/go.mod @@ -6,7 +6,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/docker/go-units v0.3.3 github.com/gorilla/websocket v1.4.1 - github.com/hashicorp/cronexpr v0.0.0-20200507212857-921335d977b6 + github.com/hashicorp/cronexpr v1.1.0 github.com/hashicorp/go-cleanhttp v0.5.1 github.com/hashicorp/go-rootcerts v1.0.2 github.com/kr/pretty v0.1.0 diff --git a/api/go.sum b/api/go.sum index 6bc4dd258da1..29fee1def02f 100644 --- a/api/go.sum +++ b/api/go.sum @@ -8,6 +8,8 @@ github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvK github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/hashicorp/cronexpr v0.0.0-20200507212857-921335d977b6 h1:hMI/9mZ+/xcLlMG1VjW/KwScPOJRDyY30b4aUzxfz0g= github.com/hashicorp/cronexpr v0.0.0-20200507212857-921335d977b6/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= +github.com/hashicorp/cronexpr v1.1.0 h1:dnNsWtH0V2ReN7JccYe8m//Bj14+PjJDntR1dz0Cixk= +github.com/hashicorp/cronexpr v1.1.0/go.mod h1:P4wA0KBl9C5q2hABiMO7cp6jcIg96CDh1Efb3g1PWA4= github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc= diff --git a/vendor/github.com/hashicorp/cronexpr/cronexpr.go b/vendor/github.com/hashicorp/cronexpr/cronexpr.go index 09572767081c..79be5f82db84 100644 --- a/vendor/github.com/hashicorp/cronexpr/cronexpr.go +++ b/vendor/github.com/hashicorp/cronexpr/cronexpr.go @@ -1,6 +1,8 @@ /*! * Copyright 2013 Raymond Hill * + * Modifications 2020 - HashiCorp + * * Project: github.com/gorhill/cronexpr * File: cronexpr.go * Version: 1.0 diff --git a/vendor/github.com/hashicorp/cronexpr/cronexpr_next.go b/vendor/github.com/hashicorp/cronexpr/cronexpr_next.go index d028a842e093..157dc93b237c 100644 --- a/vendor/github.com/hashicorp/cronexpr/cronexpr_next.go +++ b/vendor/github.com/hashicorp/cronexpr/cronexpr_next.go @@ -1,6 +1,8 @@ /*! * Copyright 2013 Raymond Hill * + * Modifications 2020 - HashiCorp + * * Project: github.com/gorhill/cronexpr * File: cronexpr_next.go * Version: 1.0 diff --git a/vendor/github.com/hashicorp/cronexpr/cronexpr_parse.go b/vendor/github.com/hashicorp/cronexpr/cronexpr_parse.go index a7f8fe041a72..d16f22aa1e47 100644 --- a/vendor/github.com/hashicorp/cronexpr/cronexpr_parse.go +++ b/vendor/github.com/hashicorp/cronexpr/cronexpr_parse.go @@ -1,6 +1,8 @@ /*! * Copyright 2013 Raymond Hill * + * Modifications 2020 - HashiCorp + * * Project: github.com/gorhill/cronexpr * File: cronexpr_parse.go * Version: 1.0 diff --git a/vendor/vendor.json b/vendor/vendor.json index f18005f7fe1e..d373c914fdcf 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -209,7 +209,7 @@ {"path":"github.com/hashicorp/consul/sdk/testutil","checksumSHA1":"BdbalXv3cKiFTZpRCy4fgIzHBEU=","revision":"b137060630b463d7ad5360f0d8f32f9347ae3b7d","revisionTime":"2020-02-13T19:55:27Z"}, {"path":"github.com/hashicorp/consul/sdk/testutil/retry","checksumSHA1":"d3PJhffDKar25kzK0iEqssVMkck=","revision":"b137060630b463d7ad5360f0d8f32f9347ae3b7d","revisionTime":"2020-02-13T19:55:27Z"}, {"path":"github.com/hashicorp/consul/version","checksumSHA1":"fRbV3oycM2uY4oOkDoSXtP4o6Tc=","revision":"b137060630b463d7ad5360f0d8f32f9347ae3b7d","revisionTime":"2020-02-13T19:55:27Z"}, - {"path":"github.com/hashicorp/cronexpr","checksumSHA1":"WeBDLvdDp60EAfJO8kJKsQ0QPck=","revision":"921335d977b67fcda1de05136426559895b11ac4","revisionTime":"2020-05-07T21:28:57Z"}, + {"path":"github.com/hashicorp/cronexpr","checksumSHA1":"lKpw2wPcDeImWS2fPAzfSJl+Gcc=","revision":"d968249ea977a46db0f3588b5c5e438243cca0cb","revisionTime":"2020-05-08T15:08:16Z","version":"v1.1.0","versionExact":"v1.1.0"}, {"path":"github.com/hashicorp/errwrap","checksumSHA1":"cdOCt0Yb+hdErz8NAQqayxPmRsY=","revision":"7554cd9344cec97297fa6649b055a8c98c2a1e55"}, {"path":"github.com/hashicorp/go-checkpoint","checksumSHA1":"D267IUMW2rcb+vNe3QU+xhfSrgY=","revision":"1545e56e46dec3bba264e41fde2c1e2aa65b5dd4","revisionTime":"2017-10-09T17:35:28Z"}, {"path":"github.com/hashicorp/go-cleanhttp","checksumSHA1":"6ihdHMkDfFx/rJ1A36com2F6bQk=","revision":"a45970658e51fea2c41445ff0f7e07106d007617","revisionTime":"2017-02-11T00:33:01Z"},