424 lines
14 KiB
Go
424 lines
14 KiB
Go
package rules
|
|
|
|
import (
|
|
"database/sql/driver"
|
|
"encoding/json"
|
|
"slices"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/pkg/errors"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
var (
|
|
ErrMissingName = errors.New("missing name")
|
|
ErrMissingSchedule = errors.New("missing schedule")
|
|
ErrMissingTimezone = errors.New("missing timezone")
|
|
ErrMissingRepeatType = errors.New("missing repeat type")
|
|
ErrMissingDuration = errors.New("missing duration")
|
|
)
|
|
|
|
type PlannedMaintenance struct {
|
|
Id int64 `json:"id" db:"id"`
|
|
Name string `json:"name" db:"name"`
|
|
Description string `json:"description" db:"description"`
|
|
Schedule *Schedule `json:"schedule" db:"schedule"`
|
|
AlertIds *AlertIds `json:"alertIds" db:"alert_ids"`
|
|
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
|
CreatedBy string `json:"createdBy" db:"created_by"`
|
|
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
|
UpdatedBy string `json:"updatedBy" db:"updated_by"`
|
|
Status string `json:"status"`
|
|
Kind string `json:"kind"`
|
|
}
|
|
|
|
type AlertIds []string
|
|
|
|
func (a *AlertIds) Scan(src interface{}) error {
|
|
if data, ok := src.([]byte); ok {
|
|
return json.Unmarshal(data, a)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *AlertIds) Value() (driver.Value, error) {
|
|
return json.Marshal(a)
|
|
}
|
|
|
|
type Schedule struct {
|
|
Timezone string `json:"timezone"`
|
|
StartTime time.Time `json:"startTime,omitempty"`
|
|
EndTime time.Time `json:"endTime,omitempty"`
|
|
Recurrence *Recurrence `json:"recurrence"`
|
|
}
|
|
|
|
func (s *Schedule) Scan(src interface{}) error {
|
|
if data, ok := src.([]byte); ok {
|
|
return json.Unmarshal(data, s)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Schedule) Value() (driver.Value, error) {
|
|
return json.Marshal(s)
|
|
}
|
|
|
|
type RepeatType string
|
|
|
|
const (
|
|
RepeatTypeDaily RepeatType = "daily"
|
|
RepeatTypeWeekly RepeatType = "weekly"
|
|
RepeatTypeMonthly RepeatType = "monthly"
|
|
)
|
|
|
|
type RepeatOn string
|
|
|
|
const (
|
|
RepeatOnSunday RepeatOn = "sunday"
|
|
RepeatOnMonday RepeatOn = "monday"
|
|
RepeatOnTuesday RepeatOn = "tuesday"
|
|
RepeatOnWednesday RepeatOn = "wednesday"
|
|
RepeatOnThursday RepeatOn = "thursday"
|
|
RepeatOnFriday RepeatOn = "friday"
|
|
RepeatOnSaturday RepeatOn = "saturday"
|
|
)
|
|
|
|
type Recurrence struct {
|
|
StartTime time.Time `json:"startTime"`
|
|
EndTime *time.Time `json:"endTime,omitempty"`
|
|
Duration Duration `json:"duration"`
|
|
RepeatType RepeatType `json:"repeatType"`
|
|
RepeatOn []RepeatOn `json:"repeatOn"`
|
|
}
|
|
|
|
func (r *Recurrence) Scan(src interface{}) error {
|
|
if data, ok := src.([]byte); ok {
|
|
return json.Unmarshal(data, r)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (r *Recurrence) Value() (driver.Value, error) {
|
|
return json.Marshal(r)
|
|
}
|
|
|
|
func (s Schedule) MarshalJSON() ([]byte, error) {
|
|
loc, err := time.LoadLocation(s.Timezone)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var startTime, endTime time.Time
|
|
if !s.StartTime.IsZero() {
|
|
startTime = time.Date(s.StartTime.Year(), s.StartTime.Month(), s.StartTime.Day(), s.StartTime.Hour(), s.StartTime.Minute(), s.StartTime.Second(), s.StartTime.Nanosecond(), loc)
|
|
}
|
|
if !s.EndTime.IsZero() {
|
|
endTime = time.Date(s.EndTime.Year(), s.EndTime.Month(), s.EndTime.Day(), s.EndTime.Hour(), s.EndTime.Minute(), s.EndTime.Second(), s.EndTime.Nanosecond(), loc)
|
|
}
|
|
|
|
var recurrence *Recurrence
|
|
if s.Recurrence != nil {
|
|
recStartTime := time.Date(s.Recurrence.StartTime.Year(), s.Recurrence.StartTime.Month(), s.Recurrence.StartTime.Day(), s.Recurrence.StartTime.Hour(), s.Recurrence.StartTime.Minute(), s.Recurrence.StartTime.Second(), s.Recurrence.StartTime.Nanosecond(), loc)
|
|
var recEndTime *time.Time
|
|
if s.Recurrence.EndTime != nil {
|
|
end := time.Date(s.Recurrence.EndTime.Year(), s.Recurrence.EndTime.Month(), s.Recurrence.EndTime.Day(), s.Recurrence.EndTime.Hour(), s.Recurrence.EndTime.Minute(), s.Recurrence.EndTime.Second(), s.Recurrence.EndTime.Nanosecond(), loc)
|
|
recEndTime = &end
|
|
}
|
|
recurrence = &Recurrence{
|
|
StartTime: recStartTime,
|
|
EndTime: recEndTime,
|
|
Duration: s.Recurrence.Duration,
|
|
RepeatType: s.Recurrence.RepeatType,
|
|
RepeatOn: s.Recurrence.RepeatOn,
|
|
}
|
|
}
|
|
|
|
return json.Marshal(&struct {
|
|
Timezone string `json:"timezone"`
|
|
StartTime string `json:"startTime"`
|
|
EndTime string `json:"endTime"`
|
|
Recurrence *Recurrence `json:"recurrence,omitempty"`
|
|
}{
|
|
Timezone: s.Timezone,
|
|
StartTime: startTime.Format(time.RFC3339),
|
|
EndTime: endTime.Format(time.RFC3339),
|
|
Recurrence: recurrence,
|
|
})
|
|
}
|
|
|
|
func (s *Schedule) UnmarshalJSON(data []byte) error {
|
|
aux := &struct {
|
|
Timezone string `json:"timezone"`
|
|
StartTime string `json:"startTime"`
|
|
EndTime string `json:"endTime"`
|
|
Recurrence *Recurrence `json:"recurrence,omitempty"`
|
|
}{}
|
|
if err := json.Unmarshal(data, aux); err != nil {
|
|
return err
|
|
}
|
|
|
|
loc, err := time.LoadLocation(aux.Timezone)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var startTime time.Time
|
|
if aux.StartTime != "" {
|
|
startTime, err = time.Parse(time.RFC3339, aux.StartTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.StartTime = time.Date(startTime.Year(), startTime.Month(), startTime.Day(), startTime.Hour(), startTime.Minute(), startTime.Second(), startTime.Nanosecond(), loc)
|
|
}
|
|
|
|
var endTime time.Time
|
|
if aux.EndTime != "" {
|
|
endTime, err = time.Parse(time.RFC3339, aux.EndTime)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
s.EndTime = time.Date(endTime.Year(), endTime.Month(), endTime.Day(), endTime.Hour(), endTime.Minute(), endTime.Second(), endTime.Nanosecond(), loc)
|
|
}
|
|
|
|
s.Timezone = aux.Timezone
|
|
|
|
if aux.Recurrence != nil {
|
|
recStartTime, err := time.Parse(time.RFC3339, aux.Recurrence.StartTime.Format(time.RFC3339))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var recEndTime *time.Time
|
|
if aux.Recurrence.EndTime != nil {
|
|
end, err := time.Parse(time.RFC3339, aux.Recurrence.EndTime.Format(time.RFC3339))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
endConverted := time.Date(end.Year(), end.Month(), end.Day(), end.Hour(), end.Minute(), end.Second(), end.Nanosecond(), loc)
|
|
recEndTime = &endConverted
|
|
}
|
|
|
|
s.Recurrence = &Recurrence{
|
|
StartTime: time.Date(recStartTime.Year(), recStartTime.Month(), recStartTime.Day(), recStartTime.Hour(), recStartTime.Minute(), recStartTime.Second(), recStartTime.Nanosecond(), loc),
|
|
EndTime: recEndTime,
|
|
Duration: aux.Recurrence.Duration,
|
|
RepeatType: aux.Recurrence.RepeatType,
|
|
RepeatOn: aux.Recurrence.RepeatOn,
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m *PlannedMaintenance) shouldSkip(ruleID string, now time.Time) bool {
|
|
|
|
found := false
|
|
if m.AlertIds != nil {
|
|
for _, alertID := range *m.AlertIds {
|
|
if alertID == ruleID {
|
|
found = true
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
// If no alert ids, then skip all alerts
|
|
if m.AlertIds == nil || len(*m.AlertIds) == 0 {
|
|
found = true
|
|
}
|
|
|
|
if found {
|
|
|
|
zap.L().Info("alert found in maintenance", zap.String("alert", ruleID), zap.Any("maintenance", m.Name))
|
|
|
|
// If alert is found, we check if it should be skipped based on the schedule
|
|
// If it should be skipped, we return true
|
|
// If it should not be skipped, we return false
|
|
|
|
// fixed schedule
|
|
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
|
|
// if the current time in the timezone is between the start and end time
|
|
loc, err := time.LoadLocation(m.Schedule.Timezone)
|
|
if err != nil {
|
|
zap.L().Error("Error loading location", zap.String("timezone", m.Schedule.Timezone), zap.Error(err))
|
|
return false
|
|
}
|
|
|
|
currentTime := now.In(loc)
|
|
zap.L().Info("checking fixed schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", m.Schedule.StartTime), zap.Time("endTime", m.Schedule.EndTime))
|
|
if currentTime.After(m.Schedule.StartTime) && currentTime.Before(m.Schedule.EndTime) {
|
|
return true
|
|
}
|
|
}
|
|
|
|
// recurring schedule
|
|
if m.Schedule.Recurrence != nil {
|
|
zap.L().Info("evaluating recurrence schedule")
|
|
start := m.Schedule.Recurrence.StartTime
|
|
end := m.Schedule.Recurrence.StartTime.Add(time.Duration(m.Schedule.Recurrence.Duration))
|
|
// if the current time in the timezone is between the start and end time
|
|
loc, err := time.LoadLocation(m.Schedule.Timezone)
|
|
if err != nil {
|
|
zap.L().Error("Error loading location", zap.String("timezone", m.Schedule.Timezone), zap.Error(err))
|
|
return false
|
|
}
|
|
currentTime := now.In(loc)
|
|
|
|
zap.L().Info("checking recurring schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", start), zap.Time("endTime", end))
|
|
|
|
// make sure the start time is not after the current time
|
|
if currentTime.Before(start.In(loc)) {
|
|
zap.L().Info("current time is before start time", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", start.In(loc)))
|
|
return false
|
|
}
|
|
|
|
var endTime time.Time
|
|
if m.Schedule.Recurrence.EndTime != nil {
|
|
endTime = *m.Schedule.Recurrence.EndTime
|
|
}
|
|
if !endTime.IsZero() && currentTime.After(endTime.In(loc)) {
|
|
zap.L().Info("current time is after end time", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("endTime", end.In(loc)))
|
|
return false
|
|
}
|
|
|
|
switch m.Schedule.Recurrence.RepeatType {
|
|
case RepeatTypeDaily:
|
|
// take the hours and minutes from the start time and add them to the current time
|
|
startTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), start.Hour(), start.Minute(), 0, 0, loc)
|
|
endTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), end.Hour(), end.Minute(), 0, 0, loc)
|
|
zap.L().Info("checking daily schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", startTime), zap.Time("endTime", endTime))
|
|
|
|
if currentTime.After(startTime) && currentTime.Before(endTime) {
|
|
return true
|
|
}
|
|
case RepeatTypeWeekly:
|
|
// if the current time in the timezone is between the start and end time on the RepeatOn day
|
|
startTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), start.Hour(), start.Minute(), 0, 0, loc)
|
|
endTime := time.Date(currentTime.Year(), currentTime.Month(), currentTime.Day(), end.Hour(), end.Minute(), 0, 0, loc)
|
|
zap.L().Info("checking weekly schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", startTime), zap.Time("endTime", endTime))
|
|
if currentTime.After(startTime) && currentTime.Before(endTime) {
|
|
if len(m.Schedule.Recurrence.RepeatOn) == 0 {
|
|
return true
|
|
} else if slices.Contains(m.Schedule.Recurrence.RepeatOn, RepeatOn(strings.ToLower(currentTime.Weekday().String()))) {
|
|
return true
|
|
}
|
|
}
|
|
case RepeatTypeMonthly:
|
|
// if the current time in the timezone is between the start and end time on the day of the current month
|
|
startTime := time.Date(currentTime.Year(), currentTime.Month(), start.Day(), start.Hour(), start.Minute(), 0, 0, loc)
|
|
endTime := time.Date(currentTime.Year(), currentTime.Month(), end.Day(), end.Hour(), end.Minute(), 0, 0, loc)
|
|
zap.L().Info("checking monthly schedule", zap.Any("rule", ruleID), zap.String("maintenance", m.Name), zap.Time("currentTime", currentTime), zap.Time("startTime", startTime), zap.Time("endTime", endTime))
|
|
if currentTime.After(startTime) && currentTime.Before(endTime) && currentTime.Day() == start.Day() {
|
|
return true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// If alert is not found, we return false
|
|
return false
|
|
}
|
|
|
|
func (m *PlannedMaintenance) IsActive(now time.Time) bool {
|
|
ruleID := "maintenance"
|
|
if m.AlertIds != nil && len(*m.AlertIds) > 0 {
|
|
ruleID = (*m.AlertIds)[0]
|
|
}
|
|
return m.shouldSkip(ruleID, now)
|
|
}
|
|
|
|
func (m *PlannedMaintenance) IsUpcoming() bool {
|
|
now := time.Now().In(time.FixedZone(m.Schedule.Timezone, 0))
|
|
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
|
|
return now.Before(m.Schedule.StartTime)
|
|
}
|
|
if m.Schedule.Recurrence != nil {
|
|
return now.Before(m.Schedule.Recurrence.StartTime)
|
|
}
|
|
return false
|
|
}
|
|
|
|
func (m *PlannedMaintenance) IsRecurring() bool {
|
|
return m.Schedule.Recurrence != nil
|
|
}
|
|
|
|
func (m *PlannedMaintenance) Validate() error {
|
|
if m.Name == "" {
|
|
return ErrMissingName
|
|
}
|
|
if m.Schedule == nil {
|
|
return ErrMissingSchedule
|
|
}
|
|
if m.Schedule.Timezone == "" {
|
|
return ErrMissingTimezone
|
|
}
|
|
|
|
_, err := time.LoadLocation(m.Schedule.Timezone)
|
|
if err != nil {
|
|
return errors.New("invalid timezone")
|
|
}
|
|
|
|
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() {
|
|
if m.Schedule.StartTime.After(m.Schedule.EndTime) {
|
|
return errors.New("start time cannot be after end time")
|
|
}
|
|
}
|
|
|
|
if m.Schedule.Recurrence != nil {
|
|
if m.Schedule.Recurrence.RepeatType == "" {
|
|
return ErrMissingRepeatType
|
|
}
|
|
if m.Schedule.Recurrence.Duration == 0 {
|
|
return ErrMissingDuration
|
|
}
|
|
if m.Schedule.Recurrence.EndTime != nil && m.Schedule.Recurrence.EndTime.Before(m.Schedule.Recurrence.StartTime) {
|
|
return errors.New("end time cannot be before start time")
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (m PlannedMaintenance) MarshalJSON() ([]byte, error) {
|
|
now := time.Now().In(time.FixedZone(m.Schedule.Timezone, 0))
|
|
var status string
|
|
if m.IsActive(now) {
|
|
status = "active"
|
|
} else if m.IsUpcoming() {
|
|
status = "upcoming"
|
|
} else {
|
|
status = "expired"
|
|
}
|
|
var kind string
|
|
|
|
if !m.Schedule.StartTime.IsZero() && !m.Schedule.EndTime.IsZero() && m.Schedule.EndTime.After(m.Schedule.StartTime) {
|
|
kind = "fixed"
|
|
} else {
|
|
kind = "recurring"
|
|
}
|
|
|
|
return json.Marshal(struct {
|
|
Id int64 `json:"id" db:"id"`
|
|
Name string `json:"name" db:"name"`
|
|
Description string `json:"description" db:"description"`
|
|
Schedule *Schedule `json:"schedule" db:"schedule"`
|
|
AlertIds *AlertIds `json:"alertIds" db:"alert_ids"`
|
|
CreatedAt time.Time `json:"createdAt" db:"created_at"`
|
|
CreatedBy string `json:"createdBy" db:"created_by"`
|
|
UpdatedAt time.Time `json:"updatedAt" db:"updated_at"`
|
|
UpdatedBy string `json:"updatedBy" db:"updated_by"`
|
|
Status string `json:"status"`
|
|
Kind string `json:"kind"`
|
|
}{
|
|
Id: m.Id,
|
|
Name: m.Name,
|
|
Description: m.Description,
|
|
Schedule: m.Schedule,
|
|
AlertIds: m.AlertIds,
|
|
CreatedAt: m.CreatedAt,
|
|
CreatedBy: m.CreatedBy,
|
|
UpdatedAt: m.UpdatedAt,
|
|
UpdatedBy: m.UpdatedBy,
|
|
Status: status,
|
|
Kind: kind,
|
|
})
|
|
}
|