From 1cb735d6a4421dbb57869904e2e472c5bc5e3707 Mon Sep 17 00:00:00 2001 From: Tonnis Wildeboer Date: Mon, 8 Jan 2018 11:17:38 -0800 Subject: [PATCH] Add support to configure times to suspend chaos. --- Dockerfile | 2 + README.md | 20 ++ main.go | 274 +++++++++++++++++++++++++-- main_test.go | 524 +++++++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 808 insertions(+), 12 deletions(-) create mode 100644 main_test.go diff --git a/Dockerfile b/Dockerfile index 1dde631c..1ab6a840 100644 --- a/Dockerfile +++ b/Dockerfile @@ -12,6 +12,8 @@ RUN go build -o /bin/chaoskube -v \ FROM alpine:3.6 MAINTAINER Linki +RUN apk --no-cache add tzdata + RUN addgroup -S chaoskube && adduser -S -g chaoskube chaoskube COPY --from=builder /bin/chaoskube /bin/chaoskube diff --git a/README.md b/README.md index 4828d1a1..4863248a 100644 --- a/README.md +++ b/README.md @@ -139,6 +139,26 @@ spec: ... ``` +## Limiting the Chaos + +You can limit when chaos introduced. To turn on the feature, add the `--limit-chaos` option and the `--location` option, which requires a timezone name from the [(IANA) tz databse](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones). Alternatively, you can use `UTC` or `Local` as the location. +By default, this will only allow chaos to be introduced between 9:30 and 14:30, and not on Saturday or Sunday. You can also explicitly add a list of `YYYY-MM-DD`-formatted dates as "holidays". See the options chart below for more details. + +## Options + +| Option | Description | Default | +|------------------|---------------------------------------------------------------------|------------------------| +| `--interval` | interval between pod terminations | 10m | +| `--labels` | label selector to filter pods by | (matches everything) | +| `--annotations` | annotation selector to filter pods by | (matches everything) | +| `--namespaces` | namespace selector to filter pods by | (all namespaces) | +| `--dry-run` | don't kill pods, only log what would have been done | true | +| `--limit-chaos` | limit chaos according to specified times/days | false | +| `--location` | timezone from tz database, e.g "America/New_York", "UTC" or "Local" | (none) | +| `--off-days` | days when chaos is to be suspended. (Or "none") | "Saturday,Sunday" | +| `--chaos-hrs` | start and end time for introducing chaos (24hr time) | "start:9:30,end:14:30" | +| `--holidays` | comma-separated, "YYYY-MM-DD" days to skip chaos | (empty list) | + ## Contributing Feel free to create issues or submit pull requests. diff --git a/main.go b/main.go index aa99def8..5fbd03ae 100644 --- a/main.go +++ b/main.go @@ -1,7 +1,10 @@ package main import ( + "fmt" "os" + "strconv" + "strings" "time" log "github.com/sirupsen/logrus" @@ -15,19 +18,56 @@ import ( "github.com/linki/chaoskube/chaoskube" ) +const ( + limitChaosOpt = "limit-chaos" + locationOpt = "location" + offDaysOpt = "off-days" + chaosHrsOpt = "chaos-hours" + holidaysOpt = "holidays" + + defaultStartHr = 9 + defaultStartMin = 30 + defaultEndHr = 16 + defaultEndMin = 30 + + iso8601 = "2006-01-02" +) + var ( - labelString string - annString string - nsString string - master string - kubeconfig string - interval time.Duration - inCluster bool - dryRun bool - debug bool - version string + labelString string + annString string + nsString string + master string + kubeconfig string + interval time.Duration + inCluster bool + dryRun bool + debug bool + version string + limitChaos bool + locationString string + offDaysString string + chaosHrsString string + holidaysString string ) +// offtimeCfg holds configuration information related to when to suspend the chaos. +type offtimeCfg struct { + // Whether chaos limiting is enabled + enabled bool + // timezone in which the worktimes are expressed + location *time.Location + // Days on which chaos is suspended + offDays []time.Weekday + // Chaos start and end hours and minutes + chaosStartHr int + chaosStartMin int + chaosEndHr int + chaosEndMin int + // holidays, assumed to be expressed in UTC, regardless of Location + holidays []time.Time +} + func init() { kingpin.Flag("labels", "A set of labels to restrict the list of affected pods. Defaults to everything.").StringVar(&labelString) kingpin.Flag("annotations", "A set of annotations to restrict the list of affected pods. Defaults to everything.").StringVar(&annString) @@ -37,6 +77,14 @@ func init() { kingpin.Flag("interval", "Interval between Pod terminations").Default("10m").DurationVar(&interval) kingpin.Flag("dry-run", "If true, don't actually do anything.").Default("true").BoolVar(&dryRun) kingpin.Flag("debug", "Enable debug logging.").BoolVar(&debug) + kingpin.Flag(limitChaosOpt, "Whether to limit chaos according to configuration. Defaults to false.").Default("false").BoolVar(&limitChaos) + kingpin.Flag(locationOpt, `Timezone location from the "tz database" (e.g. "America/Los_Angeles", not "PDT") `+ + `for interpreting chaos-period start and stop times. No default.`).StringVar(&locationString) + help := fmt.Sprintf(`Daily start and end times for introducing chaos. Defaults to "start: %d:%d, end: %d:%d".`, + defaultStartHr, defaultStartMin, defaultEndHr, defaultEndMin) + kingpin.Flag(chaosHrsOpt, help).StringVar(&chaosHrsString) + kingpin.Flag(offDaysOpt, `A list of days of the week when chaos is suspended. Defaults to "Saturday, Sunday". (Use "none" for no off days.)`).StringVar(&offDaysString) + kingpin.Flag(holidaysOpt, `A list of ISO 8601 dates (YYYY-MM-DD) when chaos is suspended. Defaults to and empty list.`).StringVar(&holidaysString) } func main() { @@ -83,6 +131,18 @@ func main() { log.Infof("Filtering pods by namespaces: %s", namespaces.String()) } + offcfg, err := handleOfftimeConfig(limitChaos, locationString, offDaysString, chaosHrsString, holidaysString) + if err != nil { + log.Fatal(err) + } + if offcfg.enabled { + log.Infof("Limiting chaos. %s: %s, %s: %s, %s: %s, %s: %s", + locationOpt, locationString, + offDaysOpt, offDaysString, + chaosHrsOpt, chaosHrsString, + holidaysOpt, holidaysString) + } + chaoskube := chaoskube.New( client, labelSelector, @@ -94,8 +154,12 @@ func main() { ) for { - if err := chaoskube.TerminateVictim(); err != nil { - log.Fatal(err) + if timeToSuspend(time.Now(), *offcfg) { + log.Debugf("Chaos currently suspended") + } else { + if err := chaoskube.TerminateVictim(); err != nil { + log.Fatal(err) + } } log.Debugf("Sleeping for %s...", interval) @@ -124,3 +188,189 @@ func newClient() (*kubernetes.Clientset, error) { return client, nil } + +func setLocation(offcfg *offtimeCfg, locationStr string) error { + var err error + if len(locationStr) == 0 { + err = fmt.Errorf("timezone location is required if %s is enabled", limitChaosOpt) + return err + } + offcfg.location, err = time.LoadLocation(locationStr) + if err != nil { + err = fmt.Errorf(err.Error()+`- %s must one of: a timezone from the "tz database" (IANA), "UTC" or "Local"`, locationOpt) + return err + } + return err +} + +func setOffDays(offcfg *offtimeCfg, offDaysStr string) error { + var err error + offcfg.offDays = make([]time.Weekday, 0, 2) + if offDaysStr == "none" { + return err + } else if len(offDaysStr) == 0 { + offcfg.offDays = append(offcfg.offDays, time.Saturday, time.Sunday) + } else { + days := strings.Split(offDaysStr, ",") + for _, day := range days { + switch strings.TrimSpace(day) { + case time.Sunday.String(): + offcfg.offDays = append(offcfg.offDays, time.Sunday) + case time.Monday.String(): + offcfg.offDays = append(offcfg.offDays, time.Monday) + case time.Tuesday.String(): + offcfg.offDays = append(offcfg.offDays, time.Tuesday) + case time.Wednesday.String(): + offcfg.offDays = append(offcfg.offDays, time.Wednesday) + case time.Thursday.String(): + offcfg.offDays = append(offcfg.offDays, time.Thursday) + case time.Friday.String(): + offcfg.offDays = append(offcfg.offDays, time.Friday) + case time.Saturday.String(): + offcfg.offDays = append(offcfg.offDays, time.Saturday) + default: + err = fmt.Errorf("unrecognized day of week in %s: %s", offDaysOpt, day) + return err + } + } + } + return err +} + +func setChaosHours(offcfg *offtimeCfg, chaosHrsStr string) error { + var err error + if len(chaosHrsStr) == 0 { + offcfg.chaosStartHr = defaultStartHr + offcfg.chaosStartMin = defaultStartMin + offcfg.chaosEndHr = defaultEndHr + offcfg.chaosEndMin = defaultEndMin + } else { + startEnd := strings.Split(chaosHrsStr, ",") + for _, item := range startEnd { + switch kv := strings.SplitN(strings.TrimSpace(item), ":", 2); kv[0] { + case "start": + offcfg.chaosStartHr, offcfg.chaosStartMin, err = getHrMin(kv[1]) + if err != nil { + err = fmt.Errorf(`in %s, could not parse "%s"`, chaosHrsOpt, item) + return err + } + case "end": + offcfg.chaosEndHr, offcfg.chaosEndMin, err = getHrMin(kv[1]) + if err != nil { + err = fmt.Errorf(`in %s, could not parse "%s"`, chaosHrsOpt, item) + return err + } + default: + err = fmt.Errorf(`%s requires this format: "start: 9:30, end: 17:30". (Got key: "%s")`, chaosHrsOpt, kv[0]) + return err + } + } + } + // Validate + v1 := offcfg.chaosStartHr*10 + offcfg.chaosStartMin + v2 := offcfg.chaosEndHr*10 + offcfg.chaosEndMin + if v1 > v2 { + err = fmt.Errorf("%s may not specify a period that spans midnight, and must be expressed in 24hr time", chaosHrsOpt) + } + return err +} + +// getHrmMin parses out the hr and min from " hr:min" +func getHrMin(hrmMinStr string) (hr, min int, err error) { + hm := strings.Split(strings.TrimSpace(hrmMinStr), ":") + hr, err = strconv.Atoi(hm[0]) + if err != nil { + return hr, min, err + } + min, err = strconv.Atoi(hm[1]) + if err != nil { + return hr, min, err + } + return hr, min, err +} + +func setHolidays(offcfg *offtimeCfg, holidaysStr string) error { + var err error + if len(holidaysStr) == 0 { + // Leave Holidays nil + return err + } + offcfg.holidays = make([]time.Time, 0) + for _, hStr := range strings.Split(holidaysStr, ",") { + layout := iso8601 + var holiday time.Time + holiday, err = time.ParseInLocation(layout, strings.TrimSpace(hStr), offcfg.location) + if err != nil { + err = fmt.Errorf(`in %s, invalid date format. "YYYY-MM-DD" required. (Got "%s")`, holidaysOpt, hStr) + return err + } + offcfg.holidays = append(offcfg.holidays, holiday) + } + return err +} + +func handleOfftimeConfig(enabled bool, locationStr, offDaysStr, chaosHrsStr, holidaysStr string) (*offtimeCfg, error) { + var err error + offcfg := &offtimeCfg{} + + offcfg.enabled = enabled + if !enabled { + // Not enabled, no need to set other values + return offcfg, err + } + + if err = setLocation(offcfg, locationStr); err != nil { + return offcfg, err + } + + if err = setOffDays(offcfg, offDaysStr); err != nil { + return offcfg, err + } + + if err = setChaosHours(offcfg, chaosHrsStr); err != nil { + return offcfg, err + } + + if err = setHolidays(offcfg, holidaysStr); err != nil { + return offcfg, err + } + + return offcfg, err +} + +// timeToSuspend examines the supplied time and offtimeCfg and determines whether it is time to suspend chaos. +func timeToSuspend(currTime time.Time, offcfg offtimeCfg) bool { + if !offcfg.enabled { + // If limiting not enabled, it's never time to suspend + return false + } + + // Localize the currTime + locTime := currTime.In(offcfg.location) + + // Check offDays + currDay := locTime.Weekday() + for _, od := range offcfg.offDays { + if currDay == od { + return true + } + } + + // Check holidays + ty, tm, td := locTime.Date() + for _, holiday := range offcfg.holidays { + hy, hm, hd := holiday.Date() + if ty == hy && tm == hm && td == hd { + return true + } + } + + // Check time of day. Start by getting today's chaos start/end times + chaosStart := time.Date(ty, tm, td, offcfg.chaosStartHr, offcfg.chaosStartMin, 0, 0, offcfg.location) + chaosEnd := time.Date(ty, tm, td, offcfg.chaosEndHr, offcfg.chaosEndMin, 0, 0, offcfg.location) + if !((chaosStart.Before(locTime) || chaosStart.Equal(locTime)) && locTime.Before(chaosEnd)) { + return true + } + + return false +} diff --git a/main_test.go b/main_test.go new file mode 100644 index 00000000..c95d3689 --- /dev/null +++ b/main_test.go @@ -0,0 +1,524 @@ +package main + +import ( + "fmt" + "strings" + "testing" + "time" +) + +const dhmsLayout = "2006-01-02 15:04:05" + +func Test_handleOfftimeConfig_location(t *testing.T) { + var err error + var descr string + var locationStr string + var offDaysStr string + var chaosHrsStr string + var holidaysStr string + var offcfg *offtimeCfg + + descr = "Zero value locationStr" + locationStr = "" + offDaysStr = "" + chaosHrsStr = "" + holidaysStr = "" + _, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err == nil { + t.Errorf("%s: Expected err", descr) + } else { + substr := "required" + msg := err.Error() + if !strings.Contains(msg, substr) { + t.Errorf(`%s: Expected "%s" in error message. Got "%s"`, descr, substr, msg) + } + } + + // ------------------------------------------------------------------------- + + descr = "Unparsable locationStr" + locationStr = "PDT" + offDaysStr = "" + chaosHrsStr = "" + holidaysStr = "" + _, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err == nil { + t.Errorf("%s: Expected err", descr) + } else { + substr := "tz database" + msg := err.Error() + if !strings.Contains(msg, substr) { + t.Errorf(`%s: Expected "%s" in error message. Got "%s"`, descr, substr, msg) + } + } + + // ------------------------------------------------------------------------- + + descr = "Good timezone locationStr" + locationStr = "America/Los_Angeles" + offDaysStr = "" + chaosHrsStr = "" + holidaysStr = "" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf("%s: Unexpected err: %s", descr, err.Error()) + } else { + actual := offcfg.location.String() + if actual != locationStr { + t.Errorf(`"%s: offcfg.location: Expected "%s", got "%s"`, descr, locationStr, actual) + } + } +} + +func Test_handleOfftimeConfig_offDays(t *testing.T) { + var err error + var descr string + var locationStr string + var offDaysStr string + var chaosHrsStr string + var holidaysStr string + var offcfg *offtimeCfg + + descr = `Empty offDays` + locationStr = "UTC" + offDaysStr = "" + chaosHrsStr = "" + holidaysStr = "" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf("%s: Unexpected err: %s", descr, err.Error()) + } else { + expected := []time.Weekday{time.Saturday, time.Sunday} + actual := offcfg.offDays + if !wkdSlicesEquivalent(expected, actual) { + t.Errorf(`%s: "offcfg.offDays: Expected "%#v", got "%#v"`, descr, expected, actual) + } + } + + // ------------------------------------------------------------------------- + + descr = `offDays = "none"` + locationStr = "UTC" + offDaysStr = "none" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf("%s: Unexpected err: %s", descr, err.Error()) + } else { + var expected []time.Weekday + actual := offcfg.offDays + if !wkdSlicesEquivalent(expected, actual) { + t.Errorf(`%s: "offcfg.offDays: Expected "%#v", got "%#v"`, descr, expected, actual) + } + } + + // ------------------------------------------------------------------------- + + descr = `Bad offDays` + locationStr = "UTC" + offDaysStr = "Saturday, Zontag" + chaosHrsStr = "" + holidaysStr = "" + _, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err == nil { + t.Errorf("%s: Expected err", descr) + } else { + substr := "unrecognized" + msg := err.Error() + if !strings.Contains(msg, substr) { + t.Errorf(`%s: Expected "%s" in error message. Got "%s"`, descr, substr, msg) + } + } + + // ------------------------------------------------------------------------- + + descr = `Three off days` + locationStr = "UTC" + offDaysStr = "Thursday, Monday,Friday" // various whitespace too + chaosHrsStr = "" + holidaysStr = "" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf("%s: Unexpected err: %s", descr, err.Error()) + } else { + expected := []time.Weekday{time.Monday, time.Thursday, time.Friday} + actual := offcfg.offDays + if !wkdSlicesEquivalent(expected, actual) { + t.Errorf(`%s: "offcfg.offDays: Expected "%#v", got "%#v"`, descr, expected, actual) + } + } +} + +func Test_handleOfftimeConfig_chaosHrs(t *testing.T) { + var err error + var descr string + var locationStr string + var offDaysStr string + var chaosHrsStr string + var holidaysStr string + var offcfg *offtimeCfg + + descr = `Empty chaos hours` + locationStr = "UTC" + offDaysStr = "" + chaosHrsStr = "" + holidaysStr = "" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf("%s: Unexpected err: %s", descr, err.Error()) + } else { + loc, _ := time.LoadLocation(locationStr) + expected := offtimeCfg{ + enabled: true, + location: loc, + offDays: []time.Weekday{time.Saturday, time.Sunday}, + chaosStartHr: defaultStartHr, + chaosStartMin: defaultStartMin, + chaosEndHr: defaultEndHr, + chaosEndMin: defaultEndMin, + } + actual := offcfg + // Just compare the start/end times + if !(actual.chaosStartHr == expected.chaosStartHr && + actual.chaosStartMin == expected.chaosStartMin && + actual.chaosEndHr == expected.chaosEndHr && + actual.chaosEndMin == expected.chaosEndMin) { + t.Errorf(`%s: Expected "%#v", got "%#v"`, descr, expected, actual) + } + } + + // ------------------------------------------------------------------------- + + descr = `chaos hours with leading zeros should be OK` + locationStr = "UTC" + offDaysStr = "" + chaosHrsStr = "start: 00:01, end: 03:00" + holidaysStr = "" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf("%s: Unexpected err: %s", descr, err.Error()) + } else { + loc, _ := time.LoadLocation(locationStr) + expected := offtimeCfg{ + enabled: true, + location: loc, + offDays: []time.Weekday{time.Saturday, time.Sunday}, + chaosStartHr: 0, + chaosStartMin: 1, + chaosEndHr: 3, + chaosEndMin: 0, + } + actual := offcfg + // Just compare the start/end times + if !(actual.chaosStartHr == expected.chaosStartHr && + actual.chaosStartMin == expected.chaosStartMin && + actual.chaosEndHr == expected.chaosEndHr && + actual.chaosEndMin == expected.chaosEndMin) { + t.Errorf(`%s: Expected "%#v", got "%#v"`, descr, expected, actual) + } + } + + // ------------------------------------------------------------------------- + + descr = `chaos hours may not span midnight` + locationStr = "UTC" + offDaysStr = "" + chaosHrsStr = "start: 11:59, end: 00:00" + holidaysStr = "" + _, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err == nil { + t.Errorf("%s: Expected error.", descr) + } else { + substr := "midnight" + msg := err.Error() + if !strings.Contains(msg, substr) { + t.Errorf(`%s: Expected "%s" in error message. Got "%s"`, descr, substr, msg) + } + } + + // ------------------------------------------------------------------------- + + descr = `chaos hours - bad number` + locationStr = "UTC" + offDaysStr = "" + chaosHrsStr = "start: 1O:59, end: 13:00" // capital O (letter) not 0 + holidaysStr = "" + _, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err == nil { + t.Errorf("%s: Expected error.", descr) + } else { + substr := "could not parse" + msg := err.Error() + if !strings.Contains(msg, substr) { + t.Errorf(`%s: Expected "%s" in error message. Got "%s"`, descr, substr, msg) + } + } +} + +func Test_handleOfftimeConfig_holidays(t *testing.T) { + var err error + var descr string + var locationStr string + var offDaysStr string + var chaosHrsStr string + var holidaysStr string + var offcfg *offtimeCfg + + locationStr = "UTC" + offDaysStr = "" + chaosHrsStr = "" + holidaysStr = "" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf("%s: Unexpected err: %s", descr, err.Error()) + } else { + actual := len(offcfg.holidays) + if actual != 0 { + t.Errorf(`%s: Expected 0 holidays, got %d`, descr, actual) + } + } + + // ------------------------------------------------------------------------- + + descr = `One holiday: New Years Day in Los Angeles` + locationStr = "America/Los_Angeles" + offDaysStr = "" + chaosHrsStr = "" + holidaysStr = "2016-01-01" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf("%s: Unexpected err: %s", descr, err.Error()) + } else { + actual := len(offcfg.holidays) + if actual != 1 { + t.Errorf(`%s: Expected 1 holiday, got %d`, descr, actual) + } + dateStr := offcfg.holidays[0].Format(time.RFC822Z) + expected := "01 Jan 16 00:00 -0800" + if dateStr != expected { + t.Errorf(`%s: Expected "%s", got "%s"`, descr, expected, dateStr) + } + } + + // ------------------------------------------------------------------------- + + descr = `Two holidays in Los Angeles` + locationStr = "America/Los_Angeles" + offDaysStr = "" + chaosHrsStr = "" + holidaysStr = "2016-01-01, 2014-12-25" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf("%s: Unexpected err: %s", descr, err.Error()) + } else { + actual := len(offcfg.holidays) + if actual != 2 { + t.Errorf(`%s: Expected 2 holidays, got %d`, descr, actual) + } + + dateStr := offcfg.holidays[0].Format(time.RFC822Z) + expected := "01 Jan 16 00:00 -0800" + if dateStr != expected { + t.Errorf(`%s: Expected "%s", got "%s"`, descr, expected, dateStr) + } + + dateStr = offcfg.holidays[1].Format(time.RFC822Z) + expected = "25 Dec 14 00:00 -0800" + if dateStr != expected { + t.Errorf(`%s: Expected "%s", got "%s"`, descr, expected, dateStr) + } + } + + // ------------------------------------------------------------------------- + + descr = `Bad holiday string` + locationStr = "UTC" + offDaysStr = "" + chaosHrsStr = "" + holidaysStr = "1/1/2016" + _, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err == nil { + t.Errorf("%s: Expected err", descr) + } else { + substr := "invalid date format" + msg := err.Error() + if !strings.Contains(msg, substr) { + t.Errorf(`%s: Expected "%s" in error message. Got "%s"`, descr, substr, msg) + } + } +} + +func Test_timeToSuspend(t *testing.T) { + var err error + var descr string + var locationStr string + var offDaysStr string + var chaosHrsStr string + var holidaysStr string + var offcfg *offtimeCfg + var testTime time.Time + + descr = "Suspend - respect stated timezone" + locationStr = "America/Los_Angeles" + offDaysStr = "none" + chaosHrsStr = "" + holidaysStr = "2016-01-01" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + return + } + utc, err := time.LoadLocation("UTC") + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + return + } + + // Set testTime to same day as holiday, but in UTC + testTime, err = time.ParseInLocation(iso8601, holidaysStr, utc) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + } else { + // Date is correct, current time is UTC, so Los Angeles is still day before + actual := timeToSuspend(testTime, *offcfg) + expected := false + if actual != expected { + t.Errorf(`%s: Got %v, expected %v`, descr, actual, expected) + } + } + + // Now use first instant of holiday in Los Angeles TZ + testTime, err = time.ParseInLocation(iso8601, holidaysStr, offcfg.location) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + } else { + actual := timeToSuspend(testTime, *offcfg) + expected := true + if actual != expected { + t.Errorf(`%s: Got %v, expected %v`, descr, actual, expected) + } + } + + // ------------------------------------------------------------------------- + + descr = "Suspend - respect off days" + locationStr = "America/Los_Angeles" + offDaysStr = "none" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + } + + var testDayStr = "2016-01-05 13:00:00" // this is a Tuesday + + testTime, err = time.ParseInLocation(dhmsLayout, testDayStr, offcfg.location) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + } else { + // No holidays, no off days, default chaosHours + actual := timeToSuspend(testTime, *offcfg) + expected := false + if actual != expected { + t.Errorf(`%s: Got %v, expected %v`, descr, actual, expected) + } + } + + // Set offDays to Wednesday + offDaysStr = "Wednesday" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + } else { + actual := timeToSuspend(testTime, *offcfg) + expected := false + if actual != expected { + t.Errorf(`%s: Got %v, expected %v`, descr, actual, expected) + } + } + + // Set offDays to Tuesday + offDaysStr = "Tuesday" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + } else { + actual := timeToSuspend(testTime, *offcfg) + expected := true + if actual != expected { + t.Errorf(`%s: Got %v, expected %v`, descr, actual, expected) + } + } + + // ------------------------------------------------------------------------- + + descr = "Suspend - respect chaos hrs" + locationStr = "America/Los_Angeles" + offDaysStr = "none" + chaosHrsStr = "" // use defaults + holidaysStr = "" + offcfg, err = handleOfftimeConfig(true, locationStr, offDaysStr, chaosHrsStr, holidaysStr) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + return + } + + var testTimeStr = fmt.Sprintf("2016-01-05 %d:%d:00", offcfg.chaosStartHr, offcfg.chaosStartMin) + testTime, err = time.ParseInLocation(dhmsLayout, testTimeStr, offcfg.location) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + } else { + // No holidays, no off days, default chaosHours - testTime is start of chaos period + actual := timeToSuspend(testTime, *offcfg) + expected := false + if actual != expected { + t.Errorf(`%s: Got %v, expected %v`, descr, actual, expected) + } + } + + // Subtract to one sec before chaos period + minusOneSec, err := time.ParseDuration("-1s") + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + } else { + testTime = testTime.Add(minusOneSec) + actual := timeToSuspend(testTime, *offcfg) + expected := true + if actual != expected { + t.Errorf(`%s: Got %v, expected %v`, descr, actual, expected) + } + } + + // Set to end of chaos period + testTimeStr = fmt.Sprintf("2016-01-05 %d:%d:00", offcfg.chaosEndHr, offcfg.chaosEndMin) + testTime, err = time.ParseInLocation(dhmsLayout, testTimeStr, offcfg.location) + if err != nil { + t.Errorf(`%s: ERROR IN TEST - %v`, descr, err) + } else { + actual := timeToSuspend(testTime, *offcfg) + expected := true + if actual != expected { + t.Errorf(`%s: Got %v, expected %v`, descr, actual, expected) + } + } +} + +// ----------------------------------------------------------------------------- + +// wkdSlicesEquivalent compares two slices of time.Weekday but ignores order. +func wkdSlicesEquivalent(a []time.Weekday, b []time.Weekday) bool { + if len(a) != len(b) { + return false + } + for _, aw := range a { + found := false + for _, bw := range b { + if bw == aw { + found = true + break + } + } + if !found { + return false + } + } + return true +}