2019-09-06 06:01:48 +02:00
package config
import (
2019-10-20 04:03:55 +02:00
"errors"
2022-09-02 02:51:37 +02:00
"fmt"
2023-01-08 23:53:37 +01:00
"io/fs"
2020-10-30 16:30:03 +01:00
"log"
"os"
2023-01-08 23:53:37 +01:00
"path/filepath"
2023-02-12 04:43:13 +01:00
"strings"
2021-05-19 04:29:15 +02:00
"time"
2020-10-30 16:30:03 +01:00
2023-01-08 23:53:37 +01:00
"github.com/TwiN/deepmerge"
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"
2023-01-08 23:53:37 +01:00
"gopkg.in/yaml.v3"
2019-09-06 06:01:48 +02:00
)
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
2023-01-08 23:53:37 +01:00
// if a custom path isn't configured through the GATUS_CONFIG_PATH 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 (
2023-01-08 23:53:37 +01:00
// ErrNoEndpointInConfig is an error returned when a configuration file or directory has no endpoints configured
ErrNoEndpointInConfig = errors . New ( "configuration should contain at least 1 endpoint" )
2020-10-23 22:07:51 +02:00
2023-01-08 23:53:37 +01:00
// ErrConfigFileNotFound is an error returned when a configuration file could not be found
2020-10-23 22:07:51 +02:00
ErrConfigFileNotFound = errors . New ( "configuration file not found" )
// ErrInvalidSecurityConfig is an error returned when the security configuration is invalid
2020-10-15 01:22:58 +02:00
ErrInvalidSecurityConfig = errors . New ( "invalid security configuration" )
2023-01-08 23:53:37 +01:00
// errEarlyReturn is returned to break out of a loop from a callback early
errEarlyReturn = errors . New ( "early escape" )
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 {
2020-10-17 05:07:14 +02:00
// Debug Whether to enable debug logs
2021-10-24 21:03:41 +02:00
Debug bool ` yaml:"debug,omitempty" `
2020-10-17 05:07:14 +02:00
// Metrics Whether to expose metrics at /metrics
2021-10-24 21:03:41 +02:00
Metrics bool ` yaml:"metrics,omitempty" `
2020-10-17 05:07:14 +02:00
2021-05-19 04:29:15 +02:00
// 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" `
2021-05-19 04:29:15 +02:00
2020-10-17 05:07:14 +02:00
// DisableMonitoringLock Whether to disable the monitoring lock
2021-10-23 22:47:12 +02:00
// The monitoring lock is what prevents multiple endpoints from being processed at the same time.
2020-10-17 05:07:14 +02:00
// Disabling this may lead to inaccurate response times
2021-10-24 21:03:41 +02:00
DisableMonitoringLock bool ` yaml:"disable-monitoring-lock,omitempty" `
2020-10-17 05:07:14 +02:00
// Security Configuration for securing access to Gatus
2021-10-24 21:03:41 +02:00
Security * security . Config ` yaml:"security,omitempty" `
2020-10-17 05:07:14 +02:00
// Alerting Configuration for alerting
2021-10-24 21:03:41 +02:00
Alerting * alerting . Config ` yaml:"alerting,omitempty" `
2020-10-17 05:07:14 +02:00
2021-10-23 22:47:12 +02:00
// Endpoints List of endpoints to monitor
2021-10-24 21:03:41 +02:00
Endpoints [ ] * core . Endpoint ` yaml:"endpoints,omitempty" `
2021-10-23 22:47:12 +02:00
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
2021-10-23 22:47:12 +02:00
// Web is the web configuration for the application
2021-10-24 21:03:41 +02:00
Web * web . Config ` yaml:"web,omitempty" `
2021-05-19 04:29:15 +02:00
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
2022-07-29 02:07:53 +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" `
2023-01-08 23:53:37 +01:00
configPath string // path to the file or directory from which config was loaded
2021-05-19 04:29:15 +02:00
lastFileModTime time . Time // last modification time
2020-06-26 03:31:34 +02:00
}
2022-08-11 03:05:34 +02:00
func ( config * Config ) GetEndpointByKey ( key string ) * core . Endpoint {
2023-01-08 23:53:37 +01:00
// TODO: Should probably add a mutex here to prevent concurrent access
2022-08-11 03:05:34 +02:00
for i := 0 ; i < len ( config . Endpoints ) ; i ++ {
ep := config . Endpoints [ i ]
if util . ConvertGroupAndEndpointNameToKey ( ep . Group , ep . Name ) == key {
return ep
}
}
return nil
}
2023-01-08 23:53:37 +01:00
// HasLoadedConfigurationBeenModified returns whether one of the file that the
2021-05-19 04:29:15 +02:00
// configuration has been loaded from has been modified since it was last read
2023-01-08 23:53:37 +01:00
func ( config Config ) HasLoadedConfigurationBeenModified ( ) bool {
lastMod := config . lastFileModTime . Unix ( )
fileInfo , err := os . Stat ( config . configPath )
if err != nil {
return false
2023-01-07 05:46:19 +01:00
}
2023-01-08 23:53:37 +01:00
if fileInfo . IsDir ( ) {
err = walkConfigDir ( config . configPath , func ( path string , d fs . DirEntry , err error ) error {
if info , err := d . Info ( ) ; err == nil && lastMod < info . ModTime ( ) . Unix ( ) {
return errEarlyReturn
}
return nil
} )
return err == errEarlyReturn
}
return ! fileInfo . ModTime ( ) . IsZero ( ) && config . lastFileModTime . Unix ( ) < fileInfo . ModTime ( ) . Unix ( )
2019-09-06 06:01:48 +02:00
}
2021-05-19 04:29:15 +02:00
// UpdateLastFileModTime refreshes Config.lastFileModTime
func ( config * Config ) UpdateLastFileModTime ( ) {
2023-01-08 23:53:37 +01:00
config . lastFileModTime = time . Now ( )
2023-01-07 09:45:43 +01:00
}
2023-01-07 05:46:19 +01:00
2023-01-08 23:53:37 +01:00
// LoadConfiguration loads the full configuration composed from the main configuration file
// and all composed configuration files
func LoadConfiguration ( configPath string ) ( * Config , error ) {
var configBytes [ ] byte
var fileInfo os . FileInfo
var usedConfigPath string
// Figure out what config path we'll use (either configPath or the default config path)
for _ , configurationPath := range [ ] string { configPath , DefaultConfigurationFilePath , DefaultFallbackConfigurationFilePath } {
if len ( configurationPath ) == 0 {
continue
2023-01-07 05:46:19 +01:00
}
2023-01-08 23:53:37 +01:00
var err error
fileInfo , err = os . Stat ( configurationPath )
if err != nil {
continue
}
2023-01-10 06:24:56 +01:00
usedConfigPath = configurationPath
2023-01-08 23:53:37 +01:00
break
2019-12-04 22:44:35 +01:00
}
2023-01-08 23:53:37 +01:00
if len ( usedConfigPath ) == 0 {
return nil , ErrConfigFileNotFound
}
var config * Config
if fileInfo . IsDir ( ) {
err := walkConfigDir ( configPath , func ( path string , d fs . DirEntry , err error ) error {
if err != nil {
log . Printf ( "[config][LoadConfiguration] Error walking path=%s: %s" , path , err )
return err
}
log . Printf ( "[config][LoadConfiguration] Reading configuration from %s" , path )
data , err := os . ReadFile ( path )
if err != nil {
log . Printf ( "[config][LoadConfiguration] Error reading configuration from %s: %s" , path , err )
return fmt . Errorf ( "error reading configuration from file %s: %w" , path , err )
}
configBytes , err = deepmerge . YAML ( configBytes , data )
return err
} )
if err != nil {
return nil , fmt . Errorf ( "error reading configuration from directory %s: %w" , usedConfigPath , err )
2019-12-04 22:44:35 +01:00
}
2023-01-08 23:53:37 +01:00
} else {
log . Printf ( "[config][LoadConfiguration] Reading configuration from configFile=%s" , configPath )
if data , err := os . ReadFile ( usedConfigPath ) ; err != nil {
return nil , err
} else {
configBytes = data
}
}
if len ( configBytes ) == 0 {
return nil , ErrConfigFileNotFound
}
config , err := parseAndValidateConfigBytes ( configBytes )
if err != nil {
2023-01-07 09:45:43 +01:00
return nil , err
2019-12-04 22:44:35 +01:00
}
2023-01-08 23:53:37 +01:00
config . configPath = usedConfigPath
config . UpdateLastFileModTime ( )
return config , err
2023-01-07 09:45:43 +01:00
}
2019-12-04 22:44:35 +01:00
2023-01-08 23:53:37 +01:00
// walkConfigDir is a wrapper for filepath.WalkDir that strips directories and non-config files
func walkConfigDir ( path string , fn fs . WalkDirFunc ) error {
if len ( path ) == 0 {
// If the user didn't provide a directory, we'll just use the default config file, so we can return nil now.
return nil
2019-09-06 06:01:48 +02:00
}
2023-01-08 23:53:37 +01:00
return filepath . WalkDir ( path , func ( path string , d fs . DirEntry , err error ) error {
if err != nil {
return nil
}
if d == nil || d . IsDir ( ) {
return nil
}
ext := filepath . Ext ( path )
if ext != ".yml" && ext != ".yaml" {
return nil
}
return fn ( path , d , err )
} )
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 ) {
2023-02-12 04:43:13 +01:00
// Replace $$ with __GATUS_LITERAL_DOLLAR_SIGN__ to prevent os.ExpandEnv from treating "$$" as if it was an
// environment variable. This allows Gatus to support literal "$" in the configuration file.
yamlBytes = [ ] byte ( strings . ReplaceAll ( string ( yamlBytes ) , "$$" , "__GATUS_LITERAL_DOLLAR_SIGN__" ) )
2019-12-04 23:27:27 +01:00
// Expand environment variables
2021-10-25 00:34:39 +02:00
yamlBytes = [ ] byte ( os . ExpandEnv ( string ( yamlBytes ) ) )
2023-02-12 04:43:13 +01:00
// Replace __GATUS_LITERAL_DOLLAR_SIGN__ with "$" to restore the literal "$" in the configuration file
yamlBytes = [ ] byte ( strings . ReplaceAll ( string ( yamlBytes ) , "__GATUS_LITERAL_DOLLAR_SIGN__" , "$" ) )
2019-12-04 23:27:27 +01:00
// 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
}
2021-10-23 22:47:12 +02:00
// 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 {
2021-10-23 22:47:12 +02:00
validateAlertingConfig ( config . Alerting , config . Endpoints , config . Debug )
2021-05-19 04:29:15 +02:00
if err := validateSecurityConfig ( config ) ; err != nil {
return nil , err
}
2021-10-23 22:47:12 +02:00
if err := validateEndpointsConfig ( config ) ; err != nil {
2021-05-19 04:29:15 +02:00
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
}
2021-05-19 04:29:15 +02:00
if err := validateStorageConfig ( config ) ; err != nil {
return nil , err
}
2022-07-29 02:07:53 +02:00
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
}
2020-09-22 23:46:40 +02:00
2022-07-29 02:07:53 +02:00
func validateRemoteConfig ( config * Config ) error {
if config . Remote != nil {
if err := config . Remote . ValidateAndSetDefaults ( ) ; err != nil {
return err
}
}
return nil
}
2021-05-19 04:29:15 +02:00
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 {
2021-08-07 18:11:35 +02:00
Type : storage . TypeMemory ,
2021-07-16 04:07:30 +02:00
}
2021-10-29 01:35:46 +02:00
} else {
if err := config . Storage . ValidateAndSetDefaults ( ) ; err != nil {
return err
}
2021-02-03 05:06:34 +01:00
}
2021-05-19 04:29:15 +02: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 {
2021-09-22 06:47:51 +02:00
config . UI = ui . GetDefaultConfig ( )
2021-09-11 07:51:14 +02:00
} else {
2021-09-22 06:47:51 +02:00
if err := config . UI . ValidateAndSetDefaults ( ) ; err != nil {
2021-09-11 07:51:14 +02:00
return err
}
}
return nil
}
2021-05-19 04:29:15 +02:00
func validateWebConfig ( config * Config ) error {
2020-11-19 19:39:48 +01:00
if config . Web == nil {
2021-09-22 06:47:51 +02:00
config . Web = web . GetDefaultConfig ( )
2020-11-19 19:39:48 +01:00
} else {
2021-09-22 06:47:51 +02:00
return config . Web . ValidateAndSetDefaults ( )
2020-11-19 19:39:48 +01:00
}
2021-05-19 04:29:15 +02:00
return nil
2020-11-19 19:39:48 +01:00
}
2021-10-23 22:47:12 +02:00
func validateEndpointsConfig ( config * Config ) error {
for _ , endpoint := range config . Endpoints {
2020-09-22 23:46:40 +02:00
if config . Debug {
2021-10-23 22:47:12 +02:00
log . Printf ( "[config][validateEndpointsConfig] Validating endpoint '%s'" , endpoint . Name )
2020-09-22 23:46:40 +02:00
}
2021-10-23 22:47:12 +02:00
if err := endpoint . ValidateAndSetDefaults ( ) ; err != nil {
2022-11-12 20:56:25 +01:00
return fmt . Errorf ( "invalid endpoint %s: %w" , endpoint . DisplayName ( ) , err )
2021-05-19 04:29:15 +02:00
}
2020-09-22 23:46:40 +02:00
}
2021-10-23 22:47:12 +02:00
log . Printf ( "[config][validateEndpointsConfig] Validated %d endpoints" , len ( config . Endpoints ) )
2021-05-19 04:29:15 +02:00
return nil
2020-09-22 23:46:40 +02:00
}
2021-05-19 04:29:15 +02:00
func validateSecurityConfig ( config * Config ) error {
2020-10-15 01:22:58 +02:00
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.
2021-05-19 04:29:15 +02:00
return ErrInvalidSecurityConfig
2020-10-15 01:22:58 +02:00
}
}
2021-05-19 04:29:15 +02:00
return nil
2020-10-15 01:22:58 +02:00
}
2021-05-16 03:31:32 +02:00
// validateAlertingConfig validates the alerting configuration
2021-10-23 22:47:12 +02:00
// 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()
2021-05-16 03:31:32 +02:00
// sets the default alert values when none are set.
2021-10-23 22:47:12 +02:00
func validateAlertingConfig ( alertingConfig * alerting . Config , endpoints [ ] * core . Endpoint , debug bool ) {
2021-05-19 04:29:15 +02:00
if alertingConfig == nil {
2020-09-22 23:46:40 +02:00
log . Printf ( "[config][validateAlertingConfig] Alerting is not configured" )
return
}
2021-05-19 04:29:15 +02:00
alertTypes := [ ] alert . Type {
alert . TypeCustom ,
alert . TypeDiscord ,
2022-12-16 05:32:04 +01:00
alert . TypeGitHub ,
2022-10-15 23:56:38 +02:00
alert . TypeGoogleChat ,
2021-12-03 03:05:17 +01:00
alert . TypeEmail ,
2022-07-19 19:05:27 +02:00
alert . TypeMatrix ,
2021-05-19 04:29:15 +02:00
alert . TypeMattermost ,
alert . TypeMessagebird ,
2022-10-05 05:26:34 +02:00
alert . TypeNtfy ,
2021-12-10 03:18:44 +01:00
alert . TypeOpsgenie ,
2021-05-19 04:29:15 +02:00
alert . TypePagerDuty ,
2023-01-29 23:32:16 +01:00
alert . TypePushover ,
2021-05-19 04:29:15 +02:00
alert . TypeSlack ,
2021-07-30 01:54:40 +02:00
alert . TypeTeams ,
2021-05-19 04:29:15 +02:00
alert . TypeTelegram ,
alert . TypeTwilio ,
2020-09-22 23:46:40 +02:00
}
2021-05-19 04:29:15 +02:00
var validProviders , invalidProviders [ ] alert . Type
2020-09-25 01:52:59 +02:00
for _ , alertType := range alertTypes {
2021-05-19 04:29:15 +02:00
alertProvider := alertingConfig . GetAlertingProviderByAlertType ( alertType )
2020-09-25 01:52:59 +02:00
if alertProvider != nil {
if alertProvider . IsValid ( ) {
2021-05-16 03:31:32 +02:00
// Parse alerts with the provider's default alert
if alertProvider . GetDefaultAlert ( ) != nil {
2021-10-23 22:47:12 +02:00
for _ , endpoint := range endpoints {
for alertIndex , endpointAlert := range endpoint . Alerts {
if alertType == endpointAlert . Type {
2021-05-19 04:29:15 +02:00
if debug {
2021-10-23 22:47:12 +02:00
log . Printf ( "[config][validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in endpoint=%s" , alertIndex , alertType , endpoint . Name )
2021-05-16 03:31:32 +02:00
}
2021-10-23 22:47:12 +02:00
provider . ParseWithDefaultAlert ( alertProvider . GetDefaultAlert ( ) , endpointAlert )
2021-05-16 03:31:32 +02:00
}
}
}
}
2020-09-25 01:52:59 +02:00
validProviders = append ( validProviders , alertType )
} else {
log . Printf ( "[config][validateAlertingConfig] Ignoring provider=%s because configuration is invalid" , alertType )
invalidProviders = append ( invalidProviders , alertType )
2022-12-16 02:54:38 +01:00
alertingConfig . SetAlertingProviderToNil ( alertProvider )
2020-09-25 01:52:59 +02:00
}
} else {
invalidProviders = append ( invalidProviders , alertType )
}
2020-09-22 23:46:40 +02:00
}
log . Printf ( "[config][validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s" , validProviders , invalidProviders )
}