diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go index 2884433551a..88dd9eb96f9 100644 --- a/pkg/services/ngalert/api/api_alertmanager.go +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -406,7 +406,7 @@ func statusForTestReceivers(v []notifier.TestReceiverResult) int { for _, next := range receiver.Configs { if next.Error != nil { var ( - invalidReceiverErr notifier.InvalidReceiverError + invalidReceiverErr alertingNotify.InvalidReceiverError receiverTimeoutErr alertingNotify.ReceiverTimeoutError ) if errors.As(next.Error, &invalidReceiverErr) { diff --git a/pkg/services/ngalert/api/api_alertmanager_test.go b/pkg/services/ngalert/api/api_alertmanager_test.go index 8cbe6168bb4..5fed1c2c87f 100644 --- a/pkg/services/ngalert/api/api_alertmanager_test.go +++ b/pkg/services/ngalert/api/api_alertmanager_test.go @@ -108,7 +108,7 @@ func TestStatusForTestReceivers(t *testing.T) { Name: "test1", UID: "uid1", Status: "failed", - Error: notifier.InvalidReceiverError{}, + Error: alertingNotify.InvalidReceiverError{}, }}, }, { Name: "test2", @@ -116,7 +116,7 @@ func TestStatusForTestReceivers(t *testing.T) { Name: "test2", UID: "uid2", Status: "failed", - Error: notifier.InvalidReceiverError{}, + Error: alertingNotify.InvalidReceiverError{}, }}, }})) }) @@ -148,7 +148,7 @@ func TestStatusForTestReceivers(t *testing.T) { Name: "test1", UID: "uid1", Status: "failed", - Error: notifier.InvalidReceiverError{}, + Error: alertingNotify.InvalidReceiverError{}, }}, }, { Name: "test2", @@ -156,7 +156,7 @@ func TestStatusForTestReceivers(t *testing.T) { Name: "test2", UID: "uid2", Status: "failed", - Error: notifier.ReceiverTimeoutError{}, + Error: alertingNotify.ReceiverTimeoutError{}, }}, }})) }) diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 17812a59cc5..1f26f219888 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -334,7 +334,7 @@ func (am *Alertmanager) buildIntegrationsMap(receivers []*apimodels.PostableApiR func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableApiReceiver, tmpl *alertingNotify.Template) ([]*alertingNotify.Integration, error) { integrations := make([]*alertingNotify.Integration, 0, len(receiver.GrafanaManagedReceivers)) for i, r := range receiver.GrafanaManagedReceivers { - n, err := am.buildReceiverIntegration(r, tmpl) + n, err := am.buildReceiverIntegration(PostableGrafanaReceiverToGrafanaReceiver(r), tmpl) if err != nil { return nil, err } @@ -343,14 +343,14 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp 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 secureSettings := make(map[string][]byte, len(r.SecureSettings)) for k, v := range r.SecureSettings { d, err := base64.StdEncoding.DecodeString(v) if err != nil { - return nil, InvalidReceiverError{ + return nil, alertingNotify.InvalidReceiverError{ Receiver: r, Err: errors.New("failed to decode secure setting"), } @@ -365,27 +365,27 @@ func (am *Alertmanager) buildReceiverIntegration(r *apimodels.PostableGrafanaRec Name: r.Name, Type: r.Type, DisableResolveMessage: r.DisableResolveMessage, - Settings: json.RawMessage(r.Settings), + Settings: r.Settings, SecureSettings: secureSettings, } ) factoryConfig, err := receivers.NewFactoryConfig(cfg, NewNotificationSender(am.NotificationService), am.decryptFn, tmpl, newImageStore(am.Store), LoggerFactory, setting.BuildVersion) if err != nil { - return nil, InvalidReceiverError{ + return nil, alertingNotify.InvalidReceiverError{ Receiver: r, Err: err, } } receiverFactory, exists := alertingNotify.Factory(r.Type) if !exists { - return nil, InvalidReceiverError{ + return nil, alertingNotify.InvalidReceiverError{ Receiver: r, Err: fmt.Errorf("notifier %s is not supported", r.Type), } } n, err := receiverFactory(factoryConfig) if err != nil { - return nil, InvalidReceiverError{ + return nil, alertingNotify.InvalidReceiverError{ Receiver: r, Err: err, } diff --git a/pkg/services/ngalert/notifier/compat.go b/pkg/services/ngalert/notifier/compat.go new file mode 100644 index 00000000000..90c4807d8f3 --- /dev/null +++ b/pkg/services/ngalert/notifier/compat.go @@ -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 +} diff --git a/pkg/services/ngalert/notifier/compat_test.go b/pkg/services/ngalert/notifier/compat_test.go new file mode 100644 index 00000000000..46d95b37405 --- /dev/null +++ b/pkg/services/ngalert/notifier/compat_test.go @@ -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]) +} diff --git a/pkg/services/ngalert/notifier/config.go b/pkg/services/ngalert/notifier/config.go index d59338ce663..01ecab34017 100644 --- a/pkg/services/ngalert/notifier/config.go +++ b/pkg/services/ngalert/notifier/config.go @@ -97,31 +97,12 @@ type AlertingConfiguration struct { AlertmanagerTemplates *alertingNotify.Template 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) { 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. - 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) + return a.ReceiverIntegrationsFunc(next, tmpl) } } diff --git a/pkg/services/ngalert/notifier/receivers.go b/pkg/services/ngalert/notifier/receivers.go index 85318f3975f..f473a035a33 100644 --- a/pkg/services/ngalert/notifier/receivers.go +++ b/pkg/services/ngalert/notifier/receivers.go @@ -3,8 +3,6 @@ package notifier import ( "context" "encoding/json" - "errors" - "fmt" "time" alertingNotify "github.com/grafana/alerting/notify" @@ -16,10 +14,6 @@ import ( apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" ) -var ( - ErrNoReceivers = errors.New("no receivers") -) - type TestReceiversResult struct { Alert types.Alert Receivers []TestReceiverResult @@ -38,24 +32,6 @@ type TestReceiverConfigResult struct { 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) { receivers := make([]*alertingNotify.APIReceiver, 0, len(c.Receivers)) for _, r := range c.Receivers { diff --git a/pkg/services/sqlstore/migrations/ualert/channel.go b/pkg/services/sqlstore/migrations/ualert/channel.go index 9fcb0bdaf6e..3fac96ae744 100644 --- a/pkg/services/sqlstore/migrations/ualert/channel.go +++ b/pkg/services/sqlstore/migrations/ualert/channel.go @@ -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. // 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) } } diff --git a/pkg/services/sqlstore/migrations/ualert/ualert.go b/pkg/services/sqlstore/migrations/ualert/ualert.go index 03a4ee47834..535c6395006 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert.go @@ -2,7 +2,6 @@ package ualert import ( "context" - "encoding/base64" "encoding/json" "errors" "fmt" @@ -12,9 +11,7 @@ import ( "strings" "time" - alertingLogging "github.com/grafana/alerting/logging" alertingNotify "github.com/grafana/alerting/notify" - "github.com/grafana/alerting/receivers" pb "github.com/prometheus/alertmanager/silence/silencepb" "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. -func (m *migration) validateAlertmanagerConfig(orgID int64, config *PostableUserConfig) error { +func (m *migration) validateAlertmanagerConfig(config *PostableUserConfig) error { for _, r := range config.AlertmanagerConfig.Receivers { 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() if err != nil { return err } var ( - cfg = &receivers.NotificationChannelConfig{ + cfg = &alertingNotify.GrafanaReceiver{ UID: gr.UID, - OrgID: orgID, Name: gr.Name, Type: gr.Type, DisableResolveMessage: gr.DisableResolveMessage, Settings: data, - SecureSettings: secureSettings, + SecureSettings: gr.SecureSettings, } ) @@ -517,17 +503,9 @@ func (m *migration) validateAlertmanagerConfig(orgID int64, config *PostableUser } return fallback } - receiverFactory, exists := alertingNotify.Factory(gr.Type) - if !exists { - return fmt.Errorf("notifier %s is not supported", gr.Type) - } - 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) + _, err = alertingNotify.BuildReceiverConfiguration(context.Background(), &alertingNotify.APIReceiver{ + GrafanaReceivers: alertingNotify.GrafanaReceivers{Receivers: []*alertingNotify.GrafanaReceiver{cfg}}, + }, decryptFunc) if err != nil { return err } diff --git a/pkg/services/sqlstore/migrations/ualert/ualert_test.go b/pkg/services/sqlstore/migrations/ualert/ualert_test.go index 1f75906b74b..ef2086ccb54 100644 --- a/pkg/services/sqlstore/migrations/ualert/ualert_test.go +++ b/pkg/services/sqlstore/migrations/ualert/ualert_test.go @@ -65,14 +65,14 @@ func Test_validateAlertmanagerConfig(t *testing.T) { name: "when a slack receiver does not have a valid URL - it should error", receivers: []*PostableGrafanaReceiver{ { - UID: util.GenerateShortUID(), + UID: "test-uid", Name: "SlackWithBadURL", Type: "slack", Settings: simplejson.NewFromAny(map[string]interface{}{}), 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", @@ -103,11 +103,10 @@ func Test_validateAlertmanagerConfig(t *testing.T) { for _, tt := range tc { t.Run(tt.name, func(t *testing.T) { mg := newTestMigration(t) - orgID := int64(1) config := configFromReceivers(t, tt.receivers) require.NoError(t, config.EncryptSecureSettings()) // make sure we encrypt the settings - err := mg.validateAlertmanagerConfig(orgID, config) + err := mg.validateAlertmanagerConfig(config) if tt.err != nil { require.Error(t, err) require.EqualError(t, err, tt.err.Error())