From 0d9e2dd74e4e38449ae4e30253c4c15ba3f693ef Mon Sep 17 00:00:00 2001 From: Martin Linkhorst Date: Mon, 5 Mar 2018 17:02:39 +0100 Subject: [PATCH] feat: specify days of a year without chaos --- chaoskube/chaoskube.go | 26 ++++++++--- chaoskube/chaoskube_test.go | 86 ++++++++++++++++++++++++++++++++++++- main.go | 15 +++++++ util/util.go | 21 +++++++++ util/util_test.go | 65 ++++++++++++++++++++++++++++ 5 files changed, 205 insertions(+), 8 deletions(-) diff --git a/chaoskube/chaoskube.go b/chaoskube/chaoskube.go index 8a0a024e..cd595024 100644 --- a/chaoskube/chaoskube.go +++ b/chaoskube/chaoskube.go @@ -31,6 +31,8 @@ type Chaoskube struct { ExcludedWeekdays []time.Weekday // a list of time periods of a day when termination is suspended ExcludedTimesOfDay []util.TimePeriod + // a list of days of a year when termination is suspended + ExcludedDaysOfYear []time.Time // the timezone to apply when detecting the current weekday Timezone *time.Location // an instance of logrus.StdLogger to write log messages to @@ -50,14 +52,18 @@ var ( msgWeekdayExcluded = "weekday excluded" // msgTimeOfDayExcluded is the log message when termination is suspended due to the time of day filter msgTimeOfDayExcluded = "time of day excluded" + // msgDayOfYearExcluded is the log message when termination is suspended due to the day of year filter + msgDayOfYearExcluded = "day of year excluded" ) -// New returns a new instance of Chaoskube. It expects a kubernetes client, a -// label, annotation and/or namespace selector to reduce the amount of affected -// pods as well as whether to enable dryRun mode and a seed to seed the randomizer -// with. You can also provide a list of weekdays and corresponding time zone when -// chaoskube should be inactive. -func New(client kubernetes.Interface, labels, annotations, namespaces labels.Selector, excludedWeekdays []time.Weekday, excludedTimesOfDay []util.TimePeriod, timezone *time.Location, logger log.FieldLogger, dryRun bool) *Chaoskube { +// New returns a new instance of Chaoskube. It expects: +// * a Kubernetes client to connect to a Kubernetes API +// * label, annotation and/or namespace selectors to reduce the amount of possible target pods +// * a list of weekdays, times of day and/or days of a year when chaos mode is disabled +// * a time zone to apply to the aforementioned time-based filters +// * a logger implementing logrus.FieldLogger to send log output to +// * whether to enable/disable dry-run mode +func New(client kubernetes.Interface, labels, annotations, namespaces labels.Selector, excludedWeekdays []time.Weekday, excludedTimesOfDay []util.TimePeriod, excludedDaysOfYear []time.Time, timezone *time.Location, logger log.FieldLogger, dryRun bool) *Chaoskube { return &Chaoskube{ Client: client, Labels: labels, @@ -65,6 +71,7 @@ func New(client kubernetes.Interface, labels, annotations, namespaces labels.Sel Namespaces: namespaces, ExcludedWeekdays: excludedWeekdays, ExcludedTimesOfDay: excludedTimesOfDay, + ExcludedDaysOfYear: excludedDaysOfYear, Timezone: timezone, Logger: logger, DryRun: dryRun, @@ -146,6 +153,13 @@ func (c *Chaoskube) TerminateVictim() error { } } + for _, d := range c.ExcludedDaysOfYear { + if d.Day() == now.Day() && d.Month() == now.Month() { + c.Logger.WithField("dayOfYear", now.Format(util.YearDay)).Debug(msgDayOfYearExcluded) + return nil + } + } + victim, err := c.Victim() if err == errPodNotFound { c.Logger.Debug(msgVictimNotFound) diff --git a/chaoskube/chaoskube_test.go b/chaoskube/chaoskube_test.go index c5f8acc6..4dded492 100644 --- a/chaoskube/chaoskube_test.go +++ b/chaoskube/chaoskube_test.go @@ -39,6 +39,7 @@ func (suite *Suite) TestNew() { namespaces, _ = labels.Parse("qux") excludedWeekdays = []time.Weekday{time.Friday} excludedTimesOfDay = []util.TimePeriod{util.TimePeriod{}} + excludedDaysOfYear = []time.Time{time.Now()} ) chaoskube := New( @@ -48,6 +49,7 @@ func (suite *Suite) TestNew() { namespaces, excludedWeekdays, excludedTimesOfDay, + excludedDaysOfYear, time.UTC, logger, false, @@ -60,6 +62,7 @@ func (suite *Suite) TestNew() { suite.Equal("qux", chaoskube.Namespaces.String()) suite.Equal(excludedWeekdays, chaoskube.ExcludedWeekdays) suite.Equal(excludedTimesOfDay, chaoskube.ExcludedTimesOfDay) + suite.Equal(excludedDaysOfYear, chaoskube.ExcludedDaysOfYear) suite.Equal(time.UTC, chaoskube.Timezone) suite.Equal(logger, chaoskube.Logger) suite.Equal(false, chaoskube.DryRun) @@ -102,6 +105,7 @@ func (suite *Suite) TestCandidates() { namespaceSelector, []time.Weekday{}, []util.TimePeriod{}, + []time.Time{}, time.UTC, false, ) @@ -134,6 +138,7 @@ func (suite *Suite) TestVictim() { labels.Everything(), []time.Weekday{}, []util.TimePeriod{}, + []time.Time{}, time.UTC, false, ) @@ -150,6 +155,7 @@ func (suite *Suite) TestNoVictimReturnsError() { labels.Everything(), []time.Weekday{}, []util.TimePeriod{}, + []time.Time{}, time.UTC, false, ) @@ -176,6 +182,7 @@ func (suite *Suite) TestDeletePod() { labels.Everything(), []time.Weekday{}, []util.TimePeriod{}, + []time.Time{}, time.UTC, tt.dryRun, ) @@ -210,6 +217,7 @@ func (suite *Suite) TestTerminateVictim() { for _, tt := range []struct { excludedWeekdays []time.Weekday excludedTimesOfDay []util.TimePeriod + excludedDaysOfYear []time.Time now func() time.Time timezone *time.Location remainingPodCount int @@ -218,6 +226,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{}, []util.TimePeriod{}, + []time.Time{}, ThankGodItsFriday{}.Now, time.UTC, 1, @@ -226,6 +235,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{time.Friday}, []util.TimePeriod{}, + []time.Time{}, ThankGodItsFriday{}.Now, time.UTC, 2, @@ -234,6 +244,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{}, []util.TimePeriod{afternoon}, + []time.Time{}, ThankGodItsFriday{}.Now, time.UTC, 2, @@ -242,6 +253,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{time.Friday}, []util.TimePeriod{}, + []time.Time{}, func() time.Time { return ThankGodItsFriday{}.Now().Add(24 * time.Hour) }, time.UTC, 1, @@ -250,6 +262,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{time.Friday}, []util.TimePeriod{}, + []time.Time{}, func() time.Time { return ThankGodItsFriday{}.Now().Add(7 * 24 * time.Hour) }, time.UTC, 2, @@ -258,6 +271,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{}, []util.TimePeriod{afternoon}, + []time.Time{}, func() time.Time { return ThankGodItsFriday{}.Now().Add(+2 * time.Hour) }, time.UTC, 1, @@ -266,6 +280,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{}, []util.TimePeriod{afternoon}, + []time.Time{}, func() time.Time { return ThankGodItsFriday{}.Now().Add(+24 * time.Hour) }, time.UTC, 2, @@ -274,6 +289,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{time.Friday}, []util.TimePeriod{}, + []time.Time{}, ThankGodItsFriday{}.Now, australia, 1, @@ -282,6 +298,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{}, []util.TimePeriod{afternoon}, + []time.Time{}, ThankGodItsFriday{}.Now, australia, 1, @@ -290,6 +307,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{time.Monday, time.Friday}, []util.TimePeriod{}, + []time.Time{}, ThankGodItsFriday{}.Now, time.UTC, 2, @@ -298,6 +316,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{}, []util.TimePeriod{morning, afternoon}, + []time.Time{}, ThankGodItsFriday{}.Now, time.UTC, 2, @@ -306,6 +325,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{}, []util.TimePeriod{midnight}, + []time.Time{}, func() time.Time { return ThankGodItsFriday{}.Now().Add(-15 * time.Hour) }, time.UTC, 2, @@ -314,6 +334,7 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{}, []util.TimePeriod{midnight}, + []time.Time{}, func() time.Time { return ThankGodItsFriday{}.Now().Add(-17 * time.Hour) }, time.UTC, 1, @@ -322,10 +343,67 @@ func (suite *Suite) TestTerminateVictim() { { []time.Weekday{}, []util.TimePeriod{midnight}, + []time.Time{}, func() time.Time { return ThankGodItsFriday{}.Now().Add(-13 * time.Hour) }, time.UTC, 1, }, + // this day of year is excluded, no pod should be killed + { + []time.Weekday{}, + []util.TimePeriod{}, + []time.Time{ + ThankGodItsFriday{}.Now(), // today + }, + func() time.Time { return ThankGodItsFriday{}.Now() }, + time.UTC, + 2, + }, + // this day of year in year 0 is excluded, no pod should be killed + { + []time.Weekday{}, + []util.TimePeriod{}, + []time.Time{ + time.Date(0, 9, 24, 0, 00, 00, 00, time.UTC), // same year day + }, + func() time.Time { return ThankGodItsFriday{}.Now() }, + time.UTC, + 2, + }, + // matching works fine even when multiple days-of-year are provided, no pod should be killed + { + []time.Weekday{}, + []util.TimePeriod{}, + []time.Time{ + time.Date(0, 9, 25, 10, 00, 00, 00, time.UTC), // different year day + time.Date(0, 9, 24, 10, 00, 00, 00, time.UTC), // same year day + }, + func() time.Time { return ThankGodItsFriday{}.Now() }, + time.UTC, + 2, + }, + // there is an excluded day of year but it's not today, one pod should be killed + { + []time.Weekday{}, + []util.TimePeriod{}, + []time.Time{ + time.Date(0, 9, 25, 10, 00, 00, 00, time.UTC), // different year day + }, + func() time.Time { return ThankGodItsFriday{}.Now() }, + time.UTC, + 1, + }, + // there is an excluded day of year but the month is different, one pod should be killed + { + []time.Weekday{}, + []util.TimePeriod{}, + []time.Time{ + time.Date(0, 10, 24, 10, 00, 00, 00, time.UTC), // different year day + }, + func() time.Time { return ThankGodItsFriday{}.Now() }, + time.UTC, + 1, + }, } { chaoskube := suite.setupWithPods( labels.Everything(), @@ -333,6 +411,7 @@ func (suite *Suite) TestTerminateVictim() { labels.Everything(), tt.excludedWeekdays, tt.excludedTimesOfDay, + tt.excludedDaysOfYear, tt.timezone, false, ) @@ -356,6 +435,7 @@ func (suite *Suite) TestTerminateNoVictimLogsInfo() { labels.Everything(), []time.Weekday{}, []util.TimePeriod{}, + []time.Time{}, time.UTC, false, ) @@ -406,13 +486,14 @@ func (suite *Suite) assertLog(level log.Level, msg string, fields log.Fields) { } } -func (suite *Suite) setupWithPods(labelSelector labels.Selector, annotations labels.Selector, namespaces labels.Selector, excludedWeekdays []time.Weekday, excludedTimesOfDay []util.TimePeriod, timezone *time.Location, dryRun bool) *Chaoskube { +func (suite *Suite) setupWithPods(labelSelector labels.Selector, annotations labels.Selector, namespaces labels.Selector, excludedWeekdays []time.Weekday, excludedTimesOfDay []util.TimePeriod, excludedDaysOfYear []time.Time, timezone *time.Location, dryRun bool) *Chaoskube { chaoskube := suite.setup( labelSelector, annotations, namespaces, excludedWeekdays, excludedTimesOfDay, + excludedDaysOfYear, timezone, dryRun, ) @@ -430,7 +511,7 @@ func (suite *Suite) setupWithPods(labelSelector labels.Selector, annotations lab return chaoskube } -func (suite *Suite) setup(labelSelector labels.Selector, annotations labels.Selector, namespaces labels.Selector, excludedWeekdays []time.Weekday, excludedTimesOfDay []util.TimePeriod, timezone *time.Location, dryRun bool) *Chaoskube { +func (suite *Suite) setup(labelSelector labels.Selector, annotations labels.Selector, namespaces labels.Selector, excludedWeekdays []time.Weekday, excludedTimesOfDay []util.TimePeriod, excludedDaysOfYear []time.Time, timezone *time.Location, dryRun bool) *Chaoskube { logOutput.Reset() return New( @@ -440,6 +521,7 @@ func (suite *Suite) setup(labelSelector labels.Selector, annotations labels.Sele namespaces, excludedWeekdays, excludedTimesOfDay, + excludedDaysOfYear, timezone, logger, dryRun, diff --git a/main.go b/main.go index a3196b61..49bdad77 100644 --- a/main.go +++ b/main.go @@ -23,6 +23,7 @@ var ( nsString string excludedWeekdays string excludedTimesOfDay string + excludedDaysOfYear string timezone string master string kubeconfig string @@ -40,6 +41,7 @@ func init() { kingpin.Flag("namespaces", "A set of namespaces to restrict the list of affected pods. Defaults to everything.").StringVar(&nsString) kingpin.Flag("excluded-weekdays", "A list of weekdays when termination is suspended, e.g. sat,sun").StringVar(&excludedWeekdays) kingpin.Flag("excluded-times-of-day", "A list of time periods of a day when termination is suspended, e.g. 22:00-08:00").StringVar(&excludedTimesOfDay) + kingpin.Flag("excluded-days-of-year", "A list of days of a year when termination is suspended, e.g. Apr1,Dec24").StringVar(&excludedDaysOfYear) kingpin.Flag("timezone", "The timezone by which to interpret the excluded weekdays and times of day, e.g. UTC, Local, Europe/Berlin. Defaults to UTC.").Default("UTC").StringVar(&timezone) kingpin.Flag("master", "The address of the Kubernetes cluster to target").StringVar(&master) kingpin.Flag("kubeconfig", "Path to a kubeconfig file").StringVar(&kubeconfig) @@ -62,6 +64,7 @@ func main() { "namespaces": nsString, "excludedWeekdays": excludedWeekdays, "excludedTimesOfDay": excludedTimesOfDay, + "excludedDaysOfYear": excludedDaysOfYear, "timezone": timezone, "master": master, "kubeconfig": kubeconfig, @@ -97,6 +100,8 @@ func main() { parsedWeekdays := util.ParseWeekdays(excludedWeekdays) parsedTimesOfDay, err := util.ParseTimePeriods(excludedTimesOfDay) + if err != nil { + parsedDaysOfYear, err := util.ParseDays(excludedDaysOfYear) if err != nil { log.Fatal(err) } @@ -104,6 +109,7 @@ func main() { log.WithFields(log.Fields{ "weekdays": parsedWeekdays, "timesOfDay": parsedTimesOfDay, + "daysOfYear": formatDays(parsedDaysOfYear), }).Info("setting quiet times") parsedTimezone, err := time.LoadLocation(timezone) @@ -125,6 +131,7 @@ func main() { namespaces, parsedWeekdays, parsedTimesOfDay, + parsedDaysOfYear, parsedTimezone, log.StandardLogger(), dryRun, @@ -169,3 +176,11 @@ func newClient() (*kubernetes.Clientset, error) { return client, nil } + +func formatDays(days []time.Time) []string { + formattedDays := make([]string, 0, len(days)) + for _, d := range days { + formattedDays = append(formattedDays, d.Format(util.YearDay)) + } + return formattedDays +} diff --git a/util/util.go b/util/util.go index 39772392..b32f8396 100644 --- a/util/util.go +++ b/util/util.go @@ -12,6 +12,8 @@ import ( const ( // a short time format; like time.Kitchen but with 24-hour notation. Kitchen24 = "15:04" + // a time format that just cares about the day and month. + YearDay = "Jan_2" ) // TimePeriod represents a time period with a single beginning and end. @@ -97,6 +99,25 @@ func ParseTimePeriods(timePeriods string) ([]TimePeriod, error) { return parsedTimePeriods, nil } +func ParseDays(days string) ([]time.Time, error) { + parsedDays := []time.Time{} + + for _, day := range strings.Split(days, ",") { + if strings.TrimSpace(day) == "" { + continue + } + + parsedDay, err := time.Parse(YearDay, strings.TrimSpace(day)) + if err != nil { + return nil, err + } + + parsedDays = append(parsedDays, parsedDay) + } + + return parsedDays, nil +} + // TimeOfDay normalizes the given point in time by returning a time object that represents the same // time of day of the given time but on the very first day (day 0). func TimeOfDay(pointInTime time.Time) time.Time { diff --git a/util/util_test.go b/util/util_test.go index 811001a0..a18ce2c0 100644 --- a/util/util_test.go +++ b/util/util_test.go @@ -300,6 +300,71 @@ func (suite *Suite) TestParseTimePeriods() { } } +func (suite *Suite) TestParseDates() { + for _, tt := range []struct { + given string + expected []time.Time + }{ + // empty string + { + "", + []time.Time{}, + }, + // single date + { + "Apr 1", + []time.Time{ + time.Date(0, 4, 1, 0, 0, 0, 0, time.UTC), + }, + }, + // single date leaving out the space + { + "Apr1", + []time.Time{ + time.Date(0, 4, 1, 0, 0, 0, 0, time.UTC), + }, + }, + // multiple dates + { + "Apr 1,Dec 24", + []time.Time{ + time.Date(0, 4, 1, 0, 0, 0, 0, time.UTC), + time.Date(0, 12, 24, 0, 0, 0, 0, time.UTC), + }, + }, + // case-insensitive + { + "apr 1,dEc 24", + []time.Time{ + time.Date(0, 4, 1, 0, 0, 0, 0, time.UTC), + time.Date(0, 12, 24, 0, 0, 0, 0, time.UTC), + }, + }, + // ignore whitespace + { + " apr 1 , dec 24 ", + []time.Time{ + time.Date(0, 4, 1, 0, 0, 0, 0, time.UTC), + time.Date(0, 12, 24, 0, 0, 0, 0, time.UTC), + }, + }, + // deal with all kinds at the same time + { + ",Apr 1, dEc 24 ,,,, ,jun08,,", + []time.Time{ + time.Date(0, 4, 1, 0, 0, 0, 0, time.UTC), + time.Date(0, 12, 24, 0, 0, 0, 0, time.UTC), + time.Date(0, 6, 8, 0, 0, 0, 0, time.UTC), + }, + }, + } { + days, err := ParseDays(tt.given) + suite.Require().NoError(err) + + suite.Equal(tt.expected, days) + } +} + func TestSuite(t *testing.T) { suite.Run(t, new(Suite)) }