2019-09-06 06:01:48 +02:00
package config
import (
2019-10-20 04:03:55 +02:00
"errors"
2020-10-30 16:30:03 +01:00
"io/ioutil"
"log"
"os"
2021-05-19 04:29:15 +02:00
"time"
2020-10-30 16:30:03 +01:00
2020-09-19 22:22:12 +02:00
"github.com/TwinProduction/gatus/alerting"
2021-05-19 04:29:15 +02:00
"github.com/TwinProduction/gatus/alerting/alert"
2020-09-25 01:52:59 +02:00
"github.com/TwinProduction/gatus/alerting/provider"
2019-09-07 02:25:31 +02:00
"github.com/TwinProduction/gatus/core"
2020-10-15 01:22:58 +02:00
"github.com/TwinProduction/gatus/security"
2021-02-03 05:06:34 +01:00
"github.com/TwinProduction/gatus/storage"
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-11-19 19:39:48 +01:00
// DefaultAddress is the default address the service will bind to
DefaultAddress = "0.0.0.0"
// DefaultPort is the default port the service will listen on
DefaultPort = 8080
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 (
2020-10-23 22:07:51 +02:00
// ErrNoServiceInConfig is an error returned when a configuration file has no services configured
ErrNoServiceInConfig = errors . New ( "configuration file should contain at least 1 service" )
// 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
2020-10-15 01:22:58 +02:00
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 {
2020-10-17 05:07:14 +02:00
// Debug Whether to enable debug logs
Debug bool ` yaml:"debug" `
// Metrics Whether to expose metrics at /metrics
Metrics bool ` yaml:"metrics" `
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
SkipInvalidConfigUpdate bool ` yaml:"skip-invalid-config-update" `
2020-10-17 05:07:14 +02:00
// DisableMonitoringLock Whether to disable the monitoring lock
// The monitoring lock is what prevents multiple services from being processed at the same time.
// Disabling this may lead to inaccurate response times
DisableMonitoringLock bool ` yaml:"disable-monitoring-lock" `
// Security Configuration for securing access to Gatus
2020-10-15 01:22:58 +02:00
Security * security . Config ` yaml:"security" `
2020-10-17 05:07:14 +02:00
// Alerting Configuration for alerting
2020-09-19 22:22:12 +02:00
Alerting * alerting . Config ` yaml:"alerting" `
2020-10-17 05:07:14 +02:00
// Services List of services to monitor
Services [ ] * core . Service ` yaml:"services" `
2020-10-30 16:30:03 +01:00
2021-02-03 05:06:34 +01:00
// Storage is the configuration for how the data is stored
Storage * storage . Config ` yaml:"storage" `
2020-11-20 23:24:56 +01:00
// Web is the configuration for the web listener
2021-02-01 07:37:56 +01:00
Web * WebConfig ` yaml:"web" `
2021-05-19 04:29:15 +02:00
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
}
2021-05-19 04:29:15 +02:00
// 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
}
2021-05-19 04:29:15 +02:00
return false
2019-09-06 06:01:48 +02:00
}
2021-05-19 04:29:15 +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 ( )
}
2021-09-04 04:28:49 +02:00
} else {
log . Println ( "[config][UpdateLastFileModTime] Ran into error updating lastFileModTime:" , err . Error ( ) )
2021-05-19 04:29:15 +02:00
}
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.
2021-05-19 04:29:15 +02:00
func Load ( configFile string ) ( * Config , error ) {
2020-09-22 23:46:40 +02:00
log . Printf ( "[config][Load] Reading configuration from configFile=%s" , configFile )
2019-12-04 22:44:35 +01:00
cfg , err := readConfigurationFile ( configFile )
if err != nil {
if os . IsNotExist ( err ) {
2021-05-19 04:29:15 +02:00
return nil , ErrConfigFileNotFound
2019-12-04 22:44:35 +01:00
}
2021-05-19 04:29:15 +02:00
return nil , err
2019-12-04 22:44:35 +01:00
}
2021-05-19 04:29:15 +02:00
cfg . filePath = configFile
cfg . UpdateLastFileModTime ( )
return cfg , nil
2019-12-04 22:44:35 +01:00
}
2020-10-20 01:26:29 +02:00
// LoadDefaultConfiguration loads the default configuration file
2021-05-19 04:29:15 +02:00
func LoadDefaultConfiguration ( ) ( * Config , error ) {
cfg , err := Load ( DefaultConfigurationFilePath )
2019-12-04 22:44:35 +01:00
if err != nil {
if err == ErrConfigFileNotFound {
2020-10-20 01:26:29 +02:00
return Load ( DefaultFallbackConfigurationFilePath )
2019-12-04 22:44:35 +01:00
}
2021-05-19 04:29:15 +02:00
return nil , err
2019-12-04 22:44:35 +01:00
}
2021-05-19 04:29:15 +02:00
return cfg , nil
2019-12-04 22:44:35 +01:00
}
2019-10-20 04:03:55 +02:00
func readConfigurationFile ( fileName string ) ( config * Config , err error ) {
var bytes [ ] byte
if bytes , err = ioutil . 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
}
2019-10-20 04:03:55 +02:00
func parseAndValidateConfigBytes ( yamlBytes [ ] byte ) ( config * Config , err error ) {
2019-12-04 23:27:27 +01:00
// Expand environment variables
yamlBytes = [ ] byte ( os . ExpandEnv ( string ( yamlBytes ) ) )
// Parse configuration file
2019-10-20 03:42:03 +02:00
err = yaml . Unmarshal ( yamlBytes , & config )
2020-10-16 18:12:00 +02:00
if err != nil {
return
}
2021-09-03 05:19:49 +02:00
// Check if the configuration file at least has services configured
if config == nil || config . Services == nil || len ( config . Services ) == 0 {
2019-10-20 04:03:55 +02:00
err = ErrNoServiceInConfig
} else {
2020-11-11 20:46:19 +01:00
// Note that the functions below may panic, and this is on purpose to prevent Gatus from starting with
// invalid configurations
2021-05-19 04:29:15 +02:00
validateAlertingConfig ( config . Alerting , config . Services , config . Debug )
if err := validateSecurityConfig ( config ) ; err != nil {
return nil , err
}
if err := validateServicesConfig ( config ) ; err != nil {
return nil , err
}
if err := validateWebConfig ( config ) ; err != nil {
return nil , err
}
if err := validateStorageConfig ( 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
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-02-03 05:06:34 +01:00
}
err := storage . Initialize ( config . Storage )
if err != nil {
2021-05-19 04:29:15 +02:00
return err
2021-02-03 05:06:34 +01:00
}
// Remove all ServiceStatus that represent services which no longer exist in the configuration
var keys [ ] string
for _ , service := range config . Services {
2021-07-12 04:22:21 +02:00
keys = append ( keys , service . Key ( ) )
2021-02-03 05:06:34 +01:00
}
numberOfServiceStatusesDeleted := storage . Get ( ) . DeleteAllServiceStatusesNotInKeys ( keys )
if numberOfServiceStatusesDeleted > 0 {
log . Printf ( "[config][validateStorageConfig] Deleted %d service statuses because their matching services no longer existed" , numberOfServiceStatusesDeleted )
}
2021-05-19 04:29:15 +02:00
return nil
2021-02-03 05:06:34 +01:00
}
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-02-01 07:37:56 +01:00
config . Web = & WebConfig { Address : DefaultAddress , Port : DefaultPort }
2020-11-19 19:39:48 +01:00
} else {
2021-05-19 04:29:15 +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-05-19 04:29:15 +02:00
func validateServicesConfig ( config * Config ) error {
2020-09-22 23:46:40 +02:00
for _ , service := range config . Services {
if config . Debug {
log . Printf ( "[config][validateServicesConfig] Validating service '%s'" , service . Name )
}
2021-05-19 04:29:15 +02:00
if err := service . ValidateAndSetDefaults ( ) ; err != nil {
return err
}
2020-09-22 23:46:40 +02:00
}
log . Printf ( "[config][validateServicesConfig] Validated %d services" , len ( config . Services ) )
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
// Note that the alerting configuration has to be validated before the service configuration, because the default alert
// returned by provider.AlertProvider.GetDefaultAlert() must be parsed before core.Service.ValidateAndSetDefaults()
// sets the default alert values when none are set.
2021-05-19 04:29:15 +02:00
func validateAlertingConfig ( alertingConfig * alerting . Config , services [ ] * core . Service , debug bool ) {
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 ,
alert . TypeMattermost ,
alert . TypeMessagebird ,
alert . TypePagerDuty ,
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-05-19 04:29:15 +02:00
for _ , service := range services {
for alertIndex , serviceAlert := range service . Alerts {
if alertType == serviceAlert . Type {
if debug {
2021-05-16 03:31:32 +02:00
log . Printf ( "[config][validateAlertingConfig] Parsing alert %d with provider's default alert for provider=%s in service=%s" , alertIndex , alertType , service . Name )
}
2021-05-19 04:29:15 +02:00
provider . ParseWithDefaultAlert ( alertProvider . GetDefaultAlert ( ) , serviceAlert )
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 )
}
} else {
invalidProviders = append ( invalidProviders , alertType )
}
2020-09-22 23:46:40 +02:00
}
log . Printf ( "[config][validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s" , validProviders , invalidProviders )
}