diff --git a/api/types/maintenance.go b/api/types/maintenance.go index 9cab6a9ad4765..47a6e55494cd5 100644 --- a/api/types/maintenance.go +++ b/api/types/maintenance.go @@ -45,10 +45,10 @@ var validWeekdays = [7]time.Weekday{ time.Saturday, } -// parseWeekday attempts to interpret a string as a time.Weekday. In the interest of flexibility, +// ParseWeekday attempts to interpret a string as a time.Weekday. In the interest of flexibility, // parsing is case-insensitive and supports the common three-letter shorthand accepted by many // common scheduling utilites (e.g. contab, systemd timers). -func parseWeekday(s string) (day time.Weekday, ok bool) { +func ParseWeekday(s string) (day time.Weekday, ok bool) { for _, w := range validWeekdays { if strings.EqualFold(w.String(), s) || strings.EqualFold(w.String()[:3], s) { return w, true @@ -58,6 +58,42 @@ func parseWeekday(s string) (day time.Weekday, ok bool) { return time.Sunday, false } +// ParseWeekdays attempts to parse a slice of strings representing week days. +// The slice must not be empty but can also contain a single value "*", representing the whole week. +// Day order doesn't matter but the same week day must not be present multiple times. +// In the interest of flexibility, parsing is case-insensitive and supports the common three-letter shorthand +// accepted by many common scheduling utilites (e.g. contab, systemd timers). +func ParseWeekdays(days []string) (map[time.Weekday]struct{}, error) { + if len(days) == 0 { + return nil, trace.BadParameter("empty weekdays list") + } + // Special case, we support wildcards. + if len(days) == 1 && days[0] == Wildcard { + return map[time.Weekday]struct{}{ + time.Monday: {}, + time.Tuesday: {}, + time.Wednesday: {}, + time.Thursday: {}, + time.Friday: {}, + time.Saturday: {}, + time.Sunday: {}, + }, nil + } + weekdays := make(map[time.Weekday]struct{}, 7) + for _, day := range days { + weekday, ok := ParseWeekday(day) + if !ok { + return nil, trace.BadParameter("failed to parse weekday: %v", day) + } + // Check if this is a duplicate + if _, ok := weekdays[weekday]; ok { + return nil, trace.BadParameter("duplicate weekday: %v", weekday.String()) + } + weekdays[weekday] = struct{}{} + } + return weekdays, nil +} + // generator builds a closure that iterates valid maintenance config from the current day onward. Used in // schedule export logic and tests. func (w *AgentUpgradeWindow) generator(from time.Time) func() (start time.Time, end time.Time) { @@ -75,7 +111,7 @@ func (w *AgentUpgradeWindow) generator(from time.Time) func() (start time.Time, var weekdays []time.Weekday for _, d := range w.Weekdays { - if p, ok := parseWeekday(d); ok { + if p, ok := ParseWeekday(d); ok { weekdays = append(weekdays, p) } } @@ -203,7 +239,7 @@ func (m *ClusterMaintenanceConfigV1) CheckAndSetDefaults() error { } for _, day := range m.Spec.AgentUpgrades.Weekdays { - if _, ok := parseWeekday(day); !ok { + if _, ok := ParseWeekday(day); !ok { return trace.BadParameter("invalid weekday in agent upgrade window: %q", day) } } @@ -248,13 +284,14 @@ func (m *ClusterMaintenanceConfigV1) WithinUpgradeWindow(t time.Time) bool { } } - weekday := t.Weekday().String() - for _, upgradeWeekday := range upgradeWindow.Weekdays { - if weekday == upgradeWeekday { - if int(upgradeWindow.UTCStartHour) == t.Hour() { - return true - } - } + upgradeWeekDays, err := ParseWeekdays(upgradeWindow.Weekdays) + if err != nil { + return false } - return false + + if _, ok := upgradeWeekDays[t.Weekday()]; !ok { + return false + } + + return int(upgradeWindow.UTCStartHour) == t.Hour() } diff --git a/api/types/maintenance_test.go b/api/types/maintenance_test.go index 203006a8dee37..464c1a4157637 100644 --- a/api/types/maintenance_test.go +++ b/api/types/maintenance_test.go @@ -205,7 +205,7 @@ func TestWeekdayParser(t *testing.T) { } for _, tt := range tts { - day, ok := parseWeekday(tt.input) + day, ok := ParseWeekday(tt.input) if tt.fail { require.False(t, ok) continue @@ -244,7 +244,7 @@ func TestWithinUpgradeWindow(t *testing.T) { desc: "within upgrade window weekday", upgradeWindow: AgentUpgradeWindow{ UTCStartHour: 8, - Weekdays: []string{"Monday"}, + Weekdays: []string{"Mon"}, }, date: "Mon, 02 Jan 2006 08:04:05 UTC", withinWindow: true, @@ -253,7 +253,7 @@ func TestWithinUpgradeWindow(t *testing.T) { desc: "not within upgrade window weekday", upgradeWindow: AgentUpgradeWindow{ UTCStartHour: 8, - Weekdays: []string{"Tuesday"}, + Weekdays: []string{"Tue"}, }, date: "Mon, 02 Jan 2006 08:04:05 UTC", withinWindow: false,