Revert "feat: Support multiple configuration files" (#395)

Revert "feat: Support multiple configuration files (#389)"

This reverts commit 8e14302765.
This commit is contained in:
TwiN 2023-01-07 03:45:43 -05:00 committed by GitHub
parent 8e14302765
commit 87740e74a6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 54 additions and 178 deletions

View File

@ -149,7 +149,7 @@ If you want to create your own configuration, see [Docker](#docker) for informat
By default, the configuration file is expected to be at `config/config.yaml`. By default, the configuration file is expected to be at `config/config.yaml`.
You can specify a custom path by setting the `GATUS_CONFIG_PATH` environment variable. If `GATUS_CONFIG_PATH` points to a directory, all `*.yaml` and `*.yml` files inside this directory and its subdirectories are concatenated. The previously used environment variable `GATUS_CONFIG_FILE` is deprecated but still works. You can specify a custom path by setting the `GATUS_CONFIG_FILE` environment variable.
Here's a simple example: Here's a simple example:
```yaml ```yaml

View File

@ -3,10 +3,8 @@ package config
import ( import (
"errors" "errors"
"fmt" "fmt"
"io/fs"
"log" "log"
"os" "os"
"path/filepath"
"time" "time"
"github.com/TwiN/gatus/v5/alerting" "github.com/TwiN/gatus/v5/alerting"
@ -42,9 +40,6 @@ var (
// ErrInvalidSecurityConfig is an error returned when the security configuration is invalid // ErrInvalidSecurityConfig is an error returned when the security configuration is invalid
ErrInvalidSecurityConfig = errors.New("invalid security configuration") ErrInvalidSecurityConfig = errors.New("invalid security configuration")
// ErrEarlyReturn is returned to break out of a loop from a callback early
ErrEarlyReturn = errors.New("early escape")
) )
// Config is the main configuration structure // Config is the main configuration structure
@ -89,7 +84,7 @@ type Config struct {
// WARNING: This is in ALPHA and may change or be completely removed in the future // WARNING: This is in ALPHA and may change or be completely removed in the future
Remote *remote.Config `yaml:"remote,omitempty"` Remote *remote.Config `yaml:"remote,omitempty"`
configPath string // path to the file or directory from which config was loaded filePath string // path to the file from which config was loaded from
lastFileModTime time.Time // last modification time lastFileModTime time.Time // last modification time
} }
@ -103,80 +98,63 @@ func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
return nil return nil
} }
// HasLoadedConfigurationFileBeenModified returns whether one of the file that the // HasLoadedConfigurationFileBeenModified returns whether the file that the
// configuration has been loaded from has been modified since it was last read // configuration has been loaded from has been modified since it was last read
func (config Config) HasLoadedConfigurationFileBeenModified() bool { func (config Config) HasLoadedConfigurationFileBeenModified() bool {
lastMod := config.lastFileModTime.Unix() if fileInfo, err := os.Stat(config.filePath); err == nil {
fileInfo, err := os.Stat(config.configPath) if !fileInfo.ModTime().IsZero() {
if err != nil { return config.lastFileModTime.Unix() != fileInfo.ModTime().Unix()
return false }
} }
if fileInfo.IsDir() { return false
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()
} }
// UpdateLastFileModTime refreshes Config.lastFileModTime // UpdateLastFileModTime refreshes Config.lastFileModTime
func (config *Config) UpdateLastFileModTime() { func (config *Config) UpdateLastFileModTime() {
config.lastFileModTime = time.Now() 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())
}
} }
// LoadConfiguration loads the full configuration composed from the main configuration file // Load loads a custom configuration file
// and all composed configuration files // Note that the misconfiguration of some fields may lead to panics. This is on purpose.
func LoadConfiguration(configPath string) (*Config, error) { func Load(configFile string) (*Config, error) {
var composedContents []byte log.Printf("[config][Load] Reading configuration from configFile=%s", configFile)
var fileInfo os.FileInfo cfg, err := readConfigurationFile(configFile)
var usedConfigPath string = "" if err != nil {
if os.IsNotExist(err) {
for _, cpath := range []string{configPath, DefaultConfigurationFilePath, DefaultFallbackConfigurationFilePath} { return nil, ErrConfigFileNotFound
if len(cpath) == 0 {
continue
} }
return nil, err
}
cfg.filePath = configFile
cfg.UpdateLastFileModTime()
return cfg, nil
}
var err error // LoadDefaultConfiguration loads the default configuration file
fileInfo, err = os.Stat(cpath) func LoadDefaultConfiguration() (*Config, error) {
if err != nil { cfg, err := Load(DefaultConfigurationFilePath)
continue if err != nil {
if err == ErrConfigFileNotFound {
return Load(DefaultFallbackConfigurationFilePath)
} }
return nil, err
}
return cfg, nil
}
usedConfigPath = configPath func readConfigurationFile(fileName string) (config *Config, err error) {
break var bytes []byte
if bytes, err = os.ReadFile(fileName); err == nil {
// file exists, so we'll parse it and return it
return parseAndValidateConfigBytes(bytes)
} }
if len(usedConfigPath) == 0 { return
return nil, ErrConfigFileNotFound
}
if fileInfo.IsDir() {
walkConfigDir(configPath, func(path string, d fs.DirEntry, err error) error {
bytes, rerr := os.ReadFile(path)
if rerr == nil {
log.Printf("[config][Load] Reading configuration from configFile=%s", path)
composedContents = append(composedContents, bytes...)
}
return nil
})
} else {
bytes, serr := os.ReadFile(usedConfigPath)
if serr == nil {
log.Printf("[config][Load] Reading configuration from configFile=%s", configPath)
composedContents = bytes
}
}
if len(composedContents) == 0 {
return nil, ErrConfigFileNotFound
}
config, err := parseAndValidateConfigBytes(composedContents)
config.configPath = usedConfigPath
config.UpdateLastFileModTime()
return config, err
} }
// parseAndValidateConfigBytes parses a Gatus configuration file into a Config struct and validates its parameters // parseAndValidateConfigBytes parses a Gatus configuration file into a Config struct and validates its parameters
@ -354,25 +332,3 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
} }
log.Printf("[config][validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders) log.Printf("[config][validateAlertingConfig] configuredProviders=%s; ignoredProviders=%s", validProviders, invalidProviders)
} }
// 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
}
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)
})
}

View File

@ -29,40 +29,19 @@ import (
) )
func TestLoadFileThatDoesNotExist(t *testing.T) { func TestLoadFileThatDoesNotExist(t *testing.T) {
_, err := LoadConfiguration("file-that-does-not-exist.yaml") _, err := Load("file-that-does-not-exist.yaml")
if err == nil { if err == nil {
t.Error("Should've returned an error, because the file specified doesn't exist") t.Error("Should've returned an error, because the file specified doesn't exist")
} }
} }
func TestLoadDefaultConfigurationFile(t *testing.T) { func TestLoadDefaultConfigurationFile(t *testing.T) {
_, err := LoadConfiguration("") _, err := LoadDefaultConfiguration()
if err == nil { if err == nil {
t.Error("Should've returned an error, because there's no configuration files at the default path nor the default fallback path") t.Error("Should've returned an error, because there's no configuration files at the default path nor the default fallback path")
} }
} }
func TestLoadConfigurationFile(t *testing.T) {
_, err := LoadConfiguration("../test-conf/config.yaml")
if nil != err {
t.Error("Should not have returned an error, because the configuration file exists at the provided path")
}
}
func TestLoadDistributedConfiguration(t *testing.T) {
_, err := LoadConfiguration("../test-conf/conf.d/")
if nil != err {
t.Error("Should not have returned an error, because configuration files exist at the provided path for distributed files")
}
}
func TestLoadCombinedConfiguration(t *testing.T) {
_, err := LoadConfiguration("../test-conf/empty-conf.d/")
if nil == err {
t.Error("Should have returned an error, because the configuration directory does not contain any configuration files")
}
}
func TestParseAndValidateConfigBytes(t *testing.T) { func TestParseAndValidateConfigBytes(t *testing.T) {
file := t.TempDir() + "/test.db" file := t.TempDir() + "/test.db"
config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(` config, err := parseAndValidateConfigBytes([]byte(fmt.Sprintf(`

14
main.go
View File

@ -52,14 +52,14 @@ func save() {
} }
} }
func loadConfiguration() (*config.Config, error) { func loadConfiguration() (cfg *config.Config, err error) {
var configPath = os.Getenv("GATUS_CONFIG_PATH") customConfigFile := os.Getenv("GATUS_CONFIG_FILE")
// Backwards compatibility if len(customConfigFile) > 0 {
if len(configPath) == 0 { cfg, err = config.Load(customConfigFile)
configPath = os.Getenv("GATUS_CONFIG_FILE") } else {
cfg, err = config.LoadDefaultConfiguration()
} }
return
return config.LoadConfiguration(configPath)
} }
// initializeStorage initializes the storage provider // initializeStorage initializes the storage provider

View File

@ -1,24 +0,0 @@
endpoints:
- name: front-end
group: core
url: "https://twin.sh/health"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[BODY].status == UP"
- "[RESPONSE_TIME] < 150"
- name: back-end
group: core
url: "https://example.org/"
interval: 5m
conditions:
- "[STATUS] == 200"
- "[CERTIFICATE_EXPIRATION] > 48h"
- name: monitoring
group: internal
url: "https://example.org/"
interval: 5m
conditions:
- "[STATUS] == 200"

View File

@ -1,29 +0,0 @@
endpoints:
- name: nas
group: internal
url: "https://example.org/"
interval: 5m
conditions:
- "[STATUS] == 200"
- name: example-dns-query
url: "8.8.8.8" # Address of the DNS server to use
interval: 5m
dns:
query-name: "example.com"
query-type: "A"
conditions:
- "[BODY] == 93.184.216.34"
- "[DNS_RCODE] == NOERROR"
- name: icmp-ping
url: "icmp://example.org"
interval: 1m
conditions:
- "[CONNECTED] == true"
- name: check-domain-expiration
url: "https://example.org/"
interval: 1h
conditions:
- "[DOMAIN_EXPIRATION] > 720h"

View File

@ -1,6 +0,0 @@
endpoints:
- name: example
url: https://example.org
interval: 30s
conditions:
- "[STATUS] == 200"

View File