feat: Implement push-based external endpoints (#724)
* refactor: Move SSH outside of endpoint.go * refactor: Use pointers for Alert receivers * feat: Implement push-based external endpoints * Fix failing tests * Validate external endpoints on start * Add tests for external endpoints * refactor some error equality checks * Improve docs and refactor some code * Fix UI-related issues with external endpoints
This commit is contained in:
@ -67,15 +67,18 @@ type Config struct {
|
||||
// Disabling this may lead to inaccurate response times
|
||||
DisableMonitoringLock bool `yaml:"disable-monitoring-lock,omitempty"`
|
||||
|
||||
// Security Configuration for securing access to Gatus
|
||||
// Security is the configuration for securing access to Gatus
|
||||
Security *security.Config `yaml:"security,omitempty"`
|
||||
|
||||
// Alerting Configuration for alerting
|
||||
// Alerting is the configuration for alerting providers
|
||||
Alerting *alerting.Config `yaml:"alerting,omitempty"`
|
||||
|
||||
// Endpoints List of endpoints to monitor
|
||||
// Endpoints is the list of endpoints to monitor
|
||||
Endpoints []*core.Endpoint `yaml:"endpoints,omitempty"`
|
||||
|
||||
// ExternalEndpoints is the list of all external endpoints
|
||||
ExternalEndpoints []*core.ExternalEndpoint `yaml:"external-endpoints,omitempty"`
|
||||
|
||||
// Storage is the configuration for how the data is stored
|
||||
Storage *storage.Config `yaml:"storage,omitempty"`
|
||||
|
||||
@ -100,7 +103,6 @@ type Config struct {
|
||||
}
|
||||
|
||||
func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
|
||||
// TODO: Should probably add a mutex here to prevent concurrent access
|
||||
for i := 0; i < len(config.Endpoints); i++ {
|
||||
ep := config.Endpoints[i]
|
||||
if util.ConvertGroupAndEndpointNameToKey(ep.Group, ep.Name) == key {
|
||||
@ -110,9 +112,19 @@ func (config *Config) GetEndpointByKey(key string) *core.Endpoint {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (config *Config) GetExternalEndpointByKey(key string) *core.ExternalEndpoint {
|
||||
for i := 0; i < len(config.ExternalEndpoints); i++ {
|
||||
ee := config.ExternalEndpoints[i]
|
||||
if util.ConvertGroupAndEndpointNameToKey(ee.Group, ee.Name) == key {
|
||||
return ee
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// HasLoadedConfigurationBeenModified returns whether one of the file that the
|
||||
// configuration has been loaded from has been modified since it was last read
|
||||
func (config Config) HasLoadedConfigurationBeenModified() bool {
|
||||
func (config *Config) HasLoadedConfigurationBeenModified() bool {
|
||||
lastMod := config.lastFileModTime.Unix()
|
||||
fileInfo, err := os.Stat(config.configPath)
|
||||
if err != nil {
|
||||
@ -125,7 +137,7 @@ func (config Config) HasLoadedConfigurationBeenModified() bool {
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return err == errEarlyReturn
|
||||
return errors.Is(err, errEarlyReturn)
|
||||
}
|
||||
return !fileInfo.ModTime().IsZero() && config.lastFileModTime.Unix() < fileInfo.ModTime().Unix()
|
||||
}
|
||||
@ -135,7 +147,7 @@ func (config *Config) UpdateLastFileModTime() {
|
||||
config.lastFileModTime = time.Now()
|
||||
}
|
||||
|
||||
// LoadConfiguration loads the full configuration composed from the main configuration file
|
||||
// LoadConfiguration loads the full configuration composed of the main configuration file
|
||||
// and all composed configuration files
|
||||
func LoadConfiguration(configPath string) (*Config, error) {
|
||||
var configBytes []byte
|
||||
@ -241,6 +253,9 @@ func parseAndValidateConfigBytes(yamlBytes []byte) (config *Config, err error) {
|
||||
if err := validateEndpointsConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateExternalEndpointsConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := validateWebConfig(config); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -336,6 +351,19 @@ func validateEndpointsConfig(config *Config) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateExternalEndpointsConfig(config *Config) error {
|
||||
for _, externalEndpoint := range config.ExternalEndpoints {
|
||||
if config.Debug {
|
||||
log.Printf("[config.validateExternalEndpointsConfig] Validating external endpoint '%s'", externalEndpoint.Name)
|
||||
}
|
||||
if err := externalEndpoint.ValidateAndSetDefaults(); err != nil {
|
||||
return fmt.Errorf("invalid external endpoint %s: %w", externalEndpoint.DisplayName(), err)
|
||||
}
|
||||
}
|
||||
log.Printf("[config.validateExternalEndpointsConfig] Validated %d external endpoints", len(config.ExternalEndpoints))
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateSecurityConfig(config *Config) error {
|
||||
if config.Security != nil {
|
||||
if config.Security.IsValid() {
|
||||
|
@ -304,11 +304,13 @@ func TestParseAndValidateConfigBytes(t *testing.T) {
|
||||
storage:
|
||||
type: sqlite
|
||||
path: %s
|
||||
|
||||
maintenance:
|
||||
enabled: true
|
||||
start: 00:00
|
||||
duration: 4h
|
||||
every: [Monday, Thursday]
|
||||
|
||||
ui:
|
||||
title: T
|
||||
header: H
|
||||
@ -318,6 +320,16 @@ ui:
|
||||
link: "https://example.org"
|
||||
- name: "Status page"
|
||||
link: "https://status.example.org"
|
||||
|
||||
external-endpoints:
|
||||
- name: ext-ep-test
|
||||
group: core
|
||||
token: "potato"
|
||||
alerts:
|
||||
- type: discord
|
||||
description: "healthcheck failed"
|
||||
send-on-resolved: true
|
||||
|
||||
endpoints:
|
||||
- name: website
|
||||
url: https://twin.sh/health
|
||||
@ -358,10 +370,33 @@ endpoints:
|
||||
if mc := config.Maintenance; mc == nil || mc.Start != "00:00" || !mc.IsEnabled() || mc.Duration != 4*time.Hour || len(mc.Every) != 2 {
|
||||
t.Error("Expected Config.Maintenance to be configured properly")
|
||||
}
|
||||
if len(config.ExternalEndpoints) != 1 {
|
||||
t.Error("Should have returned one external endpoint")
|
||||
}
|
||||
if config.ExternalEndpoints[0].Name != "ext-ep-test" {
|
||||
t.Errorf("Name should have been %s", "ext-ep-test")
|
||||
}
|
||||
if config.ExternalEndpoints[0].Group != "core" {
|
||||
t.Errorf("Group should have been %s", "core")
|
||||
}
|
||||
if config.ExternalEndpoints[0].Token != "potato" {
|
||||
t.Errorf("Token should have been %s", "potato")
|
||||
}
|
||||
if len(config.ExternalEndpoints[0].Alerts) != 1 {
|
||||
t.Error("Should have returned one alert")
|
||||
}
|
||||
if config.ExternalEndpoints[0].Alerts[0].Type != alert.TypeDiscord {
|
||||
t.Errorf("Type should have been %s", alert.TypeDiscord)
|
||||
}
|
||||
if config.ExternalEndpoints[0].Alerts[0].FailureThreshold != 3 {
|
||||
t.Errorf("FailureThreshold should have been %d, got %d", 3, config.ExternalEndpoints[0].Alerts[0].FailureThreshold)
|
||||
}
|
||||
if config.ExternalEndpoints[0].Alerts[0].SuccessThreshold != 2 {
|
||||
t.Errorf("SuccessThreshold should have been %d, got %d", 2, config.ExternalEndpoints[0].Alerts[0].SuccessThreshold)
|
||||
}
|
||||
if len(config.Endpoints) != 3 {
|
||||
t.Error("Should have returned two endpoints")
|
||||
}
|
||||
|
||||
if config.Endpoints[0].URL != "https://twin.sh/health" {
|
||||
t.Errorf("URL should have been %s", "https://twin.sh/health")
|
||||
}
|
||||
@ -383,7 +418,6 @@ endpoints:
|
||||
if len(config.Endpoints[0].Conditions) != 1 {
|
||||
t.Errorf("There should have been %d conditions", 1)
|
||||
}
|
||||
|
||||
if config.Endpoints[1].URL != "https://api.github.com/healthz" {
|
||||
t.Errorf("URL should have been %s", "https://api.github.com/healthz")
|
||||
}
|
||||
|
Reference in New Issue
Block a user