From cf374a03e25f9f6e10127ddfce4bba119ea815a8 Mon Sep 17 00:00:00 2001 From: Christoph Maser Date: Thu, 2 Nov 2023 16:30:15 +0100 Subject: [PATCH] feat: add rocketchat notifier Signed-off-by: Christoph Maser --- asset/assets_vfsdata.go | 4 +- config/config.go | 128 ++++++--- config/config_test.go | 117 +++++++- config/notifiers.go | 88 ++++++ config/receiver/receiver.go | 4 + ...nf.rocketchat-both-token-and-tokenfile.yml | 20 ++ ...ocketchat-both-tokenid-and-tokenidfile.yml | 20 ++ .../conf.rocketchat-default-token-file.yml | 22 ++ .../conf.rocketchat-default-token.yml | 22 ++ config/testdata/conf.rocketchat-no-token.yml | 19 ++ docs/configuration.md | 63 +++++ notify/notify.go | 1 + notify/rocketchat/rocketchat.go | 264 ++++++++++++++++++ notify/rocketchat/rocketchat_test.go | 66 +++++ template/default.tmpl | 8 + 15 files changed, 789 insertions(+), 57 deletions(-) create mode 100644 config/testdata/conf.rocketchat-both-token-and-tokenfile.yml create mode 100644 config/testdata/conf.rocketchat-both-tokenid-and-tokenidfile.yml create mode 100644 config/testdata/conf.rocketchat-default-token-file.yml create mode 100644 config/testdata/conf.rocketchat-default-token.yml create mode 100644 config/testdata/conf.rocketchat-no-token.yml create mode 100644 notify/rocketchat/rocketchat.go create mode 100644 notify/rocketchat/rocketchat_test.go diff --git a/asset/assets_vfsdata.go b/asset/assets_vfsdata.go index f74112ad01..6634f64fef 100644 --- a/asset/assets_vfsdata.go +++ b/asset/assets_vfsdata.go @@ -163,9 +163,9 @@ var Assets = func() http.FileSystem { "/templates/default.tmpl": &vfsgen۰CompressedFileInfo{ name: "default.tmpl", modTime: time.Date(1970, 1, 1, 0, 0, 1, 0, time.UTC), - uncompressedSize: 5951, + uncompressedSize: 6349, - compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x58\x4f\x6f\xbb\x46\x10\xbd\xf3\x29\x56\xce\x25\x3e\x98\xf4\x1c\x29\xaa\xa2\xaa\xed\x25\xaa\x2a\x47\xee\xa5\xaa\xd0\x1a\xc6\x64\xe3\xfd\x43\x76\x07\x27\x16\xe6\xbb\x57\x0b\xc4\x06\x2f\x38\x8b\xe3\xdf\xe9\xe7\x5b\xd8\xcc\xbc\x99\x7d\x6f\x98\x19\x5c\x14\x24\x81\x15\x93\x40\x26\x51\x44\x39\x68\x14\x54\xd2\x14\xf4\x84\x94\xe5\x63\xeb\xb9\x28\x08\xc8\x84\x94\x65\x30\xe8\xb2\x98\x3f\x59\xaf\xa2\x20\xe1\xef\x1f\x08\x5a\x52\xbe\x98\x3f\x91\xb2\xbc\xbb\xb9\xab\xec\xcc\xaf\x1a\x62\x60\x1b\xd0\x0f\xd6\x68\xde\x3c\x90\x1d\xc9\x35\x7f\xcb\x41\x6f\x6b\xf7\x26\x50\x37\x92\xc9\x97\xaf\x10\xa3\x8d\xf0\xaf\xf5\x7e\x46\x8a\xb9\x21\x3b\x82\x6a\x91\x65\xa0\x6b\x57\xb6\x22\xf0\xb6\xff\xe7\x64\xc5\x34\x93\xa9\xf5\xb9\xb7\x3e\xd5\x85\x4c\xf8\x47\x75\x4a\x76\x84\x83\x6c\x47\xfc\x8f\x58\xa3\x3f\xb5\xca\xb3\x27\xba\x04\x6e\xc2\x67\xa5\x11\x92\xbf\x29\xd3\x26\xfc\x87\xf2\x1c\x6c\xc0\x57\xc5\x24\x99\x10\x8b\x4a\xea\x90\x29\x92\x5b\x8b\x15\xfe\xa6\x84\x50\xb2\x76\x9e\x36\x67\x2d\xbc\x29\x29\xcb\xdb\xa2\x20\xef\x0c\x5f\xba\xc6\xe1\x1c\x84\xda\x40\x37\xfa\x5f\x54\x80\x69\x18\xed\x8b\xbe\x4f\x7c\xba\xff\x6b\x40\xa6\x04\x4c\xac\x59\x86\x4c\xc9\xc9\x09\x8e\x11\x3e\xb0\x96\x34\xe2\xcc\x60\x63\xaa\xa9\x4c\x81\x84\xa4\x2c\xeb\xbc\xee\x83\xc3\xa1\xcb\x93\x65\x65\x56\x11\x69\xd3\xb7\x4f\x0f\x64\x7f\x81\x26\xb1\x3a\xf8\xa3\x94\x0a\xa9\xcd\xa9\x03\xd9\x3a\x3e\x0f\xf7\x59\xe5\x3a\x86\xfb\x5a\x4c\x90\xa0\x29\x2a\x5d\x57\x62\xd0\x43\xd4\x49\x0a\x22\x41\xf5\x3a\x51\xef\xd2\xe1\x22\xf0\x25\xc3\x33\xeb\x60\x3c\x1d\xbe\xc8\x5e\x84\x04\xfd\x8c\x18\x4e\xe3\x75\x98\xc0\x8a\xe6\x1c\x43\x64\xc8\xa1\xa1\x02\x41\x64\x9c\x62\xf7\xe5\x0c\x87\x6a\xb0\x8b\x93\x1b\xdb\x1e\x44\x1f\x54\xb7\x09\x79\xe2\xad\x28\xe7\x4b\x1a\xaf\x1d\xbc\xde\xf4\x2d\x28\xd9\x91\xaf\x0c\x39\x93\x6b\xef\x0c\xe2\x26\x03\x96\x4c\xfc\x1c\x32\x0d\xb6\xd6\x3c\xad\x5b\x09\x9d\x64\xac\xea\xc1\x9e\x29\xb3\x58\x49\x10\xea\x95\x4d\xfc\xed\x73\xcd\x7d\x33\xf6\xbf\xdc\x4a\x29\xac\x27\x4e\xab\x06\xdb\xe6\x99\xbd\x5a\x92\xe3\x76\xef\xe2\x36\xb4\x71\xe5\xe8\x22\xc6\x9c\x81\xc4\xf3\x0b\x72\x08\xf1\x30\x15\xcf\xd3\xcc\xc5\x65\xd2\x20\x95\x31\x98\x1e\x5c\xa7\x83\x87\xc3\xac\xaa\xcc\xa4\x20\x19\xec\x81\x05\x18\x43\xd3\xf3\xde\x6f\x07\xcc\x55\xa8\x19\x78\x03\x0d\xad\x77\xc2\x05\x47\xf3\xb5\x33\xc0\xa7\xe4\x17\x32\xb3\x8d\xb3\x3a\x24\xf5\x61\xd5\x3a\x4f\x33\xd2\xdd\x02\xaa\x20\xb3\xd6\x8d\x7a\xe2\xcd\xc1\x28\xbe\x81\xe4\x28\xe2\xe7\xb1\x7f\xcc\x4f\x0f\x27\xea\xcc\x87\x52\x53\xf5\xf1\xf1\xd5\xd4\x51\xfd\x1d\xe2\x17\x8a\x63\x35\x0f\xae\xfa\x9d\xd0\xaf\xbd\x28\x2f\x34\x77\xf0\x7a\xf5\x19\x50\xfd\x48\x1f\x54\x91\x1d\x96\x83\x9d\xd4\x35\xcf\xa8\xc6\xed\x08\x7b\xa4\xa9\xaf\x35\x4d\x41\x62\x74\x3c\xe2\xba\xf5\xb5\x61\x31\x2a\xad\x32\x73\x28\x5b\xa4\x08\x51\xb7\xd0\xae\xb5\x34\xae\x17\xb8\xac\x82\x44\x86\xdb\x28\x61\x26\xe3\x74\x1b\x0d\x6c\x53\x5f\x37\x6e\x17\x59\x28\xc9\x50\x59\x42\x22\x54\x8a\x8f\x1c\x89\x9d\xd9\x95\x9b\x17\xb5\x01\x7d\x81\xfd\xd1\x81\xfa\xf1\xf5\x74\x99\x72\xf2\xaf\xa6\xcb\x15\x93\xbb\xd2\x9f\x62\xf2\xb0\xd3\x8d\x99\x29\xed\x6d\x4e\xb6\x5e\xf6\xc3\x67\xfa\xf8\x6f\x84\x16\xce\x55\xde\x31\xf2\xb6\x59\x44\xe0\x90\x6a\x2a\xfa\xa8\xfc\x69\x49\x49\x98\x89\x95\x4e\x2e\xd0\x88\x8e\x91\xae\xec\xda\x35\x61\x09\x1f\xd7\x57\xf7\xdb\x3c\x0a\x83\x40\x45\xbb\x99\x0a\x41\xf5\xf6\xac\x3a\x3d\xc6\x3a\xbf\xe2\x1d\xa4\xe6\xcb\xde\x47\xa6\x1b\x32\x4a\xa8\xd6\xcf\x6d\xdf\x56\x6c\x1f\xda\x57\xb3\x9e\xe0\x5f\x89\xf7\x7f\x00\x00\x00\xff\xff\xf6\x0e\x88\xb1\x3f\x17\x00\x00"), + compressedContent: []byte("\x1f\x8b\x08\x00\x00\x00\x00\x00\x02\xff\xec\x58\x4d\x6f\xe3\x36\x10\xbd\xeb\x57\x10\xde\xcb\xe6\x10\x6d\xcf\x01\x16\xc5\xa2\x68\x7b\x09\x8a\xc2\x41\x7a\x29\x0a\x81\x96\xc6\x0a\x63\x7e\x68\xc9\x91\x13\x43\xd6\x7f\x2f\x28\x69\x6d\xca\x94\x6c\x4a\x71\x4f\xf5\xcd\xa2\x67\xde\x0c\xdf\x1b\xcd\x50\xac\x2a\x92\xc1\x9a\x49\x20\x8b\x24\xa1\x1c\x34\x0a\x2a\x69\x0e\x7a\x41\xea\xfa\x9b\xf3\x5c\x55\x04\x64\x46\xea\x3a\x1a\x75\x79\x5e\x3e\x5a\xaf\xaa\x22\xf1\xaf\xef\x08\x5a\x52\xfe\xbc\x7c\x24\x75\xfd\xe5\xd3\x97\xc6\xce\xfc\xac\x21\x05\xb6\x05\xfd\xd5\x1a\x2d\xbb\x07\xb2\x27\xa5\xe6\xdf\x4b\xd0\xbb\xd6\xbd\x0b\xd4\x8f\x64\xca\xd5\x2b\xa4\x68\x23\xfc\x6d\xbd\x9f\x90\x62\x69\xc8\x9e\xa0\x7a\x2e\x0a\xd0\xad\x2b\x5b\x13\xf8\x7e\xf8\x73\xb1\x66\x9a\xc9\xdc\xfa\x3c\x58\x9f\x66\x43\x26\xfe\xad\x59\x25\x7b\xc2\x41\xba\x11\xff\x21\xd6\xe8\x77\xad\xca\xe2\x91\xae\x80\x9b\xf8\x49\x69\x84\xec\x4f\xca\xb4\x89\xff\xa2\xbc\x04\x1b\xf0\x55\x31\x49\x16\xc4\xa2\x92\x36\x64\x8e\xe4\xb3\xc5\x8a\x7f\x51\x42\x28\xd9\x3a\xdf\x75\x6b\x0e\xde\x1d\xa9\xeb\xcf\x55\x45\xde\x18\xbe\xf4\x8d\xe3\x25\x08\xb5\x85\x7e\xf4\x3f\xa8\x00\xd3\x31\x3a\x14\xfd\x90\xf8\xdd\xe1\xd7\x88\x4c\x19\x98\x54\xb3\x02\x99\x92\x8b\x33\x1c\x23\xbc\x63\x2b\x69\xc2\x99\xc1\xce\x54\x53\x99\x03\x89\x49\x5d\xb7\x79\x3d\x44\xc7\x45\x9f\x27\xcb\xca\x7d\x43\xa4\x4d\xdf\x3e\x7d\x25\x87\x0d\x74\x89\xb5\xc1\xbf\x49\xa9\x90\xda\x9c\x7a\x90\xce\xf2\x3c\xdc\x27\x55\xea\x14\x1e\x5a\x31\x41\x82\xa6\xa8\x74\x5b\x89\xd1\x00\x51\x67\x29\x48\x04\xd5\x9b\x4c\xbd\x49\x8f\x8b\x28\x94\x8c\xc0\xac\xa3\xe9\x74\x84\x22\x07\x11\x12\x0d\x33\x62\x38\x4d\x37\x71\x06\x6b\x5a\x72\x8c\x91\x21\x87\x8e\x0a\x04\x51\x70\x8a\xfd\x97\x33\x1e\xab\xc1\x3e\x4e\x69\x6c\x7b\x10\x43\x50\xfd\x26\x14\x88\xb7\xa6\x9c\xaf\x68\xba\xf1\xf0\x06\xd3\xb7\xa0\x64\x4f\x2e\x19\x72\x26\x37\xc1\x19\xa4\x5d\x06\x2c\x5b\x84\x39\x14\x1a\x6c\xad\x05\x5a\x3b\x09\x9d\x65\xac\xe9\xc1\x81\x29\xb3\x54\x49\x10\xea\x95\x2d\xc2\xed\x4b\xcd\x43\x33\x0e\xdf\xdc\x5a\x29\x6c\x27\x8e\x53\x83\xae\x79\x61\xb7\x96\x95\xb8\x3b\xb8\xf8\x0d\x6d\x5a\x39\xfa\x88\x29\x67\x20\x71\x7e\x41\x8e\x21\x1e\xa7\xe2\x3c\xcd\x7c\x5c\x26\x0d\x52\x99\x82\x19\xc0\xf5\x3a\x78\x3c\xce\xaa\x2a\x4c\x0e\x92\xc1\x01\x58\x80\x31\x34\x9f\xf7\x7e\x7b\x60\xbe\x42\xdd\xc0\x1b\x69\x68\x83\x13\x2e\x3a\x99\xaf\xbd\x01\x7e\x47\x7e\x22\xf7\xb6\x71\x36\x8b\xa4\x5d\x6c\x5a\xe7\x79\x46\xfa\xa7\x80\x26\xc8\xbd\xb3\xa3\x81\x78\x4b\x30\x8a\x6f\x21\x3b\x89\xf8\x63\x39\x3c\xe6\x0f\x0f\x2f\xea\x7d\x08\xa5\xa6\xe9\xe3\xd3\xab\xa9\xa7\xfa\x1b\xa4\x2f\x14\xa7\x6a\x1e\xdd\xf4\x3b\xa3\x9f\x7b\x50\x7e\xd6\xdc\xc3\x1b\xd4\x67\x44\xf5\x13\x7d\x50\x25\x76\x58\x8e\x76\x52\xdf\xbc\xa0\x1a\x77\x13\xec\x91\xe6\xa1\xd6\x34\x07\x89\xc9\xe9\x88\xeb\xd7\xd7\x96\xa5\xa8\xb4\x2a\xcc\xb1\x6c\x91\x22\x24\xfd\x42\xbb\xd5\xd2\xb4\x5e\xe0\xb3\x0a\x12\x19\xee\x92\x8c\x99\x82\xd3\x5d\x32\x72\x9a\xba\xdc\xb8\x7d\x64\xa1\x24\x43\x65\x09\x49\x50\x29\x3e\x71\x24\xf6\x66\x57\x69\x5e\xd4\x16\xf4\x15\xce\x8f\x1e\xd4\x7f\x5f\x4f\xd7\x29\xa7\xf0\x6a\xba\x5e\x31\xf9\x47\xfa\x73\x4c\x1e\xcf\x74\x53\x66\x8a\x7b\x9a\x93\xce\xcb\x7e\xfc\x4c\x9f\xfe\x8d\xe0\xe0\xdc\xe4\x9d\x22\xaf\xcb\x22\x02\x87\x5c\x53\x31\x44\xe5\xff\x96\x94\x8c\x99\x54\xe9\xec\x0a\x8d\xe8\x14\xe9\xc6\xae\x3d\x26\xac\xe0\xfd\xf6\xea\x7e\x98\x47\x61\x10\xa8\x70\x9b\xa9\x10\x54\xef\x66\xd5\xe9\x29\xd6\xfc\x8a\xf7\x90\xba\x2f\xfb\x10\x99\x3e\x91\x49\x42\x39\xd7\x6d\x1f\x56\xec\x10\x3a\x54\xb3\x81\xe0\x17\xc5\x73\x79\xd2\x2a\xdd\x00\xf6\x8f\xd7\xb3\x49\x1f\x00\xa3\x9c\x51\x33\xff\x82\x62\x2c\xbd\x0f\xdf\x2a\x0d\x00\x9f\xbf\x56\x1a\x70\xb8\x74\xb7\x34\x94\xbc\x77\xc1\xf4\x6f\x00\x00\x00\xff\xff\xd4\x6a\xf2\x42\xcd\x18\x00\x00"), }, "/templates/email.tmpl": &vfsgen۰CompressedFileInfo{ name: "email.tmpl", diff --git a/config/config.go b/config/config.go index b1760ea64b..1537d52a9b 100644 --- a/config/config.go +++ b/config/config.go @@ -258,6 +258,9 @@ func resolveFilepaths(baseDir string, cfg *Config) { for _, cfg := range receiver.MSTeamsConfigs { cfg.HTTPConfig.SetDirectory(baseDir) } + for _, cfg := range receiver.RocketchatConfigs { + cfg.HTTPConfig.SetDirectory(baseDir) + } } } @@ -353,6 +356,14 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("at most one of smtp_auth_password & smtp_auth_password_file must be configured") } + if c.Global.RocketchatToken != nil && len(c.Global.RocketchatTokenFile) > 0 { + return fmt.Errorf("at most one of rocketchat_token & rocketchat_token_file must be configured") + } + + if c.Global.RocketchatTokenID != nil && len(c.Global.RocketchatTokenIDFile) > 0 { + return fmt.Errorf("at most one of rocketchat_token_id & rocketchat_token_id_file must be configured") + } + names := map[string]struct{}{} for _, rcv := range c.Receivers { @@ -540,6 +551,28 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { return fmt.Errorf("no msteams webhook URL or URLFile provided") } } + for _, rocketchat := range rcv.RocketchatConfigs { + if rocketchat.HTTPConfig == nil { + rocketchat.HTTPConfig = c.Global.HTTPConfig + } + if rocketchat.APIURL == nil { + rocketchat.APIURL = c.Global.RocketchatAPIURL + } + if rocketchat.TokenID == nil && len(rocketchat.TokenIDFile) == 0 { + if c.Global.RocketchatTokenID == nil && len(c.Global.RocketchatTokenIDFile) == 0 { + return fmt.Errorf("no global Rocketchat TokenID set either inline or in a file") + } + rocketchat.TokenID = c.Global.RocketchatTokenID + rocketchat.TokenIDFile = c.Global.RocketchatTokenIDFile + } + if rocketchat.Token == nil && len(rocketchat.TokenFile) == 0 { + if c.Global.RocketchatToken == nil && len(c.Global.RocketchatTokenFile) == 0 { + return fmt.Errorf("no global Rocketchat Token set either inline or in a file") + } + rocketchat.Token = c.Global.RocketchatToken + rocketchat.TokenFile = c.Global.RocketchatTokenFile + } + } names[rcv.Name] = struct{}{} } @@ -633,14 +666,15 @@ func DefaultGlobalConfig() GlobalConfig { ResolveTimeout: model.Duration(5 * time.Minute), HTTPConfig: &defaultHTTPConfig, - SMTPHello: "localhost", - SMTPRequireTLS: true, - PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), - OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), - WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), - VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), - TelegramAPIUrl: mustParseURL("https://api.telegram.org"), - WebexAPIURL: mustParseURL("https://webexapis.com/v1/messages"), + SMTPHello: "localhost", + SMTPRequireTLS: true, + PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), + OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), + WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), + VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), + TelegramAPIUrl: mustParseURL("https://api.telegram.org"), + WebexAPIURL: mustParseURL("https://webexapis.com/v1/messages"), + RocketchatAPIURL: mustParseURL("https://open.rocket.chat/"), } } @@ -742,29 +776,34 @@ type GlobalConfig struct { HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` - SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` - SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` - SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` - SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` - SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` - SMTPAuthPasswordFile string `yaml:"smtp_auth_password_file,omitempty" json:"smtp_auth_password_file,omitempty"` - SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` - SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` - SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` - SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` - SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"` - PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` - OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` - OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` - OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"` - WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` - WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` - WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` - VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` - VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` - VictorOpsAPIKeyFile string `yaml:"victorops_api_key_file,omitempty" json:"victorops_api_key_file,omitempty"` - TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` - WebexAPIURL *URL `yaml:"webex_api_url,omitempty" json:"webex_api_url,omitempty"` + SMTPFrom string `yaml:"smtp_from,omitempty" json:"smtp_from,omitempty"` + SMTPHello string `yaml:"smtp_hello,omitempty" json:"smtp_hello,omitempty"` + SMTPSmarthost HostPort `yaml:"smtp_smarthost,omitempty" json:"smtp_smarthost,omitempty"` + SMTPAuthUsername string `yaml:"smtp_auth_username,omitempty" json:"smtp_auth_username,omitempty"` + SMTPAuthPassword Secret `yaml:"smtp_auth_password,omitempty" json:"smtp_auth_password,omitempty"` + SMTPAuthPasswordFile string `yaml:"smtp_auth_password_file,omitempty" json:"smtp_auth_password_file,omitempty"` + SMTPAuthSecret Secret `yaml:"smtp_auth_secret,omitempty" json:"smtp_auth_secret,omitempty"` + SMTPAuthIdentity string `yaml:"smtp_auth_identity,omitempty" json:"smtp_auth_identity,omitempty"` + SMTPRequireTLS bool `yaml:"smtp_require_tls" json:"smtp_require_tls,omitempty"` + SlackAPIURL *SecretURL `yaml:"slack_api_url,omitempty" json:"slack_api_url,omitempty"` + SlackAPIURLFile string `yaml:"slack_api_url_file,omitempty" json:"slack_api_url_file,omitempty"` + PagerdutyURL *URL `yaml:"pagerduty_url,omitempty" json:"pagerduty_url,omitempty"` + OpsGenieAPIURL *URL `yaml:"opsgenie_api_url,omitempty" json:"opsgenie_api_url,omitempty"` + OpsGenieAPIKey Secret `yaml:"opsgenie_api_key,omitempty" json:"opsgenie_api_key,omitempty"` + OpsGenieAPIKeyFile string `yaml:"opsgenie_api_key_file,omitempty" json:"opsgenie_api_key_file,omitempty"` + WeChatAPIURL *URL `yaml:"wechat_api_url,omitempty" json:"wechat_api_url,omitempty"` + WeChatAPISecret Secret `yaml:"wechat_api_secret,omitempty" json:"wechat_api_secret,omitempty"` + WeChatAPICorpID string `yaml:"wechat_api_corp_id,omitempty" json:"wechat_api_corp_id,omitempty"` + VictorOpsAPIURL *URL `yaml:"victorops_api_url,omitempty" json:"victorops_api_url,omitempty"` + VictorOpsAPIKey Secret `yaml:"victorops_api_key,omitempty" json:"victorops_api_key,omitempty"` + VictorOpsAPIKeyFile string `yaml:"victorops_api_key_file,omitempty" json:"victorops_api_key_file,omitempty"` + TelegramAPIUrl *URL `yaml:"telegram_api_url,omitempty" json:"telegram_api_url,omitempty"` + WebexAPIURL *URL `yaml:"webex_api_url,omitempty" json:"webex_api_url,omitempty"` + RocketchatAPIURL *URL `yaml:"rocketchat_api_url,omitempty" json:"rocketchat_api_url,omitempty"` + RocketchatToken *Secret `yaml:"rocketchat_token,omitempty" json:"rocketchat_token,omitempty"` + RocketchatTokenFile string `yaml:"rocketchat_token_file,omitempty" json:"rocketchat_token_file,omitempty"` + RocketchatTokenID *Secret `yaml:"rocketchat_token_id,omitempty" json:"rocketchat_token_id,omitempty"` + RocketchatTokenIDFile string `yaml:"rocketchat_token_id_file,omitempty" json:"rocketchat_token_id_file,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig. @@ -896,19 +935,20 @@ type Receiver struct { // A unique identifier for this receiver. Name string `yaml:"name" json:"name"` - DiscordConfigs []*DiscordConfig `yaml:"discord_configs,omitempty" json:"discord_configs,omitempty"` - EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` - PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` - SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` - WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` - OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"` - WechatConfigs []*WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"` - PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` - VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` - SNSConfigs []*SNSConfig `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"` - TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"` - WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"` - MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"` + DiscordConfigs []*DiscordConfig `yaml:"discord_configs,omitempty" json:"discord_configs,omitempty"` + EmailConfigs []*EmailConfig `yaml:"email_configs,omitempty" json:"email_configs,omitempty"` + PagerdutyConfigs []*PagerdutyConfig `yaml:"pagerduty_configs,omitempty" json:"pagerduty_configs,omitempty"` + SlackConfigs []*SlackConfig `yaml:"slack_configs,omitempty" json:"slack_configs,omitempty"` + WebhookConfigs []*WebhookConfig `yaml:"webhook_configs,omitempty" json:"webhook_configs,omitempty"` + OpsGenieConfigs []*OpsGenieConfig `yaml:"opsgenie_configs,omitempty" json:"opsgenie_configs,omitempty"` + WechatConfigs []*WechatConfig `yaml:"wechat_configs,omitempty" json:"wechat_configs,omitempty"` + PushoverConfigs []*PushoverConfig `yaml:"pushover_configs,omitempty" json:"pushover_configs,omitempty"` + VictorOpsConfigs []*VictorOpsConfig `yaml:"victorops_configs,omitempty" json:"victorops_configs,omitempty"` + SNSConfigs []*SNSConfig `yaml:"sns_configs,omitempty" json:"sns_configs,omitempty"` + TelegramConfigs []*TelegramConfig `yaml:"telegram_configs,omitempty" json:"telegram_configs,omitempty"` + WebexConfigs []*WebexConfig `yaml:"webex_configs,omitempty" json:"webex_configs,omitempty"` + MSTeamsConfigs []*MSTeamsConfig `yaml:"msteams_configs,omitempty" json:"msteams_configs,omitempty"` + RocketchatConfigs []*RocketchatConfig `yaml:"rocketchat_configs,omitempty" json:"rocketchat_configs,omitempty"` } // UnmarshalYAML implements the yaml.Unmarshaler interface for Receiver. diff --git a/config/config_test.go b/config/config_test.go index 7aba475f72..b10b70885b 100644 --- a/config/config_test.go +++ b/config/config_test.go @@ -857,17 +857,18 @@ func TestEmptyFieldsAndRegex(t *testing.T) { FollowRedirects: true, EnableHTTP2: true, }, - ResolveTimeout: model.Duration(5 * time.Minute), - SMTPSmarthost: HostPort{Host: "localhost", Port: "25"}, - SMTPFrom: "alertmanager@example.org", - SlackAPIURL: (*SecretURL)(mustParseURL("http://slack.example.com/")), - SMTPRequireTLS: true, - PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), - OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), - WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), - VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), - TelegramAPIUrl: mustParseURL("https://api.telegram.org"), - WebexAPIURL: mustParseURL("https://webexapis.com/v1/messages"), + ResolveTimeout: model.Duration(5 * time.Minute), + SMTPSmarthost: HostPort{Host: "localhost", Port: "25"}, + SMTPFrom: "alertmanager@example.org", + SlackAPIURL: (*SecretURL)(mustParseURL("http://slack.example.com/")), + SMTPRequireTLS: true, + PagerdutyURL: mustParseURL("https://events.pagerduty.com/v2/enqueue"), + OpsGenieAPIURL: mustParseURL("https://api.opsgenie.com/"), + WeChatAPIURL: mustParseURL("https://qyapi.weixin.qq.com/cgi-bin/"), + VictorOpsAPIURL: mustParseURL("https://alert.victorops.com/integrations/generic/20131114/alert/"), + TelegramAPIUrl: mustParseURL("https://api.telegram.org"), + WebexAPIURL: mustParseURL("https://webexapis.com/v1/messages"), + RocketchatAPIURL: mustParseURL("https://open.rocket.chat/"), }, Templates: []string{ @@ -1188,6 +1189,100 @@ func TestInvalidSNSConfig(t *testing.T) { } } +func TestRocketchatDefaultToken(t *testing.T) { + conf, err := LoadFile("testdata/conf.rocketchat-default-token.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf.rocketchat-default-token.yml", err) + } + + defaultToken := conf.Global.RocketchatToken + overrideToken := Secret("token456") + if defaultToken != conf.Receivers[0].RocketchatConfigs[0].Token { + t.Fatalf("Invalid rocketchat key: %s\nExpected: %s", string(*conf.Receivers[0].RocketchatConfigs[0].Token), string(*defaultToken)) + } + if overrideToken != *conf.Receivers[1].RocketchatConfigs[0].Token { + t.Errorf("Invalid rocketchat key: %s\nExpected: %s", string(*conf.Receivers[0].RocketchatConfigs[0].Token), string(overrideToken)) + } +} + +func TestRocketchatDefaultTokenID(t *testing.T) { + conf, err := LoadFile("testdata/conf.rocketchat-default-token.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf.rocketchat-default-token.yml", err) + } + + defaultTokenID := conf.Global.RocketchatTokenID + overrideTokenID := Secret("id456") + if defaultTokenID != conf.Receivers[0].RocketchatConfigs[0].TokenID { + t.Fatalf("Invalid rocketchat key: %s\nExpected: %s", string(*conf.Receivers[0].RocketchatConfigs[0].TokenID), string(*defaultTokenID)) + } + if overrideTokenID != *conf.Receivers[1].RocketchatConfigs[0].TokenID { + t.Errorf("Invalid rocketchat key: %s\nExpected: %s", string(*conf.Receivers[0].RocketchatConfigs[0].TokenID), string(overrideTokenID)) + } +} + +func TestRocketchatDefaultTokenFile(t *testing.T) { + conf, err := LoadFile("testdata/conf.rocketchat-default-token-file.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf.rocketchat-default-token-file.yml", err) + } + + defaultToken := conf.Global.RocketchatTokenFile + overrideToken := "/override_file" + if defaultToken != conf.Receivers[0].RocketchatConfigs[0].TokenFile { + t.Fatalf("Invalid Rocketchat key_file: %s\nExpected: %s", conf.Receivers[0].RocketchatConfigs[0].TokenFile, defaultToken) + } + if overrideToken != conf.Receivers[1].RocketchatConfigs[0].TokenFile { + t.Errorf("Invalid Rocketchat key_file: %s\nExpected: %s", conf.Receivers[0].RocketchatConfigs[0].TokenFile, overrideToken) + } +} + +func TestRocketchatDefaultIDTokenFile(t *testing.T) { + conf, err := LoadFile("testdata/conf.rocketchat-default-token-file.yml") + if err != nil { + t.Fatalf("Error parsing %s: %s", "testdata/conf.rocketchat-default-token-file.yml", err) + } + + defaultTokenID := conf.Global.RocketchatTokenIDFile + overrideTokenID := "/override_file" + if defaultTokenID != conf.Receivers[0].RocketchatConfigs[0].TokenIDFile { + t.Fatalf("Invalid Rocketchat key_file: %s\nExpected: %s", conf.Receivers[0].RocketchatConfigs[0].TokenIDFile, defaultTokenID) + } + if overrideTokenID != conf.Receivers[1].RocketchatConfigs[0].TokenIDFile { + t.Errorf("Invalid Rocketchat key_file: %s\nExpected: %s", conf.Receivers[0].RocketchatConfigs[0].TokenIDFile, overrideTokenID) + } +} + +func TestRocketchatBothTokenAndTokenFile(t *testing.T) { + _, err := LoadFile("testdata/conf.rocketchat-both-token-and-tokenfile.yml") + if err == nil { + t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.rocketchat-both-token-and-tokenfile.yml", err) + } + if err.Error() != "at most one of rocketchat_token & rocketchat_token_file must be configured" { + t.Errorf("Expected: %s\nGot: %s", "at most one of rocketchat_token & rocketchat_token_file must be configured", err.Error()) + } +} + +func TestRocketchatBothTokenIDAndTokenIDFile(t *testing.T) { + _, err := LoadFile("testdata/conf.rocketchat-both-tokenid-and-tokenidfile.yml") + if err == nil { + t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.rocketchat-both-tokenid-and-tokenidfile.yml", err) + } + if err.Error() != "at most one of rocketchat_token_id & rocketchat_token_id_file must be configured" { + t.Errorf("Expected: %s\nGot: %s", "at most one of rocketchat_token_id & rocketchat_token_id_file must be configured", err.Error()) + } +} + +func TestRocketchatNoToken(t *testing.T) { + _, err := LoadFile("testdata/conf.rocketchat-no-token.yml") + if err == nil { + t.Fatalf("Expected an error parsing %s: %s", "testdata/conf.rocketchat-no-token.yml", err) + } + if err.Error() != "no global Rocketchat Token set either inline or in a file" { + t.Errorf("Expected: %s\nGot: %s", "no global Rocketchat Token set either inline or in a file", err.Error()) + } +} + func TestUnmarshalHostPort(t *testing.T) { for _, tc := range []struct { in string diff --git a/config/notifiers.go b/config/notifiers.go index ee3a13d8fb..b1f9970cc0 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -97,6 +97,18 @@ var ( CallbackID: `{{ template "slack.default.callbackid" . }}`, Footer: `{{ template "slack.default.footer" . }}`, } + // DefaultRocketchatConfig defines default values for Rocketchat configurations. + DefaultRocketchatConfig = RocketchatConfig{ + NotifierConfig: NotifierConfig{ + VSendResolved: false, + }, + Color: `{{ if eq .Status "firing" }}red{{ else }}green{{ end }}`, + Emoji: `{{ template "rocketchat.default.emoji" . }}`, + IconURL: `{{ template "rocketchat.default.iconurl" . }}`, + Text: `{{ template "rocketchat.default.text" . }}`, + Title: `{{ template "rocketchat.default.title" . }}`, + TitleLink: `{{ template "rocketchat.default.titlelink" . }}`, + } // DefaultOpsGenieConfig defines default values for OpsGenie configurations. DefaultOpsGenieConfig = OpsGenieConfig{ @@ -825,3 +837,79 @@ func (c *MSTeamsConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { return nil } + +type RocketchatAttachmentField struct { + Short *bool `json:"short"` + Title string `json:"title,omitempty"` + Value string `json:"value,omitempty"` +} + +type RocketchatAttachmentActionType string + +const ( + RocketchatAttachmentActionTypeButton RocketchatAttachmentActionType = "button" +) + +type RocketchatMessageProcessingType string + +const ( + ProcessingTypeSendMessage RocketchatMessageProcessingType = "sendMessage" + ProcessingTypeRespondWithMessage RocketchatMessageProcessingType = "respondWithMessage" +) + +type RocketchatAttachmentAction struct { + Type RocketchatAttachmentActionType `json:"type,omitempty"` + Text string `json:"text,omitempty"` + URL string `json:"url,omitempty"` + ImageURL string `json:"image_url,omitempty"` + IsWebView bool `json:"is_webview"` + WebviewHeightRatio string `json:"webview_height_ratio,omitempty"` + Msg string `json:"msg,omitempty"` + MsgInChatWindow bool `json:"msg_in_chat_window"` + MsgProcessingType RocketchatMessageProcessingType `json:"msg_processing_type,omitempty"` +} + +// RocketchatConfig configures notifications via Rocketchat. +type RocketchatConfig struct { + NotifierConfig `yaml:",inline" json:",inline"` + + HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"` + + APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"` + TokenID *Secret `yaml:"token_id,omitempty" json:"token_id,omitempty"` + TokenIDFile string `yaml:"token_id_file,omitempty" json:"token_id_file,omitempty"` + Token *Secret `yaml:"token,omitempty" json:"token,omitempty"` + TokenFile string `yaml:"token_file,omitempty" json:"token_file,omitempty"` + + // RocketChat channel override, (like #other-channel or @username). + Channel string `yaml:"channel,omitempty" json:"channel,omitempty"` + + Color string `yaml:"color,omitempty" json:"color,omitempty"` + Title string `yaml:"title,omitempty" json:"title,omitempty"` + TitleLink string `yaml:"title_link,omitempty" json:"title_link,omitempty"` + Text string `yaml:"text,omitempty" json:"text,omitempty"` + Fields []*RocketchatAttachmentField `yaml:"fields,omitempty" json:"fields,omitempty"` + ShortFields bool `yaml:"short_fields" json:"short_fields,omitempty"` + Emoji string `yaml:"emoji,omitempty" json:"emoji,omitempty"` + IconURL string `yaml:"icon_url,omitempty" json:"icon_url,omitempty"` + ImageURL string `yaml:"image_url,omitempty" json:"image_url,omitempty"` + ThumbURL string `yaml:"thumb_url,omitempty" json:"thumb_url,omitempty"` + LinkNames bool `yaml:"link_names" json:"link_names,omitempty"` + Actions []*RocketchatAttachmentAction `yaml:"actions,omitempty" json:"actions,omitempty"` +} + +// UnmarshalYAML implements the yaml.Unmarshaler interface. +func (c *RocketchatConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { + *c = DefaultRocketchatConfig + type plain RocketchatConfig + if err := unmarshal((*plain)(c)); err != nil { + return err + } + if c.Token != nil && len(c.TokenFile) > 0 { + return fmt.Errorf("at most one of token & token_file must be configured") + } + if c.TokenID != nil && len(c.TokenIDFile) > 0 { + return fmt.Errorf("at most one of token_id & token_id_file must be configured") + } + return nil +} diff --git a/config/receiver/receiver.go b/config/receiver/receiver.go index 9bb039ef05..2fa6766d0c 100644 --- a/config/receiver/receiver.go +++ b/config/receiver/receiver.go @@ -26,6 +26,7 @@ import ( "github.com/prometheus/alertmanager/notify/opsgenie" "github.com/prometheus/alertmanager/notify/pagerduty" "github.com/prometheus/alertmanager/notify/pushover" + "github.com/prometheus/alertmanager/notify/rocketchat" "github.com/prometheus/alertmanager/notify/slack" "github.com/prometheus/alertmanager/notify/sns" "github.com/prometheus/alertmanager/notify/telegram" @@ -92,6 +93,9 @@ func BuildReceiverIntegrations(nc config.Receiver, tmpl *template.Template, logg for i, c := range nc.MSTeamsConfigs { add("msteams", i, c, func(l log.Logger) (notify.Notifier, error) { return msteams.New(c, tmpl, l, httpOpts...) }) } + for i, c := range nc.RocketchatConfigs { + add("rocketchat", i, c, func(l log.Logger) (notify.Notifier, error) { return rocketchat.New(c, tmpl, l, httpOpts...) }) + } if errs.Len() > 0 { return nil, &errs diff --git a/config/testdata/conf.rocketchat-both-token-and-tokenfile.yml b/config/testdata/conf.rocketchat-both-token-and-tokenfile.yml new file mode 100644 index 0000000000..b51cebde47 --- /dev/null +++ b/config/testdata/conf.rocketchat-both-token-and-tokenfile.yml @@ -0,0 +1,20 @@ +global: + rocketchat_token_file: /global_file + rocketchat_token: token123 +route: + group_by: ['alertname', 'cluster', 'service'] + group_wait: 30s + group_interval: 5m + repeat_interval: 3h + receiver: team-Y-rocketchat + routes: + - match: + service: foo + receiver: team-X-rocketchat +receivers: + - name: 'team-X-rocketchat' + rocketchat_configs: + - channel: '#team-X' + - name: 'team-Y-rocketchat' + rocketchat_configs: + - channel: '#team-Y' diff --git a/config/testdata/conf.rocketchat-both-tokenid-and-tokenidfile.yml b/config/testdata/conf.rocketchat-both-tokenid-and-tokenidfile.yml new file mode 100644 index 0000000000..702e0522c7 --- /dev/null +++ b/config/testdata/conf.rocketchat-both-tokenid-and-tokenidfile.yml @@ -0,0 +1,20 @@ +global: + rocketchat_token_id_file: /global_file + rocketchat_token_id: id123 +route: + group_by: ['alertname', 'cluster', 'service'] + group_wait: 30s + group_interval: 5m + repeat_interval: 3h + receiver: team-Y-rocketchat + routes: + - match: + service: foo + receiver: team-X-rocketchat +receivers: + - name: 'team-X-rocketchat' + rocketchat_configs: + - channel: '#team-X' + - name: 'team-Y-rocketchat' + rocketchat_configs: + - channel: '#team-Y' diff --git a/config/testdata/conf.rocketchat-default-token-file.yml b/config/testdata/conf.rocketchat-default-token-file.yml new file mode 100644 index 0000000000..c298422938 --- /dev/null +++ b/config/testdata/conf.rocketchat-default-token-file.yml @@ -0,0 +1,22 @@ +global: + rocketchat_token_file: /global_file + rocketchat_token_id_file: /etc/alertmanager/rocketchat_token_id +route: + group_by: ['alertname', 'cluster', 'service'] + group_wait: 30s + group_interval: 5m + repeat_interval: 3h + receiver: team-Y-rocketchat + routes: + - match: + service: foo + receiver: team-X-rocketchat +receivers: + - name: 'team-X-rocketchat' + rocketchat_configs: + - channel: '#team-X' + - name: 'team-Y-rocketchat' + rocketchat_configs: + - channel: '#team-Y' + token_file: /override_file + token_id_file: /override_file diff --git a/config/testdata/conf.rocketchat-default-token.yml b/config/testdata/conf.rocketchat-default-token.yml new file mode 100644 index 0000000000..01387472c8 --- /dev/null +++ b/config/testdata/conf.rocketchat-default-token.yml @@ -0,0 +1,22 @@ +global: + rocketchat_token: token123 + rocketchat_token_id: id123 +route: + group_by: ['alertname', 'cluster', 'service'] + group_wait: 30s + group_interval: 5m + repeat_interval: 3h + receiver: team-Y-rocketchat + routes: + - match: + service: foo + receiver: team-X-rocketchat +receivers: + - name: 'team-X-rocketchat' + rocketchat_configs: + - channel: '#team-X' + - name: 'team-Y-rocketchat' + rocketchat_configs: + - channel: '#team-Y' + token: token456 + token_id: id456 diff --git a/config/testdata/conf.rocketchat-no-token.yml b/config/testdata/conf.rocketchat-no-token.yml new file mode 100644 index 0000000000..ca44388828 --- /dev/null +++ b/config/testdata/conf.rocketchat-no-token.yml @@ -0,0 +1,19 @@ +global: + rocketchat_token_id: id123 +route: + group_by: ['alertname', 'cluster', 'service'] + group_wait: 30s + group_interval: 5m + repeat_interval: 3h + receiver: team-Y-rocketchat + routes: + - match: + service: foo + receiver: team-X-rocketchat +receivers: + - name: 'team-X-rocketchat' + rocketchat_configs: + - channel: '#team-X' + - name: 'team-Y-rocketchat' + rocketchat_configs: + - channel: '#team-Y' diff --git a/docs/configuration.md b/docs/configuration.md index 5a705110d4..c4215e66eb 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -105,6 +105,11 @@ global: [ opsgenie_api_key: ] [ opsgenie_api_key_file: ] [ opsgenie_api_url: | default = "https://api.opsgenie.com/" ] + [ rocketchat_api_url: | default = "https://open.rocket.chat/" ] + [ rocketchat_token: ] + [ rocketchat_token_file: ] + [ rocketchat_token_id: ] + [ rocketchat_token_id_file: ] [ wechat_api_url: | default = "https://qyapi.weixin.qq.com/cgi-bin/" ] [ wechat_api_secret: ] [ wechat_api_corp_id: ] @@ -701,6 +706,8 @@ pagerduty_configs: [ - , ... ] pushover_configs: [ - , ... ] +rocket_configs: + [ - , ... ] slack_configs: [ - , ... ] sns_configs: @@ -1154,6 +1161,62 @@ token_file: [ http_config: | default = global.http_config ] ``` +### `` + +Rocketchat notifications are sent via the [Rocketchat REST API](https://developer.rocket.chat/reference/api/rest-api/endpoints/messaging/chat-endpoints/postmessage). + +```yaml +# Whether to notify about resolved alerts. +[ send_resolved: | default = true ] +[ api_url: | default = global.rocketchat_api_url ] +[ channel: | default = global.rocketchat_api_url' ] + +# The sender token and token_id +# See https://docs.rocket.chat/use-rocket.chat/user-guides/user-panel/my-account#personal-access-tokens +# token and token_file are mutually exclusive. +# token_id and token_id_file are mutually exclusive. +token: +token_file: +token_id: +token_id_file: + + +[ color: ... ] +[ image_url ] +[ thumb_url ] +[ link_names ] +[ short_fields: | default = false ] +actions: + [ ... ] +``` + +#### `` + +The fields are documented in the [Rocketchat API documentation](https://developer.rocket.chat/reference/api/rest-api/endpoints/messaging/chat-endpoints/postmessage#attachment-field-objects). + +```yaml +[ title: ] +[ value: ] +[ short: | default = slack_config.short_fields ] +``` + +#### `` +The fields are documented in the [Rocketchat API api models](https://github.com/RocketChat/Rocket.Chat.Go.SDK/blob/master/models/message.go). + +```yaml +[ type: | ignored, only "button" is supported ] +[ text: ] +[ url: ] +[ msg: ] + ### `` Slack notifications can be sent via [Incoming webhooks](https://api.slack.com/messaging/webhooks) or [Bot tokens](https://api.slack.com/authentication/token-types). diff --git a/notify/notify.go b/notify/notify.go index d1065ab793..389612f336 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -365,6 +365,7 @@ func (m *Metrics) InitializeFor(receiver map[string][]Integration) { "discord", "webex", "msteams", + "rocketchat", } { m.numNotifications.WithLabelValues(integration) m.numNotificationRequestsTotal.WithLabelValues(integration) diff --git a/notify/rocketchat/rocketchat.go b/notify/rocketchat/rocketchat.go new file mode 100644 index 0000000000..a99db02aae --- /dev/null +++ b/notify/rocketchat/rocketchat.go @@ -0,0 +1,264 @@ +// Copyright 2022 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rocketchat + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "strings" + + "github.com/go-kit/log" + "github.com/go-kit/log/level" + commoncfg "github.com/prometheus/common/config" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify" + "github.com/prometheus/alertmanager/template" + "github.com/prometheus/alertmanager/types" +) + +const maxTitleLenRunes = 1024 + +type Notifier struct { + conf *config.RocketchatConfig + tmpl *template.Template + logger log.Logger + client *http.Client + retrier *notify.Retrier + token string + tokenID string + + postJSONFunc func(ctx context.Context, client *http.Client, url string, body io.Reader) (*http.Response, error) +} + +// PostMessage Payload for postmessage rest API +// +// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/ +type Attachment struct { + Title string `json:"title,omitempty"` + TitleLink string `json:"title_link,omitempty"` + Text string `json:"text,omitempty"` + ImageURL string `json:"image_url,omitempty"` + ThumbURL string `json:"thumb_url,omitempty"` + Color string `json:"color,omitempty"` + Fields []config.RocketchatAttachmentField `json:"fields,omitempty"` + Actions []config.RocketchatAttachmentAction `json:"actions,omitempty"` +} + +// PostMessage Payload for postmessage rest API +// +// https://rocket.chat/docs/developer-guides/rest-api/chat/postmessage/ +type PostMessage struct { + Channel string `json:"channel,omitempty"` + Text string `json:"text,omitempty"` + ParseUrls bool `json:"parseUrls,omitempty"` + Alias string `json:"alias,omitempty"` + Emoji string `json:"emoji,omitempty"` + Avatar string `json:"avatar,omitempty"` + Attachments []Attachment `json:"attachments,omitempty"` + Actions []config.RocketchatAttachmentAction `json:"actions,omitempty"` +} + +// New returns a new Rocketchat notification handler. +func New(c *config.RocketchatConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) { + client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "rocketchat", httpOpts...) + if err != nil { + return nil, err + } + token, err := getToken(c) + if err != nil { + return nil, err + } + tokenID, err := getTokenID(c) + if err != nil { + return nil, err + } + return &Notifier{ + conf: c, + tmpl: t, + logger: l, + client: client, + retrier: ¬ify.Retrier{}, + postJSONFunc: notify.PostJSON, + token: token, + tokenID: tokenID, + }, nil +} + +func getTokenID(c *config.RocketchatConfig) (string, error) { + if len(c.TokenIDFile) > 0 { + content, err := os.ReadFile(c.TokenIDFile) + if err != nil { + return "", fmt.Errorf("could not read %s: %w", c.TokenIDFile, err) + } + return strings.TrimSpace(string(content)), nil + } + return string(*c.TokenID), nil +} + +func getToken(c *config.RocketchatConfig) (string, error) { + if len(c.TokenFile) > 0 { + content, err := os.ReadFile(c.TokenFile) + if err != nil { + return "", fmt.Errorf("could not read %s: %w", c.TokenFile, err) + } + return strings.TrimSpace(string(content)), nil + } + return string(*c.Token), nil +} + +// Notify implements the Notifier interface. +func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { + var err error + + data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger) + tmplText := notify.TmplText(n.tmpl, data, &err) + if err != nil { + return false, err + } + title := tmplText(n.conf.Title) + if err != nil { + return false, err + } + + title, truncated := notify.TruncateInRunes(title, maxTitleLenRunes) + if truncated { + key, err := notify.ExtractGroupKey(ctx) + if err != nil { + return false, err + } + level.Warn(n.logger).Log("msg", "Truncated title", "key", key, "max_runes", maxTitleLenRunes) + } + att := &Attachment{ + Title: title, + TitleLink: tmplText(n.conf.TitleLink), + Text: tmplText(n.conf.Text), + ImageURL: tmplText(n.conf.ImageURL), + ThumbURL: tmplText(n.conf.ThumbURL), + Color: tmplText(n.conf.Color), + } + numFields := len(n.conf.Fields) + if numFields > 0 { + fields := make([]config.RocketchatAttachmentField, numFields) + for index, field := range n.conf.Fields { + // Check if short was defined for the field otherwise fallback to the global setting + var short bool + if field.Short != nil { + short = *field.Short + } else { + short = n.conf.ShortFields + } + + // Rebuild the field by executing any templates and setting the new value for short + fields[index] = config.RocketchatAttachmentField{ + Title: tmplText(field.Title), + Value: tmplText(field.Value), + Short: &short, + } + } + att.Fields = fields + } + numActions := len(n.conf.Actions) + if numActions > 0 { + actions := make([]config.RocketchatAttachmentAction, numActions) + for index, action := range n.conf.Actions { + rocketchatAction := config.RocketchatAttachmentAction{ + Type: "button", // Only button type is supported + Text: tmplText(action.Text), + URL: tmplText(action.URL), + Msg: tmplText(action.Msg), + } + + actions[index] = rocketchatAction + } + att.Actions = actions + } + + body := &PostMessage{ + Channel: n.conf.Channel, + Emoji: tmplText(n.conf.Emoji), + Avatar: tmplText(n.conf.IconURL), + Attachments: []Attachment{*att}, + } + if err != nil { + return false, err + } + + var buf bytes.Buffer + if err := json.NewEncoder(&buf).Encode(body); err != nil { + return false, err + } + req, err := http.NewRequest("POST", fmt.Sprintf("%s/%s", n.conf.APIURL.String(), "api/v1/chat.postMessage"), &buf) + req.Header.Set("X-Auth-Token", n.token) + req.Header.Set("X-User-Id", n.tokenID) + req.Header.Set("Content-Type", "application/json") + if err != nil { + return false, err + } + + resp, err := n.client.Do(req) + if err != nil { + return true, err + } + + // Use a retrier to generate an error message for non-200 responses and + // classify them as retriable or not. + retry, err := n.retrier.Check(resp.StatusCode, resp.Body) + if err != nil { + err = fmt.Errorf("%w: channel %q", err, body.Channel) + return retry, notify.NewErrorWithReason(notify.GetFailureReasonFromStatusCode(resp.StatusCode), err) + } + + // Rocketchat web API might return errors with a 200 response code. + retry, err = checkResponseError(resp) + if err != nil { + err = fmt.Errorf("%w: channel %q", err, body.Channel) + return retry, notify.NewErrorWithReason(notify.ClientErrorReason, err) + } + + return retry, nil +} + +// checkResponseError parses out the error message from Rocketchat API response. +func checkResponseError(resp *http.Response) (bool, error) { + body, err := io.ReadAll(resp.Body) + if err != nil { + return true, fmt.Errorf("%w: could not read response body: ", err) + } + + return checkJSONResponseError(body) +} + +// checkJSONResponseError classifies JSON responses from Rocketchat. +func checkJSONResponseError(body []byte) (bool, error) { + // response is for parsing out errors from the JSON response. + type response struct { + Success bool `json:"success"` + Error string `json:"error"` + } + + var data response + if err := json.Unmarshal(body, &data); err != nil { + return true, fmt.Errorf("%w: could not unmarshal JSON response %q", err, string(body)) + } + if !data.Success { + return false, fmt.Errorf("error response from Rocketchat: %s", data.Error) + } + return false, nil +} diff --git a/notify/rocketchat/rocketchat_test.go b/notify/rocketchat/rocketchat_test.go new file mode 100644 index 0000000000..35033d3db7 --- /dev/null +++ b/notify/rocketchat/rocketchat_test.go @@ -0,0 +1,66 @@ +// Copyright 2019 Prometheus Team +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rocketchat + +import ( + "fmt" + "net/url" + "os" + "testing" + + "github.com/go-kit/log" + commoncfg "github.com/prometheus/common/config" + "github.com/stretchr/testify/require" + + "github.com/prometheus/alertmanager/config" + "github.com/prometheus/alertmanager/notify/test" +) + +func TestRocketchatRetry(t *testing.T) { + secret := config.Secret("xxxxx") + notifier, err := New( + &config.RocketchatConfig{ + HTTPConfig: &commoncfg.HTTPClientConfig{}, + Token: &secret, + TokenID: &secret, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) + + for statusCode, expected := range test.RetryTests(test.DefaultRetryCodes()) { + actual, _ := notifier.retrier.Check(statusCode, nil) + require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode)) + } +} + +func TestGettingRocketchatTokenFromFile(t *testing.T) { + f, err := os.CreateTemp("", "rocketchat_test") + require.NoError(t, err, "creating temp file failed") + _, err = f.WriteString("secret") + require.NoError(t, err, "writing to temp file failed") + + _, err = New( + &config.RocketchatConfig{ + TokenFile: f.Name(), + TokenIDFile: f.Name(), + HTTPConfig: &commoncfg.HTTPClientConfig{}, + APIURL: &config.URL{URL: &url.URL{Scheme: "http", Host: "example.com", Path: "/api/v1/"}}, + }, + test.CreateTmpl(t), + log.NewNopLogger(), + ) + require.NoError(t, err) +} diff --git a/template/default.tmpl b/template/default.tmpl index 8b2bb7470e..7158ea1c3a 100644 --- a/template/default.tmpl +++ b/template/default.tmpl @@ -158,3 +158,11 @@ Alerts Resolved: {{ template "__text_alert_list_markdown" .Alerts.Resolved }} {{ end }} {{ end }} + + +{{ define "rocketchat.default.title" }}{{ template "__subject" . }}{{ end }} +{{ define "rocketchat.default.alias" }}{{ template "__alertmanager" . }}{{ end }} +{{ define "rocketchat.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }} +{{ define "rocketchat.default.emoji" }}{{ end }} +{{ define "rocketchat.default.iconurl" }}{{ end }} +{{ define "rocketchat.default.text" }}{{ end }}