feat: Implement announcements (#1204)
* feat: Implement announcements Fixes #1203 * Remove unnecessary code * Fix new announcement test * Update web/app/src/views/Home.vue Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Remove useless garbage * Require announcement timestamp --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
94
config/announcement/announcement.go
Normal file
94
config/announcement/announcement.go
Normal file
@@ -0,0 +1,94 @@
|
||||
package announcement
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
// TypeOutage represents a service outage
|
||||
TypeOutage = "outage"
|
||||
|
||||
// TypeWarning represents a warning or potential issue
|
||||
TypeWarning = "warning"
|
||||
|
||||
// TypeInformation represents general information
|
||||
TypeInformation = "information"
|
||||
|
||||
// TypeOperational represents operational status or resolved issues
|
||||
TypeOperational = "operational"
|
||||
|
||||
// TypeNone represents no specific type (default)
|
||||
TypeNone = "none"
|
||||
)
|
||||
|
||||
var (
|
||||
// ErrInvalidAnnouncementType is returned when an invalid announcement type is specified
|
||||
ErrInvalidAnnouncementType = errors.New("invalid announcement type")
|
||||
|
||||
// ErrEmptyMessage is returned when an announcement has an empty message
|
||||
ErrEmptyMessage = errors.New("announcement message cannot be empty")
|
||||
|
||||
// ErrMissingTimestamp is returned when an announcement has an empty timestamp
|
||||
ErrMissingTimestamp = errors.New("announcement timestamp must be set")
|
||||
|
||||
// validTypes contains all valid announcement types
|
||||
validTypes = map[string]bool{
|
||||
TypeOutage: true,
|
||||
TypeWarning: true,
|
||||
TypeInformation: true,
|
||||
TypeOperational: true,
|
||||
TypeNone: true,
|
||||
}
|
||||
)
|
||||
|
||||
// Announcement represents a system-wide announcement
|
||||
type Announcement struct {
|
||||
// Timestamp is the UTC timestamp when the announcement was made
|
||||
Timestamp time.Time `yaml:"timestamp" json:"timestamp"`
|
||||
|
||||
// Type is the type of announcement (outage, warning, information, operational, none)
|
||||
Type string `yaml:"type" json:"type"`
|
||||
|
||||
// Message is the user-facing text describing the announcement
|
||||
Message string `yaml:"message" json:"message"`
|
||||
}
|
||||
|
||||
// ValidateAndSetDefaults validates the announcement and sets default values if necessary
|
||||
func (a *Announcement) ValidateAndSetDefaults() error {
|
||||
// Validate message
|
||||
if a.Message == "" {
|
||||
return ErrEmptyMessage
|
||||
}
|
||||
// Set default type if empty
|
||||
if a.Type == "" {
|
||||
a.Type = TypeNone
|
||||
}
|
||||
// Validate type
|
||||
if !validTypes[a.Type] {
|
||||
return ErrInvalidAnnouncementType
|
||||
}
|
||||
// If timestamp is zero, return an error
|
||||
if a.Timestamp.IsZero() {
|
||||
return ErrMissingTimestamp
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// SortByTimestamp sorts a slice of announcements by timestamp in descending order (newest first)
|
||||
func SortByTimestamp(announcements []*Announcement) {
|
||||
sort.Slice(announcements, func(i, j int) bool {
|
||||
return announcements[i].Timestamp.After(announcements[j].Timestamp)
|
||||
})
|
||||
}
|
||||
|
||||
// ValidateAndSetDefaults validates a slice of announcements and sets defaults
|
||||
func ValidateAndSetDefaults(announcements []*Announcement) error {
|
||||
for _, announcement := range announcements {
|
||||
if err := announcement.ValidateAndSetDefaults(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"github.com/TwiN/gatus/v5/alerting"
|
||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||
"github.com/TwiN/gatus/v5/alerting/provider"
|
||||
"github.com/TwiN/gatus/v5/config/announcement"
|
||||
"github.com/TwiN/gatus/v5/config/connectivity"
|
||||
"github.com/TwiN/gatus/v5/config/endpoint"
|
||||
"github.com/TwiN/gatus/v5/config/maintenance"
|
||||
@@ -99,6 +100,9 @@ type Config struct {
|
||||
// Connectivity is the configuration for connectivity
|
||||
Connectivity *connectivity.Config `yaml:"connectivity,omitempty"`
|
||||
|
||||
// Announcements is the list of system-wide announcements
|
||||
Announcements []*announcement.Announcement `yaml:"announcements,omitempty"`
|
||||
|
||||
configPath string // path to the file or directory from which config was loaded
|
||||
lastFileModTime time.Time // last modification time
|
||||
}
|
||||
@@ -302,6 +306,9 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
||||
if err := validateConnectivityConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateAnnouncementsConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Cross-config changes
|
||||
config.UI.MaximumNumberOfResults = config.Storage.MaximumNumberOfResults
|
||||
}
|
||||
@@ -315,6 +322,17 @@ func validateConnectivityConfig(config *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateAnnouncementsConfig(config *Config) error {
|
||||
if config.Announcements != nil {
|
||||
if err := announcement.ValidateAndSetDefaults(config.Announcements); err != nil {
|
||||
return err
|
||||
}
|
||||
// Sort announcements by timestamp (newest first) for API response
|
||||
announcement.SortByTimestamp(config.Announcements)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateRemoteConfig(config *Config) error {
|
||||
if config.Remote != nil {
|
||||
if err := config.Remote.ValidateAndSetDefaults(); err != nil {
|
||||
|
||||
Reference in New Issue
Block a user