Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Notifier: Webex #3132

Merged
merged 11 commits into from Dec 15, 2022
4 changes: 2 additions & 2 deletions asset/assets_vfsdata.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 4 additions & 0 deletions cmd/alertmanager/main.go
Expand Up @@ -58,6 +58,7 @@ import (
"github.com/prometheus/alertmanager/notify/sns"
"github.com/prometheus/alertmanager/notify/telegram"
"github.com/prometheus/alertmanager/notify/victorops"
"github.com/prometheus/alertmanager/notify/webex"
"github.com/prometheus/alertmanager/notify/webhook"
"github.com/prometheus/alertmanager/notify/wechat"
"github.com/prometheus/alertmanager/provider/mem"
Expand Down Expand Up @@ -177,6 +178,9 @@ func buildReceiverIntegrations(nc *config.Receiver, tmpl *template.Template, log
for i, c := range nc.DiscordConfigs {
add("discord", i, c, func(l log.Logger) (notify.Notifier, error) { return discord.New(c, tmpl, l) })
}
for i, c := range nc.WebexConfigs {
add("webex", i, c, func(l log.Logger) (notify.Notifier, error) { return webex.New(c, tmpl, l) })
}

if errs.Len() > 0 {
return nil, &errs
Expand Down
18 changes: 18 additions & 0 deletions config/config.go
Expand Up @@ -251,6 +251,9 @@ func resolveFilepaths(baseDir string, cfg *Config) {
for _, cfg := range receiver.DiscordConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
for _, cfg := range receiver.WebexConfigs {
cfg.HTTPConfig.SetDirectory(baseDir)
}
}
}

Expand Down Expand Up @@ -513,6 +516,18 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
return fmt.Errorf("no discord webhook URL provided")
}
}
for _, webex := range rcv.WebexConfigs {
if webex.HTTPConfig == nil {
webex.HTTPConfig = c.Global.HTTPConfig
}
if webex.APIURL == nil {
if c.Global.WebexAPIURL == nil {
return fmt.Errorf("no global Webex URL set")
}

webex.APIURL = c.Global.WebexAPIURL
}
}

names[rcv.Name] = struct{}{}
}
Expand Down Expand Up @@ -613,6 +628,7 @@ func DefaultGlobalConfig() GlobalConfig {
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"),
}
}

Expand Down Expand Up @@ -736,6 +752,7 @@ type GlobalConfig struct {
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"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for GlobalConfig.
Expand Down Expand Up @@ -878,6 +895,7 @@ type Receiver struct {
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"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface for Receiver.
Expand Down
1 change: 1 addition & 0 deletions config/config_test.go
Expand Up @@ -867,6 +867,7 @@ func TestEmptyFieldsAndRegex(t *testing.T) {
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"),
},

Templates: []string{
Expand Down
40 changes: 40 additions & 0 deletions config/notifiers.go
Expand Up @@ -33,6 +33,14 @@ var (
},
}

// DefaultWebexConfig defines default values for Webex configurations.
DefaultWebexConfig = WebexConfig{
NotifierConfig: NotifierConfig{
VSendResolved: true,
},
Message: `{{ template "webex.default.message" . }}`,
}

// DefaultDiscordConfig defines default values for Discord configurations.
DefaultDiscordConfig = DiscordConfig{
NotifierConfig: NotifierConfig{
Expand Down Expand Up @@ -166,6 +174,38 @@ func (nc *NotifierConfig) SendResolved() bool {
return nc.VSendResolved
}

// WebexConfig configures notifications via Webex.
type WebexConfig 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"`

Message string `yaml:"message,omitempty" json:"message,omitempty"`
RoomID string `yaml:"room_id" json:"room_id"`
BotToken Secret `yaml:"bot_token" json:"bot_token"`
}

// UnmarshalYAML implements the yaml.Unmarshaler interface.
func (c *WebexConfig) UnmarshalYAML(unmarshal func(interface{}) error) error {
*c = DefaultWebexConfig
type plain WebexConfig
if err := unmarshal((*plain)(c)); err != nil {
return err
}

if c.RoomID == "" {
return fmt.Errorf("missing room_id on webex_config")
}

if c.BotToken == "" {
if c.HTTPConfig == nil || c.HTTPConfig.Authorization == nil {
return fmt.Errorf("missing one of webex_configs.http_config.authorization or bot_token")
}
}

return nil
}

// DiscordConfig configures notifications via Discord.
type DiscordConfig struct {
NotifierConfig `yaml:",inline" json:",inline"`
Expand Down
34 changes: 34 additions & 0 deletions config/notifiers_test.go
Expand Up @@ -14,6 +14,7 @@
package config

import (
"errors"
"strings"
"testing"

Expand Down Expand Up @@ -866,6 +867,39 @@ func TestWeChatTypeMatcher(t *testing.T) {
}
}

func TestWebexConfiguration(t *testing.T) {
tc := []struct {
name string

in string
expected error
}{
{
name: "with no room_id - it fails",
in: `
bot_token: xyz123
`,
expected: errors.New("missing room_id on webex_config"),
},
{
name: "with room_id and bot_token present - it succeeds",
in: `
room_id: 2
bot_token: xyz123
`,
},
}

for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) {
var cfg WebexConfig
err := yaml.UnmarshalStrict([]byte(tt.in), &cfg)

require.Equal(t, tt.expected, err)
})
}
}

func newBoolPointer(b bool) *bool {
return &b
}
22 changes: 20 additions & 2 deletions notify/util.go
Expand Up @@ -20,6 +20,7 @@ import (
"io"
"net/http"
"net/url"
"strings"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
Expand Down Expand Up @@ -100,15 +101,32 @@ func TruncateInRunes(s string, n int) (string, bool) {

// TruncateInBytes truncates a string to fit the given size in Bytes.
func TruncateInBytes(s string, n int) (string, bool) {
// First, measure the string the w/o a to-rune conversion.
if len(s) <= n {
return s, false
}

// The truncationMarker itself is 3 bytes, we can't return any part of the string when it's less than 3.
if n <= 3 {
return string(s[:n]), true
switch n {
case 3:
return truncationMarker, true
default:
return strings.Repeat(".", n), true
}
}

// Now, to ensure we don't butcher the string we need to remove using runes.
r := []rune(s)
truncationTarget := n - 3

// Next, let's truncate the runes to the lower possible number.
truncatedRunes := r[:truncationTarget]
for len(string(truncatedRunes)) > truncationTarget {
truncatedRunes = r[:len(truncatedRunes)-1]
}

return string(s[:n-3]) + truncationMarker, true // In bytes, the truncation marker is 3 bytes.
return string(truncatedRunes) + truncationMarker, true
}

// TmplText is using monadic error handling in order to make string templating
Expand Down
18 changes: 9 additions & 9 deletions notify/util_test.go
Expand Up @@ -49,7 +49,7 @@ func TestTruncate(t *testing.T) {
in: "abcde",
n: 2,
runes: expect{out: "ab", trunc: true},
bytes: expect{out: "ab", trunc: true},
bytes: expect{out: "..", trunc: true},
},
{
in: "abcde",
Expand All @@ -73,25 +73,25 @@ func TestTruncate(t *testing.T) {
in: "a⌘cde",
n: 5,
runes: expect{out: "a⌘cde", trunc: false},
bytes: expect{out: "a\xe2…", trunc: true},
bytes: expect{out: "a…", trunc: true},
},
{
in: "a⌘cdef",
n: 5,
runes: expect{out: "a⌘cd…", trunc: true},
bytes: expect{out: "a\xe2…", trunc: true},
bytes: expect{out: "a…", trunc: true},
},
{
in: "世界cdef",
n: 3,
runes: expect{out: "世界c", trunc: true},
bytes: expect{out: "", trunc: true},
bytes: expect{out: "", trunc: true},
},
{
in: "❤️✅🚀🔥❌",
n: 4,
runes: expect{out: "❤️✅…", trunc: true},
bytes: expect{out: "\xe2…", trunc: true},
in: "❤️✅🚀🔥❌❤️✅🚀🔥❌❤️✅🚀🔥❌❤️✅🚀🔥❌",
n: 19,
runes: expect{out: "❤️✅🚀🔥❌❤️✅🚀🔥❌❤️✅🚀🔥❌…", trunc: true},
bytes: expect{out: "❤️✅🚀…", trunc: true},
},
}

Expand All @@ -117,8 +117,8 @@ func TestTruncate(t *testing.T) {

t.Run(fmt.Sprintf("%s(%s,%d)", fnName, tc.in, tc.n), func(t *testing.T) {
s, trunc := fn(tc.in, tc.n)
require.Equal(t, truncated, trunc)
require.Equal(t, out, s)
require.Equal(t, truncated, trunc)
})
}
}
Expand Down