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"
"github.com/grafana/alerting/alerting/notifier/channels"
"github.com/prometheus/alertmanager/template"
"github.com/prometheus/alertmanager/types"
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/components/simplejson"
)
type AlertmanagerConfig struct {
@ -23,20 +20,36 @@ type AlertmanagerConfig struct {
BasicAuthPassword string
}
func NewAlertmanagerConfig(config *channels.NotificationChannelConfig, fn channels.GetDecryptedValueFn) (*AlertmanagerConfig, error) {
simpleConfig, err := simplejson.NewJson(config.Settings)
type alertmanagerSettings struct {
URLs []*url.URL
User string
Password string
}
func AlertmanagerFactory(fc channels.FactoryConfig) (channels.NotificationChannel, error) {
ch, err := buildAlertmanagerNotifier(fc)
if err != nil {
return nil, err
return nil, receiverInitError{
Reason: err.Error(),
Cfg: *fc.Config,
}
}
urlStr := simpleConfig.Get("url").MustString()
if urlStr == "" {
return nil, errors.New("could not find url property in settings")
return ch, nil
}
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(urlParts))
for _, uS := range urlParts {
urls := make([]*url.URL, 0, len(settings.URL))
for _, uS := range settings.URL {
uS = strings.TrimSpace(uS)
if uS == "" {
continue
@ -48,46 +61,29 @@ func NewAlertmanagerConfig(config *channels.NotificationChannelConfig, fn channe
}
urls = append(urls, u)
}
return &AlertmanagerConfig{
NotificationChannelConfig: config,
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,
}
if len(settings.URL) == 0 || len(urls) == 0 {
return nil, errors.New("could not find url property in settings")
}
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{
Base: channels.NewBase(config.NotificationChannelConfig),
images: images,
urls: config.URLs,
basicAuthUser: config.BasicAuthUser,
basicAuthPassword: config.BasicAuthPassword,
logger: l,
}
Base: channels.NewBase(fc.Config),
images: fc.ImageStore,
settings: alertmanagerSettings{
URLs: urls,
User: settings.User,
Password: settings.Password,
},
logger: fc.Logger,
}, nil
}
// AlertmanagerNotifier sends alert notifications to the alert manager
type AlertmanagerNotifier struct {
*channels.Base
images channels.ImageStore
urls []*url.URL
basicAuthUser string
basicAuthPassword string
logger channels.Logger
images channels.ImageStore
settings alertmanagerSettings
logger channels.Logger
}
// Notify sends alert notifications to Alertmanager.
@ -116,10 +112,10 @@ func (n *AlertmanagerNotifier) Notify(ctx context.Context, as ...*types.Alert) (
lastErr error
numErrs int
)
for _, u := range n.urls {
for _, u := range n.settings.URLs {
if _, err := sendHTTPRequest(ctx, u, httpCfg{
user: n.basicAuthUser,
password: n.basicAuthPassword,
user: n.settings.User,
password: n.settings.Password,
body: body,
}, n.logger); err != nil {
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
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)

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`,
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 {
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 {
return fallback
}
cfg, err := NewAlertmanagerConfig(m, decryptFn)
if c.expectedInitError != "" {
require.Equal(t, c.expectedInitError, err.Error())
return
fc := channels.FactoryConfig{
Config: m,
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 {
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)
sn := NewAlertmanagerNotifier(cfg, &channels.FakeLogger{}, images, tmpl, decryptFn)
var body []byte
origSendHTTPRequest := sendHTTPRequest
t.Cleanup(func() {

View File

@ -20,7 +20,6 @@ import (
"github.com/prometheus/common/model"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/util"
)
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)
}
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")