Compare commits

...
3 Commits
6 changed files with 43 additions and 38 deletions
+3
View File
@@ -745,6 +745,9 @@ endpoints:
- "[STATUS] == 200" - "[STATUS] == 200"
``` ```
> ⚠️ **WARNING**:: Tunneling may introduce additional latency, especially if the connection to the tunnel is retried frequently.
> This may lead to inaccurate response time measurements.
### Alerting ### Alerting
Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each Gatus supports multiple alerting providers, such as Slack and PagerDuty, and supports different alerts for each
+1 -4
View File
@@ -575,11 +575,8 @@ func ValidateUniqueKeys(config *Config) error {
func ValidateSecurityConfig(config *Config) error { func ValidateSecurityConfig(config *Config) error {
if config.Security != nil { if config.Security != nil {
if config.Security.ValidateAndSetDefaults() { if !config.Security.ValidateAndSetDefaults() {
logr.Debug("[config.ValidateSecurityConfig] Basic security configuration has been validated") logr.Debug("[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.
return ErrInvalidSecurityConfig return ErrInvalidSecurityConfig
} }
} }
+10 -10
View File
@@ -31,17 +31,17 @@ var (
// //
// Uses UTC by default. // Uses UTC by default.
type Config struct { type Config struct {
Enabled *bool `yaml:"enabled"` // Whether the maintenance period is enabled. Enabled by default if nil. Enabled *bool `yaml:"enabled"` // Whether the maintenance period is enabled. Enabled by default if nil.
Start string `yaml:"start"` // Time at which the maintenance period starts (e.g. 23:00) Start string `yaml:"start,omitempty"` // Time at which the maintenance period starts (e.g. 23:00)
Duration time.Duration `yaml:"duration"` // Duration of the maintenance period (e.g. 4h) Duration time.Duration `yaml:"duration,omitempty"` // Duration of the maintenance period (e.g. 4h)
Timezone string `yaml:"timezone"` // Timezone in string format which the maintenance period is configured (e.g. America/Sao_Paulo) Timezone string `yaml:"timezone,omitempty"` // Timezone in string format which the maintenance period is configured (e.g. America/Sao_Paulo)
// Every is a list of days of the week during which maintenance period applies. // Every is a list of days of the week during which maintenance period applies.
// See longDayNames for list of valid values. // See longDayNames for list of valid values.
// Every day if empty. // Every day if empty.
Every []string `yaml:"every"` Every []string `yaml:"every,omitempty"`
TimezoneLocation *time.Location // Timezone in location format which the maintenance period is configured timezoneLocation *time.Location
durationToStartFromMidnight time.Duration durationToStartFromMidnight time.Duration
} }
@@ -90,13 +90,13 @@ func (c *Config) ValidateAndSetDefaults() error {
return errInvalidMaintenanceDuration return errInvalidMaintenanceDuration
} }
if c.Timezone != "" { if c.Timezone != "" {
c.TimezoneLocation, err = time.LoadLocation(c.Timezone) c.timezoneLocation, err = time.LoadLocation(c.Timezone)
if err != nil { if err != nil {
return fmt.Errorf("%w: %w", errInvalidTimezone, err) return fmt.Errorf("%w: %w", errInvalidTimezone, err)
} }
} else { } else {
c.Timezone = "UTC" c.Timezone = "UTC"
c.TimezoneLocation = time.UTC c.timezoneLocation = time.UTC
} }
return nil return nil
} }
@@ -107,8 +107,8 @@ func (c *Config) IsUnderMaintenance() bool {
return false return false
} }
now := time.Now() now := time.Now()
if c.TimezoneLocation != nil { if c.timezoneLocation != nil {
now = now.In(c.TimezoneLocation) now = now.In(c.timezoneLocation)
} }
adjustedDate := now.Day() adjustedDate := now.Day()
if now.Hour() < int(c.durationToStartFromMidnight.Hours()) { if now.Hour() < int(c.durationToStartFromMidnight.Hours()) {
+27 -22
View File
@@ -54,7 +54,6 @@ func New(config *Config) *SSHTunnel {
tunnel := &SSHTunnel{ tunnel := &SSHTunnel{
config: config, config: config,
} }
// Parse authentication methods once during initialization to avoid // Parse authentication methods once during initialization to avoid
// expensive cryptographic operations on every connection attempt // expensive cryptographic operations on every connection attempt
if config.PrivateKey != "" { if config.PrivateKey != "" {
@@ -66,7 +65,6 @@ func New(config *Config) *SSHTunnel {
} else if config.Password != "" { } else if config.Password != "" {
tunnel.authMethods = []ssh.AuthMethod{ssh.Password(config.Password)} tunnel.authMethods = []ssh.AuthMethod{ssh.Password(config.Password)}
} }
return tunnel return tunnel
} }
@@ -131,27 +129,34 @@ func (t *SSHTunnel) Dial(network, addr string) (net.Conn, error) {
client = t.client client = t.client
t.mu.Unlock() t.mu.Unlock()
} }
// Create connection through SSH tunnel // Attempt dial with exponential backoff retry
conn, err := client.Dial(network, addr) const maxRetries = 3
if err != nil { const baseDelay = 500 * time.Millisecond
// Close stale connection before retry to prevent leak var lastErr error
t.mu.Lock() for attempt := 0; attempt < maxRetries; attempt++ {
if t.client != nil { if attempt > 0 {
t.client.Close() // Exponential backoff: 500ms, 1s, 2s
t.client = nil delay := baseDelay << (attempt - 1)
time.Sleep(delay)
// Close stale connection and reconnect
t.mu.Lock()
if t.client != nil {
_ = t.client.Close()
t.client = nil
}
if err := t.connectUnsafe(); err != nil {
t.mu.Unlock()
lastErr = fmt.Errorf("reconnect attempt %d failed: %w", attempt, err)
continue
}
client = t.client
t.mu.Unlock()
} }
t.mu.Unlock() conn, err := client.Dial(network, addr)
// Retry once - connection might be stale if err == nil {
if connErr := t.Connect(); connErr != nil { return conn, nil
return nil, fmt.Errorf("SSH tunnel dial failed: %w (retry failed: %v)", err, connErr)
}
t.mu.RLock()
client = t.client
t.mu.RUnlock()
conn, err = client.Dial(network, addr)
if err != nil {
return nil, fmt.Errorf("SSH tunnel dial failed after retry: %w", err)
} }
lastErr = err
} }
return conn, nil return nil, fmt.Errorf("SSH tunnel dial failed after %d attempts: %w", maxRetries, lastErr)
} }
+1 -1
View File
@@ -13,7 +13,7 @@ type Config struct {
// Tunnels is a map of SSH tunnel configurations in which the key is the name of the tunnel // Tunnels is a map of SSH tunnel configurations in which the key is the name of the tunnel
Tunnels map[string]*sshtunnel.Config `yaml:",inline"` Tunnels map[string]*sshtunnel.Config `yaml:",inline"`
mu sync.RWMutex `yaml:"-"` mu sync.RWMutex `yaml:"-"`
connections map[string]*sshtunnel.SSHTunnel `yaml:"-"` connections map[string]*sshtunnel.SSHTunnel `yaml:"-"`
} }
+1 -1
View File
@@ -188,4 +188,4 @@ func TestConfig_Close(t *testing.T) {
if len(config.connections) != 0 { if len(config.connections) != 0 {
t.Errorf("Close() did not clear connections map, got %d connections", len(config.connections)) t.Errorf("Close() did not clear connections map, got %d connections", len(config.connections))
} }
} }