diff --git a/.github/workflows/development.yml b/.github/workflows/development.yml index 3a4091c0..7d8cfcdd 100644 --- a/.github/workflows/development.yml +++ b/.github/workflows/development.yml @@ -262,6 +262,10 @@ jobs: TEST_EMAIL: ${{ secrets.TEST_EMAIL }} GOTIFY_URL: ${{ secrets.GOTIFY_URL }} GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }} + SNS_TOKEN: ${{ secrets.SNS_TOKEN }} + SNS_SECRET: ${{ secrets.SNS_SECRET }} + SNS_REGION: ${{ secrets.SNS_REGION }} + SNS_TOPIC: ${{ secrets.SNS_TOPIC }} - name: Coveralls Testing Coverage run: | diff --git a/.github/workflows/master.yml b/.github/workflows/master.yml index 6cabf8c8..da7be637 100644 --- a/.github/workflows/master.yml +++ b/.github/workflows/master.yml @@ -262,6 +262,10 @@ jobs: TEST_EMAIL: ${{ secrets.TEST_EMAIL }} GOTIFY_URL: ${{ secrets.GOTIFY_URL }} GOTIFY_TOKEN: ${{ secrets.GOTIFY_TOKEN }} + SNS_TOKEN: ${{ secrets.SNS_TOKEN }} + SNS_SECRET: ${{ secrets.SNS_SECRET }} + SNS_REGION: ${{ secrets.SNS_REGION }} + SNS_TOPIC: ${{ secrets.SNS_TOPIC }} - name: Coveralls Testing Coverage run: | diff --git a/CHANGELOG.md b/CHANGELOG.md index 79e15210..ae167032 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - Modified Service Group failures on index page to show 90 days of failures - Modified Service view page, updated Latency and Ping charts, added failures below - Modified Service chart on index page to show ping data along with latency +- Added AWS SNS Notifier # 0.90.63 (08-17-2020) - Modified build process to use xgo for all arch builds diff --git a/frontend/src/API.js b/frontend/src/API.js index b878eb08..89e212c2 100644 --- a/frontend/src/API.js +++ b/frontend/src/API.js @@ -4,12 +4,17 @@ const qs = require('querystring'); axios.defaults.withCredentials = true const tokenKey = "statping_auth"; +const version = "0.90.64"; +const commit = "8b54ceb16f0e2ca6c4f5b8f0fe3b5cc2598dc594"; class Api { constructor() { } + version = () => version + commit = () => commit + async oauth() { const oauth = axios.get('api/oauth').then(response => (response.data)) return oauth diff --git a/frontend/src/pages/Help.vue b/frontend/src/pages/Help.vue index f2b4f75e..af66fff0 100755 --- a/frontend/src/pages/Help.vue +++ b/frontend/src/pages/Help.vue @@ -2275,7 +2275,7 @@ OluFxewsEO0QNDrfFb+0gnjYlnGqOFcZjUMXbDdY5oLSPtXohynuTK1qyQ== </div> <div class="text-center small text-dim" v-pre> -Automatically generated from Statping's Wiki on 2020-08-19 02:31:01.555206 +0000 UTC +Automatically generated from Statping's Wiki on 2020-08-20 04:46:47.972956 +0000 UTC </div> </div> diff --git a/go.mod b/go.mod index 0a8bf024..c4366a12 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.14 require ( github.com/GeertJohan/go.rice v1.0.0 + github.com/aws/aws-sdk-go v1.30.20 github.com/dgrijalva/jwt-go v3.2.0+incompatible github.com/fatih/structs v1.1.0 github.com/foomo/simplecert v1.7.5 diff --git a/notifiers/amazon_sns.go b/notifiers/amazon_sns.go new file mode 100644 index 00000000..fb21f1b3 --- /dev/null +++ b/notifiers/amazon_sns.go @@ -0,0 +1,150 @@ +package notifiers + +import ( + "fmt" + "github.com/aws/aws-sdk-go/aws" + "github.com/aws/aws-sdk-go/aws/credentials" + "github.com/aws/aws-sdk-go/aws/session" + "github.com/aws/aws-sdk-go/service/sns" + "github.com/statping/statping/types/null" + "time" + + "github.com/statping/statping/types/failures" + "github.com/statping/statping/types/notifications" + "github.com/statping/statping/types/notifier" + "github.com/statping/statping/types/services" +) + +var _ notifier.Notifier = (*amazonSNS)(nil) + +type amazonSNS struct { + *notifications.Notification +} + +func (g *amazonSNS) Select() *notifications.Notification { + return g.Notification +} + +func (g *amazonSNS) Valid(values notifications.Values) error { + return nil +} + +var AmazonSNS = &amazonSNS{¬ifications.Notification{ + Method: "amazon_sns", + Title: "Amazon SNS", + Description: "Use amazonSNS to receive push notifications. Add your amazonSNS URL and App Token to receive notifications.", + Author: "Hunter Long", + AuthorUrl: "https://github.com/hunterlong", + Icon: "amazon", + Delay: 5 * time.Second, + Limits: 60, + SuccessData: null.NewNullString(`{{.Service.Name}} is back online and was down for {{.Service.Downtime.Human}}`), + FailureData: null.NewNullString(`{{.Service.Name}} is offline and has been down for {{.Service.Downtime.Human}}`), + DataType: "html", + Form: []notifications.NotificationForm{{ + Type: "text", + Title: "AWS Access Token", + DbField: "api_key", + Placeholder: "AKPMED5XUXSEU3O5AB6M", + Required: true, + }, { + Type: "text", + Title: "AWS Secret Key", + DbField: "api_secret", + Placeholder: "39eAZODxEosHRgzLx173ttX9sCtJVOE8rzElRE9B", + Required: true, + }, { + Type: "text", + Title: "Region", + SmallText: "Amazon Region for SNS", + DbField: "var1", + Placeholder: "us-west-2", + Required: true, + }, { + Type: "text", + Title: "SNS Topic ARN", + SmallText: "The ARN of the Topic", + DbField: "host", + Placeholder: "arn:aws:sns:us-west-2:123456789012:YourTopic", + Required: true, + }}}, +} + +func valToAttr(val interface{}) *sns.MessageAttributeValue { + dataType := "String" + switch val.(type) { + case string, bool: + dataType = "String" + case int, int64, uint, uint64, uint32: + dataType = "Number" + } + return &sns.MessageAttributeValue{ + DataType: aws.String(dataType), + StringValue: aws.String(fmt.Sprintf("%v", val)), + } +} + +func messageAttributesSNS(s services.Service, f failures.Failure) map[string]*sns.MessageAttributeValue { + attr := make(map[string]*sns.MessageAttributeValue) + attr["service_id"] = valToAttr(s.Id) + attr["online"] = valToAttr(s.Online) + attr["downtime_milliseconds"] = valToAttr(s.Downtime().Milliseconds()) + if f.Id != 0 { + attr["failure_issue"] = valToAttr(f.Issue) + attr["failure_reason"] = valToAttr(f.Reason) + attr["failure_status_code"] = valToAttr(f.ErrorCode) + attr["failure_ping"] = valToAttr(f.PingTime) + } + return attr +} + +// Send will send a HTTP Post to the amazonSNS API. It accepts type: string +func (g *amazonSNS) sendMessage(msg string, s services.Service, f failures.Failure) (string, error) { + creds := credentials.NewStaticCredentials(g.ApiKey.String, g.ApiSecret.String, "") + c := aws.NewConfig() + c.Credentials = creds + c.Region = aws.String(g.Var1.String) + sess, err := session.NewSession(c) + if err != nil { + return "", err + } + + client := sns.New(sess) + input := &sns.PublishInput{ + Message: aws.String(msg), + TopicArn: aws.String(g.Host.String), + MessageAttributes: messageAttributesSNS(s, f), + } + + result, err := client.Publish(input) + if err != nil { + return "", err + } + + return result.String(), nil +} + +// OnFailure will trigger failing service +func (g *amazonSNS) OnFailure(s services.Service, f failures.Failure) (string, error) { + msg := ReplaceVars(g.FailureData.String, s, f) + return g.sendMessage(msg, s, f) +} + +// OnSuccess will trigger successful service +func (g *amazonSNS) OnSuccess(s services.Service) (string, error) { + msg := ReplaceVars(g.SuccessData.String, s, failures.Failure{}) + return g.sendMessage(msg, s, failures.Failure{}) +} + +// OnTest will test the amazonSNS notifier +func (g *amazonSNS) OnTest() (string, error) { + s := services.Example(true) + f := failures.Example() + msg := ReplaceVars(`This is a test SNS notification from Statping. Service: {{.Service.Name}} - Downtime: {{.Service.Downtime.Human}}`, s, f) + return g.sendMessage(msg, s, f) +} + +// OnSave will trigger when this notifier is saved +func (g *amazonSNS) OnSave() (string, error) { + return "", nil +} diff --git a/notifiers/amazon_sns_test.go b/notifiers/amazon_sns_test.go new file mode 100644 index 00000000..c9988c3e --- /dev/null +++ b/notifiers/amazon_sns_test.go @@ -0,0 +1,76 @@ +package notifiers + +import ( + "testing" + "time" + + "github.com/statping/statping/database" + "github.com/statping/statping/types/core" + "github.com/statping/statping/types/failures" + "github.com/statping/statping/types/notifications" + "github.com/statping/statping/types/null" + "github.com/statping/statping/types/services" + "github.com/statping/statping/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestAmazonSNSNotifier(t *testing.T) { + err := utils.InitLogs() + require.Nil(t, err) + snsToken := utils.Params.GetString("SNS_TOKEN") + snsSecret := utils.Params.GetString("SNS_SECRET") + snsRegion := utils.Params.GetString("SNS_REGION") + snsTopic := utils.Params.GetString("SNS_TOPIC") + + db, err := database.OpenTester() + require.Nil(t, err) + db.AutoMigrate(¬ifications.Notification{}) + notifications.SetDB(db) + core.Example() + + if snsToken == "" || snsSecret == "" || snsRegion == "" || snsTopic == "" { + t.Log("SNS notifier testing skipped, missing SNS_TOKEN, SNS_SECRET, SNS_REGION, SNS_TOPIC environment variables") + t.SkipNow() + } + + t.Run("Load SNS", func(t *testing.T) { + AmazonSNS.ApiKey = null.NewNullString(snsToken) + AmazonSNS.ApiSecret = null.NewNullString(snsSecret) + AmazonSNS.Var1 = null.NewNullString(snsRegion) + AmazonSNS.Host = null.NewNullString(snsTopic) + AmazonSNS.Delay = 15 * time.Second + AmazonSNS.Enabled = null.NewNullBool(true) + + Add(AmazonSNS) + + assert.Equal(t, "Hunter Long", AmazonSNS.Author) + assert.Equal(t, snsToken, AmazonSNS.ApiKey.String) + assert.Equal(t, snsSecret, AmazonSNS.ApiSecret.String) + }) + + t.Run("SNS Notifier Tester", func(t *testing.T) { + assert.True(t, AmazonSNS.CanSend()) + }) + + t.Run("SNS Notifier Tester OnSave", func(t *testing.T) { + _, err := AmazonSNS.OnSave() + assert.Nil(t, err) + }) + + t.Run("SNS OnFailure", func(t *testing.T) { + _, err := AmazonSNS.OnFailure(services.Example(false), failures.Example()) + assert.Nil(t, err) + }) + + t.Run("SNS OnSuccess", func(t *testing.T) { + _, err := AmazonSNS.OnSuccess(services.Example(true)) + assert.Nil(t, err) + }) + + t.Run("SNS Test", func(t *testing.T) { + _, err := AmazonSNS.OnTest() + assert.Nil(t, err) + }) + +} diff --git a/notifiers/mobile_test.go b/notifiers/mobile_test.go index 14029178..b53ee849 100644 --- a/notifiers/mobile_test.go +++ b/notifiers/mobile_test.go @@ -47,7 +47,7 @@ func TestMobileNotifier(t *testing.T) { Add(Mobile) assert.Equal(t, "Hunter Long", Mobile.Author) - assert.Equal(t, mobileToken, Mobile.Var1) + assert.Equal(t, mobileToken, Mobile.Var1.String) }) t.Run("Mobile Notifier Tester", func(t *testing.T) { diff --git a/notifiers/notifiers.go b/notifiers/notifiers.go index d2cc6680..4bed95f4 100644 --- a/notifiers/notifiers.go +++ b/notifiers/notifiers.go @@ -35,6 +35,7 @@ func InitNotifiers() { Pushover, statpingMailer, Gotify, + AmazonSNS, ) } diff --git a/notifiers/slack.go b/notifiers/slack.go index f476da66..bc552b9f 100644 --- a/notifiers/slack.go +++ b/notifiers/slack.go @@ -9,6 +9,7 @@ import ( "github.com/statping/statping/types/null" "github.com/statping/statping/types/services" "github.com/statping/statping/utils" + "regexp" "strings" "time" ) @@ -93,5 +94,10 @@ func (s *slack) OnSave() (string, error) { } func (s *slack) Valid(values notifications.Values) error { + regex := `https\:\/\/hooks\.slack\.com/services/[A-Z0-9]{9}/[A-Z0-9]{10}/[a-zA-Z0-9]{22}` + r := regexp.MustCompile(regex) + if !r.MatchString(values.Host) { + return errors.New("slack webhook does not match with expected regex " + regex) + } return nil } diff --git a/source/generate_help.go b/source/generate_help.go index 99ee9150..6bccd7cf 100644 --- a/source/generate_help.go +++ b/source/generate_help.go @@ -100,6 +100,7 @@ type Render struct { } func main() { + fmt.Println("RUNNING: ./source/generate_help.go") fmt.Println("\n\nGenerating Help.vue from Statping's Wiki") fmt.Println("Cloning ", wikiUrl) cmd := exec.Command("git", "clone", wikiUrl) diff --git a/source/generate_version.go b/source/generate_version.go new file mode 100644 index 00000000..db35378a --- /dev/null +++ b/source/generate_version.go @@ -0,0 +1,41 @@ +// +build ignore + +package main + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + "os/exec" + "regexp" + "strings" +) + +const replace = `const version = "[0-9]\.[0-9]{2}\.[0-9]{2}";` +const replaceCommit = `const commit = \"[a-z0-9]{40}\"\;` + +func main() { + fmt.Println("RUNNING: ./source/generate_version.go") + version, _ := ioutil.ReadFile("../version.txt") + apiJsFile, _ := ioutil.ReadFile("../frontend/src/API.js") + + w := bytes.NewBuffer(nil) + cmd := exec.Command("git", "rev-parse", "HEAD") + cmd.Stdout = w + cmd.Run() + gitCommit := strings.TrimSpace(w.String()) + + fmt.Println("git commit: ", gitCommit) + + replaceWith := `const version = "` + strings.TrimSpace(string(version)) + `";` + replaceCommitWith := `const commit = "` + gitCommit + `";` + + vRex := regexp.MustCompile(replace) + newApiFile := vRex.ReplaceAllString(string(apiJsFile), replaceWith) + cRex := regexp.MustCompile(replaceCommit) + newApiFile = cRex.ReplaceAllString(newApiFile, replaceCommitWith) + + fmt.Printf("Setting version %s to frontend/src/API.js\n", string(version)) + ioutil.WriteFile("../frontend/src/API.js", []byte(newApiFile), os.FileMode(0755)) +} diff --git a/source/source.go b/source/source.go index 40607612..15d54e7d 100644 --- a/source/source.go +++ b/source/source.go @@ -1,6 +1,7 @@ package source //go:generate go run generate_help.go +//go:generate go run generate_version.go import ( "fmt"