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-11-11 18:40:06 +01:00
"github.com/TwinProduction/gatus/k8s"
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
2020-11-11 22:29:30 +01:00
// Kubernetes is the Kubernetes configuration
2020-11-11 18:40:06 +01:00
Kubernetes * k8s . Config ` yaml:"kubernetes" `
2020-11-19 19:39:48 +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-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
}
2020-11-11 21:10:44 +01:00
// Check if the configuration file at least has services configured or Kubernetes auto discovery enabled
if config == nil || ( ( config . Services == nil || len ( config . Services ) == 0 ) && ( config . Kubernetes == nil || ! config . Kubernetes . AutoDiscover ) ) {
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 := validateKubernetesConfig ( 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 {
Type : storage . TypeInMemory ,
}
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
// deprecated
// I don't like the current implementation.
func validateKubernetesConfig ( config * Config ) error {
2020-11-11 20:22:31 +01:00
if config . Kubernetes != nil && config . Kubernetes . AutoDiscover {
2021-07-28 03:08:30 +02:00
log . Println ( "WARNING - The Kubernetes integration is planned to be removed in v3.0.0. If you're seeing this message, it's because you're currently using it, and you may want to give your opinion at https://github.com/TwinProduction/gatus/discussions/135" )
2020-11-11 21:10:44 +01:00
if config . Kubernetes . ServiceTemplate == nil {
2021-05-19 04:29:15 +02:00
return errors . New ( "kubernetes.service-template cannot be nil" )
2020-11-11 21:10:44 +01:00
}
2020-11-11 21:14:21 +01:00
if config . Debug {
log . Println ( "[config][validateKubernetesConfig] Automatically discovering Kubernetes services..." )
}
2020-11-11 20:46:19 +01:00
discoveredServices , err := k8s . DiscoverServices ( config . Kubernetes )
if err != nil {
2021-05-19 04:29:15 +02:00
return err
2020-11-11 20:46:19 +01:00
}
2020-11-11 20:22:31 +01:00
config . Services = append ( config . Services , discoveredServices ... )
2020-11-11 21:14:21 +01:00
log . Printf ( "[config][validateKubernetesConfig] Discovered %d Kubernetes services" , len ( discoveredServices ) )
2020-11-11 20:22:31 +01:00
}
2021-05-19 04:29:15 +02:00
return nil
2020-11-11 20:22:31 +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 )
}