mirror of
https://github.com/TwiN/gatus.git
synced 2024-11-28 10:53:20 +01:00
feat(alerting): Add AWS SES Alerting Provider (#579)
* Add SES Provider * Formatting * Rename ses to aws-ses * Typo * Parse tag instead of type name * Use aws.slice to convert string array & rename awsses -> aws-ses * Rename type * Update README.md * Update alerting/config.go * Rename package aws-ses to awsses * Update README.md * PR comments --------- Co-authored-by: TwiN <twin@linux.com>
This commit is contained in:
parent
619b69f480
commit
802ad7ff8f
41
README.md
41
README.md
@ -48,6 +48,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga
|
|||||||
- [Storage](#storage)
|
- [Storage](#storage)
|
||||||
- [Client configuration](#client-configuration)
|
- [Client configuration](#client-configuration)
|
||||||
- [Alerting](#alerting)
|
- [Alerting](#alerting)
|
||||||
|
- [Configuring AWS SES alerts](#configuring-aws-ses-alerts)
|
||||||
- [Configuring Discord alerts](#configuring-discord-alerts)
|
- [Configuring Discord alerts](#configuring-discord-alerts)
|
||||||
- [Configuring Email alerts](#configuring-email-alerts)
|
- [Configuring Email alerts](#configuring-email-alerts)
|
||||||
- [Configuring GitHub alerts](#configuring-github-alerts)
|
- [Configuring GitHub alerts](#configuring-github-alerts)
|
||||||
@ -1044,6 +1045,46 @@ endpoints:
|
|||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
|
#### Configuring AWS SES alerts
|
||||||
|
| Parameter | Description | Default |
|
||||||
|
|:-------------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
|
| `alerting.aws-ses` | Settings for alerts of type `aws-ses` | `{}` |
|
||||||
|
| `alerting.aws-ses.access-key-id` | AWS Access Key ID | Optional `""` |
|
||||||
|
| `alerting.aws-ses.secret-access-key` | AWS Secret Access Key | Optional `""` |
|
||||||
|
| `alerting.aws-ses.region` | AWS Region | Required `""` |
|
||||||
|
| `alerting.aws-ses.from` | The Email address to send the emails from (should be registered in SES) | Required `""` |
|
||||||
|
| `alerting.aws-ses.to` | Comma separated list of email address to notify | Required `""` |
|
||||||
|
| `alerting.aws-ses.default-alert` | Default alert configuration. <br />See [Setting a default alert](#setting-a-default-alert) | N/A |
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
alerting:
|
||||||
|
aws-ses:
|
||||||
|
access-key-id: "..."
|
||||||
|
secret-access-key: "..."
|
||||||
|
region: "us-east-1"
|
||||||
|
from: "status@example.com"
|
||||||
|
to: "user@example.com"
|
||||||
|
|
||||||
|
endpoints:
|
||||||
|
- name: website
|
||||||
|
interval: 30s
|
||||||
|
url: "https://twin.sh/health"
|
||||||
|
conditions:
|
||||||
|
- "[STATUS] == 200"
|
||||||
|
- "[BODY].status == UP"
|
||||||
|
- "[RESPONSE_TIME] < 300"
|
||||||
|
alerts:
|
||||||
|
- type: aws-ses
|
||||||
|
failure-threshold: 5
|
||||||
|
send-on-resolved: true
|
||||||
|
description: "healthcheck failed"
|
||||||
|
```
|
||||||
|
|
||||||
|
If the `access-key-id` and `secret-access-key` are not defined Gatus will fall back to IAM authentication.
|
||||||
|
|
||||||
|
Make sure you have the ability to use `ses:SendEmail`.
|
||||||
|
|
||||||
|
|
||||||
#### Configuring custom alerts
|
#### Configuring custom alerts
|
||||||
| Parameter | Description | Default |
|
| Parameter | Description | Default |
|
||||||
|:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
|:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------|
|
||||||
|
@ -5,6 +5,9 @@ package alert
|
|||||||
type Type string
|
type Type string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
|
// TypeAWSSES is the Type for the awsses alerting provider
|
||||||
|
TypeAWSSES Type = "aws-ses"
|
||||||
|
|
||||||
// TypeCustom is the Type for the custom alerting provider
|
// TypeCustom is the Type for the custom alerting provider
|
||||||
TypeCustom Type = "custom"
|
TypeCustom Type = "custom"
|
||||||
|
|
||||||
|
@ -7,6 +7,7 @@ import (
|
|||||||
|
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider"
|
"github.com/TwiN/gatus/v5/alerting/provider"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/awsses"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
||||||
@ -28,6 +29,9 @@ import (
|
|||||||
|
|
||||||
// Config is the configuration for alerting providers
|
// Config is the configuration for alerting providers
|
||||||
type Config struct {
|
type Config struct {
|
||||||
|
// AWSSimpleEmailService is the configuration for the aws-ses alerting provider
|
||||||
|
AWSSimpleEmailService *awsses.AlertProvider `yaml:"aws-ses,omitempty"`
|
||||||
|
|
||||||
// Custom is the configuration for the custom alerting provider
|
// Custom is the configuration for the custom alerting provider
|
||||||
Custom *custom.AlertProvider `yaml:"custom,omitempty"`
|
Custom *custom.AlertProvider `yaml:"custom,omitempty"`
|
||||||
|
|
||||||
@ -85,7 +89,8 @@ func (config *Config) GetAlertingProviderByAlertType(alertType alert.Type) provi
|
|||||||
entityType := reflect.TypeOf(config).Elem()
|
entityType := reflect.TypeOf(config).Elem()
|
||||||
for i := 0; i < entityType.NumField(); i++ {
|
for i := 0; i < entityType.NumField(); i++ {
|
||||||
field := entityType.Field(i)
|
field := entityType.Field(i)
|
||||||
if strings.ToLower(field.Name) == string(alertType) {
|
tag := strings.Split(field.Tag.Get("yaml"), ",")[0]
|
||||||
|
if tag == string(alertType) {
|
||||||
fieldValue := reflect.ValueOf(config).Elem().Field(i)
|
fieldValue := reflect.ValueOf(config).Elem().Field(i)
|
||||||
if fieldValue.IsNil() {
|
if fieldValue.IsNil() {
|
||||||
return nil
|
return nil
|
||||||
|
165
alerting/provider/awsses/awsses.go
Normal file
165
alerting/provider/awsses/awsses.go
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
package awsses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/core"
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ses"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
CharSet = "UTF-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
// AlertProvider is the configuration necessary for sending an alert using AWS Simple Email Service
|
||||||
|
type AlertProvider struct {
|
||||||
|
AccessKeyID string `yaml:"access-key-id"`
|
||||||
|
SecretAccessKey string `yaml:"secret-access-key"`
|
||||||
|
Region string `yaml:"region"`
|
||||||
|
|
||||||
|
From string `yaml:"from"`
|
||||||
|
To string `yaml:"to"`
|
||||||
|
|
||||||
|
// 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"`
|
||||||
|
To string `yaml:"to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsValid returns whether the provider's configuration is valid
|
||||||
|
func (provider *AlertProvider) IsValid() bool {
|
||||||
|
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
|
||||||
|
}
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send an alert using the provider
|
||||||
|
func (provider *AlertProvider) Send(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) error {
|
||||||
|
sess, err := provider.CreateSesSession()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
svc := ses.New(sess)
|
||||||
|
subject, body := provider.buildMessageSubjectAndBody(endpoint, alert, result, resolved)
|
||||||
|
emails := strings.Split(provider.getToForGroup(endpoint.Group), ",")
|
||||||
|
|
||||||
|
input := &ses.SendEmailInput{
|
||||||
|
Destination: &ses.Destination{
|
||||||
|
ToAddresses: aws.StringSlice(emails),
|
||||||
|
},
|
||||||
|
Message: &ses.Message{
|
||||||
|
Body: &ses.Body{
|
||||||
|
Text: &ses.Content{
|
||||||
|
Charset: aws.String(CharSet),
|
||||||
|
Data: aws.String(body),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Subject: &ses.Content{
|
||||||
|
Charset: aws.String(CharSet),
|
||||||
|
Data: aws.String(subject),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Source: aws.String(provider.From),
|
||||||
|
}
|
||||||
|
_, err = svc.SendEmail(input)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
if aerr, ok := err.(awserr.Error); ok {
|
||||||
|
switch aerr.Code() {
|
||||||
|
case ses.ErrCodeMessageRejected:
|
||||||
|
fmt.Println(ses.ErrCodeMessageRejected, aerr.Error())
|
||||||
|
case ses.ErrCodeMailFromDomainNotVerifiedException:
|
||||||
|
fmt.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
|
||||||
|
case ses.ErrCodeConfigurationSetDoesNotExistException:
|
||||||
|
fmt.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
|
||||||
|
default:
|
||||||
|
fmt.Println(aerr.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Print the error, cast err to awserr.Error to get the Code and
|
||||||
|
// Message from an error.
|
||||||
|
fmt.Println(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// buildMessageSubjectAndBody builds the message subject and body
|
||||||
|
func (provider *AlertProvider) buildMessageSubjectAndBody(endpoint *core.Endpoint, alert *alert.Alert, result *core.Result, resolved bool) (string, string) {
|
||||||
|
var subject, message, results string
|
||||||
|
if resolved {
|
||||||
|
subject = fmt.Sprintf("[%s] Alert resolved", endpoint.DisplayName())
|
||||||
|
message = fmt.Sprintf("An alert for %s has been resolved after passing successfully %d time(s) in a row", endpoint.DisplayName(), alert.SuccessThreshold)
|
||||||
|
} else {
|
||||||
|
subject = fmt.Sprintf("[%s] Alert triggered", endpoint.DisplayName())
|
||||||
|
message = fmt.Sprintf("An alert for %s has been triggered due to having failed %d time(s) in a row", endpoint.DisplayName(), alert.FailureThreshold)
|
||||||
|
}
|
||||||
|
for _, conditionResult := range result.ConditionResults {
|
||||||
|
var prefix string
|
||||||
|
if conditionResult.Success {
|
||||||
|
prefix = "✅"
|
||||||
|
} else {
|
||||||
|
prefix = "❌"
|
||||||
|
}
|
||||||
|
results += fmt.Sprintf("%s %s\n", prefix, conditionResult.Condition)
|
||||||
|
}
|
||||||
|
var description string
|
||||||
|
if alertDescription := alert.GetDescription(); len(alertDescription) > 0 {
|
||||||
|
description = "\n\nAlert description: " + alertDescription
|
||||||
|
}
|
||||||
|
return subject, message + description + "\n\nCondition results:\n" + results
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) CreateSesSession() (*session.Session, error) {
|
||||||
|
config := &aws.Config{
|
||||||
|
Region: aws.String(provider.Region),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(provider.AccessKeyID) > 0 && len(provider.SecretAccessKey) > 0 {
|
||||||
|
config.Credentials = credentials.NewStaticCredentials(provider.AccessKeyID, provider.SecretAccessKey, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
return session.NewSession(config)
|
||||||
|
}
|
188
alerting/provider/awsses/awsses_test.go
Normal file
188
alerting/provider/awsses/awsses_test.go
Normal file
@ -0,0 +1,188 @@
|
|||||||
|
package awsses
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/core"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestAlertDefaultProvider_IsValid(t *testing.T) {
|
||||||
|
invalidProvider := AlertProvider{}
|
||||||
|
if invalidProvider.IsValid() {
|
||||||
|
t.Error("provider shouldn't have been valid")
|
||||||
|
}
|
||||||
|
invalidProviderWithOneKey := AlertProvider{From: "from@example.com", To: "to@example.com", AccessKeyID: "1"}
|
||||||
|
if invalidProviderWithOneKey.IsValid() {
|
||||||
|
t.Error("provider shouldn't have been valid")
|
||||||
|
}
|
||||||
|
validProvider := AlertProvider{From: "from@example.com", To: "to@example.com"}
|
||||||
|
if !validProvider.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
validProviderWithKeys := AlertProvider{From: "from@example.com", To: "to@example.com", AccessKeyID: "1", SecretAccessKey: "1"}
|
||||||
|
if !validProviderWithKeys.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_IsValidWithOverride(t *testing.T) {
|
||||||
|
providerWithInvalidOverrideGroup := AlertProvider{
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
To: "to@example.com",
|
||||||
|
Group: "",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if providerWithInvalidOverrideGroup.IsValid() {
|
||||||
|
t.Error("provider Group shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithInvalidOverrideTo := AlertProvider{
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
To: "",
|
||||||
|
Group: "group",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if providerWithInvalidOverrideTo.IsValid() {
|
||||||
|
t.Error("provider integration key shouldn't have been valid")
|
||||||
|
}
|
||||||
|
providerWithValidOverride := AlertProvider{
|
||||||
|
From: "from@example.com",
|
||||||
|
To: "to@example.com",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
To: "to@example.com",
|
||||||
|
Group: "group",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
if !providerWithValidOverride.IsValid() {
|
||||||
|
t.Error("provider should've been valid")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_buildRequestBody(t *testing.T) {
|
||||||
|
firstDescription := "description-1"
|
||||||
|
secondDescription := "description-2"
|
||||||
|
scenarios := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
Alert alert.Alert
|
||||||
|
Resolved bool
|
||||||
|
ExpectedSubject string
|
||||||
|
ExpectedBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "triggered",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &firstDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: false,
|
||||||
|
ExpectedSubject: "[endpoint-name] Alert triggered",
|
||||||
|
ExpectedBody: "An alert for endpoint-name has been triggered due to having failed 3 time(s) in a row\n\nAlert description: description-1\n\nCondition results:\n❌ [CONNECTED] == true\n❌ [STATUS] == 200\n",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "resolved",
|
||||||
|
Provider: AlertProvider{},
|
||||||
|
Alert: alert.Alert{Description: &secondDescription, SuccessThreshold: 5, FailureThreshold: 3},
|
||||||
|
Resolved: true,
|
||||||
|
ExpectedSubject: "[endpoint-name] Alert resolved",
|
||||||
|
ExpectedBody: "An alert for endpoint-name has been resolved after passing successfully 5 time(s) in a row\n\nAlert description: description-2\n\nCondition results:\n✅ [CONNECTED] == true\n✅ [STATUS] == 200\n",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for _, scenario := range scenarios {
|
||||||
|
t.Run(scenario.Name, func(t *testing.T) {
|
||||||
|
subject, body := scenario.Provider.buildMessageSubjectAndBody(
|
||||||
|
&core.Endpoint{Name: "endpoint-name"},
|
||||||
|
&scenario.Alert,
|
||||||
|
&core.Result{
|
||||||
|
ConditionResults: []*core.ConditionResult{
|
||||||
|
{Condition: "[CONNECTED] == true", Success: scenario.Resolved},
|
||||||
|
{Condition: "[STATUS] == 200", Success: scenario.Resolved},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
scenario.Resolved,
|
||||||
|
)
|
||||||
|
if subject != scenario.ExpectedSubject {
|
||||||
|
t.Errorf("expected subject to be %s, got %s", scenario.ExpectedSubject, subject)
|
||||||
|
}
|
||||||
|
if body != scenario.ExpectedBody {
|
||||||
|
t.Errorf("expected body to be %s, got %s", scenario.ExpectedBody, body)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_GetDefaultAlert(t *testing.T) {
|
||||||
|
if (AlertProvider{DefaultAlert: &alert.Alert{}}).GetDefaultAlert() == nil {
|
||||||
|
t.Error("expected default alert to be not nil")
|
||||||
|
}
|
||||||
|
if (AlertProvider{DefaultAlert: nil}).GetDefaultAlert() != nil {
|
||||||
|
t.Error("expected default alert to be nil")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAlertProvider_getToForGroup(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
Name string
|
||||||
|
Provider AlertProvider
|
||||||
|
InputGroup string
|
||||||
|
ExpectedOutput string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
To: "to@example.com",
|
||||||
|
Overrides: nil,
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
ExpectedOutput: "to@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-no-override-specify-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
To: "to@example.com",
|
||||||
|
Overrides: nil,
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
ExpectedOutput: "to@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-no-group-should-default",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
To: "to@example.com",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
To: "to01@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "",
|
||||||
|
ExpectedOutput: "to@example.com",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "provider-with-override-specify-group-should-override",
|
||||||
|
Provider: AlertProvider{
|
||||||
|
To: "to@example.com",
|
||||||
|
Overrides: []Override{
|
||||||
|
{
|
||||||
|
Group: "group",
|
||||||
|
To: "to01@example.com",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
InputGroup: "group",
|
||||||
|
ExpectedOutput: "to01@example.com",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -2,6 +2,7 @@ package provider
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/TwiN/gatus/v5/alerting/alert"
|
"github.com/TwiN/gatus/v5/alerting/alert"
|
||||||
|
"github.com/TwiN/gatus/v5/alerting/provider/awsses"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
"github.com/TwiN/gatus/v5/alerting/provider/custom"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
"github.com/TwiN/gatus/v5/alerting/provider/discord"
|
||||||
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
"github.com/TwiN/gatus/v5/alerting/provider/email"
|
||||||
@ -58,6 +59,7 @@ func ParseWithDefaultAlert(providerDefaultAlert, endpointAlert *alert.Alert) {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
// Validate interface implementation on compile
|
// Validate interface implementation on compile
|
||||||
|
_ AlertProvider = (*awsses.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*custom.AlertProvider)(nil)
|
_ AlertProvider = (*custom.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*discord.AlertProvider)(nil)
|
_ AlertProvider = (*discord.AlertProvider)(nil)
|
||||||
_ AlertProvider = (*email.AlertProvider)(nil)
|
_ AlertProvider = (*email.AlertProvider)(nil)
|
||||||
|
@ -361,6 +361,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
alertTypes := []alert.Type{
|
alertTypes := []alert.Type{
|
||||||
|
alert.TypeAWSSES,
|
||||||
alert.TypeCustom,
|
alert.TypeCustom,
|
||||||
alert.TypeDiscord,
|
alert.TypeDiscord,
|
||||||
alert.TypeGitHub,
|
alert.TypeGitHub,
|
||||||
|
2
go.mod
2
go.mod
@ -8,6 +8,7 @@ require (
|
|||||||
github.com/TwiN/gocache/v2 v2.2.0
|
github.com/TwiN/gocache/v2 v2.2.0
|
||||||
github.com/TwiN/health v1.6.0
|
github.com/TwiN/health v1.6.0
|
||||||
github.com/TwiN/whois v1.1.7
|
github.com/TwiN/whois v1.1.7
|
||||||
|
github.com/aws/aws-sdk-go v1.45.16
|
||||||
github.com/coreos/go-oidc/v3 v3.6.0
|
github.com/coreos/go-oidc/v3 v3.6.0
|
||||||
github.com/gofiber/fiber/v2 v2.49.2
|
github.com/gofiber/fiber/v2 v2.49.2
|
||||||
github.com/google/go-github/v48 v48.2.0
|
github.com/google/go-github/v48 v48.2.0
|
||||||
@ -38,6 +39,7 @@ require (
|
|||||||
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 // indirect
|
||||||
github.com/golang/protobuf v1.5.3 // indirect
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
github.com/google/go-querystring v1.1.0 // indirect
|
github.com/google/go-querystring v1.1.0 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
github.com/klauspost/compress v1.16.7 // indirect
|
github.com/klauspost/compress v1.16.7 // indirect
|
||||||
github.com/kr/text v0.2.0 // indirect
|
github.com/kr/text v0.2.0 // indirect
|
||||||
|
13
go.sum
13
go.sum
@ -10,6 +10,8 @@ github.com/TwiN/whois v1.1.7 h1:eGzLOrWhpYLAGXD8boXh0bBKllN/EmuBsLqTJT4tC/U=
|
|||||||
github.com/TwiN/whois v1.1.7/go.mod h1:VOJAH4+3chAik5gva5zxJNXv2voEHjMNCf1y07sqj9w=
|
github.com/TwiN/whois v1.1.7/go.mod h1:VOJAH4+3chAik5gva5zxJNXv2voEHjMNCf1y07sqj9w=
|
||||||
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/aws/aws-sdk-go v1.45.16 h1:spca2z7UJgoQ5V2fX6XiHDCj2E65kOJAfbUPozSkE24=
|
||||||
|
github.com/aws/aws-sdk-go v1.45.16/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI=
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc=
|
github.com/blend/go-sdk v1.20220411.3 h1:GFV4/FQX5UzXLPwWV03gP811pj7B8J2sbuq+GJQofXc=
|
||||||
@ -48,6 +50,10 @@ github.com/google/uuid v1.3.1 h1:KjJaJ9iWZ3jOFZIf1Lqf4laDRCasjl0BCmnEGxkdLb4=
|
|||||||
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
github.com/google/uuid v1.3.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062 h1:G1+wBT0dwjIrBdLy0MIG0i+E4CQxEnedHXdauJEIH6g=
|
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062 h1:G1+wBT0dwjIrBdLy0MIG0i+E4CQxEnedHXdauJEIH6g=
|
||||||
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
|
github.com/ishidawataru/sctp v0.0.0-20210707070123-9a39160e9062/go.mod h1:co9pwDoBCm1kGxawmb4sPq0cSIOOWNPT4KnHotMP1Zg=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg=
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8=
|
||||||
|
github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 h1:Z9n2FFNUXsshfwJMBgNA0RU6/i7WVaAegv3PtuIHPMs=
|
||||||
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8=
|
||||||
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
@ -69,6 +75,7 @@ github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zk
|
|||||||
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4=
|
||||||
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
|
github.com/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE=
|
||||||
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
|
github.com/miekg/dns v1.1.56/go.mod h1:cRm6Oo2C8TY9ZS/TqsSrseAcncm74lfK5G+ikN2SWWY=
|
||||||
|
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus-community/pro-bing v0.3.0 h1:SFT6gHqXwbItEDJhTkzPWVqU6CLEtqEfNAPp47RUON4=
|
github.com/prometheus-community/pro-bing v0.3.0 h1:SFT6gHqXwbItEDJhTkzPWVqU6CLEtqEfNAPp47RUON4=
|
||||||
@ -117,6 +124,7 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR
|
|||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||||
|
golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco=
|
||||||
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||||
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
golang.org/x/net v0.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8=
|
||||||
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
golang.org/x/net v0.15.0/go.mod h1:idbUs1IY1+zTqbi8yxTbhexhEEk5ur9LInksu6HrEpk=
|
||||||
@ -135,18 +143,21 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
golang.org/x/sys v0.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o=
|
||||||
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
|
golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||||
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
golang.org/x/term v0.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU=
|
||||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
||||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||||
|
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||||
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE=
|
||||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
@ -169,6 +180,8 @@ gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8
|
|||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||||
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
gopkg.in/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk=
|
||||||
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
gopkg.in/mail.v2 v2.3.1/go.mod h1:htwXN1Qh09vZJ1NVKxQqHPBaCBbzKhp5GzuJEA4VJWw=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
|
||||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
Loading…
Reference in New Issue
Block a user