From 802ad7ff8f1485d711c81bad140ccc930c218c44 Mon Sep 17 00:00:00 2001 From: Kevin Richter <1887585+beschoenen@users.noreply.github.com> Date: Thu, 26 Oct 2023 05:52:43 +0200 Subject: [PATCH] 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 --- README.md | 41 ++++++ alerting/alert/type.go | 3 + alerting/config.go | 7 +- alerting/provider/awsses/awsses.go | 165 +++++++++++++++++++++ alerting/provider/awsses/awsses_test.go | 188 ++++++++++++++++++++++++ alerting/provider/provider.go | 2 + config/config.go | 1 + go.mod | 2 + go.sum | 13 ++ 9 files changed, 421 insertions(+), 1 deletion(-) create mode 100644 alerting/provider/awsses/awsses.go create mode 100644 alerting/provider/awsses/awsses_test.go diff --git a/README.md b/README.md index c2ff37fe..56774a62 100644 --- a/README.md +++ b/README.md @@ -48,6 +48,7 @@ Have any feedback or questions? [Create a discussion](https://github.com/TwiN/ga - [Storage](#storage) - [Client configuration](#client-configuration) - [Alerting](#alerting) + - [Configuring AWS SES alerts](#configuring-aws-ses-alerts) - [Configuring Discord alerts](#configuring-discord-alerts) - [Configuring Email alerts](#configuring-email-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.
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 | Parameter | Description | Default | |:--------------------------------|:-------------------------------------------------------------------------------------------|:--------------| diff --git a/alerting/alert/type.go b/alerting/alert/type.go index c9386ccc..72e201bc 100644 --- a/alerting/alert/type.go +++ b/alerting/alert/type.go @@ -5,6 +5,9 @@ package alert type Type string const ( + // TypeAWSSES is the Type for the awsses alerting provider + TypeAWSSES Type = "aws-ses" + // TypeCustom is the Type for the custom alerting provider TypeCustom Type = "custom" diff --git a/alerting/config.go b/alerting/config.go index 32336635..6b35208d 100644 --- a/alerting/config.go +++ b/alerting/config.go @@ -7,6 +7,7 @@ import ( "github.com/TwiN/gatus/v5/alerting/alert" "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/discord" "github.com/TwiN/gatus/v5/alerting/provider/email" @@ -28,6 +29,9 @@ import ( // Config is the configuration for alerting providers 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 *custom.AlertProvider `yaml:"custom,omitempty"` @@ -85,7 +89,8 @@ func (config *Config) GetAlertingProviderByAlertType(alertType alert.Type) provi entityType := reflect.TypeOf(config).Elem() for i := 0; i < entityType.NumField(); 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) if fieldValue.IsNil() { return nil diff --git a/alerting/provider/awsses/awsses.go b/alerting/provider/awsses/awsses.go new file mode 100644 index 00000000..a5abee99 --- /dev/null +++ b/alerting/provider/awsses/awsses.go @@ -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) +} diff --git a/alerting/provider/awsses/awsses_test.go b/alerting/provider/awsses/awsses_test.go new file mode 100644 index 00000000..08c138a4 --- /dev/null +++ b/alerting/provider/awsses/awsses_test.go @@ -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) + } + }) + } +} diff --git a/alerting/provider/provider.go b/alerting/provider/provider.go index 65f6671e..ff82e501 100644 --- a/alerting/provider/provider.go +++ b/alerting/provider/provider.go @@ -2,6 +2,7 @@ package provider import ( "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/discord" "github.com/TwiN/gatus/v5/alerting/provider/email" @@ -58,6 +59,7 @@ func ParseWithDefaultAlert(providerDefaultAlert, endpointAlert *alert.Alert) { var ( // Validate interface implementation on compile + _ AlertProvider = (*awsses.AlertProvider)(nil) _ AlertProvider = (*custom.AlertProvider)(nil) _ AlertProvider = (*discord.AlertProvider)(nil) _ AlertProvider = (*email.AlertProvider)(nil) diff --git a/config/config.go b/config/config.go index 298ab932..d2824ca9 100644 --- a/config/config.go +++ b/config/config.go @@ -361,6 +361,7 @@ func validateAlertingConfig(alertingConfig *alerting.Config, endpoints []*core.E return } alertTypes := []alert.Type{ + alert.TypeAWSSES, alert.TypeCustom, alert.TypeDiscord, alert.TypeGitHub, diff --git a/go.mod b/go.mod index 69d7afea..dd2d5620 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/TwiN/gocache/v2 v2.2.0 github.com/TwiN/health v1.6.0 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/gofiber/fiber/v2 v2.49.2 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/protobuf v1.5.3 // 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/klauspost/compress v1.16.7 // indirect github.com/kr/text v0.2.0 // indirect diff --git a/go.sum b/go.sum index 48a8d84e..ce398e60 100644 --- a/go.sum +++ b/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/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs= 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/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 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/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/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/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= 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/miekg/dns v1.1.56 h1:5imZaSeoRNvpM9SzWNhEcP9QliKiz20/dA2QabIGVnE= 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/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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-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.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.15.0 h1:ugBLEUaxABaB5AJqW9enI0ACdci2RUd4eP51NTBvuJ8= 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-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.1.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.12.0 h1:CM0HF96J0hcLAwsHPJZjfdNzs0gftsLfgKt57wWHJ0o= 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-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.12.0 h1:/ZfYdc3zq+q02Rv9vGqTeSItdzZTSNDmfTi0mBAuidU= 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.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.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.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= 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/mail.v2 v2.3.1 h1:WYFn/oANrAGP2C0dcV6/pbkPzv8yGzqTjPmTeO7qoXk= 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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=