From 35cf634d89b79f29593218d9efa4b435d9e05b77 Mon Sep 17 00:00:00 2001 From: stefanprodan Date: Mon, 10 Feb 2020 11:39:57 +0200 Subject: [PATCH] Implement Discord notifier with Slack formatting --- pkg/notifier/discord.go | 81 ++++++++++++++++++++++++++++++++++++ pkg/notifier/discord_test.go | 49 ++++++++++++++++++++++ pkg/notifier/factory.go | 2 + pkg/notifier/slack.go | 19 ++++----- 4 files changed, 141 insertions(+), 10 deletions(-) create mode 100644 pkg/notifier/discord.go create mode 100644 pkg/notifier/discord_test.go diff --git a/pkg/notifier/discord.go b/pkg/notifier/discord.go new file mode 100644 index 00000000..f770493f --- /dev/null +++ b/pkg/notifier/discord.go @@ -0,0 +1,81 @@ +package notifier + +import ( + "errors" + "fmt" + "net/url" + "path" + "strings" +) + +// Discord holds the hook URL +type Discord struct { + URL string + Username string + Channel string +} + +// NewDiscord validates the URL and returns a Discord object +func NewDiscord(hookURL string, username string, channel string) (*Discord, error) { + webhook, err := url.ParseRequestURI(hookURL) + if err != nil { + return nil, fmt.Errorf("invalid Discord hook URL %s", hookURL) + } + + // use Slack formatting + // https://birdie0.github.io/discord-webhooks-guide/other/slack_formatting.html + if !strings.HasSuffix(hookURL, "/slack") { + webhook.Path = path.Join(webhook.Path, "slack") + hookURL = webhook.String() + } + + if username == "" { + return nil, errors.New("empty Discord username") + } + + if channel == "" { + return nil, errors.New("empty Discord channel") + } + + return &Discord{ + Channel: channel, + URL: hookURL, + Username: username, + }, nil +} + +// Post Discord message +func (s *Discord) Post(workload string, namespace string, message string, fields []Field, warn bool) error { + payload := SlackPayload{ + Channel: s.Channel, + Username: s.Username, + IconEmoji: ":rocket:", + } + + color := "good" + if warn { + color = "danger" + } + + sfields := make([]SlackField, 0, len(fields)) + for _, f := range fields { + sfields = append(sfields, SlackField{f.Name, f.Value, false}) + } + + a := SlackAttachment{ + Color: color, + AuthorName: fmt.Sprintf("%s.%s", workload, namespace), + Text: message, + MrkdwnIn: []string{"text"}, + Fields: sfields, + } + + payload.Attachments = []SlackAttachment{a} + + err := postMessage(s.URL, payload) + if err != nil { + return err + } + + return nil +} diff --git a/pkg/notifier/discord_test.go b/pkg/notifier/discord_test.go new file mode 100644 index 00000000..fa4acd9d --- /dev/null +++ b/pkg/notifier/discord_test.go @@ -0,0 +1,49 @@ +package notifier + +import ( + "encoding/json" + "io/ioutil" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestDiscord_Post(t *testing.T) { + fields := []Field{ + {Name: "name1", Value: "value1"}, + {Name: "name2", Value: "value2"}, + } + + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + b, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + var payload = SlackPayload{} + err = json.Unmarshal(b, &payload) + + if payload.Attachments[0].AuthorName != "podinfo.test" { + t.Fatal("wrong author name") + } + + if len(payload.Attachments[0].Fields) != len(fields) { + t.Fatal("wrong facts") + } + })) + defer ts.Close() + + discord, err := NewDiscord(ts.URL, "test", "test") + if err != nil { + t.Fatal(err) + } + + if !strings.HasSuffix(discord.URL, "/slack") { + t.Error("Invalid Discord URL, expected to have /slack prefix") + } + + err = discord.Post("podinfo", "test", "test", fields, true) + if err != nil { + t.Fatal(err) + } +} diff --git a/pkg/notifier/factory.go b/pkg/notifier/factory.go index d7ef2ec1..502522a8 100644 --- a/pkg/notifier/factory.go +++ b/pkg/notifier/factory.go @@ -18,6 +18,8 @@ func (f Factory) Notifier(provider string) (Interface, error) { switch { case provider == "slack": return NewSlack(f.URL, f.Username, f.Channel) + case provider == "discord": + return NewDiscord(f.URL, f.Username, f.Channel) case provider == "msteams": return NewMSTeams(f.URL) } diff --git a/pkg/notifier/slack.go b/pkg/notifier/slack.go index f4a3d5ce..f66eb8f5 100644 --- a/pkg/notifier/slack.go +++ b/pkg/notifier/slack.go @@ -8,10 +8,9 @@ import ( // Slack holds the hook URL type Slack struct { - URL string - Username string - Channel string - IconEmoji string + URL string + Username string + Channel string } // SlackPayload holds the channel and attachments @@ -55,18 +54,18 @@ func NewSlack(hookURL string, username string, channel string) (*Slack, error) { } return &Slack{ - Channel: channel, - URL: hookURL, - Username: username, - IconEmoji: ":rocket:", + Channel: channel, + URL: hookURL, + Username: username, }, nil } // Post Slack message func (s *Slack) Post(workload string, namespace string, message string, fields []Field, warn bool) error { payload := SlackPayload{ - Channel: s.Channel, - Username: s.Username, + Channel: s.Channel, + Username: s.Username, + IconEmoji: ":rocket:", } color := "good"