Alerting: Refactor alertmanager notifier to use encoding/json to parse settings instead of simplejson (#55507)

* replace basic auth header with method call

Co-authored-by: Yuri Tseretyan <yuriy.tseretyan@grafana.com>
This commit is contained in:
Alexander Weaver 2022-12-19 14:12:49 -06:00 committed by GitHub
parent 90185bea00
commit ca3f8ba6f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 92 additions and 60 deletions

View File

@ -9,11 +9,8 @@ import (
"strings" "strings"
"github.com/grafana/alerting/alerting/notifier/channels" "github.com/grafana/alerting/alerting/notifier/channels"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types" "github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/components/simplejson"
) )
type AlertmanagerConfig struct { type AlertmanagerConfig struct {
@ -23,20 +20,36 @@ type AlertmanagerConfig struct {
BasicAuthPassword string BasicAuthPassword string
} }
func NewAlertmanagerConfig(config *channels.NotificationChannelConfig, fn channels.GetDecryptedValueFn) (*AlertmanagerConfig, error) { type alertmanagerSettings struct {
simpleConfig, err := simplejson.NewJson(config.Settings) URLs []*url.URL
User string
Password string
}
func AlertmanagerFactory(fc channels.FactoryConfig) (channels.NotificationChannel, error) {
ch, err := buildAlertmanagerNotifier(fc)
if err != nil { if err != nil {
return nil, err return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
} }
urlStr := simpleConfig.Get("url").MustString() return ch, nil
if urlStr == "" { }
return nil, errors.New("could not find url property in settings")
func buildAlertmanagerNotifier(fc channels.FactoryConfig) (*AlertmanagerNotifier, error) {
var settings struct {
URL channels.CommaSeparatedStrings `json:"url,omitempty" yaml:"url,omitempty"`
User string `json:"basicAuthUser,omitempty" yaml:"basicAuthUser,omitempty"`
Password string `json:"basicAuthPassword,omitempty" yaml:"basicAuthPassword,omitempty"`
}
err := json.Unmarshal(fc.Config.Settings, &settings)
if err != nil {
return nil, fmt.Errorf("failed to unmarshal settings: %w", err)
} }
urlParts := strings.Split(urlStr, ",") urls := make([]*url.URL, 0, len(settings.URL))
urls := make([]*url.URL, 0, len(urlParts)) for _, uS := range settings.URL {
for _, uS := range urlParts {
uS = strings.TrimSpace(uS) uS = strings.TrimSpace(uS)
if uS == "" { if uS == "" {
continue continue
@ -48,46 +61,29 @@ func NewAlertmanagerConfig(config *channels.NotificationChannelConfig, fn channe
} }
urls = append(urls, u) urls = append(urls, u)
} }
return &AlertmanagerConfig{ if len(settings.URL) == 0 || len(urls) == 0 {
NotificationChannelConfig: config, return nil, errors.New("could not find url property in settings")
URLs: urls,
BasicAuthUser: simpleConfig.Get("basicAuthUser").MustString(),
BasicAuthPassword: fn(context.Background(), config.SecureSettings, "basicAuthPassword", simpleConfig.Get("basicAuthPassword").MustString()),
}, nil
}
func AlertmanagerFactory(fc channels.FactoryConfig) (channels.NotificationChannel, error) {
config, err := NewAlertmanagerConfig(fc.Config, fc.DecryptFunc)
if err != nil {
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
} }
return NewAlertmanagerNotifier(config, fc.Logger, fc.ImageStore, nil, fc.DecryptFunc), nil settings.Password = fc.DecryptFunc(context.Background(), fc.Config.SecureSettings, "basicAuthPassword", settings.Password)
}
// NewAlertmanagerNotifier returns a new Alertmanager notifier.
func NewAlertmanagerNotifier(config *AlertmanagerConfig, l channels.Logger, images channels.ImageStore, _ *template.Template, fn channels.GetDecryptedValueFn) *AlertmanagerNotifier {
return &AlertmanagerNotifier{ return &AlertmanagerNotifier{
Base: channels.NewBase(config.NotificationChannelConfig), Base: channels.NewBase(fc.Config),
images: images, images: fc.ImageStore,
urls: config.URLs, settings: alertmanagerSettings{
basicAuthUser: config.BasicAuthUser, URLs: urls,
basicAuthPassword: config.BasicAuthPassword, User: settings.User,
logger: l, Password: settings.Password,
} },
logger: fc.Logger,
}, nil
} }
// AlertmanagerNotifier sends alert notifications to the alert manager // AlertmanagerNotifier sends alert notifications to the alert manager
type AlertmanagerNotifier struct { type AlertmanagerNotifier struct {
*channels.Base *channels.Base
images channels.ImageStore images channels.ImageStore
settings alertmanagerSettings
urls []*url.URL logger channels.Logger
basicAuthUser string
basicAuthPassword string
logger channels.Logger
} }
// Notify sends alert notifications to Alertmanager. // Notify sends alert notifications to Alertmanager.
@ -116,10 +112,10 @@ func (n *AlertmanagerNotifier) Notify(ctx context.Context, as ...*types.Alert) (
lastErr error lastErr error
numErrs int numErrs int
) )
for _, u := range n.urls { for _, u := range n.settings.URLs {
if _, err := sendHTTPRequest(ctx, u, httpCfg{ if _, err := sendHTTPRequest(ctx, u, httpCfg{
user: n.basicAuthUser, user: n.settings.User,
password: n.basicAuthPassword, password: n.settings.Password,
body: body, body: body,
}, n.logger); err != nil { }, n.logger); err != nil {
n.logger.Warn("failed to send to Alertmanager", "error", err, "alertmanager", n.Name, "url", u.String()) n.logger.Warn("failed to send to Alertmanager", "error", err, "alertmanager", n.Name, "url", u.String())
@ -128,7 +124,7 @@ func (n *AlertmanagerNotifier) Notify(ctx context.Context, as ...*types.Alert) (
} }
} }
if numErrs == len(n.urls) { if numErrs == len(n.settings.URLs) {
// All attempts to send alerts have failed // All attempts to send alerts have failed
n.logger.Warn("all attempts to send to Alertmanager failed", "alertmanager", n.Name) n.logger.Warn("all attempts to send to Alertmanager failed", "alertmanager", n.Name)
return false, fmt.Errorf("failed to send alert to Alertmanager: %w", lastErr) return false, fmt.Errorf("failed to send alert to Alertmanager: %w", lastErr)

View File

@ -40,6 +40,30 @@ func TestNewAlertmanagerNotifier(t *testing.T) {
expectedInitError: `invalid url property in settings: parse "://alertmanager.com/api/v1/alerts": missing protocol scheme`, expectedInitError: `invalid url property in settings: parse "://alertmanager.com/api/v1/alerts": missing protocol scheme`,
receiverName: "Alertmanager", receiverName: "Alertmanager",
}, },
{
name: "Error in initing: empty URL",
settings: `{
"url": ""
}`,
expectedInitError: `could not find url property in settings`,
receiverName: "Alertmanager",
},
{
name: "Error in initing: null URL",
settings: `{
"url": null
}`,
expectedInitError: `could not find url property in settings`,
receiverName: "Alertmanager",
},
{
name: "Error in initing: one of multiple URLs is invalid",
settings: `{
"url": "https://alertmanager-01.com,://url"
}`,
expectedInitError: "invalid url property in settings: parse \"://url/api/v1/alerts\": missing protocol scheme",
receiverName: "Alertmanager",
},
} }
for _, c := range cases { for _, c := range cases {
t.Run(c.name, func(t *testing.T) { t.Run(c.name, func(t *testing.T) {
@ -55,14 +79,20 @@ func TestNewAlertmanagerNotifier(t *testing.T) {
decryptFn := func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string { decryptFn := func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string {
return fallback return fallback
} }
cfg, err := NewAlertmanagerConfig(m, decryptFn)
if c.expectedInitError != "" { fc := channels.FactoryConfig{
require.Equal(t, c.expectedInitError, err.Error()) Config: m,
return DecryptFunc: decryptFn,
ImageStore: &channels.UnavailableImageStore{},
Template: tmpl,
Logger: &channels.FakeLogger{},
}
sn, err := buildAlertmanagerNotifier(fc)
if c.expectedInitError != "" {
require.ErrorContains(t, err, c.expectedInitError)
} else {
require.NotNil(t, sn)
} }
require.NoError(t, err)
sn := NewAlertmanagerNotifier(cfg, &channels.FakeLogger{}, &channels.UnavailableImageStore{}, tmpl, decryptFn)
require.NotNil(t, sn)
}) })
} }
} }
@ -153,9 +183,16 @@ func TestAlertmanagerNotifier_Notify(t *testing.T) {
decryptFn := func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string { decryptFn := func(ctx context.Context, sjd map[string][]byte, key string, fallback string) string {
return fallback return fallback
} }
cfg, err := NewAlertmanagerConfig(m, decryptFn) fc := channels.FactoryConfig{
Config: m,
DecryptFunc: decryptFn,
ImageStore: images,
Template: tmpl,
Logger: &channels.FakeLogger{},
}
sn, err := buildAlertmanagerNotifier(fc)
require.NoError(t, err) require.NoError(t, err)
sn := NewAlertmanagerNotifier(cfg, &channels.FakeLogger{}, images, tmpl, decryptFn)
var body []byte var body []byte
origSendHTTPRequest := sendHTTPRequest origSendHTTPRequest := sendHTTPRequest
t.Cleanup(func() { t.Cleanup(func() {

View File

@ -20,7 +20,6 @@ import (
"github.com/prometheus/common/model" "github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
) )
var ( var (
@ -151,7 +150,7 @@ var sendHTTPRequest = func(ctx context.Context, url *url.URL, cfg httpCfg, logge
return nil, fmt.Errorf("failed to create HTTP request: %w", err) return nil, fmt.Errorf("failed to create HTTP request: %w", err)
} }
if cfg.user != "" && cfg.password != "" { if cfg.user != "" && cfg.password != "" {
request.Header.Set("Authorization", util.GetBasicAuthHeader(cfg.user, cfg.password)) request.SetBasicAuth(cfg.user, cfg.password)
} }
request.Header.Set("Content-Type", "application/json") request.Header.Set("Content-Type", "application/json")