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)
+ }
+ }
+}