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 return false
} }
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()
}
// 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()
} }
// LoadConfiguration loads the full configuration composed from the main configuration file
// and all composed configuration files
func LoadConfiguration(configPath string) (*Config, error) {
var composedContents []byte
var fileInfo os.FileInfo
var usedConfigPath string = ""
for _, cpath := range []string{configPath, DefaultConfigurationFilePath, DefaultFallbackConfigurationFilePath} {
if len(cpath) == 0 {
continue
}
var err error
fileInfo, err = os.Stat(cpath)
if err != nil {
continue
}
usedConfigPath = configPath
break
}
if len(usedConfigPath) == 0 {
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 { } else {
bytes, serr := os.ReadFile(usedConfigPath) log.Println("[config][UpdateLastFileModTime] Ran into error updating lastFileModTime:", err.Error())
if serr == nil {
log.Printf("[config][Load] Reading configuration from configFile=%s", configPath)
composedContents = bytes
} }
} }
if len(composedContents) == 0 { // Load loads a custom configuration file
// 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, ErrConfigFileNotFound
} }
config, err := parseAndValidateConfigBytes(composedContents) return nil, err
config.configPath = usedConfigPath }
config.UpdateLastFileModTime() cfg.filePath = configFile
return config, err cfg.UpdateLastFileModTime()
return cfg, nil
}
// LoadDefaultConfiguration loads the default configuration file
func LoadDefaultConfiguration() (*Config, error) {
cfg, err := Load(DefaultConfigurationFilePath)
if err != nil {
if err == ErrConfigFileNotFound {
return Load(DefaultFallbackConfigurationFilePath)
}
return nil, err
}
return cfg, nil
}
func readConfigurationFile(fileName string) (config *Config, err error) {
var bytes []byte
if bytes, err = os.ReadFile(fileName); err == nil {
// file exists, so we'll parse it and return it
return parseAndValidateConfigBytes(bytes)
}
return
} }
// 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