gatus/config/config.go

333 lines
11 KiB
Go
Raw Normal View History

2019-09-06 06:01:48 +02:00
package config
import (
2019-10-20 04:03:55 +02:00
"errors"
"fmt"
"log"
"os"
"time"
2022-12-06 07:41:09 +01:00
"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/maintenance"
"github.com/TwiN/gatus/v5/config/remote"
"github.com/TwiN/gatus/v5/config/ui"
"github.com/TwiN/gatus/v5/config/web"
"github.com/TwiN/gatus/v5/core"
"github.com/TwiN/gatus/v5/security"
"github.com/TwiN/gatus/v5/storage"
"github.com/TwiN/gatus/v5/util"
2019-09-06 06:01:48 +02:00
"gopkg.in/yaml.v2"
)
2020-06-26 03:31:34 +02:00
const (
2020-09-26 21:15:50 +02:00
// DefaultConfigurationFilePath is the default path that will be used to search for the configuration file
// if a custom path isn't configured through the GATUS_CONFIG_FILE environment variable
2020-06-26 03:31:34 +02:00
DefaultConfigurationFilePath = "config/config.yaml"
2020-10-20 01:26:29 +02:00
// DefaultFallbackConfigurationFilePath is the default fallback path that will be used to search for the
// configuration file if DefaultConfigurationFilePath didn't work
DefaultFallbackConfigurationFilePath = "config/config.yml"
2020-06-26 03:31:34 +02:00
)
2019-09-06 06:01:48 +02:00
2019-10-20 04:03:55 +02:00
var (
// ErrNoEndpointInConfig is an error returned when a configuration file has no endpoints configured
ErrNoEndpointInConfig = errors.New("configuration file should contain at least 1 endpoint")
2020-10-23 22:07:51 +02:00
// ErrConfigFileNotFound is an error returned when the configuration file could not be found
ErrConfigFileNotFound = errors.New("configuration file not found")
// ErrInvalidSecurityConfig is an error returned when the security configuration is invalid
ErrInvalidSecurityConfig = errors.New("invalid security configuration")
2019-10-20 04:03:55 +02:00
)
2019-09-06 06:01:48 +02:00
2020-09-26 21:15:50 +02:00
// Config is the main configuration structure
2020-06-26 03:31:34 +02:00
type Config struct {
// Debug Whether to enable debug logs
2021-10-24 21:03:41 +02:00
Debug bool `yaml:"debug,omitempty"`
// Metrics Whether to expose metrics at /metrics
2021-10-24 21:03:41 +02:00
Metrics bool `yaml:"metrics,omitempty"`
// SkipInvalidConfigUpdate Whether to make the application ignore invalid configuration
// if the configuration file is updated while the application is running
2021-10-24 21:03:41 +02:00
SkipInvalidConfigUpdate bool `yaml:"skip-invalid-config-update,omitempty"`
// DisableMonitoringLock Whether to disable the monitoring lock
// The monitoring lock is what prevents multiple endpoints from being processed at the same time.
// Disabling this may lead to inaccurate response times
2021-10-24 21:03:41 +02:00
DisableMonitoringLock bool `yaml:"disable-monitoring-lock,omitempty"`
// Security Configuration for securing access to Gatus
2021-10-24 21:03:41 +02:00
Security *security.Config `yaml:"security,omitempty"`
// Alerting Configuration for alerting
2021-10-24 21:03:41 +02:00
Alerting *alerting.Config `yaml:"alerting,omitempty"`
// Endpoints List of endpoints to monitor
2021-10-24 21:03:41 +02:00
Endpoints []*core.Endpoint `yaml:"endpoints,omitempty"`
2021-02-03 05:06:34 +01:00
// Storage is the configuration for how the data is stored
2021-10-24 21:03:41 +02:00
Storage *storage.Config `yaml:"storage,omitempty"`
2021-02-03 05:06:34 +01:00
// Web is the web configuration for the application
2021-10-24 21:03:41 +02:00
Web *web.Config `yaml:"web,omitempty"`
2021-09-11 07:51:14 +02:00
// UI is the configuration for the UI
2021-10-24 21:03:41 +02:00
UI *ui.Config `yaml:"ui,omitempty"`
2021-09-11 07:51:14 +02:00
2021-09-22 06:04:51 +02:00
// Maintenance is the configuration for creating a maintenance window in which no alerts are sent
2021-10-24 21:03:41 +02:00
Maintenance *maintenance.Config `yaml:"maintenance,omitempty"`
2021-09-22 06:04:51 +02:00
// Remote is the configuration for remote Gatus instances
// WARNING: This is in ALPHA and may change or be completely removed in the future
Remote *remote.Config `yaml:"remote,omitempty"`
filePath string // path to the file from which config was loaded from
lastFileModTime time.Time // last modification time
2020-06-26 03:31:34 +02:00
}
func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
for i := 0; i < len(config.Endpoints); i++ {
ep := config.Endpoints[i]
if util.ConvertGroupAndEndpointNameToKey(ep.Group, ep.Name) == key {
return ep
}
}
return nil
}
// HasLoadedConfigurationFileBeenModified returns whether the file that the
// configuration has been loaded from has been modified since it was last read
func (config Config) HasLoadedConfigurationFileBeenModified() bool {
if fileInfo, err := os.Stat(config.filePath); err == nil {
if !fileInfo.ModTime().IsZero() {
return config.lastFileModTime.Unix() != fileInfo.ModTime().Unix()
}
2019-09-06 06:01:48 +02:00
}
return false
2019-09-06 06:01:48 +02:00
}
// UpdateLastFileModTime refreshes Config.lastFileModTime
func (config *Config) UpdateLastFileModTime() {
if fileInfo, err := os.Stat(config.filePath); err == nil {
if !fileInfo.ModTime().IsZero() {
config.lastFileModTime = fileInfo.ModTime()
}
} else {
log.Println("[config][UpdateLastFileModTime] Ran into error updating lastFileModTime:", err.Error())
}
2021-01-10 04:55:36 +01:00
}
2020-10-20 01:26:29 +02:00
// Load loads a custom configuration file
2020-11-21 23:35:08 +01:00
// Note that the misconfiguration of some fields may lead to panics. This is on purpose.
func Load(configFile string) (*Config, error) {
log.Printf("[config][Load] Reading configuration from configFile=%s", configFile)
cfg, err := readConfigurationFile(configFile)
if err != nil {
if os.IsNotExist(err) {
return nil, ErrConfigFileNotFound
}
return nil, err
}
cfg.filePath = configFile
cfg.UpdateLastFileModTime()
return cfg, nil
}
2020-10-20 01:26:29 +02:00
// LoadDefaultConfiguration loads the default configuration file
func LoadDefaultConfiguration() (*Config, error) {
cfg, err := Load(DefaultConfigurationFilePath)
if err != nil {
if err == ErrConfigFileNotFound {
2020-10-20 01:26:29 +02:00
return Load(DefaultFallbackConfigurationFilePath)
}
return nil, err
}
return cfg, nil
}
2019-10-20 04:03:55 +02:00
func readConfigurationFile(fileName string) (config *Config, err error) {
var bytes []byte
if bytes, err = os.ReadFile(fileName); err == nil {
2019-10-20 03:39:31 +02:00
// file exists, so we'll parse it and return it
2019-10-20 04:03:55 +02:00
return parseAndValidateConfigBytes(bytes)
2019-09-06 06:01:48 +02:00
}
2019-10-20 04:03:55 +02:00
return
2019-09-06 06:01:48 +02:00
}
2021-10-24 20:51:21 +02:00
// parseAndValidateConfigBytes parses a Gatus configuration file into a Config struct and validates its parameters
2019-10-20 04:03:55 +02:00
func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
// Expand environment variables
yamlBytes = []byte(os.ExpandEnv(string(yamlBytes)))
// Parse configuration file
2021-10-24 20:51:21 +02:00
if err = yaml.Unmarshal(yamlBytes, &config); err != nil {
2020-10-16 18:12:00 +02:00
return
}
// Check if the configuration file at least has endpoints configured
if config == nil || config.Endpoints == nil || len(config.Endpoints) == 0 {
err = ErrNoEndpointInConfig
2019-10-20 04:03:55 +02:00
} else {
validateAlertingConfig(config.Alerting, config.Endpoints, config.Debug)
if err := validateSecurityConfig(config); err != nil {
return nil, err
}
if err := validateEndpointsConfig(config); err != nil {
return nil, err
}
if err := validateWebConfig(config); err != nil {
return nil, err
}
2021-09-11 07:51:14 +02:00
if err := validateUIConfig(config); err != nil {
return nil, err
}
2021-09-22 06:04:51 +02:00
if err := validateMaintenanceConfig(config); err != nil {
return nil, err
}
if err := validateStorageConfig(config); err != nil {
return nil, err
}
if err := validateRemoteConfig(config); err != nil {
return nil, err
}
2019-09-07 02:25:31 +02:00
}
2019-10-20 03:42:03 +02:00
return
2019-09-06 06:01:48 +02:00
}
func validateRemoteConfig(config *Config) error {
if config.Remote != nil {
if err := config.Remote.ValidateAndSetDefaults(); err != nil {
return err
}
}
return nil
}
func validateStorageConfig(config *Config) error {
2021-02-03 05:06:34 +01:00
if config.Storage == nil {
2021-07-16 04:07:30 +02:00
config.Storage = &storage.Config{
Type: storage.TypeMemory,
2021-07-16 04:07:30 +02:00
}
} else {
if err := config.Storage.ValidateAndSetDefaults(); err != nil {
return err
}
2021-02-03 05:06:34 +01:00
}
return nil
2021-02-03 05:06:34 +01:00
}
2021-09-22 06:04:51 +02:00
func validateMaintenanceConfig(config *Config) error {
if config.Maintenance == nil {
config.Maintenance = maintenance.GetDefaultConfig()
} else {
if err := config.Maintenance.ValidateAndSetDefaults(); err != nil {
return err
}
}
return nil
}
2021-09-11 07:51:14 +02:00
func validateUIConfig(config *Config) error {
if config.UI == nil {
config.UI = ui.GetDefaultConfig()
2021-09-11 07:51:14 +02:00
} else {
if err := config.UI.ValidateAndSetDefaults(); err != nil {
2021-09-11 07:51:14 +02:00
return err
}
}
return nil
}
func validateWebConfig(config *Config) error {
if config.Web == nil {
config.Web = web.GetDefaultConfig()
} else {
return config.Web.ValidateAndSetDefaults()
}
return nil
}
func validateEndpointsConfig(config *Config) error {
for _, endpoint := range config.Endpoints {
if config.Debug {
log.Printf("[config][validateEndpointsConfig] Validating endpoint '%s'", endpoint.Name)
}
if err := endpoint.ValidateAndSetDefaults(); err != nil {
2022-11-12 20:56:25 +01:00
return fmt.Errorf("invalid endpoint %s: %w", endpoint.DisplayName(), err)
}
}
log.Printf("[config][validateEndpointsConfig] Validated %d endpoints", len(config.Endpoints))
return nil
}
func validateSecurityConfig(config *Config) error {
if config.Security != nil {
if config.Security.IsValid() {
if config.Debug {
log.Printf("[config][validateSecurityConfig] Basic security configuration has been validated")
}
} else {
// If there was an attempt to configure security, then it must mean that some confidential or private
// data are exposed. As a result, we'll force a panic because it's better to be safe than sorry.
return ErrInvalidSecurityConfig
}
}
return nil
}
// validateAlertingConfig validates the alerting configuration
// Note that the alerting configuration has to be validated before the endpoint configuration, because the default alert
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before core.Endpoint.ValidateAndSetDefaults()
// sets the default alert values when none are set.
func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.Endpoint, debug bool) {
if alertingConfig == nil {
log.Printf("[config][validateAlertingConfig] Alerting is not configured")
return
}
alertTypes := []alert.Type{
alert.TypeCustom,
alert.TypeDiscord,
alert.TypeGoogleChat,
2021-12-03 03:05:17 +01:00
alert.TypeEmail,
alert.TypeMatrix,
alert.TypeMattermost,
alert.TypeMessagebird,
alert.TypeNtfy,
2021-12-10 03:18:44 +01:00
alert.TypeOpsgenie,
alert.TypePagerDuty,
alert.TypeSlack,
2021-07-30 01:54:40 +02:00
alert.TypeTeams,
alert.TypeTelegram,
alert.TypeTwilio,
}
var validProviders, invalidProviders []alert.Type
for _, alertType := range alertTypes {
alertProvider := alertingConfig.GetAlertingProviderByAlertType(alertType)
if alertProvider != nil {
if alertProvider.IsValid() {
// Parse alerts with the provider's default alert
if alertProvider.GetDefaultAlert() != nil {
for _, endpoint := range endpoints {
for alertIndex, endpointAlert := range endpoint.Alerts {
if alertType == endpointAlert.Type {
if debug {
log.Printf("[config][validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in endpoint=%s", alertIndex, alertType, endpoint.Name)
}
provider.ParseWithDefaultAlert(alertProvider.GetDefaultAlert(), endpointAlert)
}
}
}
}
validProviders = append(validProviders, alertType)
} else {
log.Printf("[config][validateAlertingConfig] Ignoring provider=%s because configuration is invalid", alertType)
invalidProviders = append(invalidProviders, alertType)
}
} else {
invalidProviders = append(invalidProviders, alertType)
}
}
log.Printf("[config][validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders)
}