diff --git a/alerting/alert/alert.go b/alerting/alert/alert.go
index c97feef8..d1ee06f1 100644
--- a/alerting/alert/alert.go
+++ b/alerting/alert/alert.go
@@ -36,13 +36,18 @@ type Alert struct {
//
// This is a pointer, because it is populated by YAML and we need to know whether it was explicitly set to a value
// or not for provider.ParseWithDefaultAlert to work.
- Description *string `yaml:"description"`
+ Description *string `yaml:"description,omitempty"`
// SendOnResolved defines whether to send a second notification when the issue has been resolved
//
// This is a pointer, because it is populated by YAML and we need to know whether it was explicitly set to a value
// or not for provider.ParseWithDefaultAlert to work. Use Alert.IsSendingOnResolved() for a non-pointer
- SendOnResolved *bool `yaml:"send-on-resolved"`
+ SendOnResolved *bool `yaml:"send-on-resolved,omitempty"`
+
+ // Override is an optional field that can be used to override the provider's configuration
+ // It is freeform so that it can be used for any provider-specific configuration.
+ // Deepmerged with https://github.com/TwiN/deepmerge
+ Override []byte `yaml:"override,omitempty"`
// ResolveKey is an optional field that is used by some providers (i.e. PagerDuty's dedup_key) to resolve
// ongoing/triggered incidents
diff --git a/alerting/provider/awsses/awsses.go b/alerting/provider/awsses/awsses.go
index a43a9138..621c0564 100644
--- a/alerting/provider/awsses/awsses.go
+++ b/alerting/provider/awsses/awsses.go
@@ -1,6 +1,7 @@
package awsses
import (
+ "errors"
"fmt"
"strings"
@@ -12,22 +13,18 @@ import (
"github.com/aws/aws-sdk-go/aws/credentials"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/ses"
+ "gopkg.in/yaml.v3"
)
const (
CharSet = "UTF-8"
)
-// AlertProvider is the configuration necessary for sending an alert using AWS Simple Email Service
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-
- // Overrides is a list of Override that may be prioritized over the default configuration
- Overrides []Override `yaml:"overrides,omitempty"`
-}
+var (
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+ ErrMissingFromOrToFields = errors.New("from and to fields are required")
+ ErrInvalidAWSAuthConfig = errors.New("either both or neither of access-key-id and secret-access-key must be specified")
+)
type Config struct {
AccessKeyID string `yaml:"access-key-id"`
@@ -38,38 +35,80 @@ type Config struct {
To string `yaml:"to"`
}
+func (cfg *Config) Validate() error {
+ if len(cfg.From) == 0 || len(cfg.To) == 0 {
+ return ErrMissingFromOrToFields
+ }
+ if !((len(cfg.AccessKeyID) == 0 && len(cfg.SecretAccessKey) == 0) || (len(cfg.AccessKeyID) > 0 && len(cfg.SecretAccessKey) > 0)) {
+ // if both AccessKeyID and SecretAccessKey are specified, we'll use these to authenticate,
+ // otherwise if neither are specified, then we'll fall back on IAM authentication.
+ return ErrInvalidAWSAuthConfig
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.AccessKeyID) > 0 {
+ cfg.AccessKeyID = override.AccessKeyID
+ }
+ if len(override.SecretAccessKey) > 0 {
+ cfg.SecretAccessKey = override.SecretAccessKey
+ }
+ if len(override.Region) > 0 {
+ cfg.Region = override.Region
+ }
+ if len(override.From) > 0 {
+ cfg.From = override.From
+ }
+ if len(override.To) > 0 {
+ cfg.To = override.To
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using AWS Simple Email Service
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+
+ // Overrides is a list of Override that may be prioritized over the default configuration
+ Overrides []Override `yaml:"overrides,omitempty"`
+}
+
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.To) == 0 {
- return false
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- // if both AccessKeyID and SecretAccessKey are specified, we'll use these to authenticate,
- // otherwise if neither are specified, then we'll fall back on IAM authentication.
- return len(provider.From) > 0 && len(provider.To) > 0 &&
- ((len(provider.AccessKeyID) == 0 && len(provider.SecretAccessKey) == 0) || (len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0))
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- sess, err := provider.createSession()
+ cfg, err := provider.GetConfig(ep.Group, alert)
if err != nil {
return err
}
- svc := ses.New(sess)
+ awsSession, err := provider.createSession(cfg)
+ if err != nil {
+ return err
+ }
+ svc := ses.New(awsSession)
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
- emails := strings.Split(provider.getToForGroup(ep.Group), ",")
+ emails := strings.Split(cfg.To, ",")
input := &ses.SendEmailInput{
Destination: &ses.Destination{
@@ -87,7 +126,7 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
Data: aws.String(subject),
},
},
- Source: aws.String(provider.From),
+ Source: aws.String(cfg.From),
}
if _, err = svc.SendEmail(input); err != nil {
if aerr, ok := err.(awserr.Error); ok {
@@ -112,6 +151,16 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
return nil
}
+func (provider *AlertProvider) createSession(cfg *Config) (*session.Session, error) {
+ awsConfig := &aws.Config{
+ Region: aws.String(cfg.Region),
+ }
+ if len(cfg.AccessKeyID) > 0 && len(cfg.SecretAccessKey) > 0 {
+ awsConfig.Credentials = credentials.NewStaticCredentials(cfg.AccessKeyID, cfg.SecretAccessKey, "")
+ }
+ return session.NewSession(awsConfig)
+}
+
// buildMessageSubjectAndBody builds the message subject and body
func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) (string, string) {
var subject, message string
@@ -142,29 +191,34 @@ func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint,
return subject, message + description + formattedConditionResults
}
-// getToForGroup returns the appropriate email integration to for a given group
-func (provider *AlertProvider) getToForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.To
- }
- }
- }
- return provider.To
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
-func (provider *AlertProvider) createSession() (*session.Session, error) {
- config := &aws.Config{
- Region: aws.String(provider.Region),
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
}
- if len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0 {
- config.Credentials = credentials.NewStaticCredentials(provider.AccessKeyID, provider.SecretAccessKey, "")
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
}
- return session.NewSession(config)
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
}
diff --git a/alerting/provider/awsses/awsses_test.go b/alerting/provider/awsses/awsses_test.go
index 05dadb62..2943c6a6 100644
--- a/alerting/provider/awsses/awsses_test.go
+++ b/alerting/provider/awsses/awsses_test.go
@@ -9,19 +9,19 @@ import (
func TestAlertDefaultProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
invalidProviderWithOneKey := AlertProvider{Config: Config{From: "from@example.com", To: "to@example.com", AccessKeyID: "1"}}
- if invalidProviderWithOneKey.IsValid() {
+ if invalidProviderWithOneKey.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{From: "from@example.com", To: "to@example.com"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
validProviderWithKeys := AlertProvider{Config: Config{From: "from@example.com", To: "to@example.com", AccessKeyID: "1", SecretAccessKey: "1"}}
- if !validProviderWithKeys.IsValid() {
+ if !validProviderWithKeys.Validate() {
t.Error("provider should've been valid")
}
}
@@ -35,7 +35,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
@@ -46,7 +46,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideTo.IsValid() {
+ if providerWithInvalidOverrideTo.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
@@ -61,7 +61,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
@@ -126,12 +126,13 @@ func TestAlertProvider_GetDefaultAlert(t *testing.T) {
}
}
-func TestAlertProvider_getToForGroup(t *testing.T) {
- tests := []struct {
+func TestAlertProvider_getConfigWithOverrides(t *testing.T) {
+ scenarios := []struct {
Name string
Provider AlertProvider
InputGroup string
- ExpectedOutput string
+ InputAlert alert.Alert
+ ExpectedOutput Config
}{
{
Name: "provider-no-override-specify-no-group-should-default",
@@ -142,7 +143,8 @@ func TestAlertProvider_getToForGroup(t *testing.T) {
Overrides: nil,
},
InputGroup: "",
- ExpectedOutput: "to@example.com",
+ InputAlert: alert.Alert{},
+ ExpectedOutput: Config{To: "to@example.com"},
},
{
Name: "provider-no-override-specify-group-should-default",
@@ -153,7 +155,8 @@ func TestAlertProvider_getToForGroup(t *testing.T) {
Overrides: nil,
},
InputGroup: "group",
- ExpectedOutput: "to@example.com",
+ InputAlert: alert.Alert{},
+ ExpectedOutput: Config{To: "to@example.com"},
},
{
Name: "provider-with-override-specify-no-group-should-default",
@@ -164,12 +167,13 @@ func TestAlertProvider_getToForGroup(t *testing.T) {
Overrides: []Override{
{
Group: "group",
- Config: Config{To: "to01@example.com"},
+ Config: Config{To: "groupto@example.com"},
},
},
},
InputGroup: "",
- ExpectedOutput: "to@example.com",
+ InputAlert: alert.Alert{},
+ ExpectedOutput: Config{To: "to@example.com"},
},
{
Name: "provider-with-override-specify-group-should-override",
@@ -180,18 +184,56 @@ func TestAlertProvider_getToForGroup(t *testing.T) {
Overrides: []Override{
{
Group: "group",
- Config: Config{To: "to01@example.com"},
+ Config: Config{To: "groupto@example.com"},
},
},
},
InputGroup: "group",
- ExpectedOutput: "to01@example.com",
+ InputAlert: alert.Alert{},
+ ExpectedOutput: Config{To: "groupto@example.com"},
+ },
+ {
+ Name: "provider-with-override-specify-group-but-alert-override-should-override-group-override",
+ Provider: AlertProvider{
+ Config: Config{
+ From: "from@example.com",
+ To: "to@example.com",
+ },
+ Overrides: []Override{
+ {
+ Group: "group",
+ Config: Config{To: "groupto@example.com", SecretAccessKey: "sekrit"},
+ },
+ },
+ },
+ InputGroup: "group",
+ InputAlert: alert.Alert{
+ Override: []byte(`to: alertto@example.com
+access-key-id: 123`),
+ },
+ ExpectedOutput: Config{To: "alertto@example.com", From: "from@example.com", AccessKeyID: "123", SecretAccessKey: "sekrit"},
},
}
- for _, tt := range tests {
- t.Run(tt.Name, func(t *testing.T) {
- if got := tt.Provider.getToForGroup(tt.InputGroup); got != tt.ExpectedOutput {
- t.Errorf("AlertProvider.getToForGroup() = %v, want %v", got, tt.ExpectedOutput)
+ for _, scenario := range scenarios {
+ t.Run(scenario.Name, func(t *testing.T) {
+ got, err := scenario.Provider.GetConfigWithOverrides(scenario.InputGroup, &scenario.InputAlert)
+ if err != nil {
+ t.Fatalf("unexpected error: %s", err)
+ }
+ if got.From != scenario.ExpectedOutput.From {
+ t.Errorf("expected From to be %s, got %s", scenario.ExpectedOutput.From, got.From)
+ }
+ if got.To != scenario.ExpectedOutput.To {
+ t.Errorf("expected To to be %s, got %s", scenario.ExpectedOutput.To, got.To)
+ }
+ if got.AccessKeyID != scenario.ExpectedOutput.AccessKeyID {
+ t.Errorf("expected AccessKeyID to be %s, got %s", scenario.ExpectedOutput.AccessKeyID, got.AccessKeyID)
+ }
+ if got.SecretAccessKey != scenario.ExpectedOutput.SecretAccessKey {
+ t.Errorf("expected SecretAccessKey to be %s, got %s", scenario.ExpectedOutput.SecretAccessKey, got.SecretAccessKey)
+ }
+ if got.Region != scenario.ExpectedOutput.Region {
+ t.Errorf("expected Region to be %s, got %s", scenario.ExpectedOutput.Region, got.Region)
}
})
}
diff --git a/alerting/provider/custom/custom.go b/alerting/provider/custom/custom.go
index bb83b473..701066c1 100644
--- a/alerting/provider/custom/custom.go
+++ b/alerting/provider/custom/custom.go
@@ -2,6 +2,7 @@ package custom
import (
"bytes"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,16 +11,12 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
-// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
-// Technically, all alert providers should be reachable using the custom alert provider
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-}
+var (
+ ErrURLNotSet = errors.New("url not set")
+)
type Config struct {
URL string `yaml:"url"`
@@ -32,61 +29,64 @@ type Config struct {
ClientConfig *client.Config `yaml:"client,omitempty"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if provider.ClientConfig == nil {
- provider.ClientConfig = client.GetDefaultConfig()
+func (cfg *Config) Validate() error {
+ if cfg.ClientConfig == nil {
+ cfg.ClientConfig = client.GetDefaultConfig()
}
- return len(provider.URL) > 0 && provider.ClientConfig != nil
+ if len(cfg.URL) == 0 {
+ return ErrURLNotSet
+ }
+ return nil
}
-// GetAlertStatePlaceholderValue returns the Placeholder value for ALERT_TRIGGERED_OR_RESOLVED if configured
-func (provider *AlertProvider) GetAlertStatePlaceholderValue(resolved bool) string {
- status := "TRIGGERED"
- if resolved {
- status = "RESOLVED"
+func (cfg *Config) Merge(override *Config) {
+ if len(override.URL) > 0 {
+ cfg.URL = override.URL
}
- if _, ok := provider.Placeholders["ALERT_TRIGGERED_OR_RESOLVED"]; ok {
- if val, ok := provider.Placeholders["ALERT_TRIGGERED_OR_RESOLVED"][status]; ok {
- return val
- }
+ if len(override.Method) > 0 {
+ cfg.Method = override.Method
+ }
+ if len(override.Body) > 0 {
+ cfg.Body = override.Body
+ }
+ if len(override.Headers) > 0 {
+ cfg.Headers = override.Headers
+ }
+ if len(override.Placeholders) > 0 {
+ cfg.Placeholders = override.Placeholders
}
- return status
}
-func (provider *AlertProvider) buildHTTPRequest(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) *http.Request {
- body, url, method := provider.Body, provider.URL, provider.Method
- body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
- url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
- body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", ep.Name)
- url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", ep.Name)
- body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", ep.Group)
- url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", ep.Group)
- body = strings.ReplaceAll(body, "[ENDPOINT_URL]", ep.URL)
- url = strings.ReplaceAll(url, "[ENDPOINT_URL]", ep.URL)
- body = strings.ReplaceAll(body, "[RESULT_ERRORS]", strings.Join(result.Errors, ","))
- url = strings.ReplaceAll(url, "[RESULT_ERRORS]", strings.Join(result.Errors, ","))
- if resolved {
- body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
- url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(true))
- } else {
- body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false))
- url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(false))
- }
- if len(method) == 0 {
- method = http.MethodGet
- }
- bodyBuffer := bytes.NewBuffer([]byte(body))
- request, _ := http.NewRequest(method, url, bodyBuffer)
- for k, v := range provider.Headers {
- request.Header.Set(k, v)
- }
- return request
+// AlertProvider is the configuration necessary for sending an alert using a custom HTTP request
+// Technically, all alert providers should be reachable using the custom alert provider
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+
+ // Overrides is a list of Override that may be prioritized over the default configuration
+ Overrides []Override `yaml:"overrides,omitempty"`
+}
+
+// Override is a case under which the default integration is overridden
+type Override struct {
+ Group string `yaml:"group"`
+ Config `yaml:",inline"`
+}
+
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ return provider.DefaultConfig.Validate()
}
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- request := provider.buildHTTPRequest(ep, alert, result, resolved)
- response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ request := provider.buildHTTPRequest(cfg, ep, alert, result, resolved)
+ response, err := client.GetHTTPClient(cfg.ClientConfig).Do(request)
if err != nil {
return err
}
@@ -98,7 +98,78 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
return err
}
+func (provider *AlertProvider) buildHTTPRequest(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) *http.Request {
+ body, url, method := cfg.Body, cfg.URL, cfg.Method
+ body = strings.ReplaceAll(body, "[ALERT_DESCRIPTION]", alert.GetDescription())
+ url = strings.ReplaceAll(url, "[ALERT_DESCRIPTION]", alert.GetDescription())
+ body = strings.ReplaceAll(body, "[ENDPOINT_NAME]", ep.Name)
+ url = strings.ReplaceAll(url, "[ENDPOINT_NAME]", ep.Name)
+ body = strings.ReplaceAll(body, "[ENDPOINT_GROUP]", ep.Group)
+ url = strings.ReplaceAll(url, "[ENDPOINT_GROUP]", ep.Group)
+ body = strings.ReplaceAll(body, "[ENDPOINT_URL]", ep.URL)
+ url = strings.ReplaceAll(url, "[ENDPOINT_URL]", ep.URL)
+ body = strings.ReplaceAll(body, "[RESULT_ERRORS]", strings.Join(result.Errors, ","))
+ url = strings.ReplaceAll(url, "[RESULT_ERRORS]", strings.Join(result.Errors, ","))
+ if resolved {
+ body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(cfg, true))
+ url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(cfg, true))
+ } else {
+ body = strings.ReplaceAll(body, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(cfg, false))
+ url = strings.ReplaceAll(url, "[ALERT_TRIGGERED_OR_RESOLVED]", provider.GetAlertStatePlaceholderValue(cfg, false))
+ }
+ if len(method) == 0 {
+ method = http.MethodGet
+ }
+ bodyBuffer := bytes.NewBuffer([]byte(body))
+ request, _ := http.NewRequest(method, url, bodyBuffer)
+ for k, v := range cfg.Headers {
+ request.Header.Set(k, v)
+ }
+ return request
+}
+
+// GetAlertStatePlaceholderValue returns the Placeholder value for ALERT_TRIGGERED_OR_RESOLVED if configured
+func (provider *AlertProvider) GetAlertStatePlaceholderValue(cfg *Config, resolved bool) string {
+ status := "TRIGGERED"
+ if resolved {
+ status = "RESOLVED"
+ }
+ if _, ok := cfg.Placeholders["ALERT_TRIGGERED_OR_RESOLVED"]; ok {
+ if val, ok := cfg.Placeholders["ALERT_TRIGGERED_OR_RESOLVED"][status]; ok {
+ return val
+ }
+ }
+ return status
+}
+
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/custom/custom_test.go b/alerting/provider/custom/custom_test.go
index 17171731..8dc97288 100644
--- a/alerting/provider/custom/custom_test.go
+++ b/alerting/provider/custom/custom_test.go
@@ -15,7 +15,7 @@ import (
func TestAlertProvider_IsValid(t *testing.T) {
t.Run("invalid-provider", func(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{URL: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
})
@@ -24,7 +24,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
if validProvider.ClientConfig != nil {
t.Error("provider client config should have been nil prior to IsValid() being executed")
}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
if validProvider.ClientConfig == nil {
diff --git a/alerting/provider/discord/discord.go b/alerting/provider/discord/discord.go
index 80009b9d..930dda4b 100644
--- a/alerting/provider/discord/discord.go
+++ b/alerting/provider/discord/discord.go
@@ -3,6 +3,7 @@ package discord
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,11 +11,38 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
+var (
+ ErrWebhookURLNotSet = errors.New("webhook-url not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
+
+type Config struct {
+ WebhookURL string `yaml:"webhook-url"`
+ Title string `yaml:"title,omitempty"` // Title of the message that will be sent
+}
+
+func (cfg *Config) Validate() error {
+ if len(cfg.WebhookURL) > 0 {
+ return ErrWebhookURLNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.WebhookURL) > 0 {
+ cfg.WebhookURL = override.WebhookURL
+ }
+ if len(override.Title) > 0 {
+ cfg.Title = override.Title
+ }
+}
+
// AlertProvider is the configuration necessary for sending an alert using Discord
type AlertProvider struct {
- Config `yaml:",inline"`
+ DefaultConfig Config `yaml:",inline"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -23,35 +51,33 @@ type AlertProvider struct {
Overrides []Override `yaml:"overrides,omitempty"`
}
-type Config struct {
- WebhookURL string `yaml:"webhook-url"`
- Title string `yaml:"title,omitempty"` // Title of the message that will be sent
-}
-
-// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
- return false
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.WebhookURL) > 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
- request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
+ request, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, buffer)
if err != nil {
return err
}
@@ -87,7 +113,7 @@ type Field struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string
var colorCode int
if resolved {
@@ -112,8 +138,8 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
description = ":\n> " + alertDescription
}
title := ":helmet_with_white_cross: Gatus"
- if provider.Title != "" {
- title = provider.Title
+ if cfg.Title != "" {
+ title = cfg.Title
}
body := Body{
Content: "",
@@ -136,19 +162,34 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
return bodyAsJSON
}
-// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
-func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.WebhookURL
- }
- }
- }
- return provider.WebhookURL
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/discord/discord_test.go b/alerting/provider/discord/discord_test.go
index 66863e7a..0c3c52a3 100644
--- a/alerting/provider/discord/discord_test.go
+++ b/alerting/provider/discord/discord_test.go
@@ -12,12 +12,12 @@ import (
)
func TestAlertProvider_IsValid(t *testing.T) {
- invalidProvider := AlertProvider{Config: Config{WebhookURL: ""}}
- if invalidProvider.IsValid() {
+ invalidProvider := AlertProvider{DefaultConfig: Config{WebhookURL: ""}}
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
- validProvider := AlertProvider{Config: Config{WebhookURL: "http://example.com"}}
- if !validProvider.IsValid() {
+ validProvider := AlertProvider{DefaultConfig: Config{WebhookURL: "http://example.com"}}
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -31,7 +31,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
@@ -42,11 +42,11 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideTo.IsValid() {
+ if providerWithInvalidOverrideTo.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
- Config: Config{
+ DefaultConfig: Config{
WebhookURL: "http://example.com",
},
Overrides: []Override{
@@ -56,7 +56,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
@@ -116,7 +116,7 @@ func TestAlertProvider_Send(t *testing.T) {
},
{
Name: "triggered-with-modified-title",
- Provider: AlertProvider{Config: Config{Title: title}},
+ Provider: AlertProvider{DefaultConfig: Config{Title: title}},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
MockRoundTripper: test.MockRoundTripper(func(r *http.Request) *http.Response {
@@ -177,7 +177,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
},
{
Name: "triggered-with-modified-title",
- Provider: AlertProvider{Config: Config{Title: title}},
+ Provider: AlertProvider{DefaultConfig: Config{Title: title}},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\"provider-title\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332,\"fields\":[{\"name\":\"Condition results\",\"value\":\":x: - `[CONNECTED] == true`\\n:x: - `[STATUS] == 200`\\n:x: - `[BODY] != \\\"\\\"`\\n\",\"inline\":false}]}]}",
@@ -185,7 +185,7 @@ func TestAlertProvider_buildRequestBody(t *testing.T) {
{
Name: "triggered-with-no-conditions",
NoConditions: true,
- Provider: AlertProvider{Config: Config{Title: title}},
+ Provider: AlertProvider{DefaultConfig: Config{Title: title}},
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
Resolved: false,
ExpectedBody: "{\"content\":\"\",\"embeds\":[{\"title\":\"provider-title\",\"description\":\"An alert for **endpoint-name** has been triggered due to having failed 3 time(s) in a row:\\n\\u003e description-1\",\"color\":15158332}]}",
@@ -239,8 +239,8 @@ func TestAlertProvider_getWebhookURLForGroup(t *testing.T) {
{
Name: "provider-no-override-specify-no-group-should-default",
Provider: AlertProvider{
- Config: Config{WebhookURL: "http://example.com"},
- Overrides: nil,
+ DefaultConfig: Config{WebhookURL: "http://example.com"},
+ Overrides: nil,
},
InputGroup: "",
ExpectedOutput: "http://example.com",
@@ -248,8 +248,8 @@ func TestAlertProvider_getWebhookURLForGroup(t *testing.T) {
{
Name: "provider-no-override-specify-group-should-default",
Provider: AlertProvider{
- Config: Config{WebhookURL: "http://example.com"},
- Overrides: nil,
+ DefaultConfig: Config{WebhookURL: "http://example.com"},
+ Overrides: nil,
},
InputGroup: "group",
ExpectedOutput: "http://example.com",
@@ -257,7 +257,7 @@ func TestAlertProvider_getWebhookURLForGroup(t *testing.T) {
{
Name: "provider-with-override-specify-no-group-should-default",
Provider: AlertProvider{
- Config: Config{WebhookURL: "http://example.com"},
+ DefaultConfig: Config{WebhookURL: "http://example.com"},
Overrides: []Override{
{
Group: "group",
@@ -271,7 +271,7 @@ func TestAlertProvider_getWebhookURLForGroup(t *testing.T) {
{
Name: "provider-with-override-specify-group-should-override",
Provider: AlertProvider{
- Config: Config{WebhookURL: "http://example.com"},
+ DefaultConfig: Config{WebhookURL: "http://example.com"},
Overrides: []Override{
{
Group: "group",
diff --git a/alerting/provider/email/email.go b/alerting/provider/email/email.go
index d4879980..67af5a06 100644
--- a/alerting/provider/email/email.go
+++ b/alerting/provider/email/email.go
@@ -2,6 +2,7 @@ package email
import (
"crypto/tls"
+ "errors"
"fmt"
"math"
"strings"
@@ -10,11 +11,62 @@ import (
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
gomail "gopkg.in/mail.v2"
+ "gopkg.in/yaml.v3"
)
+var (
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+ ErrMissingFromOrToFields = errors.New("from and to fields are required")
+ ErrInvalidPort = errors.New("port must be between 1 and 65535 inclusively")
+ ErrMissingHost = errors.New("host is required")
+)
+
+type Config struct {
+ From string `yaml:"from"`
+ Username string `yaml:"username"`
+ Password string `yaml:"password"`
+ Host string `yaml:"host"`
+ Port int `yaml:"port"`
+ To string `yaml:"to"`
+}
+
+func (cfg *Config) Validate() error {
+ if len(cfg.From) == 0 || len(cfg.To) == 0 {
+ return ErrMissingFromOrToFields
+ }
+ if cfg.Port < 1 || cfg.Port > math.MaxUint16 {
+ return ErrInvalidPort
+ }
+ if len(cfg.Host) == 0 {
+ return ErrMissingHost
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.From) > 0 {
+ cfg.From = override.From
+ }
+ if len(override.Username) > 0 {
+ cfg.Username = override.Username
+ }
+ if len(override.Password) > 0 {
+ cfg.Password = override.Password
+ }
+ if len(override.Host) > 0 {
+ cfg.Host = override.Host
+ }
+ if override.Port > 0 {
+ cfg.Port = override.Port
+ }
+ if len(override.To) > 0 {
+ cfg.To = override.To
+ }
+}
+
// AlertProvider is the configuration necessary for sending an alert using SMTP
type AlertProvider struct {
- Config `yaml:",inline"`
+ DefaultConfig Config `yaml:",inline"`
// ClientConfig is the configuration of the client used to communicate with the provider's target
ClientConfig *client.Config `yaml:"client,omitempty"`
@@ -26,63 +78,58 @@ type AlertProvider struct {
Overrides []Override `yaml:"overrides,omitempty"`
}
-type Config struct {
- From string `yaml:"from"`
- Username string `yaml:"username"`
- Password string `yaml:"password"`
- Host string `yaml:"host"`
- Port int `yaml:"port"`
- To string `yaml:"to"`
-}
-
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.To) == 0 {
- return false
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.From) > 0 && len(provider.Host) > 0 && len(provider.To) > 0 && provider.Port > 0 && provider.Port < math.MaxUint16
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
var username string
- if len(provider.Username) > 0 {
- username = provider.Username
+ if len(cfg.Username) > 0 {
+ username = cfg.Username
} else {
- username = provider.From
+ username = cfg.From
}
subject, body := provider.buildMessageSubjectAndBody(ep, alert, result, resolved)
m := gomail.NewMessage()
- m.SetHeader("From", provider.From)
- m.SetHeader("To", strings.Split(provider.getToForGroup(ep.Group), ",")...)
+ m.SetHeader("From", cfg.From)
+ m.SetHeader("To", strings.Split(cfg.To, ",")...)
m.SetHeader("Subject", subject)
m.SetBody("text/plain", body)
var d *gomail.Dialer
- if len(provider.Password) == 0 {
+ if len(cfg.Password) == 0 {
// Get the domain in the From address
localName := "localhost"
- fromParts := strings.Split(provider.From, `@`)
+ fromParts := strings.Split(cfg.From, `@`)
if len(fromParts) == 2 {
localName = fromParts[1]
}
// Create a dialer with no authentication
- d = &gomail.Dialer{Host: provider.Host, Port: provider.Port, LocalName: localName}
+ d = &gomail.Dialer{Host: cfg.Host, Port: cfg.Port, LocalName: localName}
} else {
// Create an authenticated dialer
- d = gomail.NewDialer(provider.Host, provider.Port, username, provider.Password)
+ d = gomail.NewDialer(cfg.Host, cfg.Port, username, cfg.Password)
}
if provider.ClientConfig != nil && provider.ClientConfig.Insecure {
d.TLSConfig = &tls.Config{InsecureSkipVerify: true}
@@ -120,19 +167,34 @@ func (provider *AlertProvider) buildMessageSubjectAndBody(ep *endpoint.Endpoint,
return subject, message + description + formattedConditionResults
}
-// getToForGroup returns the appropriate email integration to for a given group
-func (provider *AlertProvider) getToForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.To
- }
- }
- }
- return provider.To
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/email/email_test.go b/alerting/provider/email/email_test.go
index ba00e4d2..1ae9d6a5 100644
--- a/alerting/provider/email/email_test.go
+++ b/alerting/provider/email/email_test.go
@@ -9,18 +9,18 @@ import (
func TestAlertDefaultProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{From: "from@example.com", Password: "password", Host: "smtp.gmail.com", Port: 587, To: "to@example.com"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
func TestAlertProvider_IsValidWithNoCredentials(t *testing.T) {
validProvider := AlertProvider{Config: Config{From: "from@example.com", Host: "smtp-relay.gmail.com", Port: 587, To: "to@example.com"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -34,7 +34,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
@@ -45,7 +45,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideTo.IsValid() {
+ if providerWithInvalidOverrideTo.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
@@ -63,7 +63,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/gitea/gitea.go b/alerting/provider/gitea/gitea.go
index 3adea3a4..cb222cb1 100644
--- a/alerting/provider/gitea/gitea.go
+++ b/alerting/provider/gitea/gitea.go
@@ -2,6 +2,7 @@ package gitea
import (
"crypto/tls"
+ "errors"
"fmt"
"net/http"
"net/url"
@@ -11,18 +12,14 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
-// AlertProvider is the configuration necessary for sending an alert using Discord
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-
- // ClientConfig is the configuration of the client used to communicate with the provider's target
- ClientConfig *client.Config `yaml:"client,omitempty"`
-}
+var (
+ ErrRepositoryURLNotSet = errors.New("repository-url not set")
+ ErrInvalidRepositoryURL = errors.New("invalid repository-url")
+ ErrTokenNotSet = errors.New("token not set")
+)
type Config struct {
RepositoryURL string `yaml:"repository-url"` // The URL of the Gitea repository to create issues in
@@ -33,32 +30,37 @@ type Config struct {
repositoryOwner string
repositoryName string
giteaClient *gitea.Client
+
+ // ClientConfig is the configuration of the client used to communicate with the provider's target
+ ClientConfig *client.Config `yaml:"client,omitempty"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if provider.ClientConfig == nil {
- provider.ClientConfig = client.GetDefaultConfig()
+func (cfg *Config) Validate() error {
+ if cfg.ClientConfig == nil {
+ cfg.ClientConfig = client.GetDefaultConfig()
}
- if len(provider.Token) == 0 || len(provider.RepositoryURL) == 0 {
- return false
+ if len(cfg.RepositoryURL) == 0 {
+ return ErrRepositoryURLNotSet
+ }
+ if len(cfg.Token) == 0 {
+ return ErrTokenNotSet
}
// Validate format of the repository URL
- repositoryURL, err := url.Parse(provider.RepositoryURL)
+ repositoryURL, err := url.Parse(cfg.RepositoryURL)
if err != nil {
- return false
+ return err
}
baseURL := repositoryURL.Scheme + "://" + repositoryURL.Host
pathParts := strings.Split(repositoryURL.Path, "/")
if len(pathParts) != 3 {
- return false
+ return ErrInvalidRepositoryURL
}
- provider.repositoryOwner = pathParts[1]
- provider.repositoryName = pathParts[2]
+ cfg.repositoryOwner = pathParts[1]
+ cfg.repositoryName = pathParts[2]
opts := []gitea.ClientOption{
- gitea.SetToken(provider.Token),
+ gitea.SetToken(cfg.Token),
}
- if provider.ClientConfig != nil && provider.ClientConfig.Insecure {
+ if cfg.ClientConfig != nil && cfg.ClientConfig.Insecure {
// add new http client for skip verify
httpClient := &http.Client{
Transport: &http.Transport{
@@ -67,30 +69,51 @@ func (provider *AlertProvider) IsValid() bool {
}
opts = append(opts, gitea.SetHTTPClient(httpClient))
}
- provider.giteaClient, err = gitea.NewClient(baseURL, opts...)
+ cfg.giteaClient, err = gitea.NewClient(baseURL, opts...)
if err != nil {
- return false
+ return err
}
- user, _, err := provider.giteaClient.GetMyUserInfo()
+ user, _, err := cfg.giteaClient.GetMyUserInfo()
if err != nil {
- return false
+ return err
}
- provider.username = user.UserName
- return true
+ cfg.username = user.UserName
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Discord
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+}
+
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ return provider.DefaultConfig.Validate()
}
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
// or closes the relevant issue(s) if the resolved parameter passed is true.
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
+ cfg, err := provider.GetConfig(alert)
+ if err != nil {
+ return err
+ }
title := "alert(gatus): " + ep.DisplayName()
if !resolved {
- _, _, err := provider.giteaClient.CreateIssue(
- provider.repositoryOwner,
- provider.repositoryName,
+ _, _, err := cfg.giteaClient.CreateIssue(
+ cfg.repositoryOwner,
+ cfg.repositoryName,
gitea.CreateIssueOption{
Title: title,
Body: provider.buildIssueBody(ep, alert, result),
- Assignees: provider.Assignees,
+ Assignees: cfg.Assignees,
},
)
if err != nil {
@@ -98,13 +121,12 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
}
return nil
}
-
- issues, _, err := provider.giteaClient.ListRepoIssues(
- provider.repositoryOwner,
- provider.repositoryName,
+ issues, _, err := cfg.giteaClient.ListRepoIssues(
+ cfg.repositoryOwner,
+ cfg.repositoryName,
gitea.ListIssueOption{
State: gitea.StateOpen,
- CreatedBy: provider.username,
+ CreatedBy: cfg.username,
ListOptions: gitea.ListOptions{
Page: 100,
},
@@ -113,13 +135,12 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
if err != nil {
return fmt.Errorf("failed to list issues: %w", err)
}
-
for _, issue := range issues {
if issue.Title == title {
stateClosed := gitea.StateClosed
- _, _, err = provider.giteaClient.EditIssue(
- provider.repositoryOwner,
- provider.repositoryName,
+ _, _, err = cfg.giteaClient.EditIssue(
+ cfg.repositoryOwner,
+ cfg.repositoryName,
issue.ID,
gitea.EditIssueOption{
State: &stateClosed,
@@ -160,3 +181,21 @@ func (provider *AlertProvider) buildIssueBody(ep *endpoint.Endpoint, alert *aler
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/gitea/gitea_test.go b/alerting/provider/gitea/gitea_test.go
index 97ba90b3..37eab1c5 100644
--- a/alerting/provider/gitea/gitea_test.go
+++ b/alerting/provider/gitea/gitea_test.go
@@ -46,8 +46,8 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
- if scenario.Provider.IsValid() != scenario.Expected {
- t.Errorf("expected %t, got %t", scenario.Expected, scenario.Provider.IsValid())
+ if scenario.Provider.Validate() != scenario.Expected {
+ t.Errorf("expected %t, got %t", scenario.Expected, scenario.Provider.Validate())
}
})
}
diff --git a/alerting/provider/github/github.go b/alerting/provider/github/github.go
index 06ab4986..193512a2 100644
--- a/alerting/provider/github/github.go
+++ b/alerting/provider/github/github.go
@@ -2,6 +2,7 @@ package github
import (
"context"
+ "errors"
"fmt"
"net/url"
"strings"
@@ -11,15 +12,14 @@ import (
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/google/go-github/v48/github"
"golang.org/x/oauth2"
+ "gopkg.in/yaml.v3"
)
-// AlertProvider is the configuration necessary for sending an alert using Discord
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-}
+var (
+ ErrRepositoryURLNotSet = errors.New("repository-url not set")
+ ErrInvalidRepositoryURL = errors.New("invalid repository-url")
+ ErrTokenNotSet = errors.New("token not set")
+)
type Config struct {
RepositoryURL string `yaml:"repository-url"` // The URL of the GitHub repository to create issues in
@@ -31,53 +31,81 @@ type Config struct {
githubClient *github.Client
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if len(provider.Token) == 0 || len(provider.RepositoryURL) == 0 {
- return false
+func (cfg *Config) Validate() error {
+ if len(cfg.RepositoryURL) == 0 {
+ return ErrRepositoryURLNotSet
+ }
+ if len(cfg.Token) == 0 {
+ return ErrTokenNotSet
}
// Validate format of the repository URL
- repositoryURL, err := url.Parse(provider.RepositoryURL)
+ repositoryURL, err := url.Parse(cfg.RepositoryURL)
if err != nil {
- return false
+ return err
}
baseURL := repositoryURL.Scheme + "://" + repositoryURL.Host
pathParts := strings.Split(repositoryURL.Path, "/")
if len(pathParts) != 3 {
- return false
+ return ErrInvalidRepositoryURL
}
- provider.repositoryOwner = pathParts[1]
- provider.repositoryName = pathParts[2]
+ cfg.repositoryOwner = pathParts[1]
+ cfg.repositoryName = pathParts[2]
// Create oauth2 HTTP client with GitHub token
httpClientWithStaticTokenSource := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(&oauth2.Token{
- AccessToken: provider.Token,
+ AccessToken: cfg.Token,
}))
// Create GitHub client
if baseURL == "https://github.com" {
- provider.githubClient = github.NewClient(httpClientWithStaticTokenSource)
+ cfg.githubClient = github.NewClient(httpClientWithStaticTokenSource)
} else {
- provider.githubClient, err = github.NewEnterpriseClient(baseURL, baseURL, httpClientWithStaticTokenSource)
+ cfg.githubClient, err = github.NewEnterpriseClient(baseURL, baseURL, httpClientWithStaticTokenSource)
if err != nil {
- return false
+ return fmt.Errorf("failed to create enterprise GitHub client: %w", err)
}
}
// Retrieve the username once to validate that the token is valid
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
- user, _, err := provider.githubClient.Users.Get(ctx, "")
+ user, _, err := cfg.githubClient.Users.Get(ctx, "")
if err != nil {
- return false
+ return fmt.Errorf("failed to retrieve GitHub user: %w", err)
}
- provider.username = *user.Login
- return true
+ cfg.username = *user.Login
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.RepositoryURL) > 0 {
+ cfg.RepositoryURL = override.RepositoryURL
+ }
+ if len(override.Token) > 0 {
+ cfg.Token = override.Token
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Discord
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+}
+
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ return provider.DefaultConfig.Validate()
}
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
// or closes the relevant issue(s) if the resolved parameter passed is true.
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
+ cfg, err := provider.GetConfig(alert)
+ if err != nil {
+ return err
+ }
title := "alert(gatus): " + ep.DisplayName()
if !resolved {
- _, _, err := provider.githubClient.Issues.Create(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueRequest{
+ _, _, err := cfg.githubClient.Issues.Create(context.Background(), cfg.repositoryOwner, cfg.repositoryName, &github.IssueRequest{
Title: github.String(title),
Body: github.String(provider.buildIssueBody(ep, alert, result)),
})
@@ -85,9 +113,9 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
return fmt.Errorf("failed to create issue: %w", err)
}
} else {
- issues, _, err := provider.githubClient.Issues.ListByRepo(context.Background(), provider.repositoryOwner, provider.repositoryName, &github.IssueListByRepoOptions{
+ issues, _, err := cfg.githubClient.Issues.ListByRepo(context.Background(), cfg.repositoryOwner, cfg.repositoryName, &github.IssueListByRepoOptions{
State: "open",
- Creator: provider.username,
+ Creator: cfg.username,
ListOptions: github.ListOptions{PerPage: 100},
})
if err != nil {
@@ -95,7 +123,7 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
}
for _, issue := range issues {
if *issue.Title == title {
- _, _, err = provider.githubClient.Issues.Edit(context.Background(), provider.repositoryOwner, provider.repositoryName, *issue.Number, &github.IssueRequest{
+ _, _, err = cfg.githubClient.Issues.Edit(context.Background(), cfg.repositoryOwner, cfg.repositoryName, *issue.Number, &github.IssueRequest{
State: github.String("closed"),
})
if err != nil {
@@ -134,3 +162,21 @@ func (provider *AlertProvider) buildIssueBody(ep *endpoint.Endpoint, alert *aler
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/github/github_test.go b/alerting/provider/github/github_test.go
index 3e4f2a94..7919c537 100644
--- a/alerting/provider/github/github_test.go
+++ b/alerting/provider/github/github_test.go
@@ -46,8 +46,8 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
- if scenario.Provider.IsValid() != scenario.Expected {
- t.Errorf("expected %t, got %t", scenario.Expected, scenario.Provider.IsValid())
+ if scenario.Provider.Validate() != scenario.Expected {
+ t.Errorf("expected %t, got %t", scenario.Expected, scenario.Provider.Validate())
}
})
}
diff --git a/alerting/provider/gitlab/gitlab.go b/alerting/provider/gitlab/gitlab.go
index 896aa67a..47954e17 100644
--- a/alerting/provider/gitlab/gitlab.go
+++ b/alerting/provider/gitlab/gitlab.go
@@ -6,22 +6,19 @@ import (
"fmt"
"io"
"net/http"
- "net/url"
"time"
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/google/uuid"
+ "gopkg.in/yaml.v3"
)
-// AlertProvider is the configuration necessary for sending an alert using GitLab
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-}
+var (
+ ErrWebhookURLNotSet = fmt.Errorf("webhook-url not set")
+ ErrAuthorizationKeyNotSet = fmt.Errorf("authorization-key not set")
+)
type Config struct {
WebhookURL string `yaml:"webhook-url"` // The webhook url provided by GitLab
@@ -32,32 +29,73 @@ type Config struct {
Service string `yaml:"service,omitempty"` // Service affected. Defaults to the endpoint's display name
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if len(provider.AuthorizationKey) == 0 || len(provider.WebhookURL) == 0 {
- return false
+func (cfg *Config) Validate() error {
+ if len(cfg.WebhookURL) == 0 {
+ return ErrWebhookURLNotSet
}
- // Validate format of the repository URL
- _, err := url.Parse(provider.WebhookURL)
- if err != nil {
- return false
+ if len(cfg.AuthorizationKey) == 0 {
+ return ErrAuthorizationKeyNotSet
}
- return true
+ if len(cfg.Severity) == 0 {
+ cfg.Severity = "critical"
+ }
+ if len(cfg.MonitoringTool) == 0 {
+ cfg.MonitoringTool = "gatus"
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.WebhookURL) > 0 {
+ cfg.WebhookURL = override.WebhookURL
+ }
+ if len(override.AuthorizationKey) > 0 {
+ cfg.AuthorizationKey = override.AuthorizationKey
+ }
+ if len(override.Severity) > 0 {
+ cfg.Severity = override.Severity
+ }
+ if len(override.MonitoringTool) > 0 {
+ cfg.MonitoringTool = override.MonitoringTool
+ }
+ if len(override.EnvironmentName) > 0 {
+ cfg.EnvironmentName = override.EnvironmentName
+ }
+ if len(override.Service) > 0 {
+ cfg.Service = override.Service
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using GitLab
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+}
+
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ return provider.DefaultConfig.Validate()
}
// Send creates an issue in the designed RepositoryURL if the resolved parameter passed is false,
// or closes the relevant issue(s) if the resolved parameter passed is true.
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
+ cfg, err := provider.GetConfig(alert)
+ if err != nil {
+ return err
+ }
if len(alert.ResolveKey) == 0 {
alert.ResolveKey = uuid.NewString()
}
- buffer := bytes.NewBuffer(provider.buildAlertBody(ep, alert, result, resolved))
- request, err := http.NewRequest(http.MethodPost, provider.WebhookURL, buffer)
+ buffer := bytes.NewBuffer(provider.buildAlertBody(cfg, ep, alert, result, resolved))
+ request, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
- request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", provider.AuthorizationKey))
+ request.Header.Set("Authorization", fmt.Sprintf("Bearer %s", cfg.AuthorizationKey))
response, err := client.GetHTTPClient(nil).Do(request)
if err != nil {
return err
@@ -83,30 +121,20 @@ type AlertBody struct {
GitlabEnvironmentName string `json:"gitlab_environment_name,omitempty"` // The name of the associated GitLab environment. Required to display alerts on a dashboard.
}
-func (provider *AlertProvider) monitoringTool() string {
- if len(provider.MonitoringTool) > 0 {
- return provider.MonitoringTool
- }
- return "gatus"
-}
-
-func (provider *AlertProvider) service(ep *endpoint.Endpoint) string {
- if len(provider.Service) > 0 {
- return provider.Service
- }
- return ep.DisplayName()
-}
-
// buildAlertBody builds the body of the alert
-func (provider *AlertProvider) buildAlertBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildAlertBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+ service := cfg.Service
+ if len(service) == 0 {
+ service = ep.DisplayName()
+ }
body := AlertBody{
- Title: fmt.Sprintf("alert(%s): %s", provider.monitoringTool(), provider.service(ep)),
+ Title: fmt.Sprintf("alert(%s): %s", cfg.MonitoringTool, service),
StartTime: result.Timestamp.Format(time.RFC3339),
- Service: provider.service(ep),
- MonitoringTool: provider.monitoringTool(),
+ Service: service,
+ MonitoringTool: cfg.MonitoringTool,
Hosts: ep.URL,
- GitlabEnvironmentName: provider.EnvironmentName,
- Severity: provider.Severity,
+ GitlabEnvironmentName: cfg.EnvironmentName,
+ Severity: cfg.Severity,
Fingerprint: alert.ResolveKey,
}
if resolved {
@@ -144,3 +172,21 @@ func (provider *AlertProvider) buildAlertBody(ep *endpoint.Endpoint, alert *aler
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/gitlab/gitlab_test.go b/alerting/provider/gitlab/gitlab_test.go
index 97154ee9..b3dbb730 100644
--- a/alerting/provider/gitlab/gitlab_test.go
+++ b/alerting/provider/gitlab/gitlab_test.go
@@ -40,8 +40,8 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
}
for _, scenario := range scenarios {
t.Run(scenario.Name, func(t *testing.T) {
- if scenario.Provider.IsValid() != scenario.Expected {
- t.Errorf("expected %t, got %t", scenario.Expected, scenario.Provider.IsValid())
+ if scenario.Provider.Validate() != scenario.Expected {
+ t.Errorf("expected %t, got %t", scenario.Expected, scenario.Provider.Validate())
}
})
}
diff --git a/alerting/provider/googlechat/googlechat.go b/alerting/provider/googlechat/googlechat.go
index bbfb987b..192696f6 100644
--- a/alerting/provider/googlechat/googlechat.go
+++ b/alerting/provider/googlechat/googlechat.go
@@ -3,6 +3,7 @@ package googlechat
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,11 +11,38 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
+var (
+ ErrWebhookURLNotSet = errors.New("webhook URL not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
+
+type Config struct {
+ WebhookURL string `yaml:"webhook-url"`
+ ClientConfig *client.Config `yaml:"client,omitempty"`
+}
+
+func (cfg *Config) Validate() error {
+ if cfg.ClientConfig == nil {
+ cfg.ClientConfig = client.GetDefaultConfig()
+ }
+ if len(cfg.WebhookURL) == 0 {
+ return ErrWebhookURLNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.WebhookURL) > 0 {
+ cfg.WebhookURL = override.WebhookURL
+ }
+}
+
// AlertProvider is the configuration necessary for sending an alert using Google chat
type AlertProvider struct {
- Config `yaml:",inline"`
+ DefaultConfig Config `yaml:",inline"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -23,43 +51,39 @@ type AlertProvider struct {
Overrides []Override `yaml:"overrides,omitempty"`
}
-type Config struct {
- WebhookURL string `yaml:"webhook-url"`
- ClientConfig *client.Config `yaml:"client,omitempty"` // Configuration of the client used to communicate with the provider's target
-}
-
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if provider.ClientConfig == nil {
- provider.ClientConfig = client.GetDefaultConfig()
- }
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
- return false
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.WebhookURL) > 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
- request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
+ request, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
- response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
+ response, err := client.GetHTTPClient(cfg.ClientConfig).Do(request)
if err != nil {
return err
}
@@ -187,19 +211,34 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
return bodyAsJSON
}
-// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
-func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.WebhookURL
- }
- }
- }
- return provider.WebhookURL
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/googlechat/googlechat_test.go b/alerting/provider/googlechat/googlechat_test.go
index a05a81c3..19561c19 100644
--- a/alerting/provider/googlechat/googlechat_test.go
+++ b/alerting/provider/googlechat/googlechat_test.go
@@ -13,11 +13,11 @@ import (
func TestAlertDefaultProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{WebhookURL: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{WebhookURL: "http://example.com"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -31,7 +31,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
@@ -42,7 +42,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideTo.IsValid() {
+ if providerWithInvalidOverrideTo.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
@@ -54,7 +54,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/gotify/gotify.go b/alerting/provider/gotify/gotify.go
index 70a3ef70..46aab4d5 100644
--- a/alerting/provider/gotify/gotify.go
+++ b/alerting/provider/gotify/gotify.go
@@ -3,6 +3,7 @@ package gotify
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,37 +11,72 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
const DefaultPriority = 5
+var (
+ ErrServerURLNotSet = errors.New("server URL not set")
+ ErrTokenNotSet = errors.New("token not set")
+)
+
+type Config struct {
+ ServerURL string `yaml:"server-url"` // URL of the Gotify server
+ Token string `yaml:"token"` // Token to use when sending a message to the Gotify server
+ Priority int `yaml:"priority,omitempty"` // Priority of the message. Defaults to DefaultPriority.
+ Title string `yaml:"title,omitempty"` // Title of the message that will be sent
+}
+
+func (cfg *Config) Validate() error {
+ if cfg.Priority == 0 {
+ cfg.Priority = DefaultPriority
+ }
+ if len(cfg.ServerURL) == 0 {
+ return ErrServerURLNotSet
+ }
+ if len(cfg.Token) == 0 {
+ return ErrTokenNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.ServerURL) > 0 {
+ cfg.ServerURL = override.ServerURL
+ }
+ if len(override.Token) > 0 {
+ cfg.Token = override.Token
+ }
+ if override.Priority != 0 {
+ cfg.Priority = override.Priority
+ }
+ if len(override.Title) > 0 {
+ cfg.Title = override.Title
+ }
+}
+
// AlertProvider is the configuration necessary for sending an alert using Gotify
type AlertProvider struct {
- Config `yaml:",inline"`
+ DefaultConfig Config `yaml:",inline"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
}
-type Config struct {
- ServerURL string `yaml:"server-url"` // ServerURL is the URL of the Gotify server
- Token string `yaml:"token"` // Token is the token to use when sending a message to the Gotify server
- Priority int `yaml:"priority,omitempty"` // Priority is the priority of the message. Defaults to DefaultPriority.
- Title string `yaml:"title,omitempty"` // Title of the message that will be sent
-}
-
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if provider.Priority == 0 {
- provider.Priority = DefaultPriority
- }
- return len(provider.ServerURL) > 0 && len(provider.Token) > 0
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
- request, err := http.NewRequest(http.MethodPost, provider.ServerURL+"/message?token="+provider.Token, buffer)
+ cfg, err := provider.GetConfig(alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
+ request, err := http.NewRequest(http.MethodPost, cfg.ServerURL+"/message?token="+cfg.Token, buffer)
if err != nil {
return err
}
@@ -64,7 +100,7 @@ type Body struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string
if resolved {
message = fmt.Sprintf("An alert for `%s` has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
@@ -86,13 +122,13 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
}
message += formattedConditionResults
title := "Gatus: " + ep.DisplayName()
- if provider.Title != "" {
- title = provider.Title
+ if cfg.Title != "" {
+ title = cfg.Title
}
bodyAsJSON, _ := json.Marshal(Body{
Message: message,
Title: title,
- Priority: provider.Priority,
+ Priority: cfg.Priority,
})
return bodyAsJSON
}
@@ -101,3 +137,21 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/gotify/gotify_test.go b/alerting/provider/gotify/gotify_test.go
index b8ffbc8f..3f2dad86 100644
--- a/alerting/provider/gotify/gotify_test.go
+++ b/alerting/provider/gotify/gotify_test.go
@@ -38,8 +38,8 @@ func TestAlertProvider_IsValid(t *testing.T) {
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
- if scenario.provider.IsValid() != scenario.expected {
- t.Errorf("expected %t, got %t", scenario.expected, scenario.provider.IsValid())
+ if scenario.provider.Validate() != scenario.expected {
+ t.Errorf("expected %t, got %t", scenario.expected, scenario.provider.Validate())
}
})
}
diff --git a/alerting/provider/jetbrainsspace/jetbrainsspace.go b/alerting/provider/jetbrainsspace/jetbrainsspace.go
index 72c2b841..d5b786fa 100644
--- a/alerting/provider/jetbrainsspace/jetbrainsspace.go
+++ b/alerting/provider/jetbrainsspace/jetbrainsspace.go
@@ -3,6 +3,7 @@ package jetbrainsspace
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,11 +11,50 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
+var (
+ ErrProjectNotSet = errors.New("project not set")
+ ErrChannelIDNotSet = errors.New("channel-id not set")
+ ErrTokenNotSet = errors.New("token not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
+
+type Config struct {
+ Project string `yaml:"project"` // Project name
+ ChannelID string `yaml:"channel-id"` // Chat Channel ID
+ Token string `yaml:"token"` // Bearer Token
+}
+
+func (cfg *Config) Validate() error {
+ if len(cfg.Project) == 0 {
+ return ErrProjectNotSet
+ }
+ if len(cfg.ChannelID) == 0 {
+ return ErrChannelIDNotSet
+ }
+ if len(cfg.Token) == 0 {
+ return ErrTokenNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.Project) > 0 {
+ cfg.Project = override.Project
+ }
+ if len(override.ChannelID) > 0 {
+ cfg.ChannelID = override.ChannelID
+ }
+ if len(override.Token) > 0 {
+ cfg.Token = override.Token
+ }
+}
+
// AlertProvider is the configuration necessary for sending an alert using JetBrains Space
type AlertProvider struct {
- Config `yaml:",inline"`
+ DefaultConfig Config `yaml:",inline"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -23,42 +63,40 @@ type AlertProvider struct {
Overrides []Override `yaml:"overrides,omitempty"`
}
-type Config struct {
- Project string `yaml:"project"` // Project name
- ChannelID string `yaml:"channel-id"` // Chat Channel ID
- Token string `yaml:"token"` // Bearer Token
-}
-
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
- if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.ChannelID) == 0 {
- return false
+ if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.Project) > 0 && len(provider.ChannelID) > 0 && len(provider.Token) > 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
- url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", provider.Project)
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
+ url := fmt.Sprintf("https://%s.jetbrains.space/api/http/chats/messages/send-message", cfg.Project)
request, err := http.NewRequest(http.MethodPost, url, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
- request.Header.Set("Authorization", "Bearer "+provider.Token)
+ request.Header.Set("Authorization", "Bearer "+cfg.Token)
response, err := client.GetHTTPClient(nil).Do(request)
if err != nil {
return err
@@ -107,9 +145,9 @@ type Icon struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
body := Body{
- Channel: "id:" + provider.getChannelIDForGroup(ep.Group),
+ Channel: "id:" + cfg.ChannelID,
Content: Content{
ClassName: "ChatMessage.Block",
Sections: []Section{{
@@ -148,19 +186,34 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
return bodyAsJSON
}
-// getChannelIDForGroup returns the appropriate channel ID to for a given group override
-func (provider *AlertProvider) getChannelIDForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.ChannelID
- }
- }
- }
- return provider.ChannelID
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/jetbrainsspace/jetbrainsspace_test.go b/alerting/provider/jetbrainsspace/jetbrainsspace_test.go
index 3775f783..3818fb60 100644
--- a/alerting/provider/jetbrainsspace/jetbrainsspace_test.go
+++ b/alerting/provider/jetbrainsspace/jetbrainsspace_test.go
@@ -13,11 +13,11 @@ import (
func TestAlertDefaultProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{Project: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{Project: "foo", ChannelID: "bar", Token: "baz"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -32,7 +32,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
@@ -44,7 +44,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideTo.IsValid() {
+ if providerWithInvalidOverrideTo.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
@@ -60,7 +60,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/matrix/matrix.go b/alerting/provider/matrix/matrix.go
index a93136d1..142591f8 100644
--- a/alerting/provider/matrix/matrix.go
+++ b/alerting/provider/matrix/matrix.go
@@ -3,6 +3,7 @@ package matrix
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"math/rand"
@@ -13,20 +14,16 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
const defaultServerURL = "https://matrix-client.matrix.org"
-// AlertProvider is the configuration necessary for sending an alert using Matrix
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-
- // Overrides is a list of Override that may be prioritized over the default configuration
- Overrides []Override `yaml:"overrides,omitempty"`
-}
+var (
+ ErrAccessTokenNotSet = errors.New("access-token not set")
+ ErrInternalRoomID = errors.New("internal-room-id not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
type Config struct {
// ServerURL is the custom homeserver to use (optional)
@@ -39,42 +36,78 @@ type Config struct {
InternalRoomID string `yaml:"internal-room-id"`
}
+func (cfg *Config) Validate() error {
+ if len(cfg.ServerURL) == 0 {
+ cfg.ServerURL = defaultServerURL
+ }
+ if len(cfg.AccessToken) == 0 {
+ return ErrAccessTokenNotSet
+ }
+ if len(cfg.InternalRoomID) == 0 {
+ return ErrInternalRoomID
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.ServerURL) > 0 {
+ cfg.ServerURL = override.ServerURL
+ }
+ if len(override.AccessToken) > 0 {
+ cfg.AccessToken = override.AccessToken
+ }
+ if len(override.InternalRoomID) > 0 {
+ cfg.InternalRoomID = override.InternalRoomID
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Matrix
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+
+ // Overrides is a list of Override that may be prioritized over the default configuration
+ Overrides []Override `yaml:"overrides,omitempty"`
+}
+
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.AccessToken) == 0 || len(override.InternalRoomID) == 0 {
- return false
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.AccessToken) > 0 && len(provider.InternalRoomID) > 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
- config := provider.getConfigForGroup(ep.Group)
- if config.ServerURL == "" {
- config.ServerURL = defaultServerURL
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
}
+ buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
// The Matrix endpoint requires a unique transaction ID for each event sent
txnId := randStringBytes(24)
request, err := http.NewRequest(
http.MethodPut,
fmt.Sprintf("%s/_matrix/client/v3/rooms/%s/send/m.room.message/%s?access_token=%s",
- config.ServerURL,
- url.PathEscape(config.InternalRoomID),
+ cfg.ServerURL,
+ url.PathEscape(cfg.InternalRoomID),
txnId,
- url.QueryEscape(config.AccessToken),
+ url.QueryEscape(cfg.AccessToken),
),
buffer,
)
@@ -166,18 +199,6 @@ func buildHTMLMessageBody(ep *endpoint.Endpoint, alert *alert.Alert, result *end
return fmt.Sprintf("
%s
%s%s", message, description, formattedConditionResults)
}
-// getConfigForGroup returns the appropriate configuration for a given group
-func (provider *AlertProvider) getConfigForGroup(group string) Config {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.Config
- }
- }
- }
- return provider.Config
-}
-
func randStringBytes(n int) string {
// All the compatible characters to use in a transaction ID
const availableCharacterBytes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
@@ -193,3 +214,30 @@ func randStringBytes(n int) string {
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/matrix/matrix_test.go b/alerting/provider/matrix/matrix_test.go
index c1255c65..9827220a 100644
--- a/alerting/provider/matrix/matrix_test.go
+++ b/alerting/provider/matrix/matrix_test.go
@@ -18,7 +18,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
InternalRoomID: "",
},
}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{
@@ -27,7 +27,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
InternalRoomID: "!a:example.com",
},
}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
validProviderWithHomeserver := AlertProvider{
@@ -37,7 +37,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
InternalRoomID: "!a:example.com",
},
}
- if !validProviderWithHomeserver.IsValid() {
+ if !validProviderWithHomeserver.Validate() {
t.Error("provider with homeserver should've been valid")
}
}
@@ -54,7 +54,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
@@ -68,7 +68,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideTo.IsValid() {
+ if providerWithInvalidOverrideTo.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
@@ -87,7 +87,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/mattermost/mattermost.go b/alerting/provider/mattermost/mattermost.go
index 6b74596c..94f77592 100644
--- a/alerting/provider/mattermost/mattermost.go
+++ b/alerting/provider/mattermost/mattermost.go
@@ -3,6 +3,7 @@ package mattermost
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,11 +11,42 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
+var (
+ ErrWebhookURLNotSet = errors.New("webhook URL not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
+
+type Config struct {
+ WebhookURL string `yaml:"webhook-url"`
+ Channel string `yaml:"channel,omitempty"`
+ ClientConfig *client.Config `yaml:"client,omitempty"`
+}
+
+func (cfg *Config) Validate() error {
+ if len(cfg.WebhookURL) == 0 {
+ return ErrWebhookURLNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if cfg.ClientConfig == nil {
+ cfg.ClientConfig = client.GetDefaultConfig()
+ }
+ if len(override.WebhookURL) > 0 {
+ cfg.WebhookURL = override.WebhookURL
+ }
+ if len(override.Channel) > 0 {
+ cfg.Channel = override.Channel
+ }
+}
+
// AlertProvider is the configuration necessary for sending an alert using Mattermost
type AlertProvider struct {
- Config `yaml:",inline"`
+ DefaultConfig Config `yaml:",inline"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -23,44 +55,39 @@ type AlertProvider struct {
Overrides []Override `yaml:"overrides,omitempty"`
}
-type Config struct {
- WebhookURL string `yaml:"webhook-url"`
- Channel string `yaml:"channel,omitempty"`
- ClientConfig *client.Config `yaml:"client,omitempty"`
-}
-
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if provider.ClientConfig == nil {
- provider.ClientConfig = client.GetDefaultConfig()
- }
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
if provider.Overrides != nil {
registeredGroups := make(map[string]bool)
for _, override := range provider.Overrides {
if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
- return false
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.WebhookURL) > 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
- request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(cfg, ep, alert, result, resolved)))
+ request, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
- response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
+ response, err := client.GetHTTPClient(cfg.ClientConfig).Do(request)
if err != nil {
return err
}
@@ -96,7 +123,7 @@ type Field struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, color string
if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
@@ -122,7 +149,7 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
description = ":\n> " + alertDescription
}
body := Body{
- Channel: provider.Channel,
+ Channel: cfg.Channel,
Text: "",
Username: "gatus",
IconURL: "https://raw.githubusercontent.com/TwiN/gatus/master/.github/assets/logo.png",
@@ -147,19 +174,34 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
return bodyAsJSON
}
-// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
-func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.WebhookURL
- }
- }
- }
- return provider.WebhookURL
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/mattermost/mattermost_test.go b/alerting/provider/mattermost/mattermost_test.go
index 7e7297a9..e4ee5a3c 100644
--- a/alerting/provider/mattermost/mattermost_test.go
+++ b/alerting/provider/mattermost/mattermost_test.go
@@ -13,11 +13,11 @@ import (
func TestAlertProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{WebhookURL: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{WebhookURL: "http://example.com"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -32,7 +32,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
@@ -44,7 +44,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideWebHookUrl.IsValid() {
+ if providerWithInvalidOverrideWebHookUrl.Validate() {
t.Error("provider WebHookURL shouldn't have been valid")
}
@@ -57,7 +57,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/messagebird/messagebird.go b/alerting/provider/messagebird/messagebird.go
index 9c507651..db153757 100644
--- a/alerting/provider/messagebird/messagebird.go
+++ b/alerting/provider/messagebird/messagebird.go
@@ -3,6 +3,7 @@ package messagebird
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,41 +11,75 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
-const (
- restAPIURL = "https://rest.messagebird.com/messages"
+const restAPIURL = "https://rest.messagebird.com/messages"
+
+var (
+ ErrorAccessKeyNotSet = errors.New("access-key not set")
+ ErrorOriginatorNotSet = errors.New("originator not set")
+ ErrorRecipientsNotSet = errors.New("recipients not set")
)
-// AlertProvider is the configuration necessary for sending an alert using Messagebird
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-}
-
type Config struct {
AccessKey string `yaml:"access-key"`
Originator string `yaml:"originator"`
Recipients string `yaml:"recipients"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- return len(provider.AccessKey) > 0 && len(provider.Originator) > 0 && len(provider.Recipients) > 0
+func (cfg *Config) Validate() error {
+ if len(cfg.AccessKey) == 0 {
+ return ErrorAccessKeyNotSet
+ }
+ if len(cfg.Originator) == 0 {
+ return ErrorOriginatorNotSet
+ }
+ if len(cfg.Recipients) == 0 {
+ return ErrorRecipientsNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.AccessKey) > 0 {
+ cfg.AccessKey = override.AccessKey
+ }
+ if len(override.Originator) > 0 {
+ cfg.Originator = override.Originator
+ }
+ if len(override.Recipients) > 0 {
+ cfg.Recipients = override.Recipients
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Messagebird
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+}
+
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
// Reference doc for messagebird: https://developers.messagebird.com/api/sms-messaging/#send-outbound-sms
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
+ cfg, err := provider.GetConfig(alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
- request.Header.Set("Authorization", fmt.Sprintf("AccessKey %s", provider.AccessKey))
+ request.Header.Set("Authorization", fmt.Sprintf("AccessKey %s", cfg.AccessKey))
response, err := client.GetHTTPClient(nil).Do(request)
if err != nil {
return err
@@ -64,7 +99,7 @@ type Body struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string
if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
@@ -72,8 +107,8 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
}
body, _ := json.Marshal(Body{
- Originator: provider.Originator,
- Recipients: provider.Recipients,
+ Originator: cfg.Originator,
+ Recipients: cfg.Recipients,
Body: message,
})
return body
@@ -83,3 +118,21 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/messagebird/messagebird_test.go b/alerting/provider/messagebird/messagebird_test.go
index d9341144..4a2d3add 100644
--- a/alerting/provider/messagebird/messagebird_test.go
+++ b/alerting/provider/messagebird/messagebird_test.go
@@ -13,7 +13,7 @@ import (
func TestMessagebirdAlertProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{
@@ -23,7 +23,7 @@ func TestMessagebirdAlertProvider_IsValid(t *testing.T) {
Recipients: "1",
},
}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/ntfy/ntfy.go b/alerting/provider/ntfy/ntfy.go
index d4bdae92..7229755f 100644
--- a/alerting/provider/ntfy/ntfy.go
+++ b/alerting/provider/ntfy/ntfy.go
@@ -3,6 +3,7 @@ package ntfy
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -12,6 +13,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
const (
@@ -20,16 +22,12 @@ const (
TokenPrefix = "tk_"
)
-// AlertProvider is the configuration necessary for sending an alert using Slack
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-
- // Overrides is a list of Override that may be prioritized over the default configuration
- Overrides []Override `yaml:"overrides,omitempty"`
-}
+var (
+ ErrInvalidToken = errors.New("invalid token")
+ ErrTopicNotSet = errors.New("topic not set")
+ ErrInvalidPriority = errors.New("priority must between 1 and 5 inclusively")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
type Config struct {
Topic string `yaml:"topic"`
@@ -42,63 +40,113 @@ type Config struct {
DisableCache bool `yaml:"disable-cache,omitempty"` // Defaults to false
}
+func (cfg *Config) Validate() error {
+ if len(cfg.URL) == 0 {
+ cfg.URL = DefaultURL
+ }
+ if cfg.Priority == 0 {
+ cfg.Priority = DefaultPriority
+ }
+ if len(cfg.Token) > 0 && !strings.HasPrefix(cfg.Token, TokenPrefix) {
+ return ErrInvalidToken
+ }
+ if len(cfg.Topic) == 0 {
+ return ErrTopicNotSet
+ }
+ if cfg.Priority < 1 || cfg.Priority > 5 {
+ return ErrInvalidPriority
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.Topic) > 0 {
+ cfg.Topic = override.Topic
+ }
+ if len(override.URL) > 0 {
+ cfg.URL = override.URL
+ }
+ if override.Priority > 0 {
+ cfg.Priority = override.Priority
+ }
+ if len(override.Token) > 0 {
+ cfg.Token = override.Token
+ }
+ if len(override.Email) > 0 {
+ cfg.Email = override.Email
+ }
+ if len(override.Click) > 0 {
+ cfg.Click = override.Click
+ }
+ if override.DisableFirebase {
+ cfg.DisableFirebase = true
+ }
+ if override.DisableCache {
+ cfg.DisableCache = true
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Slack
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+
+ // Overrides is a list of Override that may be prioritized over the default configuration
+ Overrides []Override `yaml:"overrides,omitempty"`
+}
+
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if len(provider.URL) == 0 {
- provider.URL = DefaultURL
- }
- if provider.Priority == 0 {
- provider.Priority = DefaultPriority
- }
- isTokenValid := true
- if len(provider.Token) > 0 {
- isTokenValid = strings.HasPrefix(provider.Token, TokenPrefix)
- }
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if len(override.Group) == 0 {
- return false
+ return ErrDuplicateGroupOverride
}
if _, ok := registeredGroups[override.Group]; ok {
- return false
+ return ErrDuplicateGroupOverride
}
if len(override.Token) > 0 && !strings.HasPrefix(override.Token, TokenPrefix) {
- return false
+ return ErrDuplicateGroupOverride
}
if override.Priority < 0 || override.Priority >= 6 {
- return false
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.URL) > 0 && len(provider.Topic) > 0 && provider.Priority > 0 && provider.Priority < 6 && isTokenValid
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- override := provider.getGroupOverride(ep.Group)
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved, override))
- url := provider.getURL(override)
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
+ url := cfg.URL
request, err := http.NewRequest(http.MethodPost, url, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
- if token := provider.getToken(override); len(token) > 0 {
+ if token := cfg.Token; len(token) > 0 {
request.Header.Set("Authorization", "Bearer "+token)
}
- if provider.DisableFirebase {
+ if cfg.DisableFirebase {
request.Header.Set("Firebase", "no")
}
- if provider.DisableCache {
+ if cfg.DisableCache {
request.Header.Set("Cache", "no")
}
response, err := client.GetHTTPClient(nil).Do(request)
@@ -124,7 +172,7 @@ type Body struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool, override *Override) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, formattedConditionResults, tag string
if resolved {
tag = "white_check_mark"
@@ -147,13 +195,13 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
}
message += formattedConditionResults
body, _ := json.Marshal(Body{
- Topic: provider.getTopic(override),
+ Topic: cfg.Topic,
Title: "Gatus: " + ep.DisplayName(),
Message: message,
Tags: []string{tag},
- Priority: provider.getPriority(override),
- Email: provider.getEmail(override),
- Click: provider.getClick(override),
+ Priority: cfg.Priority,
+ Email: cfg.Email,
+ Click: cfg.Click,
})
return body
}
@@ -163,55 +211,29 @@ func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
-func (provider *AlertProvider) getGroupOverride(group string) *Override {
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
if provider.Overrides != nil {
for _, override := range provider.Overrides {
if group == override.Group {
- return &override
+ cfg.Merge(&override.Config)
+ break
}
}
}
- return nil
-}
-
-func (provider *AlertProvider) getTopic(override *Override) string {
- if override != nil && len(override.Topic) > 0 {
- return override.Topic
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
}
- return provider.Topic
-}
-
-func (provider *AlertProvider) getURL(override *Override) string {
- if override != nil && len(override.URL) > 0 {
- return override.URL
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
}
- return provider.URL
-}
-
-func (provider *AlertProvider) getPriority(override *Override) int {
- if override != nil && override.Priority > 0 {
- return override.Priority
- }
- return provider.Priority
-}
-
-func (provider *AlertProvider) getToken(override *Override) string {
- if override != nil && len(override.Token) > 0 {
- return override.Token
- }
- return provider.Token
-}
-
-func (provider *AlertProvider) getEmail(override *Override) string {
- if override != nil && len(override.Email) > 0 {
- return override.Email
- }
- return provider.Email
-}
-
-func (provider *AlertProvider) getClick(override *Override) string {
- if override != nil && len(override.Click) > 0 {
- return override.Click
- }
- return provider.Click
+ return &cfg, nil
}
diff --git a/alerting/provider/ntfy/ntfy_test.go b/alerting/provider/ntfy/ntfy_test.go
index edbee0e3..a68bec27 100644
--- a/alerting/provider/ntfy/ntfy_test.go
+++ b/alerting/provider/ntfy/ntfy_test.go
@@ -85,8 +85,8 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
}
for _, scenario := range scenarios {
t.Run(scenario.name, func(t *testing.T) {
- if scenario.provider.IsValid() != scenario.expected {
- t.Errorf("expected %t, got %t", scenario.expected, scenario.provider.IsValid())
+ if scenario.provider.Validate() != scenario.expected {
+ t.Errorf("expected %t, got %t", scenario.expected, scenario.provider.Validate())
}
})
}
diff --git a/alerting/provider/opsgenie/opsgenie.go b/alerting/provider/opsgenie/opsgenie.go
index 1a21da6c..89753656 100644
--- a/alerting/provider/opsgenie/opsgenie.go
+++ b/alerting/provider/opsgenie/opsgenie.go
@@ -3,6 +3,7 @@ package opsgenie
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -12,18 +13,16 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
const (
restAPI = "https://api.opsgenie.com/v2/alerts"
)
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-}
+var (
+ ErrAPIKeyNotSet = errors.New("api-key not set")
+)
type Config struct {
// APIKey to use for
@@ -55,21 +54,72 @@ type Config struct {
Tags []string `yaml:"tags"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- return len(provider.APIKey) > 0
+func (cfg *Config) Validate() error {
+ if len(cfg.APIKey) == 0 {
+ return ErrAPIKeyNotSet
+ }
+ if len(cfg.Source) == 0 {
+ cfg.Source = "gatus"
+ }
+ if len(cfg.EntityPrefix) == 0 {
+ cfg.EntityPrefix = "gatus-"
+ }
+ if len(cfg.AliasPrefix) == 0 {
+ cfg.AliasPrefix = "gatus-healthcheck-"
+ }
+ if len(cfg.Priority) == 0 {
+ cfg.Priority = "P1"
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.APIKey) > 0 {
+ cfg.APIKey = override.APIKey
+ }
+ if len(override.Priority) > 0 {
+ cfg.Priority = override.Priority
+ }
+ if len(override.Source) > 0 {
+ cfg.Source = override.Source
+ }
+ if len(override.EntityPrefix) > 0 {
+ cfg.EntityPrefix = override.EntityPrefix
+ }
+ if len(override.AliasPrefix) > 0 {
+ cfg.AliasPrefix = override.AliasPrefix
+ }
+ if len(override.Tags) > 0 {
+ cfg.Tags = override.Tags
+ }
+}
+
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+}
+
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
//
// Relevant: https://docs.opsgenie.com/docs/alert-api
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- err := provider.createAlert(ep, alert, result, resolved)
+ cfg, err := provider.GetConfig(alert)
+ if err != nil {
+ return err
+ }
+ err = provider.sendAlertRequest(cfg, ep, alert, result, resolved)
if err != nil {
return err
}
if resolved {
- err = provider.closeAlert(ep, alert)
+ err = provider.closeAlert(cfg, ep, alert)
if err != nil {
return err
}
@@ -79,24 +129,24 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
// The alert has been resolved and there's no error, so we can clear the alert's ResolveKey
alert.ResolveKey = ""
} else {
- alert.ResolveKey = provider.alias(buildKey(ep))
+ alert.ResolveKey = cfg.AliasPrefix + buildKey(ep)
}
}
return nil
}
-func (provider *AlertProvider) createAlert(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- payload := provider.buildCreateRequestBody(ep, alert, result, resolved)
- return provider.sendRequest(restAPI, http.MethodPost, payload)
+func (provider *AlertProvider) sendAlertRequest(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
+ payload := provider.buildCreateRequestBody(cfg, ep, alert, result, resolved)
+ return provider.sendRequest(cfg, restAPI, http.MethodPost, payload)
}
-func (provider *AlertProvider) closeAlert(ep *endpoint.Endpoint, alert *alert.Alert) error {
+func (provider *AlertProvider) closeAlert(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert) error {
payload := provider.buildCloseRequestBody(ep, alert)
- url := restAPI + "/" + provider.alias(buildKey(ep)) + "/close?identifierType=alias"
- return provider.sendRequest(url, http.MethodPost, payload)
+ url := restAPI + "/" + cfg.AliasPrefix + buildKey(ep) + "/close?identifierType=alias"
+ return provider.sendRequest(cfg, url, http.MethodPost, payload)
}
-func (provider *AlertProvider) sendRequest(url, method string, payload interface{}) error {
+func (provider *AlertProvider) sendRequest(cfg *Config, url, method string, payload interface{}) error {
body, err := json.Marshal(payload)
if err != nil {
return fmt.Errorf("error build alert with payload %v: %w", payload, err)
@@ -106,7 +156,7 @@ func (provider *AlertProvider) sendRequest(url, method string, payload interface
return err
}
request.Header.Set("Content-Type", "application/json")
- request.Header.Set("Authorization", "GenieKey "+provider.APIKey)
+ request.Header.Set("Authorization", "GenieKey "+cfg.APIKey)
response, err := client.GetHTTPClient(nil).Do(request)
if err != nil {
return err
@@ -119,7 +169,7 @@ func (provider *AlertProvider) sendRequest(url, method string, payload interface
return nil
}
-func (provider *AlertProvider) buildCreateRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) alertCreateRequest {
+func (provider *AlertProvider) buildCreateRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) alertCreateRequest {
var message, description string
if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", ep.Name, alert.GetDescription())
@@ -162,11 +212,11 @@ func (provider *AlertProvider) buildCreateRequestBody(ep *endpoint.Endpoint, ale
return alertCreateRequest{
Message: message,
Description: description,
- Source: provider.source(),
- Priority: provider.priority(),
- Alias: provider.alias(key),
- Entity: provider.entity(key),
- Tags: provider.Tags,
+ Source: cfg.Source,
+ Priority: cfg.Priority,
+ Alias: cfg.AliasPrefix + key,
+ Entity: cfg.EntityPrefix + key,
+ Tags: cfg.Tags,
Details: details,
}
}
@@ -178,43 +228,29 @@ func (provider *AlertProvider) buildCloseRequestBody(ep *endpoint.Endpoint, aler
}
}
-func (provider *AlertProvider) source() string {
- source := provider.Source
- if source == "" {
- return "gatus"
- }
- return source
-}
-
-func (provider *AlertProvider) alias(key string) string {
- alias := provider.AliasPrefix
- if alias == "" {
- alias = "gatus-healthcheck-"
- }
- return alias + key
-}
-
-func (provider *AlertProvider) entity(key string) string {
- alias := provider.EntityPrefix
- if alias == "" {
- alias = "gatus-"
- }
- return alias + key
-}
-
-func (provider *AlertProvider) priority() string {
- priority := provider.Priority
- if priority == "" {
- return "P1"
- }
- return priority
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
+
func buildKey(ep *endpoint.Endpoint) string {
name := toKebabCase(ep.Name)
if ep.Group == "" {
diff --git a/alerting/provider/opsgenie/opsgenie_test.go b/alerting/provider/opsgenie/opsgenie_test.go
index f38c9b27..58e6b0f2 100644
--- a/alerting/provider/opsgenie/opsgenie_test.go
+++ b/alerting/provider/opsgenie/opsgenie_test.go
@@ -13,11 +13,11 @@ import (
func TestAlertProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{APIKey: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{APIKey: "00000000-0000-0000-0000-000000000000"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/pagerduty/pagerduty.go b/alerting/provider/pagerduty/pagerduty.go
index eb384aeb..fbad227b 100644
--- a/alerting/provider/pagerduty/pagerduty.go
+++ b/alerting/provider/pagerduty/pagerduty.go
@@ -3,6 +3,7 @@ package pagerduty
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -11,15 +12,38 @@ import (
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
"github.com/TwiN/logr"
+ "gopkg.in/yaml.v3"
)
const (
restAPIURL = "https://events.pagerduty.com/v2/enqueue"
)
+var (
+ ErrIntegrationKeyNotSet = errors.New("integration-key must have exactly 32 characters")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
+
+type Config struct {
+ IntegrationKey string `yaml:"integration-key"`
+}
+
+func (cfg *Config) Validate() error {
+ if len(cfg.IntegrationKey) != 32 {
+ return ErrIntegrationKeyNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.IntegrationKey) > 0 {
+ cfg.IntegrationKey = override.IntegrationKey
+ }
+}
+
// AlertProvider is the configuration necessary for sending an alert using PagerDuty
type AlertProvider struct {
- Config `yaml:",inline"`
+ DefaultConfig Config `yaml:",inline"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -28,36 +52,36 @@ type AlertProvider struct {
Overrides []Override `yaml:"overrides,omitempty"`
}
-type Config struct {
- IntegrationKey string `yaml:"integration-key"`
-}
-
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
- if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.IntegrationKey) != 32 {
- return false
+ if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
// Either the default integration key has the right length, or there are overrides who are properly configured.
- return len(provider.IntegrationKey) == 32 || len(provider.Overrides) != 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
//
// Relevant: https://developer.pagerduty.com/docs/events-api-v2/trigger-events/
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
if err != nil {
return err
@@ -104,7 +128,7 @@ type Payload struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, eventAction, resolveKey string
if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
@@ -116,7 +140,7 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
resolveKey = ""
}
body, _ := json.Marshal(Body{
- RoutingKey: provider.getIntegrationKeyForGroup(ep.Group),
+ RoutingKey: cfg.IntegrationKey,
DedupKey: resolveKey,
EventAction: eventAction,
Payload: Payload{
@@ -128,23 +152,38 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
return body
}
-// getIntegrationKeyForGroup returns the appropriate pagerduty integration key for a given group
-func (provider *AlertProvider) getIntegrationKeyForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.IntegrationKey
- }
- }
- }
- return provider.IntegrationKey
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
+
type pagerDutyResponsePayload struct {
Status string `json:"status"`
Message string `json:"message"`
diff --git a/alerting/provider/pagerduty/pagerduty_test.go b/alerting/provider/pagerduty/pagerduty_test.go
index f991ba84..0c67c91e 100644
--- a/alerting/provider/pagerduty/pagerduty_test.go
+++ b/alerting/provider/pagerduty/pagerduty_test.go
@@ -13,11 +13,11 @@ import (
func TestAlertProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{IntegrationKey: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{IntegrationKey: "00000000000000000000000000000000"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -31,7 +31,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideIntegrationKey := AlertProvider{
@@ -42,7 +42,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideIntegrationKey.IsValid() {
+ if providerWithInvalidOverrideIntegrationKey.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
@@ -53,7 +53,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go
index f9386b94..7635addd 100644
--- a/alerting/provider/provider.go
+++ b/alerting/provider/provider.go
@@ -29,14 +29,19 @@ import (
// AlertProvider is the interface that each provider should implement
type AlertProvider interface {
- // IsValid returns whether the provider's configuration is valid
- IsValid() bool
-
- // GetDefaultAlert returns the provider's default alert configuration
- GetDefaultAlert() *alert.Alert
+ // Validate the provider's configuration
+ Validate() error
// Send an alert using the provider
Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error
+
+ // GetDefaultAlert returns the provider's default alert configuration
+ GetDefaultAlert() *alert.Alert
+}
+
+type Config[T any] interface {
+ Validate() error
+ Merge(override *T)
}
// ParseWithDefaultAlert parses an Endpoint alert by using the provider's default alert as a baseline
@@ -62,7 +67,7 @@ func ParseWithDefaultAlert(providerDefaultAlert, endpointAlert *alert.Alert) {
}
var (
- // Validate interface implementation on compile
+ // Validate provider interface implementation on compile
_ AlertProvider = (*awsses.AlertProvider)(nil)
_ AlertProvider = (*custom.AlertProvider)(nil)
_ AlertProvider = (*discord.AlertProvider)(nil)
@@ -85,4 +90,28 @@ var (
_ AlertProvider = (*telegram.AlertProvider)(nil)
_ AlertProvider = (*twilio.AlertProvider)(nil)
_ AlertProvider = (*zulip.AlertProvider)(nil)
+
+ // Validate config interface implementation on compile
+ _ Config[awsses.Config] = (*awsses.Config)(nil)
+ _ Config[custom.Config] = (*custom.Config)(nil)
+ _ Config[discord.Config] = (*discord.Config)(nil)
+ _ Config[email.Config] = (*email.Config)(nil)
+ _ Config[gitea.Config] = (*gitea.Config)(nil)
+ _ Config[github.Config] = (*github.Config)(nil)
+ _ Config[gitlab.Config] = (*gitlab.Config)(nil)
+ _ Config[googlechat.Config] = (*googlechat.Config)(nil)
+ _ Config[jetbrainsspace.Config] = (*jetbrainsspace.Config)(nil)
+ _ Config[matrix.Config] = (*matrix.Config)(nil)
+ _ Config[mattermost.Config] = (*mattermost.Config)(nil)
+ _ Config[messagebird.Config] = (*messagebird.Config)(nil)
+ _ Config[ntfy.Config] = (*ntfy.Config)(nil)
+ _ Config[opsgenie.Config] = (*opsgenie.Config)(nil)
+ _ Config[pagerduty.Config] = (*pagerduty.Config)(nil)
+ _ Config[pushover.Config] = (*pushover.Config)(nil)
+ _ Config[slack.Config] = (*slack.Config)(nil)
+ _ Config[teams.Config] = (*teams.Config)(nil)
+ _ Config[teamsworkflows.Config] = (*teamsworkflows.Config)(nil)
+ _ Config[telegram.Config] = (*telegram.Config)(nil)
+ _ Config[twilio.Config] = (*twilio.Config)(nil)
+ _ Config[zulip.Config] = (*zulip.Config)(nil)
)
diff --git a/alerting/provider/pushover/pushover.go b/alerting/provider/pushover/pushover.go
index 452ed66a..9e197225 100644
--- a/alerting/provider/pushover/pushover.go
+++ b/alerting/provider/pushover/pushover.go
@@ -3,6 +3,7 @@ package pushover
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,6 +11,7 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
const (
@@ -17,13 +19,11 @@ const (
defaultPriority = 0
)
-// AlertProvider is the configuration necessary for sending an alert using Pushover
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-}
+var (
+ ErrInvalidApplicationToken = errors.New("application-token must be 30 characters long")
+ ErrInvalidUserKey = errors.New("user-key must be 30 characters long")
+ ErrInvalidPriority = errors.New("priority and resolved-priority must be between -2 and 2")
+)
type Config struct {
// Key used to authenticate the application sending
@@ -50,21 +50,67 @@ type Config struct {
Sound string `yaml:"sound,omitempty"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if provider.Priority == 0 {
- provider.Priority = defaultPriority
+func (cfg *Config) Validate() error {
+ if cfg.Priority == 0 {
+ cfg.Priority = defaultPriority
}
- if provider.ResolvedPriority == 0 {
- provider.ResolvedPriority = defaultPriority
+ if cfg.ResolvedPriority == 0 {
+ cfg.ResolvedPriority = defaultPriority
}
- return len(provider.ApplicationToken) == 30 && len(provider.UserKey) == 30 && provider.Priority >= -2 && provider.Priority <= 2 && provider.ResolvedPriority >= -2 && provider.ResolvedPriority <= 2
+ if len(cfg.ApplicationToken) != 30 {
+ return ErrInvalidApplicationToken
+ }
+ if len(cfg.UserKey) != 30 {
+ return ErrInvalidUserKey
+ }
+ if cfg.Priority < -2 || cfg.Priority > 2 || cfg.ResolvedPriority < -2 || cfg.ResolvedPriority > 2 {
+ return ErrInvalidPriority
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.ApplicationToken) > 0 {
+ cfg.ApplicationToken = override.ApplicationToken
+ }
+ if len(override.UserKey) > 0 {
+ cfg.UserKey = override.UserKey
+ }
+ if len(override.Title) > 0 {
+ cfg.Title = override.Title
+ }
+ if override.Priority != 0 {
+ cfg.Priority = override.Priority
+ }
+ if override.ResolvedPriority != 0 {
+ cfg.ResolvedPriority = override.ResolvedPriority
+ }
+ if len(override.Sound) > 0 {
+ cfg.Sound = override.Sound
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Pushover
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+}
+
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
// Reference doc for pushover: https://pushover.net/api
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
request, err := http.NewRequest(http.MethodPost, restAPIURL, buffer)
if err != nil {
return err
@@ -92,38 +138,47 @@ type Body struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string
if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
} else {
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
}
+ priority := cfg.Priority
+ if resolved {
+ priority = cfg.ResolvedPriority
+ }
body, _ := json.Marshal(Body{
- Token: provider.ApplicationToken,
- User: provider.UserKey,
- Title: provider.Title,
+ Token: cfg.ApplicationToken,
+ User: cfg.UserKey,
+ Title: cfg.Title,
Message: message,
- Priority: provider.priority(resolved),
- Sound: provider.Sound,
+ Priority: priority,
+ Sound: cfg.Sound,
})
return body
}
-func (provider *AlertProvider) priority(resolved bool) int {
- if resolved && provider.ResolvedPriority == 0 {
- return defaultPriority
- }
- if !resolved && provider.Priority == 0 {
- return defaultPriority
- }
- if resolved {
- return provider.ResolvedPriority
- }
- return provider.Priority
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/pushover/pushover_test.go b/alerting/provider/pushover/pushover_test.go
index 581bd3de..3e7379c2 100644
--- a/alerting/provider/pushover/pushover_test.go
+++ b/alerting/provider/pushover/pushover_test.go
@@ -13,7 +13,7 @@ import (
func TestPushoverAlertProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{
@@ -25,7 +25,7 @@ func TestPushoverAlertProvider_IsValid(t *testing.T) {
ResolvedPriority: 1,
},
}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -38,7 +38,7 @@ func TestPushoverAlertProvider_IsInvalid(t *testing.T) {
Priority: 5,
},
}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider should've been invalid")
}
}
diff --git a/alerting/provider/slack/slack.go b/alerting/provider/slack/slack.go
index eaebf7d1..2f69acf7 100644
--- a/alerting/provider/slack/slack.go
+++ b/alerting/provider/slack/slack.go
@@ -3,6 +3,7 @@ package slack
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,11 +11,34 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
+var (
+ ErrWebhookURLNotSet = errors.New("webhook-url not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
+
+type Config struct {
+ WebhookURL string `yaml:"webhook-url"` // Slack webhook URL
+}
+
+func (cfg *Config) Validate() error {
+ if len(cfg.WebhookURL) == 0 {
+ return ErrWebhookURLNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.WebhookURL) > 0 {
+ cfg.WebhookURL = override.WebhookURL
+ }
+}
+
// AlertProvider is the configuration necessary for sending an alert using Slack
type AlertProvider struct {
- Config `yaml:",inline"`
+ DefaultConfig Config `yaml:",inline"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -23,34 +47,34 @@ type AlertProvider struct {
Overrides []Override `yaml:"overrides,omitempty"`
}
-type Config struct {
- WebhookURL string `yaml:"webhook-url"` // Slack webhook URL
-}
-
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
- if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
- return false
+ if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.WebhookURL) > 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
- request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
+ request, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, buffer)
if err != nil {
return err
}
@@ -132,19 +156,34 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
return bodyAsJSON
}
-// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
-func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.WebhookURL
- }
- }
- }
- return provider.WebhookURL
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/slack/slack_test.go b/alerting/provider/slack/slack_test.go
index 40a7e4a4..05c24a88 100644
--- a/alerting/provider/slack/slack_test.go
+++ b/alerting/provider/slack/slack_test.go
@@ -13,11 +13,11 @@ import (
func TestAlertDefaultProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{WebhookURL: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{WebhookURL: "https://example.com"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -31,7 +31,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
@@ -42,7 +42,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideTo.IsValid() {
+ if providerWithInvalidOverrideTo.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
@@ -54,7 +54,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/teams/teams.go b/alerting/provider/teams/teams.go
index ce0180ff..407831ee 100644
--- a/alerting/provider/teams/teams.go
+++ b/alerting/provider/teams/teams.go
@@ -3,6 +3,7 @@ package teams
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,25 +11,53 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
-// AlertProvider is the configuration necessary for sending an alert using Teams
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-
- // ClientConfig is the configuration of the client used to communicate with the provider's target
- ClientConfig *client.Config `yaml:"client,omitempty"`
-
- // Overrides is a list of Override that may be prioritized over the default configuration
- Overrides []Override `yaml:"overrides,omitempty"`
-}
+var (
+ ErrWebhookURLNotSet = errors.New("webhook-url not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
type Config struct {
WebhookURL string `yaml:"webhook-url"`
Title string `yaml:"title,omitempty"` // Title of the message that will be sent
+
+ // ClientConfig is the configuration of the client used to communicate with the provider's target
+ ClientConfig *client.Config `yaml:"client,omitempty"`
+}
+
+func (cfg *Config) Validate() error {
+ if cfg.ClientConfig == nil {
+ cfg.ClientConfig = client.GetDefaultConfig()
+ }
+ if len(cfg.WebhookURL) == 0 {
+ return ErrWebhookURLNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if cfg.ClientConfig == nil {
+ cfg.ClientConfig = client.GetDefaultConfig()
+ }
+ if len(override.WebhookURL) > 0 {
+ cfg.WebhookURL = override.WebhookURL
+ }
+ if len(override.Title) > 0 {
+ cfg.Title = override.Title
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Teams
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+
+ // Overrides is a list of Override that may be prioritized over the default configuration
+ Overrides []Override `yaml:"overrides,omitempty"`
}
// Override is a case under which the default integration is overridden
@@ -37,29 +66,33 @@ type Override struct {
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
- if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
- return false
+ if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.WebhookURL) > 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
- request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
+ request, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
- response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
+ response, err := client.GetHTTPClient(cfg.ClientConfig).Do(request)
if err != nil {
return err
}
@@ -86,7 +119,7 @@ type Section struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message, color string
if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
@@ -113,7 +146,7 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
Type: "MessageCard",
Context: "http://schema.org/extensions",
ThemeColor: color,
- Title: provider.Title,
+ Title: cfg.Title,
Text: message + description,
}
if len(body.Title) == 0 {
@@ -129,19 +162,34 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
return bodyAsJSON
}
-// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
-func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.WebhookURL
- }
- }
- }
- return provider.WebhookURL
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/teams/teams_test.go b/alerting/provider/teams/teams_test.go
index 718486b9..a1f105c7 100644
--- a/alerting/provider/teams/teams_test.go
+++ b/alerting/provider/teams/teams_test.go
@@ -13,11 +13,11 @@ import (
func TestAlertDefaultProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{WebhookURL: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{WebhookURL: "http://example.com"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -31,7 +31,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
@@ -42,7 +42,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideTo.IsValid() {
+ if providerWithInvalidOverrideTo.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
@@ -54,7 +54,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/teamsworkflows/teamsworkflows.go b/alerting/provider/teamsworkflows/teamsworkflows.go
index a52bc39f..11de6ac5 100644
--- a/alerting/provider/teamsworkflows/teamsworkflows.go
+++ b/alerting/provider/teamsworkflows/teamsworkflows.go
@@ -3,6 +3,7 @@ package teamsworkflows
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,11 +11,38 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
+var (
+ ErrWebhookURLNotSet = errors.New("webhook-url not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
+
+type Config struct {
+ WebhookURL string `yaml:"webhook-url"`
+ Title string `yaml:"title,omitempty"` // Title of the message that will be sent
+}
+
+func (cfg *Config) Validate() error {
+ if len(cfg.WebhookURL) > 0 {
+ return ErrWebhookURLNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.WebhookURL) > 0 {
+ cfg.WebhookURL = override.WebhookURL
+ }
+ if len(override.Title) > 0 {
+ cfg.Title = override.Title
+ }
+}
+
// AlertProvider is the configuration necessary for sending an alert using Teams
type AlertProvider struct {
- Config `yaml:",inline"`
+ DefaultConfig Config `yaml:",inline"`
// DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
@@ -23,35 +51,34 @@ type AlertProvider struct {
Overrides []Override `yaml:"overrides,omitempty"`
}
-type Config struct {
- WebhookURL string `yaml:"webhook-url"`
- Title string `yaml:"title,omitempty"` // Title of the message that will be sent
-}
-
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
- if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" || len(override.WebhookURL) == 0 {
- return false
+ if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return len(provider.WebhookURL) > 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
- request, err := http.NewRequest(http.MethodPost, provider.getWebhookURLForGroup(ep.Group), buffer)
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
+ request, err := http.NewRequest(http.MethodPost, cfg.WebhookURL, buffer)
if err != nil {
return err
}
@@ -108,7 +135,7 @@ type Fact struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string
var themeColor string
if resolved {
@@ -121,8 +148,8 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
// Configure default title if it's not provided
title := "⛑ Gatus"
- if provider.Title != "" {
- title = provider.Title
+ if cfg.Title != "" {
+ title = cfg.Title
}
// Build the facts from the condition results
@@ -191,19 +218,34 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
return bodyAsJSON
}
-// getWebhookURLForGroup returns the appropriate Webhook URL integration to for a given group
-func (provider *AlertProvider) getWebhookURLForGroup(group string) string {
- if provider.Overrides != nil {
- for _, override := range provider.Overrides {
- if group == override.Group {
- return override.WebhookURL
- }
- }
- }
- return provider.WebhookURL
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/teamsworkflows/teamsworkflows_test.go b/alerting/provider/teamsworkflows/teamsworkflows_test.go
index cc53e2da..03259ff7 100644
--- a/alerting/provider/teamsworkflows/teamsworkflows_test.go
+++ b/alerting/provider/teamsworkflows/teamsworkflows_test.go
@@ -13,11 +13,11 @@ import (
func TestAlertDefaultProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{WebhookURL: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{Config: Config{WebhookURL: "http://example.com"}}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
@@ -31,7 +31,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideGroup.IsValid() {
+ if providerWithInvalidOverrideGroup.Validate() {
t.Error("provider Group shouldn't have been valid")
}
providerWithInvalidOverrideTo := AlertProvider{
@@ -42,7 +42,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if providerWithInvalidOverrideTo.IsValid() {
+ if providerWithInvalidOverrideTo.Validate() {
t.Error("provider integration key shouldn't have been valid")
}
providerWithValidOverride := AlertProvider{
@@ -54,7 +54,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
},
},
}
- if !providerWithValidOverride.IsValid() {
+ if !providerWithValidOverride.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/telegram/telegram.go b/alerting/provider/telegram/telegram.go
index 73e690c6..0db47cec 100644
--- a/alerting/provider/telegram/telegram.go
+++ b/alerting/provider/telegram/telegram.go
@@ -3,6 +3,7 @@ package telegram
import (
"bytes"
"encoding/json"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,69 +11,100 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
-const defaultAPIURL = "https://api.telegram.org"
+const defaultApiUrl = "https://api.telegram.org"
-// AlertProvider is the configuration necessary for sending an alert using Telegram
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-
- // Overrides is a list of Overrid that may be prioritized over the default configuration
- Overrides []*Override `yaml:"overrides,omitempty"`
-}
+var (
+ ErrTokenNotSet = errors.New("token not set")
+ ErrIDNotSet = errors.New("id not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
type Config struct {
Token string `yaml:"token"`
ID string `yaml:"id"`
- APIURL string `yaml:"api-url"`
+ ApiUrl string `yaml:"api-url"`
- // ClientConfig is the configuration of the client used to communicate with the provider's target
ClientConfig *client.Config `yaml:"client,omitempty"`
}
+func (cfg *Config) Validate() error {
+ if cfg.ClientConfig == nil {
+ cfg.ClientConfig = client.GetDefaultConfig()
+ }
+ if len(cfg.ApiUrl) == 0 {
+ cfg.ApiUrl = defaultApiUrl
+ }
+ if len(cfg.Token) == 0 {
+ return ErrTokenNotSet
+ }
+ if len(cfg.ID) == 0 {
+ return ErrIDNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if cfg.ClientConfig == nil {
+ cfg.ClientConfig = client.GetDefaultConfig()
+ }
+ if len(override.Token) > 0 {
+ cfg.Token = override.Token
+ }
+ if len(override.ID) > 0 {
+ cfg.ID = override.ID
+ }
+ if len(override.ApiUrl) > 0 {
+ cfg.ApiUrl = override.ApiUrl
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Telegram
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+
+ // Overrides is a list of overrides that may be prioritized over the default configuration
+ Overrides []*Override `yaml:"overrides,omitempty"`
+}
+
// Override is a configuration that may be prioritized over the default configuration
type Override struct {
- group string `yaml:"group"`
+ Group string `yaml:"group"`
Config `yaml:",inline"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- if provider.ClientConfig == nil {
- provider.ClientConfig = client.GetDefaultConfig()
- }
-
- registerGroups := make(map[string]bool)
- for _, override := range provider.Overrides {
- if len(override.group) == 0 {
- return false
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ registeredGroups := make(map[string]bool)
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
+ return ErrDuplicateGroupOverride
+ }
+ registeredGroups[override.Group] = true
}
- if _, ok := registerGroups[override.group]; ok {
- return false
- }
- registerGroups[override.group] = true
}
-
- return len(provider.Token) > 0 && len(provider.ID) > 0
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer(provider.buildRequestBody(ep, alert, result, resolved))
- apiURL := provider.APIURL
- if apiURL == "" {
- apiURL = defaultAPIURL
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
}
- request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/bot%s/sendMessage", apiURL, provider.getTokenForGroup(ep.Group)), buffer)
+ buffer := bytes.NewBuffer(provider.buildRequestBody(cfg, ep, alert, result, resolved))
+ request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("%s/bot%s/sendMessage", cfg.ApiUrl, cfg.Token), buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/json")
- response, err := client.GetHTTPClient(provider.ClientConfig).Do(request)
+ response, err := client.GetHTTPClient(cfg.ClientConfig).Do(request)
if err != nil {
return err
}
@@ -84,15 +116,6 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
return err
}
-func (provider *AlertProvider) getTokenForGroup(group string) string {
- for _, override := range provider.Overrides {
- if override.group == group && len(override.Token) > 0 {
- return override.Token
- }
- }
- return provider.Token
-}
-
type Body struct {
ChatID string `json:"chat_id"`
Text string `json:"text"`
@@ -100,7 +123,7 @@ type Body struct {
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) []byte {
var message string
if resolved {
message = fmt.Sprintf("An alert for *%s* has been resolved:\nā\n _healthcheck passing successfully %d time(s) in a row_\nā ", ep.DisplayName(), alert.SuccessThreshold)
@@ -127,23 +150,41 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
text = fmt.Sprintf("ā *Gatus* \n%s%s", message, formattedConditionResults)
}
bodyAsJSON, _ := json.Marshal(Body{
- ChatID: provider.getIDForGroup(ep.Group),
+ ChatID: cfg.ID,
Text: text,
ParseMode: "MARKDOWN",
})
return bodyAsJSON
}
-func (provider *AlertProvider) getIDForGroup(group string) string {
- for _, override := range provider.Overrides {
- if override.group == group && len(override.ID) > 0 {
- return override.ID
- }
- }
- return provider.ID
-}
-
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/telegram/telegram_test.go b/alerting/provider/telegram/telegram_test.go
index 7701bfe1..7450a5f5 100644
--- a/alerting/provider/telegram/telegram_test.go
+++ b/alerting/provider/telegram/telegram_test.go
@@ -14,7 +14,7 @@ import (
func TestAlertDefaultProvider_IsValid(t *testing.T) {
t.Run("invalid-provider", func(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{Token: "", ID: ""}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
})
@@ -23,7 +23,7 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
if validProvider.ClientConfig != nil {
t.Error("provider client config should have been nil prior to IsValid() being executed")
}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
if validProvider.ClientConfig == nil {
@@ -35,22 +35,22 @@ func TestAlertDefaultProvider_IsValid(t *testing.T) {
func TestAlertProvider_IsValidWithOverrides(t *testing.T) {
t.Run("invalid-provider-override-nonexist-group", func(t *testing.T) {
invalidProvider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{Config: Config{Token: "token", ID: "id"}}}}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
})
t.Run("invalid-provider-override-duplicate-group", func(t *testing.T) {
- invalidProvider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{group: "group1", Config: Config{Token: "token", ID: "id"}}, {group: "group1", Config: Config{ID: "id2"}}}}
- if invalidProvider.IsValid() {
+ invalidProvider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{Group: "group1", Config: Config{Token: "token", ID: "id"}}, {Group: "group1", Config: Config{ID: "id2"}}}}
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
})
t.Run("valid-provider", func(t *testing.T) {
- validProvider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{group: "group", Config: Config{Token: "token", ID: "id"}}}}
+ validProvider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{Group: "group", Config: Config{Token: "token", ID: "id"}}}}
if validProvider.ClientConfig != nil {
t.Error("provider client config should have been nil prior to IsValid() being executed")
}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
if validProvider.ClientConfig == nil {
@@ -61,7 +61,7 @@ func TestAlertProvider_IsValidWithOverrides(t *testing.T) {
func TestAlertProvider_getTokenAndIDForGroup(t *testing.T) {
t.Run("get-token-with-override", func(t *testing.T) {
- provider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{group: "group", Config: Config{Token: "overrideToken", ID: "overrideID"}}}}
+ provider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{Group: "group", Config: Config{Token: "overrideToken", ID: "overrideID"}}}}
token := provider.getTokenForGroup("group")
if token != "overrideToken" {
t.Error("token should have been 'overrideToken'")
@@ -72,7 +72,7 @@ func TestAlertProvider_getTokenAndIDForGroup(t *testing.T) {
}
})
t.Run("get-default-token-with-overridden-id", func(t *testing.T) {
- provider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{group: "group", Config: Config{ID: "overrideID"}}}}
+ provider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{Group: "group", Config: Config{ID: "overrideID"}}}}
token := provider.getTokenForGroup("group")
if token != provider.Token {
t.Error("token should have been the default token")
@@ -83,7 +83,7 @@ func TestAlertProvider_getTokenAndIDForGroup(t *testing.T) {
}
})
t.Run("get-default-token-with-overridden-token", func(t *testing.T) {
- provider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{group: "group", Config: Config{Token: "overrideToken"}}}}
+ provider := AlertProvider{Config: Config{Token: "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", ID: "12345678"}, Overrides: []*Override{{Group: "group", Config: Config{Token: "overrideToken"}}}}
token := provider.getTokenForGroup("group")
if token != "overrideToken" {
t.Error("token should have been 'overrideToken'")
diff --git a/alerting/provider/twilio/twilio.go b/alerting/provider/twilio/twilio.go
index 01e69c06..7cb30920 100644
--- a/alerting/provider/twilio/twilio.go
+++ b/alerting/provider/twilio/twilio.go
@@ -3,6 +3,7 @@ package twilio
import (
"bytes"
"encoding/base64"
+ "errors"
"fmt"
"io"
"net/http"
@@ -11,15 +12,15 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
-// AlertProvider is the configuration necessary for sending an alert using Twilio
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-}
+var (
+ ErrSIDNotSet = errors.New("sid not set")
+ ErrTokenNotSet = errors.New("token not set")
+ ErrFromNotSet = errors.New("from not set")
+ ErrToNotSet = errors.New("to not set")
+)
type Config struct {
SID string `yaml:"sid"`
@@ -28,20 +29,63 @@ type Config struct {
To string `yaml:"to"`
}
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
- return len(provider.Token) > 0 && len(provider.SID) > 0 && len(provider.From) > 0 && len(provider.To) > 0
+func (cfg *Config) Validate() error {
+ if len(cfg.SID) == 0 {
+ return ErrSIDNotSet
+ }
+ if len(cfg.Token) == 0 {
+ return ErrTokenNotSet
+ }
+ if len(cfg.From) == 0 {
+ return ErrFromNotSet
+ }
+ if len(cfg.To) == 0 {
+ return ErrToNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.SID) > 0 {
+ cfg.SID = override.SID
+ }
+ if len(override.Token) > 0 {
+ cfg.Token = override.Token
+ }
+ if len(override.From) > 0 {
+ cfg.From = override.From
+ }
+ if len(override.To) > 0 {
+ cfg.To = override.To
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Twilio
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+}
+
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(ep, alert, result, resolved)))
- request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", provider.SID), buffer)
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBuffer([]byte(provider.buildRequestBody(cfg, ep, alert, result, resolved)))
+ request, err := http.NewRequest(http.MethodPost, fmt.Sprintf("https://api.twilio.com/2010-04-01/Accounts/%s/Messages.json", cfg.SID), buffer)
if err != nil {
return err
}
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
- request.Header.Set("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(provider.SID+":"+provider.Token))))
+ request.Header.Set("Authorization", fmt.Sprintf("Basic %s", base64.StdEncoding.EncodeToString([]byte(cfg.SID+":"+cfg.Token))))
response, err := client.GetHTTPClient(nil).Do(request)
if err != nil {
return err
@@ -55,7 +99,7 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
}
// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
var message string
if resolved {
message = fmt.Sprintf("RESOLVED: %s - %s", ep.DisplayName(), alert.GetDescription())
@@ -63,8 +107,8 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
message = fmt.Sprintf("TRIGGERED: %s - %s", ep.DisplayName(), alert.GetDescription())
}
return url.Values{
- "To": {provider.To},
- "From": {provider.From},
+ "To": {cfg.To},
+ "From": {cfg.From},
"Body": {message},
}.Encode()
}
@@ -73,3 +117,21 @@ func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *al
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/twilio/twilio_test.go b/alerting/provider/twilio/twilio_test.go
index d770e0e2..d3b817b5 100644
--- a/alerting/provider/twilio/twilio_test.go
+++ b/alerting/provider/twilio/twilio_test.go
@@ -9,7 +9,7 @@ import (
func TestTwilioAlertProvider_IsValid(t *testing.T) {
invalidProvider := AlertProvider{}
- if invalidProvider.IsValid() {
+ if invalidProvider.Validate() {
t.Error("provider shouldn't have been valid")
}
validProvider := AlertProvider{
@@ -20,7 +20,7 @@ func TestTwilioAlertProvider_IsValid(t *testing.T) {
To: "1",
},
}
- if !validProvider.IsValid() {
+ if !validProvider.Validate() {
t.Error("provider should've been valid")
}
}
diff --git a/alerting/provider/zulip/zulip.go b/alerting/provider/zulip/zulip.go
index c4f1ccf7..bd491775 100644
--- a/alerting/provider/zulip/zulip.go
+++ b/alerting/provider/zulip/zulip.go
@@ -2,6 +2,7 @@ package zulip
import (
"bytes"
+ "errors"
"fmt"
"io"
"net/http"
@@ -10,18 +11,16 @@ import (
"github.com/TwiN/gatus/v5/alerting/alert"
"github.com/TwiN/gatus/v5/client"
"github.com/TwiN/gatus/v5/config/endpoint"
+ "gopkg.in/yaml.v3"
)
-// AlertProvider is the configuration necessary for sending an alert using Zulip
-type AlertProvider struct {
- Config `yaml:",inline"`
-
- // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
- DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
-
- // Overrides is a list of Override that may be prioritized over the default configuration
- Overrides []Override `yaml:"overrides,omitempty"`
-}
+var (
+ ErrBotEmailNotSet = errors.New("bot-email not set")
+ ErrBotAPIKeyNotSet = errors.New("bot-api-key not set")
+ ErrDomainNotSet = errors.New("domain not set")
+ ErrChannelIDNotSet = errors.New("channel-id not set")
+ ErrDuplicateGroupOverride = errors.New("duplicate group override")
+)
type Config struct {
BotEmail string `yaml:"bot-email"` // Email of the bot user
@@ -30,86 +29,81 @@ type Config struct {
ChannelID string `yaml:"channel-id"` // ID of the channel to send the message to
}
+func (cfg *Config) Validate() error {
+ if len(cfg.BotEmail) == 0 {
+ return ErrBotEmailNotSet
+ }
+ if len(cfg.BotAPIKey) == 0 {
+ return ErrBotAPIKeyNotSet
+ }
+ if len(cfg.Domain) == 0 {
+ return ErrDomainNotSet
+ }
+ if len(cfg.ChannelID) == 0 {
+ return ErrChannelIDNotSet
+ }
+ return nil
+}
+
+func (cfg *Config) Merge(override *Config) {
+ if len(override.BotEmail) > 0 {
+ cfg.BotEmail = override.BotEmail
+ }
+ if len(override.BotAPIKey) > 0 {
+ cfg.BotAPIKey = override.BotAPIKey
+ }
+ if len(override.Domain) > 0 {
+ cfg.Domain = override.Domain
+ }
+ if len(override.ChannelID) > 0 {
+ cfg.ChannelID = override.ChannelID
+ }
+}
+
+// AlertProvider is the configuration necessary for sending an alert using Zulip
+type AlertProvider struct {
+ DefaultConfig Config `yaml:",inline"`
+
+ // DefaultAlert is the default alert configuration to use for endpoints with an alert of the appropriate type
+ DefaultAlert *alert.Alert `yaml:"default-alert,omitempty"`
+
+ // Overrides is a list of Override that may be prioritized over the default configuration
+ Overrides []Override `yaml:"overrides,omitempty"`
+}
+
// Override is a case under which the default integration is overridden
type Override struct {
Group string `yaml:"group"`
Config `yaml:",inline"`
}
-func (provider *AlertProvider) validateConfig(conf *Config) bool {
- return len(conf.BotEmail) > 0 && len(conf.BotAPIKey) > 0 && len(conf.Domain) > 0 && len(conf.ChannelID) > 0
-}
-
-// IsValid returns whether the provider's configuration is valid
-func (provider *AlertProvider) IsValid() bool {
+// Validate the provider's configuration
+func (provider *AlertProvider) Validate() error {
registeredGroups := make(map[string]bool)
if provider.Overrides != nil {
for _, override := range provider.Overrides {
- isAlreadyRegistered := registeredGroups[override.Group]
- if isAlreadyRegistered || override.Group == "" || !provider.validateConfig(&override.Config) {
- return false
+ if isAlreadyRegistered := registeredGroups[override.Group]; isAlreadyRegistered || override.Group == "" {
+ return ErrDuplicateGroupOverride
}
registeredGroups[override.Group] = true
}
}
- return provider.validateConfig(&provider.Config)
-}
-
-// getChannelIdForGroup returns the channel ID for the provided group
-func (provider *AlertProvider) getChannelIdForGroup(group string) string {
- for _, override := range provider.Overrides {
- if override.Group == group {
- return override.ChannelID
- }
- }
- return provider.ChannelID
-}
-
-// buildRequestBody builds the request body for the provider
-func (provider *AlertProvider) buildRequestBody(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
- var message string
- if resolved {
- message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
- } else {
- message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
- }
-
- if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
- message += "\n> " + alertDescription + "\n"
- }
-
- for _, conditionResult := range result.ConditionResults {
- var prefix string
- if conditionResult.Success {
- prefix = ":check:"
- } else {
- prefix = ":cross_mark:"
- }
- message += fmt.Sprintf("\n%s - `%s`", prefix, conditionResult.Condition)
- }
-
- postData := map[string]string{
- "type": "channel",
- "to": provider.getChannelIdForGroup(ep.Group),
- "topic": "Gatus",
- "content": message,
- }
- bodyParams := url.Values{}
- for field, value := range postData {
- bodyParams.Add(field, value)
- }
- return bodyParams.Encode()
+ return provider.DefaultConfig.Validate()
}
// Send an alert using the provider
func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) error {
- buffer := bytes.NewBufferString(provider.buildRequestBody(ep, alert, result, resolved))
- zulipEndpoint := fmt.Sprintf("https://%s/api/v1/messages", provider.Domain)
+ cfg, err := provider.GetConfig(ep.Group, alert)
+ if err != nil {
+ return err
+ }
+ buffer := bytes.NewBufferString(provider.buildRequestBody(cfg, ep, alert, result, resolved))
+ zulipEndpoint := fmt.Sprintf("https://%s/api/v1/messages", cfg.Domain)
request, err := http.NewRequest(http.MethodPost, zulipEndpoint, buffer)
if err != nil {
return err
}
- request.SetBasicAuth(provider.BotEmail, provider.BotAPIKey)
+ request.SetBasicAuth(cfg.BotEmail, cfg.BotAPIKey)
request.Header.Set("Content-Type", "application/x-www-form-urlencoded")
request.Header.Set("User-Agent", "Gatus")
response, err := client.GetHTTPClient(nil).Do(request)
@@ -124,7 +118,62 @@ func (provider *AlertProvider) Send(ep *endpoint.Endpoint, alert *alert.Alert, r
return nil
}
+// buildRequestBody builds the request body for the provider
+func (provider *AlertProvider) buildRequestBody(cfg *Config, ep *endpoint.Endpoint, alert *alert.Alert, result *endpoint.Result, resolved bool) string {
+ var message string
+ if resolved {
+ message = fmt.Sprintf("An alert for **%s** has been resolved after passing successfully %d time(s) in a row", ep.DisplayName(), alert.SuccessThreshold)
+ } else {
+ message = fmt.Sprintf("An alert for **%s** has been triggered due to having failed %d time(s) in a row", ep.DisplayName(), alert.FailureThreshold)
+ }
+ if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
+ message += "\n> " + alertDescription + "\n"
+ }
+ for _, conditionResult := range result.ConditionResults {
+ var prefix string
+ if conditionResult.Success {
+ prefix = ":check:"
+ } else {
+ prefix = ":cross_mark:"
+ }
+ message += fmt.Sprintf("\n%s - `%s`", prefix, conditionResult.Condition)
+ }
+ return url.Values{
+ "type": {"channel"},
+ "to": {cfg.ChannelID},
+ "topic": {"Gatus"},
+ "content": {message},
+ }.Encode()
+}
+
// GetDefaultAlert returns the provider's default alert configuration
func (provider *AlertProvider) GetDefaultAlert() *alert.Alert {
return provider.DefaultAlert
}
+
+// GetConfig returns the configuration for the provider with the overrides applied
+func (provider *AlertProvider) GetConfig(group string, alert *alert.Alert) (*Config, error) {
+ cfg := provider.DefaultConfig
+ // Handle group overrides
+ if provider.Overrides != nil {
+ for _, override := range provider.Overrides {
+ if group == override.Group {
+ cfg.Merge(&override.Config)
+ break
+ }
+ }
+ }
+ // Handle alert overrides
+ if len(alert.Override) != 0 {
+ overrideConfig := Config{}
+ if err := yaml.Unmarshal(alert.Override, &overrideConfig); err != nil {
+ return nil, err
+ }
+ cfg.Merge(&overrideConfig)
+ }
+ // Validate the configuration
+ if err := cfg.Validate(); err != nil {
+ return nil, err
+ }
+ return &cfg, nil
+}
diff --git a/alerting/provider/zulip/zulip_test.go b/alerting/provider/zulip/zulip_test.go
index 3d481ecd..8f59e21d 100644
--- a/alerting/provider/zulip/zulip_test.go
+++ b/alerting/provider/zulip/zulip_test.go
@@ -82,7 +82,7 @@ func TestAlertProvider_IsValid(t *testing.T) {
}
for _, tc := range testCase {
t.Run(tc.name, func(t *testing.T) {
- if tc.alertProvider.IsValid() != tc.expected {
+ if tc.alertProvider.Validate() != tc.expected {
t.Errorf("IsValid assertion failed (expected %v, got %v)", tc.expected, !tc.expected)
}
})
@@ -211,7 +211,7 @@ func TestAlertProvider_IsValidWithOverride(t *testing.T) {
}
for _, tc := range testCase {
t.Run(tc.name, func(t *testing.T) {
- if tc.alertProvider.IsValid() != tc.expected {
+ if tc.alertProvider.Validate() != tc.expected {
t.Errorf("IsValid assertion failed (expected %v, got %v)", tc.expected, !tc.expected)
}
})
diff --git a/config/config.go b/config/config.go
index 210752f4..b15485ab 100644
--- a/config/config.go
+++ b/config/config.go
@@ -421,7 +421,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi
for _, alertType := range alertTypes {
alertProvider := alertingConfig.GetAlertingProviderByAlertType(alertType)
if alertProvider != nil {
- if alertProvider.IsValid() {
+ if err := alertProvider.Validate(); err == nil {
// Parse alerts with the provider's default alert
if alertProvider.GetDefaultAlert() != nil {
for _, ep := range endpoints {
@@ -443,7 +443,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*endpoi
}
validProviders = append(validProviders, alertType)
} else {
- logr.Warnf("[config.validateAlertingConfig] Ignoring provider=%s because configuration is invalid", alertType)
+ logr.Warnf("[config.validateAlertingConfig] Ignoring provider=%s due to error=%s", alertType, err.Error())
invalidProviders = append(invalidProviders, alertType)
alertingConfig.SetAlertingProviderToNil(alertProvider)
}
diff --git a/watchdog/alerting_test.go b/watchdog/alerting_test.go
index ee7e2e5a..9ba8cb47 100644
--- a/watchdog/alerting_test.go
+++ b/watchdog/alerting_test.go
@@ -272,7 +272,7 @@ func TestHandleAlertingWithProviderThatReturnsAnError(t *testing.T) {
AlertType: alert.TypeDiscord,
AlertingConfig: &alerting.Config{
Discord: &discord.AlertProvider{
- Config: discord.Config{
+ DefaultConfig: discord.Config{
WebhookURL: "https://example.com",
},
},