Alerting: use alerting GrafanaReceiver and BuildReceiverConfiguration in Grafana (#65224)

* replace receiver errors with one from alerting
* add the converter to alerting models
* update buildReceiverIntegration to accept GrafanaReceiver
---------

Co-authored-by: George Robinson <george.robinson@grafana.com>
This commit is contained in:
Yuri Tseretyan 2023-04-13 12:25:32 -04:00 committed by GitHub
parent e5e0a1cbbf
commit afd52d0866
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 209 additions and 90 deletions

View File

@ -406,7 +406,7 @@ func statusForTestReceivers(v []notifier.TestReceiverResult) int {
for _, next := range receiver.Configs { for _, next := range receiver.Configs {
if next.Error != nil { if next.Error != nil {
var ( var (
invalidReceiverErr notifier.InvalidReceiverError invalidReceiverErr alertingNotify.InvalidReceiverError
receiverTimeoutErr alertingNotify.ReceiverTimeoutError receiverTimeoutErr alertingNotify.ReceiverTimeoutError
) )
if errors.As(next.Error, &invalidReceiverErr) { if errors.As(next.Error, &invalidReceiverErr) {

View File

@ -108,7 +108,7 @@ func TestStatusForTestReceivers(t *testing.T) {
Name: "test1", Name: "test1",
UID: "uid1", UID: "uid1",
Status: "failed", Status: "failed",
Error: notifier.InvalidReceiverError{}, Error: alertingNotify.InvalidReceiverError{},
}}, }},
}, { }, {
Name: "test2", Name: "test2",
@ -116,7 +116,7 @@ func TestStatusForTestReceivers(t *testing.T) {
Name: "test2", Name: "test2",
UID: "uid2", UID: "uid2",
Status: "failed", Status: "failed",
Error: notifier.InvalidReceiverError{}, Error: alertingNotify.InvalidReceiverError{},
}}, }},
}})) }}))
}) })
@ -148,7 +148,7 @@ func TestStatusForTestReceivers(t *testing.T) {
Name: "test1", Name: "test1",
UID: "uid1", UID: "uid1",
Status: "failed", Status: "failed",
Error: notifier.InvalidReceiverError{}, Error: alertingNotify.InvalidReceiverError{},
}}, }},
}, { }, {
Name: "test2", Name: "test2",
@ -156,7 +156,7 @@ func TestStatusForTestReceivers(t *testing.T) {
Name: "test2", Name: "test2",
UID: "uid2", UID: "uid2",
Status: "failed", Status: "failed",
Error: notifier.ReceiverTimeoutError{}, Error: alertingNotify.ReceiverTimeoutError{},
}}, }},
}})) }}))
}) })

View File

@ -334,7 +334,7 @@ func (am *Alertmanager) buildIntegrationsMap(receivers []*apimodels.PostableApiR
func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableApiReceiver, tmpl *alertingNotify.Template) ([]*alertingNotify.Integration, error) { func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableApiReceiver, tmpl *alertingNotify.Template) ([]*alertingNotify.Integration, error) {
integrations := make([]*alertingNotify.Integration, 0, len(receiver.GrafanaManagedReceivers)) integrations := make([]*alertingNotify.Integration, 0, len(receiver.GrafanaManagedReceivers))
for i, r := range receiver.GrafanaManagedReceivers { for i, r := range receiver.GrafanaManagedReceivers {
n, err := am.buildReceiverIntegration(r, tmpl) n, err := am.buildReceiverIntegration(PostableGrafanaReceiverToGrafanaReceiver(r), tmpl)
if err != nil { if err != nil {
return nil, err return nil, err
} }
@ -343,14 +343,14 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp
return integrations, nil return integrations, nil
} }
func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaReceiver, tmpl *alertingNotify.Template) (alertingNotify.NotificationChannel, error) { func (am *Alertmanager) buildReceiverIntegration(r *alertingNotify.GrafanaReceiver, tmpl *alertingNotify.Template) (alertingNotify.NotificationChannel, error) {
// secure settings are already encrypted at this point // secure settings are already encrypted at this point
secureSettings := make(map[string][]byte, len(r.SecureSettings)) secureSettings := make(map[string][]byte, len(r.SecureSettings))
for k, v := range r.SecureSettings { for k, v := range r.SecureSettings {
d, err := base64.StdEncoding.DecodeString(v) d, err := base64.StdEncoding.DecodeString(v)
if err != nil { if err != nil {
return nil, InvalidReceiverError{ return nil, alertingNotify.InvalidReceiverError{
Receiver: r, Receiver: r,
Err: errors.New("failed to decode secure setting"), Err: errors.New("failed to decode secure setting"),
} }
@ -365,27 +365,27 @@ func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaRec
Name: r.Name, Name: r.Name,
Type: r.Type, Type: r.Type,
DisableResolveMessage: r.DisableResolveMessage, DisableResolveMessage: r.DisableResolveMessage,
Settings: json.RawMessage(r.Settings), Settings: r.Settings,
SecureSettings: secureSettings, SecureSettings: secureSettings,
} }
) )
factoryConfig, err := receivers.NewFactoryConfig(cfg, NewNotificationSender(am.NotificationService), am.decryptFn, tmpl, newImageStore(am.Store), LoggerFactory, setting.BuildVersion) factoryConfig, err := receivers.NewFactoryConfig(cfg, NewNotificationSender(am.NotificationService), am.decryptFn, tmpl, newImageStore(am.Store), LoggerFactory, setting.BuildVersion)
if err != nil { if err != nil {
return nil, InvalidReceiverError{ return nil, alertingNotify.InvalidReceiverError{
Receiver: r, Receiver: r,
Err: err, Err: err,
} }
} }
receiverFactory, exists := alertingNotify.Factory(r.Type) receiverFactory, exists := alertingNotify.Factory(r.Type)
if !exists { if !exists {
return nil, InvalidReceiverError{ return nil, alertingNotify.InvalidReceiverError{
Receiver: r, Receiver: r,
Err: fmt.Errorf("notifier %s is not supported", r.Type), Err: fmt.Errorf("notifier %s is not supported", r.Type),
} }
} }
n, err := receiverFactory(factoryConfig) n, err := receiverFactory(factoryConfig)
if err != nil { if err != nil {
return nil, InvalidReceiverError{ return nil, alertingNotify.InvalidReceiverError{
Receiver: r, Receiver: r,
Err: err, Err: err,
} }

View File

@ -0,0 +1,42 @@
package notifier
import (
"encoding/json"
alertingNotify "github.com/grafana/alerting/notify"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
func PostableGrafanaReceiverToGrafanaReceiver(p *apimodels.PostableGrafanaReceiver) *alertingNotify.GrafanaReceiver {
return &alertingNotify.GrafanaReceiver{
UID: p.UID,
Name: p.Name,
Type: p.Type,
DisableResolveMessage: p.DisableResolveMessage,
Settings: json.RawMessage(p.Settings),
SecureSettings: p.SecureSettings,
}
}
func PostableApiReceiverToApiReceiver(r *apimodels.PostableApiReceiver) *alertingNotify.APIReceiver {
receivers := alertingNotify.GrafanaReceivers{
Receivers: make([]*alertingNotify.GrafanaReceiver, 0, len(r.GrafanaManagedReceivers)),
}
for _, receiver := range r.GrafanaManagedReceivers {
receivers.Receivers = append(receivers.Receivers, PostableGrafanaReceiverToGrafanaReceiver(receiver))
}
return &alertingNotify.APIReceiver{
ConfigReceiver: r.Receiver,
GrafanaReceivers: receivers,
}
}
func PostableApiAlertingConfigToApiReceivers(c apimodels.PostableApiAlertingConfig) []*alertingNotify.APIReceiver {
apiReceivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers))
for _, receiver := range c.Receivers {
apiReceivers = append(apiReceivers, PostableApiReceiverToApiReceiver(receiver))
}
return apiReceivers
}

View File

@ -0,0 +1,143 @@
package notifier
import (
"encoding/json"
"testing"
alertingNotify "github.com/grafana/alerting/notify"
"github.com/prometheus/alertmanager/config"
"github.com/stretchr/testify/require"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
)
func TestPostableGrafanaReceiverToGrafanaReceiver(t *testing.T) {
r := &apimodels.PostableGrafanaReceiver{
UID: "test-uid",
Name: "test-name",
Type: "slack",
DisableResolveMessage: false,
Settings: apimodels.RawMessage(`{ "data" : "test" }`),
SecureSettings: map[string]string{
"test": "data",
},
}
actual := PostableGrafanaReceiverToGrafanaReceiver(r)
require.Equal(t, alertingNotify.GrafanaReceiver{
UID: "test-uid",
Name: "test-name",
Type: "slack",
DisableResolveMessage: false,
Settings: json.RawMessage(`{ "data" : "test" }`),
SecureSettings: map[string]string{
"test": "data",
},
}, *actual)
}
func TestPostableApiReceiverToApiReceiver(t *testing.T) {
t.Run("returns empty when no receivers", func(t *testing.T) {
r := &apimodels.PostableApiReceiver{
Receiver: config.Receiver{
Name: "test-receiver",
},
}
actual := PostableApiReceiverToApiReceiver(r)
require.Empty(t, actual.Receivers)
require.Equal(t, r.Receiver, actual.ConfigReceiver)
})
t.Run("converts receivers", func(t *testing.T) {
r := &apimodels.PostableApiReceiver{
Receiver: config.Receiver{
Name: "test-receiver",
},
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{
{
UID: "test-uid",
Name: "test-name",
Type: "slack",
DisableResolveMessage: false,
Settings: apimodels.RawMessage(`{ "data" : "test" }`),
SecureSettings: map[string]string{
"test": "data",
},
},
{
UID: "test-uid2",
Name: "test-name2",
Type: "webhook",
DisableResolveMessage: false,
Settings: apimodels.RawMessage(`{ "data2" : "test2" }`),
SecureSettings: map[string]string{
"test2": "data2",
},
},
},
},
}
actual := PostableApiReceiverToApiReceiver(r)
require.Len(t, actual.Receivers, 2)
require.Equal(t, r.Receiver, actual.ConfigReceiver)
require.Equal(t, *PostableGrafanaReceiverToGrafanaReceiver(r.GrafanaManagedReceivers[0]), *actual.Receivers[0])
require.Equal(t, *PostableGrafanaReceiverToGrafanaReceiver(r.GrafanaManagedReceivers[1]), *actual.Receivers[1])
})
}
func TestPostableApiAlertingConfigToApiReceivers(t *testing.T) {
t.Run("returns empty when no receivers", func(t *testing.T) {
r := apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{},
}
actual := PostableApiAlertingConfigToApiReceivers(r)
require.Empty(t, actual)
})
c := apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{},
Receivers: []*apimodels.PostableApiReceiver{
{
Receiver: config.Receiver{
Name: "test-receiver",
},
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{
{
UID: "test-uid",
Name: "test-name",
Type: "slack",
DisableResolveMessage: false,
Settings: apimodels.RawMessage(`{ "data" : "test" }`),
SecureSettings: map[string]string{
"test": "data",
},
},
},
},
},
{
Receiver: config.Receiver{
Name: "test-receiver2",
},
PostableGrafanaReceivers: apimodels.PostableGrafanaReceivers{
GrafanaManagedReceivers: []*apimodels.PostableGrafanaReceiver{
{
UID: "test-uid2",
Name: "test-name1",
Type: "slack",
DisableResolveMessage: false,
Settings: apimodels.RawMessage(`{ "data" : "test" }`),
SecureSettings: map[string]string{
"test": "data",
},
},
},
},
},
},
}
actual := PostableApiAlertingConfigToApiReceivers(c)
require.Len(t, actual, 2)
require.Equal(t, PostableApiReceiverToApiReceiver(c.Receivers[0]), actual[0])
require.Equal(t, PostableApiReceiverToApiReceiver(c.Receivers[1]), actual[1])
}

View File

@ -97,31 +97,12 @@ type AlertingConfiguration struct {
AlertmanagerTemplates *alertingNotify.Template AlertmanagerTemplates *alertingNotify.Template
IntegrationsFunc func(receivers []*api.PostableApiReceiver, templates *alertingNotify.Template) (map[string][]*alertingNotify.Integration, error) IntegrationsFunc func(receivers []*api.PostableApiReceiver, templates *alertingNotify.Template) (map[string][]*alertingNotify.Integration, error)
ReceiverIntegrationsFunc func(r *api.PostableGrafanaReceiver, tmpl *alertingNotify.Template) (alertingNotify.NotificationChannel, error) ReceiverIntegrationsFunc func(r *alertingNotify.GrafanaReceiver, tmpl *alertingNotify.Template) (alertingNotify.NotificationChannel, error)
} }
func (a AlertingConfiguration) BuildReceiverIntegrationsFunc() func(next *alertingNotify.GrafanaReceiver, tmpl *alertingNotify.Template) (alertingNotify.Notifier, error) { func (a AlertingConfiguration) BuildReceiverIntegrationsFunc() func(next *alertingNotify.GrafanaReceiver, tmpl *alertingNotify.Template) (alertingNotify.Notifier, error) {
return func(next *alertingNotify.GrafanaReceiver, tmpl *alertingNotify.Template) (alertingNotify.Notifier, error) { return func(next *alertingNotify.GrafanaReceiver, tmpl *alertingNotify.Template) (alertingNotify.Notifier, error) {
// TODO: We shouldn't need to do all of this marshalling - there should be no difference between types. return a.ReceiverIntegrationsFunc(next, tmpl)
var out api.RawMessage
settingsJSON, err := json.Marshal(next.Settings)
if err != nil {
return nil, fmt.Errorf("unable to marshal settings to JSON: %v", err)
}
err = out.UnmarshalJSON(settingsJSON)
if err != nil {
return nil, fmt.Errorf("unable to marshal JSON to RawMessage: %v", err)
}
gr := &api.PostableGrafanaReceiver{
UID: next.UID,
Name: next.Name,
Type: next.Type,
DisableResolveMessage: next.DisableResolveMessage,
Settings: out,
SecureSettings: next.SecureSettings,
}
return a.ReceiverIntegrationsFunc(gr, tmpl)
} }
} }

View File

@ -3,8 +3,6 @@ package notifier
import ( import (
"context" "context"
"encoding/json" "encoding/json"
"errors"
"fmt"
"time" "time"
alertingNotify "github.com/grafana/alerting/notify" alertingNotify "github.com/grafana/alerting/notify"
@ -16,10 +14,6 @@ import (
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
) )
var (
ErrNoReceivers = errors.New("no receivers")
)
type TestReceiversResult struct { type TestReceiversResult struct {
Alert types.Alert Alert types.Alert
Receivers []TestReceiverResult Receivers []TestReceiverResult
@ -38,24 +32,6 @@ type TestReceiverConfigResult struct {
Error error Error error
} }
type InvalidReceiverError struct {
Receiver *apimodels.PostableGrafanaReceiver
Err error
}
func (e InvalidReceiverError) Error() string {
return fmt.Sprintf("the receiver is invalid: %s", e.Err)
}
type ReceiverTimeoutError struct {
Receiver *apimodels.PostableGrafanaReceiver
Err error
}
func (e ReceiverTimeoutError) Error() string {
return fmt.Sprintf("the receiver timed out: %s", e.Err)
}
func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*TestReceiversResult, error) { func (am *Alertmanager) TestReceivers(ctx context.Context, c apimodels.TestReceiversConfigBodyParams) (*TestReceiversResult, error) {
receivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) receivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers))
for _, r := range c.Receivers { for _, r := range c.Receivers {

View File

@ -121,7 +121,7 @@ func (m *migration) setupAlertmanagerConfigs(rulesPerOrg map[int64]map[*alertRul
// Validate the alertmanager configuration produced, this gives a chance to catch bad configuration at migration time. // Validate the alertmanager configuration produced, this gives a chance to catch bad configuration at migration time.
// Validation between legacy and unified alerting can be different (e.g. due to bug fixes) so this would fail the migration in that case. // Validation between legacy and unified alerting can be different (e.g. due to bug fixes) so this would fail the migration in that case.
if err := m.validateAlertmanagerConfig(orgID, amConfig); err != nil { if err := m.validateAlertmanagerConfig(amConfig); err != nil {
return nil, fmt.Errorf("failed to validate AlertmanagerConfig in orgId %d: %w", orgID, err) return nil, fmt.Errorf("failed to validate AlertmanagerConfig in orgId %d: %w", orgID, err)
} }
} }

View File

@ -2,7 +2,6 @@ package ualert
import ( import (
"context" "context"
"encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt" "fmt"
@ -12,9 +11,7 @@ import (
"strings" "strings"
"time" "time"
alertingLogging "github.com/grafana/alerting/logging"
alertingNotify "github.com/grafana/alerting/notify" alertingNotify "github.com/grafana/alerting/notify"
"github.com/grafana/alerting/receivers"
pb "github.com/prometheus/alertmanager/silence/silencepb" pb "github.com/prometheus/alertmanager/silence/silencepb"
"xorm.io/xorm" "xorm.io/xorm"
@ -475,32 +472,21 @@ func (m *migration) writeAlertmanagerConfig(orgID int64, amConfig *PostableUserC
} }
// validateAlertmanagerConfig validates the alertmanager configuration produced by the migration against the receivers. // validateAlertmanagerConfig validates the alertmanager configuration produced by the migration against the receivers.
func (m *migration) validateAlertmanagerConfig(orgID int64, config *PostableUserConfig) error { func (m *migration) validateAlertmanagerConfig(config *PostableUserConfig) error {
for _, r := range config.AlertmanagerConfig.Receivers { for _, r := range config.AlertmanagerConfig.Receivers {
for _, gr := range r.GrafanaManagedReceivers { for _, gr := range r.GrafanaManagedReceivers {
// First, let's decode the secure settings - given they're stored as base64.
secureSettings := make(map[string][]byte, len(gr.SecureSettings))
for k, v := range gr.SecureSettings {
d, err := base64.StdEncoding.DecodeString(v)
if err != nil {
return err
}
secureSettings[k] = d
}
data, err := gr.Settings.MarshalJSON() data, err := gr.Settings.MarshalJSON()
if err != nil { if err != nil {
return err return err
} }
var ( var (
cfg = &receivers.NotificationChannelConfig{ cfg = &alertingNotify.GrafanaReceiver{
UID: gr.UID, UID: gr.UID,
OrgID: orgID,
Name: gr.Name, Name: gr.Name,
Type: gr.Type, Type: gr.Type,
DisableResolveMessage: gr.DisableResolveMessage, DisableResolveMessage: gr.DisableResolveMessage,
Settings: data, Settings: data,
SecureSettings: secureSettings, SecureSettings: gr.SecureSettings,
} }
) )
@ -517,17 +503,9 @@ func (m *migration) validateAlertmanagerConfig(orgID int64, config *PostableUser
} }
return fallback return fallback
} }
receiverFactory, exists := alertingNotify.Factory(gr.Type) _, err = alertingNotify.BuildReceiverConfiguration(context.Background(), &alertingNotify.APIReceiver{
if !exists { GrafanaReceivers: alertingNotify.GrafanaReceivers{Receivers: []*alertingNotify.GrafanaReceiver{cfg}},
return fmt.Errorf("notifier %s is not supported", gr.Type) }, decryptFunc)
}
factoryConfig, err := receivers.NewFactoryConfig(cfg, nil, decryptFunc, nil, nil, func(ctx ...interface{}) alertingLogging.Logger {
return &alertingLogging.FakeLogger{}
}, setting.BuildVersion)
if err != nil {
return err
}
_, err = receiverFactory(factoryConfig)
if err != nil { if err != nil {
return err return err
} }

View File

@ -65,14 +65,14 @@ func Test_validateAlertmanagerConfig(t *testing.T) {
name: "when a slack receiver does not have a valid URL - it should error", name: "when a slack receiver does not have a valid URL - it should error",
receivers: []*PostableGrafanaReceiver{ receivers: []*PostableGrafanaReceiver{
{ {
UID: util.GenerateShortUID(), UID: "test-uid",
Name: "SlackWithBadURL", Name: "SlackWithBadURL",
Type: "slack", Type: "slack",
Settings: simplejson.NewFromAny(map[string]interface{}{}), Settings: simplejson.NewFromAny(map[string]interface{}{}),
SecureSettings: map[string]string{"url": invalidUri}, SecureSettings: map[string]string{"url": invalidUri},
}, },
}, },
err: fmt.Errorf("failed to validate receiver \"SlackWithBadURL\" of type \"slack\": invalid URL %q", invalidUri), err: fmt.Errorf("failed to validate receiver \"SlackWithBadURL\" of type \"slack\": failed to parse notifier SlackWithBadURL (UID: test-uid): invalid URL %q", invalidUri),
}, },
{ {
name: "when a slack receiver has an invalid recipient - it should not error", name: "when a slack receiver has an invalid recipient - it should not error",
@ -103,11 +103,10 @@ func Test_validateAlertmanagerConfig(t *testing.T) {
for _, tt := range tc { for _, tt := range tc {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
mg := newTestMigration(t) mg := newTestMigration(t)
orgID := int64(1)
config := configFromReceivers(t, tt.receivers) config := configFromReceivers(t, tt.receivers)
require.NoError(t, config.EncryptSecureSettings()) // make sure we encrypt the settings require.NoError(t, config.EncryptSecureSettings()) // make sure we encrypt the settings
err := mg.validateAlertmanagerConfig(orgID, config) err := mg.validateAlertmanagerConfig(config)
if tt.err != nil { if tt.err != nil {
require.Error(t, err) require.Error(t, err)
require.EqualError(t, err, tt.err.Error()) require.EqualError(t, err, tt.err.Error())