diff --git a/README.md b/README.md index 7344699..e5049bb 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) @@ -313,6 +315,89 @@ terraform: ``` + +
+For Mattermost + +```yaml +--- +ci: circleci +notifier: + mattermost: + webhook: $MATTERMOST_WEBHOOK + channel: $MATTERMOST_CHANNEL + bot: $MATTERMOST_BOT_NAME +terraform: + fmt: + template: | + {{if .Message}}**`Message`**: {{ .Message }}{{end}} + **`Context`**: [*(Click me) Explore the change(s) further*]( {{ .Link }} ) + + {{if .Result}} + ## Result + ``` + {{ .Result }} + ``` + {{end}} + + ## Details + + View the summary below + + ``` + {{ .Body }} + ``` + + plan: + template: | + {{if .Message}}**`Message`**: {{ .Message }}{{end}} + **`Context`**: [*(Click me) Explore the change(s) further*]( {{ .Link }} ) + + {{if .Result}} + ## Result + ``` + {{ .Result }} + ``` + {{end}} + + ## 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: | + {{if .Message}}**`Message`**: {{ .Message }}{{end}} + **`Context`**: [*(Click me) Explore the change(s) further*]( {{ .Link }} ) + + {{if .Result}} + ## Result + ``` + {{ .Result }} + ``` + {{end}} + + ## 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. + +
+
For Slack diff --git a/config/config.go b/config/config.go index 0933f4b..04d60f5 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"` @@ -178,6 +186,17 @@ func (cfg *Config) Validation() error { return fmt.Errorf("slack channel id is missing") } } + + if cfg.isDefinedMattermost() { + if cfg.Notifier.Mattermost.Channel == "" { + fmt.Println("[info] mattermost channel id not provided. Targetting 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") @@ -200,6 +219,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{}) @@ -218,6 +242,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 7ca7a99..4a9404b 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{ @@ -86,6 +91,11 @@ func TestLoadFile(t *testing.T) { Token: "", TopicID: "", }, + Mattermost: MattermostNotifier{ + Webhook: "", + Channel: "", + Bot: "", + }, }, Terraform: Terraform{ Default: Default{ @@ -143,6 +153,11 @@ func TestLoadFile(t *testing.T) { Token: "", TopicID: "", }, + Mattermost: MattermostNotifier{ + Webhook: "", + Channel: "", + Bot: "", + }, }, Terraform: Terraform{ Default: Default{ @@ -245,6 +260,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 @@ -279,6 +302,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 @@ -403,6 +445,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 cdd0f6d..5dad5b1 100644 --- a/go.mod +++ b/go.mod @@ -3,14 +3,19 @@ module github.com/mercari/tfnotify go 1.12 require ( + github.com/ashwanthkumar/slack-go-webhook v0.0.0-20200209025033-430dd4e66960 github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect + 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/pdebug v0.0.0-20210111095411-35b07dbf089b // indirect github.com/lestrrat-go/slack v0.0.0-20190827134815-1aaae719550a github.com/mattn/go-colorable v0.1.12 github.com/nulab/go-typetalk v2.1.1+incompatible + github.com/parnurzeal/gorequest v0.2.16 // indirect github.com/pkg/errors v0.9.1 // indirect + github.com/smartystreets/goconvey v1.7.2 // indirect + github.com/stretchr/testify v1.7.0 github.com/urfave/cli v1.22.9 github.com/xanzy/go-gitlab v0.69.0 golang.org/x/net v0.0.0-20220708220712-1185a9018129 // indirect @@ -19,4 +24,5 @@ require ( golang.org/x/time v0.0.0-20220609170525-579cf78fd858 // indirect gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15 // indirect gopkg.in/yaml.v2 v2.4.0 + moul.io/http2curl v1.0.0 // indirect ) diff --git a/go.sum b/go.sum index 6c6d032..95ff9e6 100644 --- a/go.sum +++ b/go.sum @@ -58,6 +58,8 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +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/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= @@ -81,6 +83,10 @@ github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/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/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= @@ -177,6 +183,8 @@ github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/Oth github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= +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/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -192,6 +200,8 @@ github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1: github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +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/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.0 h1:s5hAObm+yFO5uHYt5dYjxi2rXrsnmRpJx4OYvIWUaQs= @@ -210,6 +220,8 @@ github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9 github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 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/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -217,11 +229,16 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= +github.com/rogpeppe/go-charset v0.0.0-20180617210344-2471d30d28b4/go.mod h1:qgYeAmZ5ZIpBWTGllZSQnw97Dj+woV0toclVaRGI8pc= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= 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/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= @@ -450,6 +467,7 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3 golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -692,6 +710,8 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= +moul.io/http2curl v1.0.0 h1:6XwpyZOYsgZJrU8exnG87ncVkU1FVCcTRpwzOkTDUi8= +moul.io/http2curl v1.0.0/go.mod h1:f6cULg+e4Md/oW1cYmwW4IWQOVl2lGbmCNGOHvzX2kE= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/main.go b/main.go index 73cae13..e9f6466 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/misc/images/4.png b/misc/images/4.png new file mode 100644 index 0000000..e5058e8 Binary files /dev/null and b/misc/images/4.png differ diff --git a/notifier/mattermost/client.go b/notifier/mattermost/client.go new file mode 100644 index 0000000..b064e85 --- /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 Mattermost channel ID +const EnvChannelID = "MATTERMOST_CHANNEL_ID" + +// EnvBotName is Mattermost bot name +const EnvBotName = "MATTERMOST_BOT_NAME" + +// Client is a API client for Mattermost +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..92b4b77 --- /dev/null +++ b/notifier/mattermost/mattermost.go @@ -0,0 +1,45 @@ +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, + Markdown: true, + } + + 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..8b022ab --- /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 + ChatPostMessageError error +} + +func (f *fakeAPI) ChatPostMessage(attachments []slack.Attachment) error { + return f.ChatPostMessageError +} 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..fdcf652 --- /dev/null +++ b/notifier/mattermost/notify_test.go @@ -0,0 +1,88 @@ +package mattermost + +import ( + "fmt" + "testing" + + "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 + fakeAPI *fakeAPI + expectedApiErrorString string + }{ + { + 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, + fakeAPI: &fakeAPI{ + ChatPostMessageError: 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: true, + fakeAPI: &fakeAPI{ + ChatPostMessageError: 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", + }, + } + + for _, testCase := range testCases { + client, err := NewClient(testCase.config) + if err != nil { + t.Fatal(err) + } + client.API = testCase.fakeAPI + 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) + } + if err != nil { + assert.EqualError(t, err, testCase.expectedApiErrorString) + } + } +}