From 5c6edc92ba997a20329ef5737df6dacc23eecfd9 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 30 Oct 2022 01:34:14 +0100 Subject: [PATCH 1/5] W.I.P Mattermost Integration - Added logic to config to map mattermost notifier - Added packages needed to send a message with mattermost notifier - Added client for interacting with mattermost - Added mattermost notifier case - Added mattermost notifier to README --- README.md | 50 +++++++++++++++ config/config.go | 35 +++++++++-- config/config_test.go | 46 ++++++++++++++ go.mod | 3 + go.sum | 6 ++ main.go | 16 +++++ notifier/mattermost/client.go | 84 ++++++++++++++++++++++++++ notifier/mattermost/client_test.go | 73 ++++++++++++++++++++++ notifier/mattermost/mattermost.go | 44 ++++++++++++++ notifier/mattermost/mattermost_test.go | 14 +++++ notifier/mattermost/notify.go | 70 +++++++++++++++++++++ notifier/mattermost/notify_test.go | 64 ++++++++++++++++++++ 12 files changed, 501 insertions(+), 4 deletions(-) create mode 100644 notifier/mattermost/client.go create mode 100644 notifier/mattermost/client_test.go create mode 100644 notifier/mattermost/mattermost.go create mode 100644 notifier/mattermost/mattermost_test.go create mode 100644 notifier/mattermost/notify.go create mode 100644 notifier/mattermost/notify_test.go diff --git a/README.md b/README.md index 8d4234c..46a96b6 100644 --- a/README.md +++ b/README.md @@ -313,6 +313,56 @@ terraform: ``` + +
+For Mattermost + +```yaml +--- +ci: circleci +notifier: + mattermost: + webhook: $MATTERMOST_WEBHOOK + channel: $MATTERMOST_CHANNEL + bot: $MATTERMOST_BOT_NAME +terraform: + fmt: + template: | + {{ .Title }} + + {{ .Message }} + + {{ .Result }} + + {{ .Body }} + plan: + template: | + {{ .Title }} [CI link]( {{ .Link }} ) + {{ .Message }} + {{if .Result}} +
{{ .Result }}
+      
+ {{end}} +
Details (Click me) + +
{{ .Body }}
+      
+ apply: + template: | + {{ .Title }} + {{ .Message }} + {{if .Result}} +
{{ .Result }}
+      
+ {{end}} +
Details (Click me) + +
{{ .Body }}
+      
+``` + +
+
For Slack diff --git a/config/config.go b/config/config.go index b80f79c..7c02c79 100644 --- a/config/config.go +++ b/config/config.go @@ -21,10 +21,11 @@ type Config struct { // Notifier is a notification notifier type Notifier struct { - Github GithubNotifier `yaml:"github"` - Gitlab GitlabNotifier `yaml:"gitlab"` - Slack SlackNotifier `yaml:"slack"` - Typetalk TypetalkNotifier `yaml:"typetalk"` + Github GithubNotifier `yaml:"github"` + Gitlab GitlabNotifier `yaml:"gitlab"` + Slack SlackNotifier `yaml:"slack"` + Typetalk TypetalkNotifier `yaml:"typetalk"` + Mattermost MattermostNotifier `yaml:"mattermost"` } // GithubNotifier is a notifier for GitHub @@ -54,6 +55,13 @@ type SlackNotifier struct { Bot string `yaml:"bot"` } +// MattermostNotifier is a notifier for Mattermost +type MattermostNotifier struct { + Webhook string `yaml:"webhook"` + Channel string `yaml:"channel"` + Bot string `yaml:"bot"` +} + // TypetalkNotifier is a notifier for Typetalk type TypetalkNotifier struct { Token string `yaml:"token"` @@ -172,6 +180,17 @@ func (cfg *Config) Validation() error { return fmt.Errorf("slack channel id is missing") } } + + if cfg.isDefinedMattermost() { + if cfg.Notifier.Mattermost.Channel == "" { + fmt.Println("mattermost channel id not provided.\nTargetting webhook's default channel") + } + + if cfg.Notifier.Mattermost.Webhook == "" { + return fmt.Errorf("mattermost webhook is missing") + } + } + if cfg.isDefinedTypetalk() { if cfg.Notifier.Typetalk.TopicID == "" { return fmt.Errorf("Typetalk topic id is missing") @@ -194,6 +213,11 @@ func (cfg *Config) isDefinedGitlab() bool { return cfg.Notifier.Gitlab != (GitlabNotifier{}) } +func (cfg *Config) isDefinedMattermost() bool { + // not empty + return cfg.Notifier.Mattermost != (MattermostNotifier{}) +} + func (cfg *Config) isDefinedSlack() bool { // not empty return cfg.Notifier.Slack != (SlackNotifier{}) @@ -212,6 +236,9 @@ func (cfg *Config) GetNotifierType() string { if cfg.isDefinedGitlab() { return "gitlab" } + if cfg.isDefinedMattermost() { + return "mattermost" + } if cfg.isDefinedSlack() { return "slack" } diff --git a/config/config_test.go b/config/config_test.go index 74a79e4..9799b0b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -41,6 +41,11 @@ func TestLoadFile(t *testing.T) { Token: "", TopicID: "", }, + Mattermost: MattermostNotifier{ + Webhook: "", + Channel: "", + Bot: "", + }, }, Terraform: Terraform{ Default: Default{ @@ -83,6 +88,11 @@ func TestLoadFile(t *testing.T) { Token: "", TopicID: "", }, + Mattermost: MattermostNotifier{ + Webhook: "", + Channel: "", + Bot: "", + }, }, Terraform: Terraform{ Default: Default{ @@ -137,6 +147,11 @@ func TestLoadFile(t *testing.T) { Token: "", TopicID: "", }, + Mattermost: MattermostNotifier{ + Webhook: "", + Channel: "", + Bot: "", + }, }, Terraform: Terraform{ Default: Default{ @@ -236,6 +251,14 @@ func TestValidation(t *testing.T) { contents: []byte("ci: circleci\nnotifier:\n github:\n token: token\n"), expected: "repository owner is missing", }, + { + contents: []byte("ci: circleci\nnotifier:\n mattermost:\n channel: test-channel\n"), + expected: "mattermost webhook is missing", + }, + { + contents: []byte("ci: circleci\nnotifier:\n mattermost:\n webhook: webhook\n"), + expected: "", + }, { contents: []byte(` ci: circleci @@ -270,6 +293,25 @@ notifier: { contents: []byte(` ci: circleci +notifier: + mattermost: + channel: channel +`), + expected: "mattermost webhook is missing", + }, + { + contents: []byte(` +ci: circleci +notifier: + mattermost: + webhook: webhook + channel: channel +`), + expected: "", + }, + { + contents: []byte(` +ci: circleci notifier: slack: token: token @@ -394,6 +436,10 @@ func TestGetNotifierType(t *testing.T) { contents: []byte("repository:\n owner: a\n name: b\nci: gitlabci\nnotifier:\n gitlab:\n token: token\n"), expected: "gitlab", }, + { + contents: []byte("repository:\n owner: a\n name: b\nci: circleci\nnotifier:\n mattermost:\n webhook: webhook\n"), + expected: "mattermost", + }, } for _, testCase := range testCases { cfg, err := helperLoadConfig(testCase.contents) diff --git a/go.mod b/go.mod index 14938af..f2a930b 100644 --- a/go.mod +++ b/go.mod @@ -3,12 +3,14 @@ module github.com/mercari/tfnotify go 1.12 require ( + github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 github.com/google/go-github v17.0.0+incompatible github.com/kr/pretty v0.2.0 // indirect github.com/lestrrat-go/slack v0.0.0-20190827134815-1aaae719550a github.com/mattn/go-colorable v0.1.4 github.com/mattn/go-isatty v0.0.12 // indirect github.com/nulab/go-typetalk v2.1.1+incompatible + github.com/parnurzeal/gorequest v0.2.16 // indirect github.com/urfave/cli v1.22.2 github.com/xanzy/go-gitlab v0.22.3 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect @@ -17,4 +19,5 @@ require ( golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.2.7 + moul.io/http2curl v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 658575d..88cd40a 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,7 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= +github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 h1:MIEURpsIpyLyy+dZ+GnL8T5P49Tco0ik9cYaUQNnAxE= +github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960/go.mod h1:97O1qkjJBHSSaWJxsTShRIeFy0HWiygk+jnugO9aX3I= github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSYgptZMwQh2aRr3LuazLJIa+Pg3Kc1ylSYVY= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= @@ -29,6 +31,8 @@ github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHX github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/nulab/go-typetalk v2.1.1+incompatible h1:XdLjQuNVh77Wp7Qu4w8fqFdptxozGyy+Cq7n7/uUvMw= github.com/nulab/go-typetalk v2.1.1+incompatible/go.mod h1:m2ResEyH1dMB+0oMK/iCwd/qzCdCT+WdncQILJo/7t4= +github.com/parnurzeal/gorequest v0.2.16 h1:T/5x+/4BT+nj+3eSknXmCTnEVGSzFzPGdpqmUVVZXHQ= +github.com/parnurzeal/gorequest v0.2.16/go.mod h1:3Kh2QUMJoqw3icWAecsyzkpY7UzRfDhbRdTjtNwNiUE= github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= @@ -75,3 +79,5 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.7 h1:VUgggvou5XRW9mHwD/yXxIYSMtY0zoKQf/v226p2nyo= gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= +moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= diff --git a/main.go b/main.go index 505c9d7..d431944 100644 --- a/main.go +++ b/main.go @@ -9,6 +9,7 @@ import ( "github.com/mercari/tfnotify/notifier" "github.com/mercari/tfnotify/notifier/github" "github.com/mercari/tfnotify/notifier/gitlab" + "github.com/mercari/tfnotify/notifier/mattermost" "github.com/mercari/tfnotify/notifier/slack" "github.com/mercari/tfnotify/notifier/typetalk" "github.com/mercari/tfnotify/terraform" @@ -151,6 +152,21 @@ func (t *tfnotify) Run() error { return err } notifier = client.Notify + case "mattermost": + client, err := mattermost.NewClient(mattermost.Config{ + Webhook: t.config.Notifier.Mattermost.Webhook, + Channel: t.config.Notifier.Mattermost.Channel, + Botname: t.config.Notifier.Mattermost.Bot, + Title: t.context.String("title"), + Message: t.context.String("message"), + CI: ci.URL, + Parser: t.parser, + Template: t.template, + }) + if err != nil { + return err + } + notifier = client.Notify case "slack": client, err := slack.NewClient(slack.Config{ Token: t.config.Notifier.Slack.Token, diff --git a/notifier/mattermost/client.go b/notifier/mattermost/client.go new file mode 100644 index 0000000..e548614 --- /dev/null +++ b/notifier/mattermost/client.go @@ -0,0 +1,84 @@ +package mattermost + +import ( + "errors" + "os" + "strings" + + "github.com/ashwanthkumar/slack-go-webhook" + "github.com/mercari/tfnotify/terraform" +) + +// EnvWebhook is Mattermost webhook +const EnvWebhook = "MATTERMOST_WEBHOOK" + +// EnvChannelID is Slack channel ID +const EnvChannelID = "SLACK_CHANNEL_ID" + +// EnvBotName is Slack bot name +const EnvBotName = "SLACK_BOT_NAME" + +// Client is a API client for Slack +type Client struct { + *slack.Payload + + Config Config + + common service + + Notify *NotifyService + + API API +} + +// Config is a configuration for Mattermost client +type Config struct { + Webhook string + Channel string + Botname string + Title string + Message string + CI string + Parser terraform.Parser + Template terraform.Template +} + +type service struct { + client *Client +} + +// NewClient returns Client initialized with Config +func NewClient(cfg Config) (*Client, error) { + webhook := cfg.Webhook + webhook = strings.TrimPrefix(webhook, "$") + if webhook == EnvWebhook { + webhook = os.Getenv(EnvWebhook) + } + if webhook == "" { + return &Client{}, errors.New("mattermost webhook is missing") + } + + channel := cfg.Channel + channel = strings.TrimPrefix(channel, "$") + if channel == EnvChannelID { + channel = os.Getenv(EnvChannelID) + } + + botname := cfg.Botname + botname = strings.TrimPrefix(botname, "$") + if botname == EnvBotName { + botname = os.Getenv(EnvBotName) + } + + c := &Client{ + Config: cfg, + } + c.common.client = c + c.Notify = (*NotifyService)(&c.common) + c.API = &Mattermost{ + Webhook: webhook, + Channel: channel, + Botname: botname, + } + return c, nil +} diff --git a/notifier/mattermost/client_test.go b/notifier/mattermost/client_test.go new file mode 100644 index 0000000..b998085 --- /dev/null +++ b/notifier/mattermost/client_test.go @@ -0,0 +1,73 @@ +package mattermost + +import ( + "os" + "testing" +) + +func TestNewClient(t *testing.T) { + mattermostWebhook := os.Getenv(EnvWebhook) + defer func() { + os.Setenv(EnvWebhook, mattermostWebhook) + }() + os.Setenv(EnvWebhook, "") + + testCases := []struct { + config Config + EnvWebhook string + expect string + }{ + { + // specify directly + config: Config{Webhook: "abcdefg"}, + EnvWebhook: "", + expect: "", + }, + { + // specify via env but not to be set env (part 1) + config: Config{Webhook: "MATTERMOST_WEBHOOK"}, + EnvWebhook: "", + expect: "mattermost webhook is missing", + }, + { + // specify via env (part 1) + config: Config{Webhook: "MATTERMOST_WEBHOOK"}, + EnvWebhook: "abcdefg", + expect: "", + }, + { + // specify via env but not to be set env (part 2) + config: Config{Webhook: "$MATTERMOST_WEBHOOK"}, + EnvWebhook: "", + expect: "mattermost webhook is missing", + }, + { + // specify via env (part 2) + config: Config{Webhook: "$MATTERMOST_WEBHOOK"}, + EnvWebhook: "abcdefg", + expect: "", + }, + { + // no specification (part 1) + config: Config{}, + EnvWebhook: "", + expect: "mattermost webhook is missing", + }, + { + // no specification (part 2) + config: Config{}, + EnvWebhook: "abcdefg", + expect: "mattermost webhook is missing", + }, + } + for _, testCase := range testCases { + os.Setenv(EnvWebhook, testCase.EnvWebhook) + _, err := NewClient(testCase.config) + if err == nil { + continue + } + if err.Error() != testCase.expect { + t.Errorf("got %q but want %q", err.Error(), testCase.expect) + } + } +} diff --git a/notifier/mattermost/mattermost.go b/notifier/mattermost/mattermost.go new file mode 100644 index 0000000..17d3c65 --- /dev/null +++ b/notifier/mattermost/mattermost.go @@ -0,0 +1,44 @@ +package mattermost + +import ( + "fmt" + + "github.com/ashwanthkumar/slack-go-webhook" +) + +// API is Mattermost API interface +type API interface { + ChatPostMessage(attachments []slack.Attachment) error +} + +// Mattermost represents the attribute information necessary for requesting Mattermost Webhook API +type Mattermost struct { + Webhook string + Channel string + Botname string +} + +// ChatPostMessage is a wrapper of https://pkg.go.dev/github.com/ashwanthkumar/slack-go-webhook#Send +func (m *Mattermost) ChatPostMessage(attachments []slack.Attachment) error { + + payload := slack.Payload{ + Username: func() string { + if m.Botname != "" { + return m.Botname + } else { + return "tfnotify" + } + }(), + Channel: m.Channel, + IconUrl: "https://docs.mattermost.com/_images/icon-76x76.png", + Attachments: attachments, + } + + errs := slack.Send(m.Webhook, "", payload) + if len(errs) > 0 { + _, err := fmt.Printf("error: %s\n", errs) + return err + } + + return nil +} diff --git a/notifier/mattermost/mattermost_test.go b/notifier/mattermost/mattermost_test.go new file mode 100644 index 0000000..35d073b --- /dev/null +++ b/notifier/mattermost/mattermost_test.go @@ -0,0 +1,14 @@ +package mattermost + +import ( + "github.com/ashwanthkumar/slack-go-webhook" +) + +type fakeAPI struct { + API + FakeChatPostMessage func(attachments []slack.Attachment) error +} + +func (f *fakeAPI) ChatPostMessage(attachments []slack.Attachment) error { + return f.FakeChatPostMessage(attachments) +} diff --git a/notifier/mattermost/notify.go b/notifier/mattermost/notify.go new file mode 100644 index 0000000..835f654 --- /dev/null +++ b/notifier/mattermost/notify.go @@ -0,0 +1,70 @@ +package mattermost + +import ( + "errors" + + "github.com/ashwanthkumar/slack-go-webhook" + "github.com/mercari/tfnotify/terraform" +) + +// NotifyService handles communication with the notification related +// methods of Slack API +type NotifyService service + +// mmString handles converting string to pointer +func mmString(s string) *string { + return &s +} + +// Notify posts comment optimized for notifications +func (m *NotifyService) Notify(body string) (exit int, err error) { + cfg := m.client.Config + parser := m.client.Config.Parser + template := m.client.Config.Template + + if cfg.Webhook == "" { + return terraform.ExitFail, errors.New("webhook is required") + } + + result := parser.Parse(body) + if result.Error != nil { + return result.ExitCode, result.Error + } + if result.Result == "" { + return result.ExitCode, result.Error + } + + color := "warning" + switch result.ExitCode { + case terraform.ExitPass: + color = "good" + case terraform.ExitFail: + color = "danger" + } + + template.SetValue(terraform.CommonTemplate{ + Title: cfg.Title, + Message: cfg.Message, + Result: result.Result, + Body: body, + Link: cfg.CI, + }) + text, err := template.Execute() + if err != nil { + return result.ExitCode, err + } + + var attachments []slack.Attachment + attachment := slack.Attachment{ + Color: mmString(color), + Fallback: mmString(text), + Footer: mmString(cfg.CI), + Text: mmString(text), + Title: mmString(template.GetValue().Title), + } + + attachments = append(attachments, attachment) + + err = m.client.API.ChatPostMessage(attachments) + return result.ExitCode, err +} diff --git a/notifier/mattermost/notify_test.go b/notifier/mattermost/notify_test.go new file mode 100644 index 0000000..435f112 --- /dev/null +++ b/notifier/mattermost/notify_test.go @@ -0,0 +1,64 @@ +package mattermost + +import ( + "testing" + + "github.com/ashwanthkumar/slack-go-webhook" + "github.com/mercari/tfnotify/terraform" +) + +func TestNotify(t *testing.T) { + testCases := []struct { + config Config + body string + exitCode int + ok bool + }{ + { + config: Config{ + Webhook: "webhook", + Channel: "channel", + Botname: "botname", + Message: "", + Parser: terraform.NewPlanParser(), + Template: terraform.NewPlanTemplate(terraform.DefaultPlanTemplate), + }, + body: "Plan: 1 to add", + exitCode: 0, + ok: true, + }, + { + config: Config{ + Webhook: "webhook", + Channel: "", + Botname: "botname", + Message: "", + Parser: terraform.NewPlanParser(), + Template: terraform.NewPlanTemplate(terraform.DefaultPlanTemplate), + }, + body: "Plan: 1 to add", + exitCode: 0, + ok: true, + }, + } + fake := fakeAPI{ + FakeChatPostMessage: func(attachments []slack.Attachment) error { + return nil + }, + } + + for _, testCase := range testCases { + client, err := NewClient(testCase.config) + if err != nil { + t.Fatal(err) + } + client.API = &fake + exitCode, err := client.Notify.Notify(testCase.body) + if (err == nil) != testCase.ok { + t.Errorf("got error %q", err) + } + if exitCode != testCase.exitCode { + t.Errorf("got %q but want %q", exitCode, testCase.exitCode) + } + } +} From eefd4109eaed4f9e8b9375810459621fca1bd10c Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 30 Oct 2022 04:21:45 +0000 Subject: [PATCH 2/5] - Updated message when channel not specified - Updated env vars name (slack > mattermost) --- config/config.go | 2 +- notifier/mattermost/client.go | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/config/config.go b/config/config.go index 7c02c79..16a87b7 100644 --- a/config/config.go +++ b/config/config.go @@ -183,7 +183,7 @@ func (cfg *Config) Validation() error { if cfg.isDefinedMattermost() { if cfg.Notifier.Mattermost.Channel == "" { - fmt.Println("mattermost channel id not provided.\nTargetting webhook's default channel") + fmt.Println("[info] mattermost channel id not provided. Targetting webhook's default channel") } if cfg.Notifier.Mattermost.Webhook == "" { diff --git a/notifier/mattermost/client.go b/notifier/mattermost/client.go index e548614..b064e85 100644 --- a/notifier/mattermost/client.go +++ b/notifier/mattermost/client.go @@ -12,13 +12,13 @@ import ( // EnvWebhook is Mattermost webhook const EnvWebhook = "MATTERMOST_WEBHOOK" -// EnvChannelID is Slack channel ID -const EnvChannelID = "SLACK_CHANNEL_ID" +// EnvChannelID is Mattermost channel ID +const EnvChannelID = "MATTERMOST_CHANNEL_ID" -// EnvBotName is Slack bot name -const EnvBotName = "SLACK_BOT_NAME" +// EnvBotName is Mattermost bot name +const EnvBotName = "MATTERMOST_BOT_NAME" -// Client is a API client for Slack +// Client is a API client for Mattermost type Client struct { *slack.Payload From 53ac7408fbe4a9f8e6ecfd029242bd103de6cbe2 Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Sun, 30 Oct 2022 08:51:11 +0000 Subject: [PATCH 3/5] - Updated template for mattermost in example - Enabled Markdown in payload - Added screenshot for Mattermost message --- README.md | 69 ++++++++++++++++++++++-------- misc/images/4.png | Bin 0 -> 27331 bytes notifier/mattermost/mattermost.go | 1 + 3 files changed, 53 insertions(+), 17 deletions(-) create mode 100644 misc/images/4.png diff --git a/README.md b/README.md index 46a96b6..792a87c 100644 --- a/README.md +++ b/README.md @@ -25,6 +25,8 @@ You can do this by using this command. + + ## Installation Grab the binary from GitHub Releases (Recommended) @@ -328,39 +330,72 @@ notifier: terraform: fmt: template: | - {{ .Title }} + {{if .Message}}**`Message`**: {{ .Message }}{{end}} + **`Context`**: [*(Click me) Explore the change(s) further*]( {{ .Link }} ) + + {{if .Result}} + ## Result + ``` + {{ .Result }} + ``` + {{end}} - {{ .Message }} + ## Details - {{ .Result }} + View the summary below + ``` {{ .Body }} + ``` + plan: template: | - {{ .Title }} [CI link]( {{ .Link }} ) - {{ .Message }} + {{if .Message}}**`Message`**: {{ .Message }}{{end}} + **`Context`**: [*(Click me) Explore the change(s) further*]( {{ .Link }} ) + {{if .Result}} -
{{ .Result }}
-      
+ ## Result + ``` + {{ .Result }} + ``` {{end}} -
Details (Click me) -
{{ .Body }}
-      
+ ## Details + + View the summary below + + ``` + {{ .Body }} + ``` + + when_destroy: + template: | + ## :warning: WARNING: Resource Deletion will happen :warning: + + This plan contains **resource deletion**. Please check the plan result very carefully! apply: template: | - {{ .Title }} - {{ .Message }} + {{if .Message}}**`Message`**: {{ .Message }}{{end}} + **`Context`**: [*(Click me) Explore the change(s) further*]( {{ .Link }} ) + {{if .Result}} -
{{ .Result }}
-      
+ ## Result + ``` + {{ .Result }} + ``` {{end}} -
Details (Click me) -
{{ .Body }}
-      
+ ## Details + + View the summary below + + ``` + {{ .Body }} + ``` ``` +> Note, for `notifier.mattermost.bot` to work override, you must ensure you enable application/ webhook overrides on your Mattermost system. If you do not, the webhook will, by default, take the name of its creator. If you cannot enable application/ webhook overrides, you might have to consider creating a dedicated `tfnotify` Mattermost user account. +
diff --git a/misc/images/4.png b/misc/images/4.png new file mode 100644 index 0000000000000000000000000000000000000000..e5058e80547a2c08511d7df3cd6153a3684680d7 GIT binary patch literal 27331 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 literal 0 HcmV?d00001 diff --git a/notifier/mattermost/mattermost.go b/notifier/mattermost/mattermost.go index 17d3c65..92b4b77 100644 --- a/notifier/mattermost/mattermost.go +++ b/notifier/mattermost/mattermost.go @@ -32,6 +32,7 @@ func (m *Mattermost) ChatPostMessage(attachments []slack.Attachment) error { Channel: m.Channel, IconUrl: "https://docs.mattermost.com/_images/icon-76x76.png", Attachments: attachments, + Markdown: true, } errs := slack.Send(m.Webhook, "", payload) From 514eaaac9eb08fb42d43f47cb980a048fe54490d Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 1 Nov 2022 06:05:22 +0000 Subject: [PATCH 4/5] Updated notify_test and fakeApi so we can verify that Notify handles the ChatPostMessage errors as we expect --- notifier/mattermost/mattermost_test.go | 4 +-- notifier/mattermost/notify_test.go | 44 ++++++++++++++++++++------ 2 files changed, 36 insertions(+), 12 deletions(-) diff --git a/notifier/mattermost/mattermost_test.go b/notifier/mattermost/mattermost_test.go index 35d073b..8b022ab 100644 --- a/notifier/mattermost/mattermost_test.go +++ b/notifier/mattermost/mattermost_test.go @@ -6,9 +6,9 @@ import ( type fakeAPI struct { API - FakeChatPostMessage func(attachments []slack.Attachment) error + ChatPostMessageError error } func (f *fakeAPI) ChatPostMessage(attachments []slack.Attachment) error { - return f.FakeChatPostMessage(attachments) + return f.ChatPostMessageError } diff --git a/notifier/mattermost/notify_test.go b/notifier/mattermost/notify_test.go index 435f112..fdcf652 100644 --- a/notifier/mattermost/notify_test.go +++ b/notifier/mattermost/notify_test.go @@ -1,18 +1,21 @@ package mattermost import ( + "fmt" "testing" - "github.com/ashwanthkumar/slack-go-webhook" "github.com/mercari/tfnotify/terraform" + "github.com/stretchr/testify/assert" ) func TestNotify(t *testing.T) { testCases := []struct { - config Config - body string - exitCode int - ok bool + config Config + body string + exitCode int + ok bool + fakeAPI *fakeAPI + expectedApiErrorString string }{ { config: Config{ @@ -26,6 +29,9 @@ func TestNotify(t *testing.T) { body: "Plan: 1 to add", exitCode: 0, ok: true, + fakeAPI: &fakeAPI{ + ChatPostMessageError: nil, + }, }, { config: Config{ @@ -39,11 +45,26 @@ func TestNotify(t *testing.T) { body: "Plan: 1 to add", exitCode: 0, ok: true, + fakeAPI: &fakeAPI{ + ChatPostMessageError: nil, + }, }, - } - fake := fakeAPI{ - FakeChatPostMessage: func(attachments []slack.Attachment) error { - return nil + { + config: Config{ + Webhook: "webhook", + Channel: "", + Botname: "botname", + Message: "", + Parser: terraform.NewPlanParser(), + Template: terraform.NewPlanTemplate(terraform.DefaultPlanTemplate), + }, + body: "Plan: 1 to add", + exitCode: 0, + ok: false, + fakeAPI: &fakeAPI{ + ChatPostMessageError: fmt.Errorf("500 Internal Server Error"), + }, + expectedApiErrorString: "500 Internal Server Error", }, } @@ -52,7 +73,7 @@ func TestNotify(t *testing.T) { if err != nil { t.Fatal(err) } - client.API = &fake + client.API = testCase.fakeAPI exitCode, err := client.Notify.Notify(testCase.body) if (err == nil) != testCase.ok { t.Errorf("got error %q", err) @@ -60,5 +81,8 @@ func TestNotify(t *testing.T) { if exitCode != testCase.exitCode { t.Errorf("got %q but want %q", exitCode, testCase.exitCode) } + if err != nil { + assert.EqualError(t, err, testCase.expectedApiErrorString) + } } } From 8e5532271676bbf2b34d0c61b01408ede1a371ff Mon Sep 17 00:00:00 2001 From: Leon Silcott Date: Tue, 1 Nov 2022 06:20:12 +0000 Subject: [PATCH 5/5] Added required packages --- go.mod | 3 +++ go.sum | 18 +++++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index f2a930b..b5edb4f 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.12 require ( github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 + github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 // indirect github.com/google/go-github v17.0.0+incompatible github.com/kr/pretty v0.2.0 // indirect github.com/lestrrat-go/slack v0.0.0-20190827134815-1aaae719550a @@ -11,6 +12,8 @@ require ( github.com/mattn/go-isatty v0.0.12 // indirect github.com/nulab/go-typetalk v2.1.1+incompatible github.com/parnurzeal/gorequest v0.2.16 // indirect + github.com/smartystreets/goconvey v1.7.2 // indirect + github.com/stretchr/testify v1.4.0 github.com/urfave/cli v1.22.2 github.com/xanzy/go-gitlab v0.22.3 golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa // indirect diff --git a/go.sum b/go.sum index 88cd40a..333af4d 100644 --- a/go.sum +++ b/go.sum @@ -7,13 +7,21 @@ github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d h1:U+s90UTSY github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819 h1:RIB4cRk+lBqKK3Oy0r2gRX4ui7tuhiZq2SuTtTCi0/0= +github.com/elazarl/goproxy v0.0.0-20221015165544-a0805db90819/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2 h1:dWB6v3RcOy03t/bUadywsbyrQwCqZeNIEX6M1OtSZOM= +github.com/elazarl/goproxy/ext v0.0.0-20190711103511-473e67f1d7d2/go.mod h1:gNh8nYJoAm43RfaxurUnxr+N1PwuFV3ZMl/efxlIlY8= github.com/golang/protobuf v1.2.0 h1:P3YflyNX/ehuJFLhxviNdFxQPkGK5cDcApsge1SqnvM= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/google/go-github v17.0.0+incompatible h1:N0LgJ1j65A7kfXrZnUDaYCs/Sf4rEjNlfyDHW9dolSY= github.com/google/go-github v17.0.0+incompatible/go.mod h1:zLgOLi98H3fifZn+44m+umXrS52loVEgC2AApnigrVQ= github.com/google/go-querystring v1.0.0 h1:Xkwi/a1rcvNg1PPYe5vI8GbeBY/jrVuDX5ASuANWTrk= github.com/google/go-querystring v1.0.0/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= github.com/kr/pretty v0.2.0/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= @@ -25,7 +33,6 @@ github.com/lestrrat-go/slack v0.0.0-20190827134815-1aaae719550a h1:1SkDXQ4ho+45i github.com/lestrrat-go/slack v0.0.0-20190827134815-1aaae719550a/go.mod h1:2wxkiO7lExZoef2wz5rt3Mt2IGSYzQchxuQoWV7mfRM= github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= -github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -37,10 +44,15 @@ github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= github.com/pkg/errors v0.8.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/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/russross/blackfriday/v2 v2.0.1 h1:lPqVAte+HuHNfhJ/0LC98ESWRz8afy9tM/0RK8m9o+Q= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/shurcooL/sanitized_anchor_name v1.0.0 h1:PdmoCO6wvbs+7yrJyMORt4/BmY5IYyJwS/kOiWx8mHo= github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= +github.com/smartystreets/assertions v1.2.0 h1:42S6lae5dvLc7BrLu/0ugRtcFVjoJNMC/N3yZFZkDFs= +github.com/smartystreets/assertions v1.2.0/go.mod h1:tcbTF8ujkAEcZ8TElKY+i30BzYlVhC/LOxJk7iOWnoo= +github.com/smartystreets/goconvey v1.7.2 h1:9RBaZCeXEQ3UselpuwUQHltGVXvdwm6cv1hgR6gDIPg= +github.com/smartystreets/goconvey v1.7.2/go.mod h1:Vw0tHAZW6lzCRk3xgdin6fKYcG+G3Pg9vgXWeJpQFMM= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -51,8 +63,8 @@ github.com/xanzy/go-gitlab v0.22.3/go.mod h1:t4Bmvnxj7k37S4Y17lfLx+nLqkf/oQwT2Ha golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181108082009-03003ca0c849/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e h1:bRhVy7zSSasaqNksaRZiA5EEI+Ei4I1nO5Jh72wfHlg= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa h1:F+8P+gmewFQYRk6JoLQLwjBCTu3mcIURZfNkVweuRKA= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/oauth2 v0.0.0-20181106182150-f42d05182288/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= @@ -64,12 +76,12 @@ golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223 h1:DH4skfRX4EBpamg7iV4ZlCpblAHI6s6TDM39bFZumv8= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200121082415-34d275377bf9 h1:N19i1HjUnR7TF7rMt8O4p3dLvqvmYyzB6ifMFmrbY50= golang.org/x/sys v0.0.0-20200121082415-34d275377bf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= google.golang.org/appengine v1.3.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0 h1:/wp5JvzpHIxhs/dumFmF7BXTf3Z+dd4uXta4kVyO508= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=