diff --git a/pkg/services/ngalert/api/api_notifications.go b/pkg/services/ngalert/api/api_notifications.go index 73b59e5e250..be0b10d14fa 100644 --- a/pkg/services/ngalert/api/api_notifications.go +++ b/pkg/services/ngalert/api/api_notifications.go @@ -2,7 +2,6 @@ package api import ( "context" - "errors" "net/http" "github.com/grafana/grafana/pkg/api/response" @@ -11,7 +10,6 @@ import ( contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/ngalert/notifier" ) type NotificationSrv struct { @@ -50,13 +48,7 @@ func (srv *NotificationSrv) RouteGetReceiver(c *contextmodel.ReqContext, name st receiver, err := srv.receiverService.GetReceiver(c.Req.Context(), q, c.SignedInUser) if err != nil { - if errors.Is(err, notifier.ErrNotFound) { - return ErrResp(http.StatusNotFound, err, "receiver not found") - } - if errors.Is(err, notifier.ErrPermissionDenied) { - return ErrResp(http.StatusForbidden, err, "permission denied") - } - return ErrResp(http.StatusInternalServerError, err, "failed to get receiver") + return response.ErrOrFallback(http.StatusInternalServerError, "failed to get receiver", err) } return response.JSON(http.StatusOK, receiver) @@ -73,10 +65,7 @@ func (srv *NotificationSrv) RouteGetReceivers(c *contextmodel.ReqContext) respon receivers, err := srv.receiverService.GetReceivers(c.Req.Context(), q, c.SignedInUser) if err != nil { - if errors.Is(err, notifier.ErrPermissionDenied) { - return ErrResp(http.StatusForbidden, err, "permission denied") - } - return ErrResp(http.StatusInternalServerError, err, "failed to get receiver groups") + return response.ErrOrFallback(http.StatusInternalServerError, "failed to get receiver groups", err) } return response.JSON(http.StatusOK, receivers) diff --git a/pkg/services/ngalert/api/api_notifications_test.go b/pkg/services/ngalert/api/api_notifications_test.go index 779521dd4f1..3c2bae6f209 100644 --- a/pkg/services/ngalert/api/api_notifications_test.go +++ b/pkg/services/ngalert/api/api_notifications_test.go @@ -3,18 +3,24 @@ package api import ( "context" "encoding/json" + "fmt" "net/http" "net/http/httptest" "net/url" + "strconv" + "strings" "testing" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/infra/log/logtest" + "github.com/grafana/grafana/pkg/services/accesscontrol" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" + ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/user" "github.com/grafana/grafana/pkg/web" @@ -75,7 +81,7 @@ func TestRouteGetReceiver(t *testing.T) { t.Run("should pass along not found response", func(t *testing.T) { fakeReceiverSvc.GetReceiverFn = func(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error) { - return definitions.GettableApiReceiver{}, notifier.ErrNotFound + return definitions.GettableApiReceiver{}, notifier.ErrReceiverNotFound.Errorf("") } handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc)) rc := testReqCtx("GET") @@ -85,7 +91,7 @@ func TestRouteGetReceiver(t *testing.T) { t.Run("should pass along permission denied response", func(t *testing.T) { fakeReceiverSvc.GetReceiverFn = func(ctx context.Context, q models.GetReceiverQuery, u identity.Requester) (definitions.GettableApiReceiver, error) { - return definitions.GettableApiReceiver{}, notifier.ErrPermissionDenied + return definitions.GettableApiReceiver{}, ac.ErrAuthorizationBase.Errorf("") } handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc)) rc := testReqCtx("GET") @@ -155,7 +161,7 @@ func TestRouteGetReceivers(t *testing.T) { t.Run("should pass along permission denied response", func(t *testing.T) { fakeReceiverSvc.GetReceiversFn = func(ctx context.Context, q models.GetReceiversQuery, u identity.Requester) ([]definitions.GettableApiReceiver, error) { - return nil, notifier.ErrPermissionDenied + return nil, ac.ErrAuthorizationBase.Errorf("") } handler := NewNotificationsApi(newNotificationSrv(fakeReceiverSvc)) rc := testReqCtx("GET") @@ -164,6 +170,246 @@ func TestRouteGetReceivers(t *testing.T) { }) } +func TestRouteGetReceiversResponses(t *testing.T) { + createTestEnv := func(t *testing.T, testConfig string) testEnvironment { + env := createTestEnv(t, testConfig) + env.ac = &recordingAccessControlFake{ + Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingNotificationsRead) { + return true, nil + } + if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingReceiversList) { + return true, nil + } + return false, nil + }, + } + return env + } + + t.Run("list receivers", func(t *testing.T) { + t.Run("GET returns 200", func(t *testing.T) { + env := createTestEnv(t, testConfig) + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + response := sut.RouteGetReceivers(&rc) + + require.Equal(t, 200, response.Status()) + }) + t.Run("decrypt true without alert.provisioning.secrets:read permissions returns 403", func(t *testing.T) { + recPermCheck := false + env := createTestEnv(t, testConfig) + env.ac = &recordingAccessControlFake{ + Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingProvisioningReadSecrets) { + recPermCheck = true + } + return false, nil + }, + } + + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + rc.Context.Req.Form.Set("decrypt", "true") + + response := sut.RouteGetReceivers(&rc) + + require.True(t, recPermCheck) + require.Equal(t, 403, response.Status()) + }) + + t.Run("json body content is as expected", func(t *testing.T) { + expectedDecryptedResponse := `[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"ad95bd8a-49ed-4adc-bf89-1b444fa1aa5b","name":"grafana-default-email","type":"email","disableResolveMessage":false,"settings":{"addresses":"\u003cexample@email.com\u003e"},"secureFields":{}}]},{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthPassword":"testpass","basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","url":"some url","use_discord_username":true},"secureFields":{}}]},{"name":"pagerduty test","grafana_managed_receiver_configs":[{"uid":"b9bf06f8-bde2-4438-9d4a-bba0522dcd4d","name":"pagerduty test","type":"pagerduty","disableResolveMessage":false,"settings":{"client":"some client","integrationKey":"some key","severity":"criticalish"},"secureFields":{"integrationKey":true}}]},{"name":"slack test","grafana_managed_receiver_configs":[{"uid":"cbfd0976-8228-4126-b672-4419f30a9e50","name":"slack test","type":"slack","disableResolveMessage":true,"settings":{"text":"title body test","title":"title test","url":"some secure slack webhook"},"secureFields":{"url":true}}]}]` + expectedRedactedResponse := `[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"ad95bd8a-49ed-4adc-bf89-1b444fa1aa5b","name":"grafana-default-email","type":"email","disableResolveMessage":false,"settings":{"addresses":"\u003cexample@email.com\u003e"},"secureFields":{}}]},{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthPassword":"[REDACTED]","basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","url":"some url","use_discord_username":true},"secureFields":{}}]},{"name":"pagerduty test","grafana_managed_receiver_configs":[{"uid":"b9bf06f8-bde2-4438-9d4a-bba0522dcd4d","name":"pagerduty test","type":"pagerduty","disableResolveMessage":false,"settings":{"client":"some client","integrationKey":"[REDACTED]","severity":"criticalish"},"secureFields":{"integrationKey":true}}]},{"name":"slack test","grafana_managed_receiver_configs":[{"uid":"cbfd0976-8228-4126-b672-4419f30a9e50","name":"slack test","type":"slack","disableResolveMessage":true,"settings":{"text":"title body test","title":"title test","url":"[REDACTED]"},"secureFields":{"url":true}}]}]` + expectedListResponse := `[{"name":"grafana-default-email","grafana_managed_receiver_configs":[{"uid":"ad95bd8a-49ed-4adc-bf89-1b444fa1aa5b","name":"grafana-default-email","type":"email","disableResolveMessage":false,"secureFields":null}]},{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":false,"secureFields":null},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"secureFields":null}]},{"name":"pagerduty test","grafana_managed_receiver_configs":[{"uid":"b9bf06f8-bde2-4438-9d4a-bba0522dcd4d","name":"pagerduty test","type":"pagerduty","disableResolveMessage":false,"secureFields":null}]},{"name":"slack test","grafana_managed_receiver_configs":[{"uid":"cbfd0976-8228-4126-b672-4419f30a9e50","name":"slack test","type":"slack","disableResolveMessage":false,"secureFields":null}]}]` + t.Run("limit offset", func(t *testing.T) { + env := createTestEnv(t, testContactPointConfig) + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/json") + rc.Context.Req.Form.Set("decrypt", "false") + + var expected []definitions.GettableApiReceiver + err := json.Unmarshal([]byte(expectedRedactedResponse), &expected) + require.NoError(t, err) + type testcase struct { + limit int + offset int + expected []definitions.GettableApiReceiver + } + testcases := []testcase{ + {limit: 1, offset: 0, expected: expected[:1]}, + {limit: 2, offset: 0, expected: expected[:2]}, + {limit: 4, offset: 0, expected: expected[:4]}, + {limit: 1, offset: 1, expected: expected[1:2]}, + {limit: 2, offset: 2, expected: expected[2:4]}, + {limit: 2, offset: 99, expected: nil}, + {limit: 0, offset: 0, expected: expected}, + {limit: 0, offset: 1, expected: expected[1:]}, + } + for _, tc := range testcases { + t.Run(fmt.Sprintf("limit %d offset %d", tc.limit, tc.offset), func(t *testing.T) { + rc.Context.Req.Form.Set("limit", strconv.Itoa(tc.limit)) + rc.Context.Req.Form.Set("offset", strconv.Itoa(tc.offset)) + + response := sut.RouteGetReceivers(&rc) + require.Equal(t, 200, response.Status()) + + var configs []definitions.GettableApiReceiver + err := json.Unmarshal(response.Body(), &configs) + require.NoError(t, err) + + require.Equal(t, configs, tc.expected) + }) + } + }) + t.Run("decrypt false with read permissions is redacted", func(t *testing.T) { + env := createTestEnv(t, testContactPointConfig) + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/json") + rc.Context.Req.Form.Set("decrypt", "false") + + response := sut.RouteGetReceivers(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedRedactedResponse, string(response.Body())) // TODO: Should this endpoint ever return settings? + }) + t.Run("decrypt false with only list permissions, does not have settings", func(t *testing.T) { + env := createTestEnv(t, testContactPointConfig) + env.ac = &recordingAccessControlFake{ + Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingReceiversList) { + return true, nil + } + return false, nil + }, + } + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/json") + rc.Context.Req.Form.Set("decrypt", "false") + + response := sut.RouteGetReceivers(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedListResponse, string(response.Body())) + }) + t.Run("decrypt true with all permissions, contains decrypted settings", func(t *testing.T) { + env := createTestEnv(t, testContactPointConfig) + env.ac = &recordingAccessControlFake{ + Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + return true, nil + }, + } + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/json") + rc.Context.Req.Form.Set("decrypt", "true") + + response := sut.RouteGetReceivers(&rc) + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedDecryptedResponse, string(response.Body())) // TODO: Should this endpoint ever return settings? + }) + }) + }) + + t.Run("get receiver", func(t *testing.T) { + t.Run("GET returns 200", func(t *testing.T) { + env := createTestEnv(t, testConfig) + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + response := sut.RouteGetReceiver(&rc, "grafana-default-email") + + require.Equal(t, 200, response.Status()) + }) + + t.Run("decrypt true without secrets:read permissions returns 403", func(t *testing.T) { + recPermCheck := false + env := createTestEnv(t, testConfig) + env.ac = &recordingAccessControlFake{ + Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + if strings.Contains(evaluator.String(), accesscontrol.ActionAlertingReceiversReadSecrets) { + recPermCheck = true + } + return false, nil + }, + } + + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + rc.Context.Req.Form.Set("decrypt", "true") + + response := sut.RouteGetReceiver(&rc, "grafana-default-email") + + require.True(t, recPermCheck) + require.Equal(t, 403, response.Status()) + }) + + t.Run("json body content is as expected", func(t *testing.T) { + expectedRedactedResponse := `{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthPassword":"[REDACTED]","basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","url":"some url","use_discord_username":true},"secureFields":{}}]}` + expectedDecryptedResponse := `{"name":"multiple integrations","grafana_managed_receiver_configs":[{"uid":"c2090fda-f824-4add-b545-5a4d5c2ef082","name":"multiple integrations","type":"prometheus-alertmanager","disableResolveMessage":true,"settings":{"basicAuthPassword":"testpass","basicAuthUser":"test","url":"http://localhost:9093"},"secureFields":{"basicAuthPassword":true}},{"uid":"c84539ec-f87e-4fc5-9a91-7a687d34bbd1","name":"multiple integrations","type":"discord","disableResolveMessage":false,"settings":{"avatar_url":"some avatar","url":"some url","use_discord_username":true},"secureFields":{}}]}` + t.Run("decrypt false", func(t *testing.T) { + env := createTestEnv(t, testContactPointConfig) + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/json") + rc.Context.Req.Form.Set("decrypt", "false") + + response := sut.RouteGetReceiver(&rc, "multiple integrations") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedRedactedResponse, string(response.Body())) + }) + t.Run("decrypt true", func(t *testing.T) { + env := createTestEnv(t, testContactPointConfig) + env.ac = &recordingAccessControlFake{ + Callback: func(user *user.SignedInUser, evaluator accesscontrol.Evaluator) (bool, error) { + return true, nil + }, + } + sut := createNotificationSrvSutFromEnv(t, &env) + rc := createTestRequestCtx() + + rc.Context.Req.Header.Add("Accept", "application/json") + rc.Context.Req.Form.Set("decrypt", "true") + + response := sut.RouteGetReceiver(&rc, "multiple integrations") + + require.Equal(t, 200, response.Status()) + require.Equal(t, expectedDecryptedResponse, string(response.Body())) + }) + }) + }) +} + +func createNotificationSrvSutFromEnv(t *testing.T, env *testEnvironment) NotificationSrv { + t.Helper() + + receiverSvc := notifier.NewReceiverService( + env.ac, + legacy_storage.NewAlertmanagerConfigStore(env.configs), + env.prov, + env.secrets, + env.xact, + env.log, + ) + return NotificationSrv{ + logger: env.log, + receiverService: receiverSvc, + } +} + func newNotificationSrv(receiverService ReceiverService) *NotificationSrv { return &NotificationSrv{ logger: log.NewNopLogger(), diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index dc6567100c2..4b2f8befe21 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -138,11 +138,9 @@ func (srv *ProvisioningSrv) RouteGetContactPoints(c *contextmodel.ReqContext) re } cps, err := srv.contactPointService.GetContactPoints(c.Req.Context(), q, c.SignedInUser) if err != nil { - if errors.Is(err, provisioning.ErrPermissionDenied) { - return ErrResp(http.StatusForbidden, err, "") - } - return ErrResp(http.StatusInternalServerError, err, "") + return response.ErrOrFallback(http.StatusInternalServerError, "", err) } + return response.JSON(http.StatusOK, cps) } @@ -154,10 +152,7 @@ func (srv *ProvisioningSrv) RouteGetContactPointsExport(c *contextmodel.ReqConte } cps, err := srv.contactPointService.GetContactPoints(c.Req.Context(), q, c.SignedInUser) if err != nil { - if errors.Is(err, provisioning.ErrPermissionDenied) { - return ErrResp(http.StatusForbidden, err, "") - } - return ErrResp(http.StatusInternalServerError, err, "") + return response.ErrOrFallback(http.StatusInternalServerError, "", err) } e, err := AlertingFileExportFromEmbeddedContactPoints(c.SignedInUser.GetOrgID(), cps) diff --git a/pkg/services/ngalert/api/api_provisioning_test.go b/pkg/services/ngalert/api/api_provisioning_test.go index b8a9d26523e..7c1b876d1eb 100644 --- a/pkg/services/ngalert/api/api_provisioning_test.go +++ b/pkg/services/ngalert/api/api_provisioning_test.go @@ -39,6 +39,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/quota/quotatest" @@ -1753,7 +1754,7 @@ type testEnvironment struct { store store.DBstore folderService folder.Service dashboardService dashboards.DashboardService - configs provisioning.AMConfigStore + configs legacy_storage.AMConfigStore xact provisioning.TransactionManager quotas provisioning.QuotaChecker prov provisioning.ProvisioningStore @@ -1780,7 +1781,7 @@ func createTestEnv(t *testing.T, testConfig string) testEnvironment { require.NoError(t, err) log := log.NewNopLogger() - configs := &provisioning.MockAMConfigStore{} + configs := &legacy_storage.MockAMConfigStore{} configs.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: string(raw), @@ -1885,13 +1886,21 @@ func createProvisioningSrvSut(t *testing.T) ProvisioningSrv { func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) ProvisioningSrv { t.Helper() - receiverSvc := notifier.NewReceiverService(env.ac, env.configs, env.prov, env.secrets, env.xact, env.log) + configStore := legacy_storage.NewAlertmanagerConfigStore(env.configs) + receiverSvc := notifier.NewReceiverService( + env.ac, + configStore, + env.prov, + env.secrets, + env.xact, + env.log, + ) return ProvisioningSrv{ log: env.log, policies: newFakeNotificationPolicyService(), - contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, receiverSvc, env.log, env.store), - templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log), - muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log, env.store), + contactPointService: provisioning.NewContactPointService(configStore, env.secrets, env.prov, env.xact, receiverSvc, env.log, env.store), + templates: provisioning.NewTemplateService(configStore, env.prov, env.xact, env.log), + muteTimings: provisioning.NewMuteTimingService(configStore, env.prov, env.xact, env.log, env.store), alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.folderService, env.quotas, env.xact, 60, 10, 100, env.log, &provisioning.NotificationSettingsValidatorProviderFake{}, env.rulesAuthz), folderSvc: env.folderService, featureManager: env.features, diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 3fe6fc36942..96457f04aa7 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -35,6 +35,7 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/remote" "github.com/grafana/grafana/pkg/services/ngalert/schedule" @@ -408,13 +409,21 @@ func (ng *AlertNG) init() error { ng.stateManager = stateManager ng.schedule = scheduler - receiverService := notifier.NewReceiverService(ng.accesscontrol, ng.store, ng.store, ng.SecretsService, ng.store, ng.Log) + configStore := legacy_storage.NewAlertmanagerConfigStore(ng.store) + receiverService := notifier.NewReceiverService( + ng.accesscontrol, + configStore, + ng.store, + ng.SecretsService, + ng.store, + ng.Log, + ) // Provisioning - policyService := provisioning.NewNotificationPolicyService(ng.store, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log) - contactPointService := provisioning.NewContactPointService(ng.store, ng.SecretsService, ng.store, ng.store, receiverService, ng.Log, ng.store) - templateService := provisioning.NewTemplateService(ng.store, ng.store, ng.store, ng.Log) - muteTimingService := provisioning.NewMuteTimingService(ng.store, ng.store, ng.store, ng.Log, ng.store) + policyService := provisioning.NewNotificationPolicyService(configStore, ng.store, ng.store, ng.Cfg.UnifiedAlerting, ng.Log) + contactPointService := provisioning.NewContactPointService(configStore, ng.SecretsService, ng.store, ng.store, receiverService, ng.Log, ng.store) + templateService := provisioning.NewTemplateService(configStore, ng.store, ng.store, ng.Log) + muteTimingService := provisioning.NewMuteTimingService(configStore, ng.store, ng.store, ng.Log, ng.store) alertRuleService := provisioning.NewAlertRuleService(ng.store, ng.store, ng.folderService, ng.QuotaService, ng.store, int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), int64(ng.Cfg.UnifiedAlerting.BaseInterval.Seconds()), diff --git a/pkg/services/ngalert/notifier/legacy_storage/compat.go b/pkg/services/ngalert/notifier/legacy_storage/compat.go new file mode 100644 index 00000000000..ef77ad30888 --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/compat.go @@ -0,0 +1,17 @@ +package legacy_storage + +import ( + "encoding/base64" +) + +func NameToUid(name string) string { + return base64.RawURLEncoding.EncodeToString([]byte(name)) +} + +func UidToName(uid string) (string, error) { + data, err := base64.RawURLEncoding.DecodeString(uid) + if err != nil { + return uid, err + } + return string(data), nil +} diff --git a/pkg/services/ngalert/notifier/legacy_storage/config.go b/pkg/services/ngalert/notifier/legacy_storage/config.go new file mode 100644 index 00000000000..0a7e904544f --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/config.go @@ -0,0 +1,92 @@ +package legacy_storage + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +type amConfigStore interface { + GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error) + UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error +} + +func DeserializeAlertmanagerConfig(config []byte) (*definitions.PostableUserConfig, error) { + result := definitions.PostableUserConfig{} + if err := json.Unmarshal(config, &result); err != nil { + return nil, makeErrBadAlertmanagerConfiguration(err) + } + return &result, nil +} + +func SerializeAlertmanagerConfig(config definitions.PostableUserConfig) ([]byte, error) { + return json.Marshal(config) +} + +type ConfigRevision struct { + Config *definitions.PostableUserConfig + ConcurrencyToken string + Version string +} + +func getLastConfiguration(ctx context.Context, orgID int64, store amConfigStore) (*ConfigRevision, error) { + alertManagerConfig, err := store.GetLatestAlertmanagerConfiguration(ctx, orgID) + if err != nil { + return nil, err + } + + if alertManagerConfig == nil { + return nil, ErrNoAlertmanagerConfiguration.Errorf("") + } + + concurrencyToken := alertManagerConfig.ConfigurationHash + cfg, err := DeserializeAlertmanagerConfig([]byte(alertManagerConfig.AlertmanagerConfiguration)) + if err != nil { + return nil, err + } + + return &ConfigRevision{ + Config: cfg, + ConcurrencyToken: concurrencyToken, + Version: alertManagerConfig.ConfigurationVersion, + }, nil +} + +type alertmanagerConfigStoreImpl struct { + store amConfigStore +} + +func NewAlertmanagerConfigStore(store amConfigStore) *alertmanagerConfigStoreImpl { + return &alertmanagerConfigStoreImpl{store: store} +} + +func (a alertmanagerConfigStoreImpl) Get(ctx context.Context, orgID int64) (*ConfigRevision, error) { + return getLastConfiguration(ctx, orgID, a.store) +} + +func (a alertmanagerConfigStoreImpl) Save(ctx context.Context, revision *ConfigRevision, orgID int64) error { + serialized, err := SerializeAlertmanagerConfig(*revision.Config) + if err != nil { + return err + } + cmd := models.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: string(serialized), + ConfigurationVersion: revision.Version, + FetchedConfigurationHash: revision.ConcurrencyToken, + Default: false, + OrgID: orgID, + } + return a.PersistConfig(ctx, &cmd) +} + +// PersistConfig validates to config before eventually persisting it if no error occurs +func (a alertmanagerConfigStoreImpl) PersistConfig(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error { + cfg := &definitions.PostableUserConfig{} + if err := json.Unmarshal([]byte(cmd.AlertmanagerConfiguration), cfg); err != nil { + return fmt.Errorf("change would result in an invalid configuration state: %w", err) + } + return a.store.UpdateAlertmanagerConfiguration(ctx, cmd) +} diff --git a/pkg/services/ngalert/provisioning/config_test.go b/pkg/services/ngalert/notifier/legacy_storage/config_test.go similarity index 87% rename from pkg/services/ngalert/provisioning/config_test.go rename to pkg/services/ngalert/notifier/legacy_storage/config_test.go index 454f7921098..549fdab12ea 100644 --- a/pkg/services/ngalert/provisioning/config_test.go +++ b/pkg/services/ngalert/notifier/legacy_storage/config_test.go @@ -1,4 +1,4 @@ -package provisioning +package legacy_storage import ( "context" @@ -13,8 +13,11 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/setting" ) +var defaultConfig = setting.GetAlertmanagerDefaultConfiguration() + func TestAlertmanagerConfigStoreGet(t *testing.T) { orgID := int64(1) @@ -40,9 +43,9 @@ func TestAlertmanagerConfigStoreGet(t *testing.T) { revision, err := store.Get(context.Background(), orgID) require.NoError(t, err) - require.Equal(t, expected.ConfigurationVersion, revision.version) - require.Equal(t, expected.ConfigurationHash, revision.concurrencyToken) - require.Equal(t, expectedCfg, *revision.cfg) + require.Equal(t, expected.ConfigurationVersion, revision.Version) + require.Equal(t, expected.ConfigurationHash, revision.ConcurrencyToken) + require.Equal(t, expectedCfg, *revision.Config) storeMock.AssertCalled(t, "GetLatestAlertmanagerConfiguration", mock.Anything, orgID) }) @@ -85,13 +88,13 @@ func TestAlertmanagerConfigStoreSave(t *testing.T) { cfg := definitions.PostableUserConfig{} require.NoError(t, json.Unmarshal([]byte(defaultConfig), &cfg)) - expectedCfg, err := serializeAlertmanagerConfig(cfg) + expectedCfg, err := SerializeAlertmanagerConfig(cfg) require.NoError(t, err) - revision := cfgRevision{ - cfg: &cfg, - concurrencyToken: "config-hash-123", - version: "123", + revision := ConfigRevision{ + Config: &cfg, + ConcurrencyToken: "config-hash-123", + Version: "123", } t.Run("should save the config to store", func(t *testing.T) { @@ -101,9 +104,9 @@ func TestAlertmanagerConfigStoreSave(t *testing.T) { storeMock.EXPECT().UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).RunAndReturn(func(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error { assert.Equal(t, string(expectedCfg), cmd.AlertmanagerConfiguration) assert.Equal(t, orgID, cmd.OrgID) - assert.Equal(t, revision.version, cmd.ConfigurationVersion) + assert.Equal(t, revision.Version, cmd.ConfigurationVersion) assert.Equal(t, false, cmd.Default) - assert.Equal(t, revision.concurrencyToken, cmd.FetchedConfigurationHash) + assert.Equal(t, revision.ConcurrencyToken, cmd.FetchedConfigurationHash) return nil }) diff --git a/pkg/services/ngalert/notifier/legacy_storage/errors.go b/pkg/services/ngalert/notifier/legacy_storage/errors.go new file mode 100644 index 00000000000..e6c1487d48c --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/errors.go @@ -0,0 +1,18 @@ +package legacy_storage + +import "github.com/grafana/grafana/pkg/apimachinery/errutil" + +var ( + ErrNoAlertmanagerConfiguration = errutil.Internal("alerting.notification.configMissing", errutil.WithPublicMessage("No alertmanager configuration present in this organization")) + ErrBadAlertmanagerConfiguration = errutil.Internal("alerting.notification.configCorrupted").MustTemplate("Failed to unmarshal the Alertmanager configuration", errutil.WithPublic("Current Alertmanager configuration in the storage is corrupted. Reset the configuration or rollback to a recent valid one.")) +) + +func makeErrBadAlertmanagerConfiguration(err error) error { + data := errutil.TemplateData{ + Public: map[string]interface{}{ + "Error": err.Error(), + }, + Error: err, + } + return ErrBadAlertmanagerConfiguration.Build(data) +} diff --git a/pkg/services/ngalert/notifier/legacy_storage/persist.go b/pkg/services/ngalert/notifier/legacy_storage/persist.go new file mode 100644 index 00000000000..141d375485b --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/persist.go @@ -0,0 +1,15 @@ +package legacy_storage + +import ( + "context" + + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +// AMStore is a store of Alertmanager configurations. +// +//go:generate mockery --name AMConfigStore --structname MockAMConfigStore --inpackage --filename persist_mock.go --with-expecter +type AMConfigStore interface { + GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error) + UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error +} diff --git a/pkg/services/ngalert/provisioning/persist_mock.go b/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go similarity index 99% rename from pkg/services/ngalert/provisioning/persist_mock.go rename to pkg/services/ngalert/notifier/legacy_storage/persist_mock.go index 53cd220762b..0520bbe03ec 100644 --- a/pkg/services/ngalert/provisioning/persist_mock.go +++ b/pkg/services/ngalert/notifier/legacy_storage/persist_mock.go @@ -1,6 +1,6 @@ // Code generated by mockery v2.34.2. DO NOT EDIT. -package provisioning +package legacy_storage import ( context "context" diff --git a/pkg/services/ngalert/notifier/legacy_storage/receivers.go b/pkg/services/ngalert/notifier/legacy_storage/receivers.go new file mode 100644 index 00000000000..01268a1e8ed --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/receivers.go @@ -0,0 +1,53 @@ +package legacy_storage + +import ( + "slices" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" +) + +func (rev *ConfigRevision) DeleteReceiver(uid string) { + // Remove the receiver from the configuration. + rev.Config.AlertmanagerConfig.Receivers = slices.DeleteFunc(rev.Config.AlertmanagerConfig.Receivers, func(r *definitions.PostableApiReceiver) bool { + return NameToUid(r.GetName()) == uid + }) +} + +func (rev *ConfigRevision) ReceiverNameUsedByRoutes(name string) bool { + return isReceiverInUse(name, []*definitions.Route{rev.Config.AlertmanagerConfig.Route}) +} + +func (rev *ConfigRevision) GetReceiver(uid string) *definitions.PostableApiReceiver { + for _, r := range rev.Config.AlertmanagerConfig.Receivers { + if NameToUid(r.GetName()) == uid { + return r + } + } + return nil +} + +func (rev *ConfigRevision) GetReceivers(uids []string) []*definitions.PostableApiReceiver { + receivers := make([]*definitions.PostableApiReceiver, 0, len(uids)) + for _, r := range rev.Config.AlertmanagerConfig.Receivers { + if len(uids) == 0 || slices.Contains(uids, NameToUid(r.GetName())) { + receivers = append(receivers, r) + } + } + return receivers +} + +// isReceiverInUse checks if a receiver is used in a route or any of its sub-routes. +func isReceiverInUse(name string, routes []*definitions.Route) bool { + if len(routes) == 0 { + return false + } + for _, route := range routes { + if route.Receiver == name { + return true + } + if isReceiverInUse(name, route.Routes) { + return true + } + } + return false +} diff --git a/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go b/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go new file mode 100644 index 00000000000..acc406a96c4 --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/receivers_test.go @@ -0,0 +1,40 @@ +package legacy_storage + +import ( + "testing" + + "github.com/stretchr/testify/require" + + "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" +) + +func TestReceiverInUse(t *testing.T) { + result := isReceiverInUse("test", []*definitions.Route{ + { + Receiver: "not-test", + Routes: []*definitions.Route{ + { + Receiver: "not-test", + }, + { + Receiver: "test", + }, + }, + }, + }) + require.True(t, result) + result = isReceiverInUse("test", []*definitions.Route{ + { + Receiver: "not-test", + Routes: []*definitions.Route{ + { + Receiver: "not-test", + }, + { + Receiver: "not-test", + }, + }, + }, + }) + require.False(t, result) +} diff --git a/pkg/services/ngalert/notifier/legacy_storage/testing.go b/pkg/services/ngalert/notifier/legacy_storage/testing.go new file mode 100644 index 00000000000..a4b5d9d5eba --- /dev/null +++ b/pkg/services/ngalert/notifier/legacy_storage/testing.go @@ -0,0 +1,61 @@ +package legacy_storage + +import ( + "context" + + "github.com/stretchr/testify/mock" + + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +func (m *MockAMConfigStore_Expecter) GetsConfig(ac models.AlertConfiguration) *MockAMConfigStore_Expecter { + m.GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(&ac, nil) + return m +} + +func (m *MockAMConfigStore_Expecter) SaveSucceeds() *MockAMConfigStore_Expecter { + m.UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil) + return m +} + +func (m *MockAMConfigStore_Expecter) SaveSucceedsIntercept(intercepted *models.SaveAlertmanagerConfigurationCmd) *MockAMConfigStore_Expecter { + m.UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). + Return(nil). + Run(func(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) { + *intercepted = *cmd + }) + return m +} + +type methodCall struct { + Method string + Args []interface{} +} + +type AlertmanagerConfigStoreFake struct { + Calls []methodCall + GetFn func(ctx context.Context, orgID int64) (*ConfigRevision, error) + SaveFn func(ctx context.Context, revision *ConfigRevision) error +} + +func (a *AlertmanagerConfigStoreFake) Get(ctx context.Context, orgID int64) (*ConfigRevision, error) { + a.Calls = append(a.Calls, methodCall{ + Method: "Get", + Args: []interface{}{ctx, orgID}, + }) + if a.GetFn != nil { + return a.GetFn(ctx, orgID) + } + return nil, nil +} + +func (a *AlertmanagerConfigStoreFake) Save(ctx context.Context, revision *ConfigRevision, orgID int64) error { + a.Calls = append(a.Calls, methodCall{ + Method: "Save", + Args: []interface{}{ctx, revision, orgID}, + }) + if a.SaveFn != nil { + return a.SaveFn(ctx, revision) + } + return nil +} diff --git a/pkg/services/ngalert/notifier/receiver_svc.go b/pkg/services/ngalert/notifier/receiver_svc.go index e7349eac53b..bfa3f530304 100644 --- a/pkg/services/ngalert/notifier/receiver_svc.go +++ b/pkg/services/ngalert/notifier/receiver_svc.go @@ -3,52 +3,45 @@ package notifier import ( "context" "encoding/base64" - "encoding/json" - "errors" - "fmt" - "hash/fnv" - "slices" + + "github.com/grafana/alerting/definition" "github.com/grafana/grafana/pkg/apimachinery/errutil" "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/accesscontrol" + ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation" "github.com/grafana/grafana/pkg/services/secrets" ) var ( - // ErrPermissionDenied is returned when the user does not have permission to perform the requested action. - ErrPermissionDenied = errors.New("permission denied") // TODO: convert to errutil - // ErrNotFound is returned when the requested resource does not exist. - ErrNotFound = errors.New("not found") // TODO: convert to errutil -) - -var ( - ErrReceiverInUse = errutil.Conflict("alerting.notifications.receiver.used", errutil.WithPublicMessage("Receiver is used by one or many notification policies")) - ErrVersionConflict = errutil.Conflict("alerting.notifications.receiver.conflict") + ErrReceiverNotFound = errutil.NotFound("alerting.notifications.receiver.notFound") + ErrReceiverInUse = errutil.Conflict("alerting.notifications.receiver.used").MustTemplate("Receiver is used by notification policies or alert rules") ) // ReceiverService is the service for managing alertmanager receivers. type ReceiverService struct { ac accesscontrol.AccessControl provisioningStore provisoningStore - cfgStore configStore + cfgStore alertmanagerConfigStore encryptionService secrets.Service xact transactionManager log log.Logger validator validation.ProvenanceStatusTransitionValidator } -type configStore interface { - GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error) - UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error +type alertmanagerConfigStore interface { + Get(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) + Save(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64) error } type provisoningStore interface { GetProvenances(ctx context.Context, org int64, resourceType string) (map[string]models.Provenance, error) + SetProvenance(ctx context.Context, o models.Provisionable, org int64, p models.Provenance) error DeleteProvenance(ctx context.Context, o models.Provisionable, org int64) error } @@ -58,7 +51,7 @@ type transactionManager interface { func NewReceiverService( ac accesscontrol.AccessControl, - cfgStore configStore, + cfgStore alertmanagerConfigStore, provisioningStore provisoningStore, encryptionService secrets.Service, xact transactionManager, @@ -82,7 +75,7 @@ func (rs *ReceiverService) shouldDecrypt(ctx context.Context, user identity.Requ } if reqDecrypt && !decryptAccess { - return false, ErrPermissionDenied + return false, ac.NewAuthorizationErrorWithPermissions("read any decrypted receiver", nil) // TODO: Replace with authz service. } return decryptAccess && reqDecrypt, nil @@ -117,60 +110,51 @@ func (rs *ReceiverService) hasList(ctx context.Context, user identity.Requester) // The receiver's secure settings are decrypted if requested and the user has access to do so. func (rs *ReceiverService) GetReceiver(ctx context.Context, q models.GetReceiverQuery, user identity.Requester) (definitions.GettableApiReceiver, error) { if q.Decrypt && user == nil { - return definitions.GettableApiReceiver{}, ErrPermissionDenied + return definitions.GettableApiReceiver{}, ac.NewAuthorizationErrorWithPermissions("read any decrypted receiver", nil) // TODO: Replace with authz service. } - baseCfg, err := rs.cfgStore.GetLatestAlertmanagerConfiguration(ctx, q.OrgID) + revision, err := rs.cfgStore.Get(ctx, q.OrgID) + if err != nil { + return definitions.GettableApiReceiver{}, err + } + postable := revision.GetReceiver(legacy_storage.NameToUid(q.Name)) + if postable == nil { + return definitions.GettableApiReceiver{}, ErrReceiverNotFound.Errorf("") + } + + decrypt, err := rs.shouldDecrypt(ctx, user, q.Decrypt) + if err != nil { + return definitions.GettableApiReceiver{}, err + } + decryptFn := rs.decryptOrRedact(ctx, decrypt, q.Name, "") + + storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType()) if err != nil { return definitions.GettableApiReceiver{}, err } - cfg := definitions.PostableUserConfig{} - err = json.Unmarshal([]byte(baseCfg.AlertmanagerConfiguration), &cfg) - if err != nil { - return definitions.GettableApiReceiver{}, err - } - - provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType()) - if err != nil { - return definitions.GettableApiReceiver{}, err - } - - receivers := cfg.AlertmanagerConfig.Receivers - for _, r := range receivers { - if r.Name == q.Name { - decrypt, err := rs.shouldDecrypt(ctx, user, q.Decrypt) - if err != nil { - return definitions.GettableApiReceiver{}, err - } - decryptFn := rs.decryptOrRedact(ctx, decrypt, q.Name, "") - - return PostableToGettableApiReceiver(r, provenances, decryptFn, false) - } - } - - return definitions.GettableApiReceiver{}, ErrNotFound + return PostableToGettableApiReceiver(postable, storedProvenances, decryptFn, false) } // GetReceivers returns a list of receivers a user has access to. // Receivers can be filtered by name, and secure settings are decrypted if requested and the user has access to do so. func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceiversQuery, user identity.Requester) ([]definitions.GettableApiReceiver, error) { if q.Decrypt && user == nil { - return nil, ErrPermissionDenied + return nil, ac.NewAuthorizationErrorWithPermissions("read any decrypted receiver", nil) // TODO: Replace with authz service. } - baseCfg, err := rs.cfgStore.GetLatestAlertmanagerConfiguration(ctx, q.OrgID) + uids := make([]string, 0, len(q.Names)) + for _, name := range q.Names { + uids = append(uids, legacy_storage.NameToUid(name)) + } + + revision, err := rs.cfgStore.Get(ctx, q.OrgID) if err != nil { return nil, err } + postables := revision.GetReceivers(uids) - cfg := definitions.PostableUserConfig{} - err = json.Unmarshal([]byte(baseCfg.AlertmanagerConfiguration), &cfg) - if err != nil { - return nil, err - } - - provenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType()) + storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType()) if err != nil { return nil, err } @@ -188,15 +172,12 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive // User doesn't have any permissions on the receivers. // This is mostly a safeguard as it should not be possible with current API endpoints + middleware authentication. if !listAccess && !readRedactedAccess { - return nil, ErrPermissionDenied + return nil, ac.NewAuthorizationErrorWithPermissions("read any receiver", nil) // TODO: Replace with authz service. } var output []definitions.GettableApiReceiver - for i := q.Offset; i < len(cfg.AlertmanagerConfig.Receivers); i++ { - r := cfg.AlertmanagerConfig.Receivers[i] - if len(q.Names) > 0 && !slices.Contains(q.Names, r.Name) { - continue - } + for i := q.Offset; i < len(postables); i++ { + r := postables[i] decrypt, err := rs.shouldDecrypt(ctx, user, q.Decrypt) if err != nil { @@ -210,7 +191,7 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive // - Doesn't have ReadRedacted (or ReadDecrypted permission since it's a subset). listOnly := !readRedactedAccess - res, err := PostableToGettableApiReceiver(r, provenances, decryptFn, listOnly) + res, err := PostableToGettableApiReceiver(r, storedProvenances, decryptFn, listOnly) if err != nil { return nil, err } @@ -229,25 +210,18 @@ func (rs *ReceiverService) GetReceivers(ctx context.Context, q models.GetReceive // UID field currently does not exist, we assume the uid is a particular hashed value of the receiver name. func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, orgID int64, callerProvenance definitions.Provenance, version string) error { //TODO: Check delete permissions. - baseCfg, err := rs.cfgStore.GetLatestAlertmanagerConfiguration(ctx, orgID) + revision, err := rs.cfgStore.Get(ctx, orgID) if err != nil { return err } - - cfg := definitions.PostableUserConfig{} - err = json.Unmarshal([]byte(baseCfg.AlertmanagerConfiguration), &cfg) - if err != nil { - return err - } - - idx, recv := getReceiverByUID(cfg, uid) - if recv == nil { - return ErrNotFound // TODO: nil? + postable := revision.GetReceiver(uid) + if postable == nil { + return ErrReceiverNotFound.Errorf("") } // TODO: Implement + check optimistic concurrency. - storedProvenance, err := rs.getContactPointProvenance(ctx, recv, orgID) + storedProvenance, err := rs.getContactPointProvenance(ctx, postable, orgID) if err != nil { return err } @@ -256,39 +230,24 @@ func (rs *ReceiverService) DeleteReceiver(ctx context.Context, uid string, orgID return err } - if isReceiverInUse(recv.Name, []*definitions.Route{cfg.AlertmanagerConfig.Route}) { - return ErrReceiverInUse.Errorf("") + usedByRoutes := revision.ReceiverNameUsedByRoutes(postable.GetName()) + usedByRules, err := rs.UsedByRules(ctx, orgID, uid) + if err != nil { + return err } - // Remove the receiver from the configuration. - cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers[:idx], cfg.AlertmanagerConfig.Receivers[idx+1:]...) + if usedByRoutes || len(usedByRules) > 0 { + return makeReceiverInUseErr(usedByRoutes, usedByRules) + } + + revision.DeleteReceiver(uid) return rs.xact.InTransaction(ctx, func(ctx context.Context) error { - serialized, err := json.Marshal(cfg) + err = rs.cfgStore.Save(ctx, revision, orgID) if err != nil { return err } - cmd := models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(serialized), - ConfigurationVersion: baseCfg.ConfigurationVersion, - FetchedConfigurationHash: baseCfg.ConfigurationHash, - Default: false, - OrgID: orgID, - } - - err = rs.cfgStore.UpdateAlertmanagerConfiguration(ctx, &cmd) - if err != nil { - return err - } - - // Remove provenance for all integrations in the receiver. - for _, integration := range recv.GrafanaManagedReceivers { - target := definitions.EmbeddedContactPoint{UID: integration.UID} - if err := rs.provisioningStore.DeleteProvenance(ctx, &target, orgID); err != nil { - return err - } - } - return nil + return rs.deleteProvenances(ctx, orgID, postable.GrafanaManagedReceivers) }) } @@ -302,6 +261,22 @@ func (rs *ReceiverService) UpdateReceiver(ctx context.Context, r definitions.Get panic("not implemented") } +func (rs *ReceiverService) UsedByRules(ctx context.Context, orgID int64, uid string) ([]models.AlertRuleKey, error) { + //TODO: Implement + return []models.AlertRuleKey{}, nil +} + +func (rs *ReceiverService) deleteProvenances(ctx context.Context, orgID int64, integrations []*definition.PostableGrafanaReceiver) error { + // Delete provenance for all integrations. + for _, integration := range integrations { + target := definitions.EmbeddedContactPoint{UID: integration.UID} + if err := rs.provisioningStore.DeleteProvenance(ctx, &target, orgID); err != nil { + return err + } + } + return nil +} + func (rs *ReceiverService) decryptOrRedact(ctx context.Context, decrypt bool, name, fallback string) func(value string) string { return func(value string) string { if !decrypt { @@ -345,37 +320,21 @@ func (rs *ReceiverService) getContactPointProvenance(ctx context.Context, r *def return models.ProvenanceNone, nil } -// getReceiverByUID returns the index and receiver with the given UID. -func getReceiverByUID(cfg definitions.PostableUserConfig, uid string) (int, *definitions.PostableApiReceiver) { - for i, r := range cfg.AlertmanagerConfig.Receivers { - if getUID(r) == uid { - return i, r - } +func makeReceiverInUseErr(usedByRoutes bool, rules []models.AlertRuleKey) error { + uids := make([]string, 0, len(rules)) + for _, key := range rules { + uids = append(uids, key.UID) + } + data := make(map[string]any, 2) + if len(uids) > 0 { + data["UsedByRules"] = uids + } + if usedByRoutes { + data["UsedByRoutes"] = true } - return 0, nil -} -// getUID returns the UID of a PostableApiReceiver. -// Currently, the UID is a hash of the receiver name. -func getUID(t *definitions.PostableApiReceiver) string { // TODO replace to stable UID when we switch to normal storage - sum := fnv.New64() - _, _ = sum.Write([]byte(t.Name)) - return fmt.Sprintf("%016x", sum.Sum64()) -} - -// TODO: Check if the contact point is used directly in an alert rule. -// isReceiverInUse checks if a receiver is used in a route or any of its sub-routes. -func isReceiverInUse(name string, routes []*definitions.Route) bool { - if len(routes) == 0 { - return false - } - for _, route := range routes { - if route.Receiver == name { - return true - } - if isReceiverInUse(name, route.Routes) { - return true - } - } - return false + return ErrReceiverInUse.Build(errutil.TemplateData{ + Public: data, + Error: nil, + }) } diff --git a/pkg/services/ngalert/notifier/receiver_svc_test.go b/pkg/services/ngalert/notifier/receiver_svc_test.go index 0d719d29746..806ab86530d 100644 --- a/pkg/services/ngalert/notifier/receiver_svc_test.go +++ b/pkg/services/ngalert/notifier/receiver_svc_test.go @@ -8,6 +8,7 @@ import ( "github.com/stretchr/testify/require" + "github.com/grafana/grafana/pkg/apimachinery/identity" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/db" "github.com/grafana/grafana/pkg/infra/log" @@ -17,7 +18,7 @@ import ( "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/database" @@ -49,7 +50,7 @@ func TestReceiverService_GetReceiver(t *testing.T) { sut := createReceiverServiceSut(t, secretsService) _, err := sut.GetReceiver(context.Background(), singleQ(1, "nonexistent"), redactedUser) - require.ErrorIs(t, err, ErrNotFound) + require.ErrorIs(t, err, ErrReceiverNotFound.Errorf("")) }) } @@ -109,32 +110,32 @@ func TestReceiverService_DecryptRedact(t *testing.T) { for _, tc := range []struct { name string decrypt bool - user *user.SignedInUser - err error + user identity.Requester + err string }{ { name: "service redacts receivers by default", decrypt: false, user: readUser, - err: nil, + err: "", }, { name: "service returns error when trying to decrypt without permission", decrypt: true, user: readUser, - err: ErrPermissionDenied, + err: "[alerting.unauthorized] user is not authorized to read any decrypted receiver", }, { name: "service returns error if user is nil and decrypt is true", decrypt: true, user: nil, - err: ErrPermissionDenied, + err: "[alerting.unauthorized] user is not authorized to read any decrypted receiver", }, { name: "service decrypts receivers with permission", decrypt: true, user: secretUser, - err: nil, + err: "", }, } { for _, method := range getMethods { @@ -152,14 +153,18 @@ func TestReceiverService_DecryptRedact(t *testing.T) { q.Decrypt = tc.decrypt var multiRes []definitions.GettableApiReceiver multiRes, err = sut.GetReceivers(context.Background(), q, tc.user) - if tc.err == nil { + if tc.err == "" { require.Len(t, multiRes, 1) res = multiRes[0] } } - require.ErrorIs(t, err, tc.err) + if tc.err == "" { + require.NoError(t, err) + } else { + require.ErrorContains(t, err, tc.err) + } - if tc.err == nil { + if tc.err == "" { require.Equal(t, "slack receiver", res.Name) require.Len(t, res.GrafanaManagedReceivers, 1) require.Equal(t, "UID2", res.GrafanaManagedReceivers[0].UID) @@ -183,15 +188,14 @@ func createReceiverServiceSut(t *testing.T, encryptSvc secrets.Service) *Receive xact := newNopTransactionManager() provisioningStore := fakes.NewFakeProvisioningStore() - return &ReceiverService{ - ac: acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), - provisioningStore: provisioningStore, - cfgStore: store, - encryptionService: encryptSvc, - xact: xact, - log: log.NewNopLogger(), - validator: validation.ValidateProvenanceRelaxed, - } + return NewReceiverService( + acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), + legacy_storage.NewAlertmanagerConfigStore(store), + provisioningStore, + encryptSvc, + xact, + log.NewNopLogger(), + ) } func createEncryptedConfig(t *testing.T, secretService secrets.Service) string { diff --git a/pkg/services/ngalert/provisioning/config.go b/pkg/services/ngalert/provisioning/config.go deleted file mode 100644 index f711b02d718..00000000000 --- a/pkg/services/ngalert/provisioning/config.go +++ /dev/null @@ -1,78 +0,0 @@ -package provisioning - -import ( - "context" - "encoding/json" - - "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/services/ngalert/models" -) - -func deserializeAlertmanagerConfig(config []byte) (*definitions.PostableUserConfig, error) { - result := definitions.PostableUserConfig{} - if err := json.Unmarshal(config, &result); err != nil { - return nil, makeErrBadAlertmanagerConfiguration(err) - } - return &result, nil -} - -func serializeAlertmanagerConfig(config definitions.PostableUserConfig) ([]byte, error) { - return json.Marshal(config) -} - -type cfgRevision struct { - cfg *definitions.PostableUserConfig - concurrencyToken string - version string -} - -func getLastConfiguration(ctx context.Context, orgID int64, store AMConfigStore) (*cfgRevision, error) { - alertManagerConfig, err := store.GetLatestAlertmanagerConfiguration(ctx, orgID) - if err != nil { - return nil, err - } - - if alertManagerConfig == nil { - return nil, ErrNoAlertmanagerConfiguration.Errorf("") - } - - concurrencyToken := alertManagerConfig.ConfigurationHash - cfg, err := deserializeAlertmanagerConfig([]byte(alertManagerConfig.AlertmanagerConfiguration)) - if err != nil { - return nil, err - } - - return &cfgRevision{ - cfg: cfg, - concurrencyToken: concurrencyToken, - version: alertManagerConfig.ConfigurationVersion, - }, nil -} - -type alertmanagerConfigStore interface { - Get(ctx context.Context, orgID int64) (*cfgRevision, error) - Save(ctx context.Context, revision *cfgRevision, orgID int64) error -} - -type alertmanagerConfigStoreImpl struct { - store AMConfigStore -} - -func (a alertmanagerConfigStoreImpl) Get(ctx context.Context, orgID int64) (*cfgRevision, error) { - return getLastConfiguration(ctx, orgID, a.store) -} - -func (a alertmanagerConfigStoreImpl) Save(ctx context.Context, revision *cfgRevision, orgID int64) error { - serialized, err := serializeAlertmanagerConfig(*revision.cfg) - if err != nil { - return err - } - cmd := models.SaveAlertmanagerConfigurationCmd{ - AlertmanagerConfiguration: string(serialized), - ConfigurationVersion: revision.version, - FetchedConfigurationHash: revision.concurrencyToken, - Default: false, - OrgID: orgID, - } - return PersistConfig(ctx, a.store, &cmd) -} diff --git a/pkg/services/ngalert/provisioning/contactpoints.go b/pkg/services/ngalert/provisioning/contactpoints.go index c475f09ffcf..78959045adf 100644 --- a/pkg/services/ngalert/provisioning/contactpoints.go +++ b/pkg/services/ngalert/provisioning/contactpoints.go @@ -15,8 +15,8 @@ import ( "github.com/grafana/grafana/pkg/infra/log" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/ngalert/notifier" "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels_config" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/util" @@ -28,7 +28,7 @@ type AlertRuleNotificationSettingsStore interface { } type ContactPointService struct { - configStore *alertmanagerConfigStoreImpl + configStore alertmanagerConfigStore encryptionService secrets.Service provenanceStore ProvisioningStore notificationSettingsStore AlertRuleNotificationSettingsStore @@ -41,13 +41,11 @@ type receiverService interface { GetReceivers(ctx context.Context, query models.GetReceiversQuery, user identity.Requester) ([]apimodels.GettableApiReceiver, error) } -func NewContactPointService(store AMConfigStore, encryptionService secrets.Service, +func NewContactPointService(store alertmanagerConfigStore, encryptionService secrets.Service, provenanceStore ProvisioningStore, xact TransactionManager, receiverService receiverService, log log.Logger, nsStore AlertRuleNotificationSettingsStore) *ContactPointService { return &ContactPointService{ - configStore: &alertmanagerConfigStoreImpl{ - store: store, - }, + configStore: store, receiverService: receiverService, encryptionService: encryptionService, provenanceStore: provenanceStore, @@ -118,7 +116,7 @@ func (ecp *ContactPointService) getContactPointDecrypted(ctx context.Context, or if err != nil { return apimodels.EmbeddedContactPoint{}, err } - for _, receiver := range revision.cfg.GetGrafanaReceiverMap() { + for _, receiver := range revision.Config.GetGrafanaReceiverMap() { if receiver.UID != uid { continue } @@ -180,7 +178,7 @@ func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID in } receiverFound := false - for _, receiver := range revision.cfg.AlertmanagerConfig.Receivers { + for _, receiver := range revision.Config.AlertmanagerConfig.Receivers { // check if uid is already used in receiver for _, rec := range receiver.PostableGrafanaReceivers.GrafanaManagedReceivers { if grafanaReceiver.UID == rec.UID { @@ -197,7 +195,7 @@ func (ecp *ContactPointService) CreateContactPoint(ctx context.Context, orgID in } if !receiverFound { - revision.cfg.AlertmanagerConfig.Receivers = append(revision.cfg.AlertmanagerConfig.Receivers, &apimodels.PostableApiReceiver{ + revision.Config.AlertmanagerConfig.Receivers = append(revision.Config.AlertmanagerConfig.Receivers, &apimodels.PostableApiReceiver{ Receiver: config.Receiver{ Name: grafanaReceiver.Name, }, @@ -286,7 +284,7 @@ func (ecp *ContactPointService) UpdateContactPoint(ctx context.Context, orgID in return err } - configModified, renamedReceiver := stitchReceiver(revision.cfg, mergedReceiver) + configModified, renamedReceiver := stitchReceiver(revision.Config, mergedReceiver) if !configModified { return fmt.Errorf("contact point with uid '%s' not found", mergedReceiver.UID) } @@ -324,7 +322,7 @@ func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID in // Name of the contact point that will be removed, might be used if a // full removal is done to check if it's referenced in any route. name := "" - for i, receiver := range revision.cfg.AlertmanagerConfig.Receivers { + for i, receiver := range revision.Config.AlertmanagerConfig.Receivers { for j, grafanaReceiver := range receiver.GrafanaManagedReceivers { if grafanaReceiver.UID == uid { name = grafanaReceiver.Name @@ -332,13 +330,13 @@ func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID in // if this was the last receiver we removed, we remove the whole receiver if len(receiver.GrafanaManagedReceivers) == 0 { fullRemoval = true - revision.cfg.AlertmanagerConfig.Receivers = append(revision.cfg.AlertmanagerConfig.Receivers[:i], revision.cfg.AlertmanagerConfig.Receivers[i+1:]...) + revision.Config.AlertmanagerConfig.Receivers = append(revision.Config.AlertmanagerConfig.Receivers[:i], revision.Config.AlertmanagerConfig.Receivers[i+1:]...) } break } } } - if fullRemoval && isContactPointInUse(name, []*apimodels.Route{revision.cfg.AlertmanagerConfig.Route}) { + if fullRemoval && revision.ReceiverNameUsedByRoutes(name) { return ErrContactPointReferenced.Errorf("") } @@ -368,21 +366,6 @@ func (ecp *ContactPointService) DeleteContactPoint(ctx context.Context, orgID in }) } -func isContactPointInUse(name string, routes []*apimodels.Route) bool { - if len(routes) == 0 { - return false - } - for _, route := range routes { - if route.Receiver == name { - return true - } - if isContactPointInUse(name, route.Routes) { - return true - } - } - return false -} - // decryptValueOrRedacted returns a function that decodes a string from Base64 and then decrypts using secrets.Service. // If argument 'decrypt' is false, then returns definitions.RedactedValue regardless of the decrypted value. // Otherwise, it returns the decoded and decrypted value. The function returns empty string in the case of errors, which are logged @@ -540,22 +523,10 @@ func RemoveSecretsForContactPoint(e *apimodels.EmbeddedContactPoint) (map[string return s, nil } -// handleWrappedError unwraps an error and wraps it with a new expected error type. If the error is not wrapped, it returns just the expected error. -func handleWrappedError(err error, expected error) error { - err = errors.Unwrap(err) - if err == nil { - return expected - } - return fmt.Errorf("%w: %s", expected, err.Error()) -} - // convertRecSvcErr converts errors from notifier.ReceiverService to errors expected from ContactPointService. func convertRecSvcErr(err error) error { - if errors.Is(err, notifier.ErrPermissionDenied) { - return handleWrappedError(err, ErrPermissionDenied) - } if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { - return ErrNoAlertmanagerConfiguration.Errorf("") + return legacy_storage.ErrNoAlertmanagerConfiguration.Errorf("") } return err } diff --git a/pkg/services/ngalert/provisioning/contactpoints_test.go b/pkg/services/ngalert/provisioning/contactpoints_test.go index 44877c37198..9ae599166e1 100644 --- a/pkg/services/ngalert/provisioning/contactpoints_test.go +++ b/pkg/services/ngalert/provisioning/contactpoints_test.go @@ -18,9 +18,11 @@ import ( "github.com/grafana/grafana/pkg/services/accesscontrol/acimpl" "github.com/grafana/grafana/pkg/services/authz/zanzana" "github.com/grafana/grafana/pkg/services/featuremgmt" + ac "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/services/secrets" "github.com/grafana/grafana/pkg/services/secrets/database" @@ -249,17 +251,18 @@ func TestContactPointService(t *testing.T) { }) t.Run("service respects concurrency token when updating", func(t *testing.T) { - sut := createContactPointServiceSut(t, secretsService) + cfg := createEncryptedConfig(t, secretsService) + fakeConfigStore := fakes.NewFakeAlertmanagerConfigStore(cfg) + sut := createContactPointServiceSutWithConfigStore(t, secretsService, fakeConfigStore) newCp := createTestContactPoint() - config, err := sut.configStore.store.GetLatestAlertmanagerConfiguration(context.Background(), 1) + config, err := sut.configStore.Get(context.Background(), 1) require.NoError(t, err) - expectedConcurrencyToken := config.ConfigurationHash + expectedConcurrencyToken := config.ConcurrencyToken _, err = sut.CreateContactPoint(context.Background(), 1, newCp, models.ProvenanceAPI) require.NoError(t, err) - fake := sut.configStore.store.(*fakes.FakeAlertmanagerConfigStore) - intercepted := fake.LastSaveCommand + intercepted := fakeConfigStore.LastSaveCommand require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash) }) } @@ -296,7 +299,7 @@ func TestContactPointServiceDecryptRedact(t *testing.T) { q := cpsQuery(1) q.Decrypt = true _, err := sut.GetContactPoints(context.Background(), q, redactedUser) - require.ErrorIs(t, err, ErrPermissionDenied) + require.ErrorIs(t, err, ac.ErrAuthorizationBase) }) t.Run("GetContactPoints errors when Decrypt = true and user is nil", func(t *testing.T) { sut := createContactPointServiceSut(t, secretsService) @@ -304,7 +307,7 @@ func TestContactPointServiceDecryptRedact(t *testing.T) { q := cpsQuery(1) q.Decrypt = true _, err := sut.GetContactPoints(context.Background(), q, nil) - require.ErrorIs(t, err, ErrPermissionDenied) + require.ErrorIs(t, err, ac.ErrAuthorizationBase) }) t.Run("GetContactPoints gets decrypted contact points when Decrypt = true and user has permissions", func(t *testing.T) { @@ -322,47 +325,21 @@ func TestContactPointServiceDecryptRedact(t *testing.T) { }) } -func TestContactPointInUse(t *testing.T) { - result := isContactPointInUse("test", []*definitions.Route{ - { - Receiver: "not-test", - Routes: []*definitions.Route{ - { - Receiver: "not-test", - }, - { - Receiver: "test", - }, - }, - }, - }) - require.True(t, result) - result = isContactPointInUse("test", []*definitions.Route{ - { - Receiver: "not-test", - Routes: []*definitions.Route{ - { - Receiver: "not-test", - }, - { - Receiver: "not-test", - }, - }, - }, - }) - require.False(t, result) -} - func createContactPointServiceSut(t *testing.T, secretService secrets.Service) *ContactPointService { // Encrypt secure settings. cfg := createEncryptedConfig(t, secretService) store := fakes.NewFakeAlertmanagerConfigStore(cfg) + return createContactPointServiceSutWithConfigStore(t, secretService, store) +} + +func createContactPointServiceSutWithConfigStore(t *testing.T, secretService secrets.Service, configStore legacy_storage.AMConfigStore) *ContactPointService { + // Encrypt secure settings. xact := newNopTransactionManager() provisioningStore := fakes.NewFakeProvisioningStore() receiverService := notifier.NewReceiverService( acimpl.ProvideAccessControl(featuremgmt.WithFeatures(), zanzana.NewNoopClient()), - store, + legacy_storage.NewAlertmanagerConfigStore(configStore), provisioningStore, secretService, xact, @@ -370,7 +347,7 @@ func createContactPointServiceSut(t *testing.T, secretService secrets.Service) * ) return &ContactPointService{ - configStore: &alertmanagerConfigStoreImpl{store: store}, + configStore: legacy_storage.NewAlertmanagerConfigStore(configStore), provenanceStore: provisioningStore, receiverService: receiverService, xact: xact, diff --git a/pkg/services/ngalert/provisioning/errors.go b/pkg/services/ngalert/provisioning/errors.go index 39f762bfe22..e0e7a6097d1 100644 --- a/pkg/services/ngalert/provisioning/errors.go +++ b/pkg/services/ngalert/provisioning/errors.go @@ -1,7 +1,6 @@ package provisioning import ( - "errors" "fmt" "github.com/grafana/grafana/pkg/apimachinery/errutil" @@ -10,12 +9,8 @@ import ( var ErrValidation = fmt.Errorf("invalid object specification") var ErrNotFound = fmt.Errorf("object not found") -var ErrPermissionDenied = errors.New("permission denied") var ( - ErrNoAlertmanagerConfiguration = errutil.Internal("alerting.notification.configMissing", errutil.WithPublicMessage("No alertmanager configuration present in this organization")) - ErrBadAlertmanagerConfiguration = errutil.Internal("alerting.notification.configCorrupted").MustTemplate("Failed to unmarshal the Alertmanager configuration", errutil.WithPublic("Current Alertmanager configuration in the storage is corrupted. Reset the configuration or rollback to a recent valid one.")) - ErrVersionConflict = errutil.Conflict("alerting.notifications.conflict") ErrTimeIntervalNotFound = errutil.NotFound("alerting.notifications.time-intervals.notFound") @@ -27,16 +22,6 @@ var ( ErrContactPointUsedInRule = errutil.Conflict("alerting.notifications.contact-points.used-by-rule", errutil.WithPublicMessage("Contact point is currently used in the notification settings of one or many alert rules.")) ) -func makeErrBadAlertmanagerConfiguration(err error) error { - data := errutil.TemplateData{ - Public: map[string]interface{}{ - "Error": err.Error(), - }, - Error: err, - } - return ErrBadAlertmanagerConfiguration.Build(data) -} - // MakeErrTimeIntervalInvalid creates an error with the ErrTimeIntervalInvalid template func MakeErrTimeIntervalInvalid(err error) error { data := errutil.TemplateData{ diff --git a/pkg/services/ngalert/provisioning/mute_timings.go b/pkg/services/ngalert/provisioning/mute_timings.go index 72d4ca1a34c..e14eef94bbd 100644 --- a/pkg/services/ngalert/provisioning/mute_timings.go +++ b/pkg/services/ngalert/provisioning/mute_timings.go @@ -2,7 +2,6 @@ package provisioning import ( "context" - "encoding/base64" "encoding/binary" "errors" "fmt" @@ -17,6 +16,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/provisioning/validation" ) @@ -29,9 +29,9 @@ type MuteTimingService struct { ruleNotificationsStore AlertRuleNotificationSettingsStore } -func NewMuteTimingService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger, ns AlertRuleNotificationSettingsStore) *MuteTimingService { +func NewMuteTimingService(config alertmanagerConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger, ns AlertRuleNotificationSettingsStore) *MuteTimingService { return &MuteTimingService{ - configStore: &alertmanagerConfigStoreImpl{store: config}, + configStore: config, provenanceStore: prov, xact: xact, log: log, @@ -47,7 +47,7 @@ func (svc *MuteTimingService) GetMuteTimings(ctx context.Context, orgID int64) ( return nil, err } - if rev.cfg.AlertmanagerConfig.MuteTimeIntervals == nil { + if rev.Config.AlertmanagerConfig.MuteTimeIntervals == nil { return []definitions.MuteTimeInterval{}, nil } @@ -56,11 +56,11 @@ func (svc *MuteTimingService) GetMuteTimings(ctx context.Context, orgID int64) ( return nil, err } - result := make([]definitions.MuteTimeInterval, 0, len(rev.cfg.AlertmanagerConfig.MuteTimeIntervals)) - for _, interval := range rev.cfg.AlertmanagerConfig.MuteTimeIntervals { + result := make([]definitions.MuteTimeInterval, 0, len(rev.Config.AlertmanagerConfig.MuteTimeIntervals)) + for _, interval := range rev.Config.AlertmanagerConfig.MuteTimeIntervals { version := calculateMuteTimeIntervalFingerprint(interval) def := definitions.MuteTimeInterval{ - UID: getIntervalUID(interval), + UID: legacy_storage.NameToUid(interval.Name), MuteTimeInterval: interval, Version: version, } @@ -81,7 +81,7 @@ func (svc *MuteTimingService) GetMuteTiming(ctx context.Context, nameOrUID strin mt, idx := getMuteTimingByName(rev, nameOrUID) if idx == -1 { - name, err := uidToName(nameOrUID) + name, err := legacy_storage.UidToName(nameOrUID) if err == nil { mt, idx = getMuteTimingByName(rev, name) } @@ -91,7 +91,7 @@ func (svc *MuteTimingService) GetMuteTiming(ctx context.Context, nameOrUID strin } result := definitions.MuteTimeInterval{ - UID: getIntervalUID(mt), + UID: legacy_storage.NameToUid(mt.Name), MuteTimeInterval: mt, Version: calculateMuteTimeIntervalFingerprint(mt), } @@ -119,7 +119,7 @@ func (svc *MuteTimingService) CreateMuteTiming(ctx context.Context, mt definitio if idx != -1 { return definitions.MuteTimeInterval{}, ErrTimeIntervalExists.Errorf("") } - revision.cfg.AlertmanagerConfig.MuteTimeIntervals = append(revision.cfg.AlertmanagerConfig.MuteTimeIntervals, mt.MuteTimeInterval) + revision.Config.AlertmanagerConfig.MuteTimeIntervals = append(revision.Config.AlertmanagerConfig.MuteTimeIntervals, mt.MuteTimeInterval) err = svc.xact.InTransaction(ctx, func(ctx context.Context) error { if err := svc.configStore.Save(ctx, revision, orgID); err != nil { @@ -131,7 +131,7 @@ func (svc *MuteTimingService) CreateMuteTiming(ctx context.Context, mt definitio return definitions.MuteTimeInterval{}, err } return definitions.MuteTimeInterval{ - UID: getIntervalUID(mt.MuteTimeInterval), + UID: legacy_storage.NameToUid(mt.Name), MuteTimeInterval: mt.MuteTimeInterval, Version: calculateMuteTimeIntervalFingerprint(mt.MuteTimeInterval), Provenance: mt.Provenance, @@ -152,7 +152,7 @@ func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitio var old config.MuteTimeInterval var idx = -1 if mt.UID != "" { - name, err := uidToName(mt.UID) + name, err := legacy_storage.UidToName(mt.UID) if err == nil { old, idx = getMuteTimingByName(revision, name) } @@ -184,7 +184,7 @@ func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitio return definitions.MuteTimeInterval{}, MakeErrTimeIntervalInvalid(errors.New("name change is not allowed")) } - revision.cfg.AlertmanagerConfig.MuteTimeIntervals[idx] = mt.MuteTimeInterval + revision.Config.AlertmanagerConfig.MuteTimeIntervals[idx] = mt.MuteTimeInterval // TODO add diff and noop detection err = svc.xact.InTransaction(ctx, func(ctx context.Context) error { @@ -197,7 +197,7 @@ func (svc *MuteTimingService) UpdateMuteTiming(ctx context.Context, mt definitio return definitions.MuteTimeInterval{}, err } return definitions.MuteTimeInterval{ - UID: getIntervalUID(mt.MuteTimeInterval), + UID: legacy_storage.NameToUid(mt.Name), MuteTimeInterval: mt.MuteTimeInterval, Version: calculateMuteTimeIntervalFingerprint(mt.MuteTimeInterval), Provenance: mt.Provenance, @@ -213,7 +213,7 @@ func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, nameOrUID st existing, idx := getMuteTimingByName(revision, nameOrUID) if idx == -1 { - name, err := uidToName(nameOrUID) + name, err := legacy_storage.UidToName(nameOrUID) if err == nil { existing, idx = getMuteTimingByName(revision, name) } @@ -233,7 +233,7 @@ func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, nameOrUID st return err } - if isMuteTimeInUseInRoutes(existing.Name, revision.cfg.AlertmanagerConfig.Route) { + if isMuteTimeInUseInRoutes(existing.Name, revision.Config.AlertmanagerConfig.Route) { ns, _ := svc.ruleNotificationsStore.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgID, TimeIntervalName: existing.Name}) // ignore error here because it's not important return MakeErrTimeIntervalInUse(true, maps.Keys(ns)) @@ -243,7 +243,7 @@ func (svc *MuteTimingService) DeleteMuteTiming(ctx context.Context, nameOrUID st if err != nil { return err } - revision.cfg.AlertmanagerConfig.MuteTimeIntervals = slices.Delete(revision.cfg.AlertmanagerConfig.MuteTimeIntervals, idx, idx+1) + revision.Config.AlertmanagerConfig.MuteTimeIntervals = slices.Delete(revision.Config.AlertmanagerConfig.MuteTimeIntervals, idx, idx+1) return svc.xact.InTransaction(ctx, func(ctx context.Context) error { keys, err := svc.ruleNotificationsStore.ListNotificationSettings(ctx, models.ListNotificationSettingsQuery{OrgID: orgID, TimeIntervalName: existing.Name}) @@ -276,14 +276,14 @@ func isMuteTimeInUseInRoutes(name string, route *definitions.Route) bool { return false } -func getMuteTimingByName(rev *cfgRevision, name string) (config.MuteTimeInterval, int) { - idx := slices.IndexFunc(rev.cfg.AlertmanagerConfig.MuteTimeIntervals, func(interval config.MuteTimeInterval) bool { +func getMuteTimingByName(rev *legacy_storage.ConfigRevision, name string) (config.MuteTimeInterval, int) { + idx := slices.IndexFunc(rev.Config.AlertmanagerConfig.MuteTimeIntervals, func(interval config.MuteTimeInterval) bool { return interval.Name == name }) if idx == -1 { return config.MuteTimeInterval{}, idx } - return rev.cfg.AlertmanagerConfig.MuteTimeIntervals[idx], idx + return rev.Config.AlertmanagerConfig.MuteTimeIntervals[idx], idx } func calculateMuteTimeIntervalFingerprint(interval config.MuteTimeInterval) string { @@ -355,15 +355,3 @@ func (svc *MuteTimingService) checkOptimisticConcurrency(current config.MuteTime } return nil } - -func getIntervalUID(t config.MuteTimeInterval) string { - return base64.RawURLEncoding.EncodeToString([]byte(t.Name)) -} - -func uidToName(uid string) (string, error) { - data, err := base64.RawURLEncoding.DecodeString(uid) - if err != nil { - return uid, err - } - return string(data), nil -} diff --git a/pkg/services/ngalert/provisioning/mute_timings_test.go b/pkg/services/ngalert/provisioning/mute_timings_test.go index dd4154d9e42..ba20a2fb825 100644 --- a/pkg/services/ngalert/provisioning/mute_timings_test.go +++ b/pkg/services/ngalert/provisioning/mute_timings_test.go @@ -16,12 +16,13 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" ) func TestGetMuteTimings(t *testing.T) { orgID := int64(1) - revision := &cfgRevision{ - cfg: &definitions.PostableUserConfig{ + revision := &legacy_storage.ConfigRevision{ + Config: &definitions.PostableUserConfig{ AlertmanagerConfig: definitions.PostableApiAlertingConfig{ Config: definitions.Config{ MuteTimeIntervals: []config.MuteTimeInterval{ @@ -50,7 +51,7 @@ func TestGetMuteTimings(t *testing.T) { t.Run("service returns timings from config file", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return revision, nil } @@ -59,21 +60,21 @@ func TestGetMuteTimings(t *testing.T) { result, err := sut.GetMuteTimings(context.Background(), 1) require.NoError(t, err) - require.Len(t, result, len(revision.cfg.AlertmanagerConfig.MuteTimeIntervals)) + require.Len(t, result, len(revision.Config.AlertmanagerConfig.MuteTimeIntervals)) require.Equal(t, "Test1", result[0].Name) require.EqualValues(t, provenances["Test1"], result[0].Provenance) require.NotEmpty(t, result[0].Version) - require.Equal(t, getIntervalUID(result[0].MuteTimeInterval), result[0].UID) + require.Equal(t, legacy_storage.NameToUid(result[0].Name), result[0].UID) require.Equal(t, "Test2", result[1].Name) require.EqualValues(t, provenances["Test2"], result[1].Provenance) require.NotEmpty(t, result[1].Version) - require.Equal(t, getIntervalUID(result[1].MuteTimeInterval), result[1].UID) + require.Equal(t, legacy_storage.NameToUid(result[1].Name), result[1].UID) require.Equal(t, "Test3", result[2].Name) require.EqualValues(t, "", result[2].Provenance) require.NotEmpty(t, result[2].Version) - require.Equal(t, getIntervalUID(result[2].MuteTimeInterval), result[2].UID) + require.Equal(t, legacy_storage.NameToUid(result[2].Name), result[2].UID) require.Len(t, store.Calls, 1) require.Equal(t, "Get", store.Calls[0].Method) @@ -84,8 +85,8 @@ func TestGetMuteTimings(t *testing.T) { t.Run("service returns empty list when config file contains no mute timings", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: &definitions.PostableUserConfig{}}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: &definitions.PostableUserConfig{}}, nil } result, err := sut.GetMuteTimings(context.Background(), 1) @@ -98,7 +99,7 @@ func TestGetMuteTimings(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() expected := fmt.Errorf("failed") - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return nil, expected } @@ -109,7 +110,7 @@ func TestGetMuteTimings(t *testing.T) { t.Run("when unable to read provenance", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return revision, nil } expected := fmt.Errorf("failed") @@ -124,8 +125,8 @@ func TestGetMuteTimings(t *testing.T) { func TestGetMuteTiming(t *testing.T) { orgID := int64(1) - revision := &cfgRevision{ - cfg: &definitions.PostableUserConfig{ + revision := &legacy_storage.ConfigRevision{ + Config: &definitions.PostableUserConfig{ AlertmanagerConfig: definitions.PostableApiAlertingConfig{ Config: definitions.Config{ MuteTimeIntervals: []config.MuteTimeInterval{ @@ -141,7 +142,7 @@ func TestGetMuteTiming(t *testing.T) { t.Run("service returns timing by name", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return revision, nil } prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) @@ -152,7 +153,7 @@ func TestGetMuteTiming(t *testing.T) { require.Equal(t, "Test1", result.Name) require.EqualValues(t, models.ProvenanceAPI, result.Provenance) - require.Equal(t, getIntervalUID(result.MuteTimeInterval), result.UID) + require.Equal(t, legacy_storage.NameToUid(result.Name), result.UID) require.NotEmpty(t, result.Version) require.Len(t, store.Calls, 1) @@ -172,8 +173,8 @@ func TestGetMuteTiming(t *testing.T) { t.Run("service returns ErrTimeIntervalNotFound if no mute timings", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: &definitions.PostableUserConfig{}}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: &definitions.PostableUserConfig{}}, nil } _, err := sut.GetMuteTiming(context.Background(), "Test1", orgID) @@ -183,7 +184,7 @@ func TestGetMuteTiming(t *testing.T) { t.Run("service returns ErrTimeIntervalNotFound if no mute timing by name", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return revision, nil } @@ -196,7 +197,7 @@ func TestGetMuteTiming(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() expected := fmt.Errorf("failed") - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return nil, expected } @@ -207,7 +208,7 @@ func TestGetMuteTiming(t *testing.T) { t.Run("when unable to read provenance", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return revision, nil } expected := fmt.Errorf("failed") @@ -273,8 +274,8 @@ func TestCreateMuteTimings(t *testing.T) { t.Run("returns ErrTimeIntervalExists if mute timing with the name exists", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } existing := initialConfig().AlertmanagerConfig.MuteTimeIntervals[0] @@ -290,10 +291,10 @@ func TestCreateMuteTimings(t *testing.T) { t.Run("saves mute timing and provenance in a transaction", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } - store.SaveFn = func(ctx context.Context, revision *cfgRevision) error { + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { assertInTransaction(t, ctx) return nil } @@ -308,7 +309,7 @@ func TestCreateMuteTimings(t *testing.T) { require.EqualValues(t, expected, result.MuteTimeInterval) require.EqualValues(t, expectedProvenance, result.Provenance) - require.Equal(t, getIntervalUID(expected), result.UID) + require.Equal(t, legacy_storage.NameToUid(expected.Name), result.UID) require.NotEmpty(t, result.Version) require.Len(t, store.Calls, 2) @@ -317,10 +318,10 @@ func TestCreateMuteTimings(t *testing.T) { require.Equal(t, "Save", store.Calls[1].Method) require.Equal(t, orgID, store.Calls[1].Args[2]) - revision := store.Calls[1].Args[1].(*cfgRevision) + revision := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) expectedTimings := append(initialConfig().AlertmanagerConfig.MuteTimeIntervals, expected) - require.EqualValues(t, expectedTimings, revision.cfg.AlertmanagerConfig.MuteTimeIntervals) + require.EqualValues(t, expectedTimings, revision.Config.AlertmanagerConfig.MuteTimeIntervals) prov.AssertCalled(t, "SetProvenance", mock.Anything, &timing, orgID, expectedProvenance) }) @@ -329,7 +330,7 @@ func TestCreateMuteTimings(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() expectedErr := errors.New("test-err") - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return nil, expectedErr } _, err := sut.CreateMuteTiming(context.Background(), timing, orgID) @@ -338,8 +339,8 @@ func TestCreateMuteTimings(t *testing.T) { t.Run("when provenance fails to save", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } expectedErr := fmt.Errorf("failed to save provenance") sut.provenanceStore.(*MockProvisioningStore).EXPECT(). @@ -359,11 +360,11 @@ func TestCreateMuteTimings(t *testing.T) { t.Run("when AM config fails to save", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } expectedErr := errors.New("test-err") - store.SaveFn = func(ctx context.Context, revision *cfgRevision) error { + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { return expectedErr } @@ -417,7 +418,7 @@ func TestUpdateMuteTimings(t *testing.T) { } expectedProvenance := models.ProvenanceAPI expectedVersion := calculateMuteTimeIntervalFingerprint(expected) - expectedUID := getIntervalUID(expected) + expectedUID := legacy_storage.NameToUid(expected.Name) timing := definitions.MuteTimeInterval{ MuteTimeInterval: expected, Version: originalVersion, @@ -440,8 +441,8 @@ func TestUpdateMuteTimings(t *testing.T) { t.Run("rejects mute timings if provenance is not right", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } expectedErr := errors.New("test") sut.validator = func(from, to models.Provenance) error { @@ -461,8 +462,8 @@ func TestUpdateMuteTimings(t *testing.T) { t.Run("rejects if mute timing is renamed", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(expectedProvenance, nil) @@ -481,8 +482,8 @@ func TestUpdateMuteTimings(t *testing.T) { t.Run("rejects mute timings if provenance is not right", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } expectedErr := errors.New("test") sut.validator = func(from, to models.Provenance) error { @@ -502,8 +503,8 @@ func TestUpdateMuteTimings(t *testing.T) { t.Run("returns ErrVersionConflict if storage version does not match", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } timing := definitions.MuteTimeInterval{ @@ -521,8 +522,8 @@ func TestUpdateMuteTimings(t *testing.T) { t.Run("returns ErrMuteTimingsNotFound if mute timing does not exist", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(expectedProvenance, nil) timing := definitions.MuteTimeInterval{ @@ -539,8 +540,8 @@ func TestUpdateMuteTimings(t *testing.T) { t.Run("returns ErrMuteTimingsNotFound if mute timing does not exist", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(expectedProvenance, nil) @@ -572,10 +573,10 @@ func TestUpdateMuteTimings(t *testing.T) { t.Run("saves mute timing and provenance in a transaction if optimistic concurrency passes", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } - store.SaveFn = func(ctx context.Context, revision *cfgRevision) error { + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { assertInTransaction(t, ctx) return nil } @@ -592,7 +593,7 @@ func TestUpdateMuteTimings(t *testing.T) { require.EqualValues(t, expected, result.MuteTimeInterval) require.EqualValues(t, expectedProvenance, result.Provenance) require.EqualValues(t, expectedVersion, result.Version) - require.Equal(t, getIntervalUID(result.MuteTimeInterval), result.UID) + require.Equal(t, legacy_storage.NameToUid(result.Name), result.UID) require.Len(t, store.Calls, 2) require.Equal(t, "Get", store.Calls[0].Method) @@ -600,9 +601,9 @@ func TestUpdateMuteTimings(t *testing.T) { require.Equal(t, "Save", store.Calls[1].Method) require.Equal(t, orgID, store.Calls[1].Args[2]) - revision := store.Calls[1].Args[1].(*cfgRevision) + revision := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) - require.EqualValues(t, []config.MuteTimeInterval{expected}, revision.cfg.AlertmanagerConfig.MuteTimeIntervals) + require.EqualValues(t, []config.MuteTimeInterval{expected}, revision.Config.AlertmanagerConfig.MuteTimeIntervals) prov.AssertCalled(t, "SetProvenance", mock.Anything, &timing, orgID, expectedProvenance) @@ -636,9 +637,9 @@ func TestUpdateMuteTimings(t *testing.T) { require.Equal(t, "Save", store.Calls[1].Method) require.Equal(t, orgID, store.Calls[1].Args[2]) - revision := store.Calls[1].Args[1].(*cfgRevision) + revision := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) - require.EqualValues(t, []config.MuteTimeInterval{timing.MuteTimeInterval}, revision.cfg.AlertmanagerConfig.MuteTimeIntervals) + require.EqualValues(t, []config.MuteTimeInterval{timing.MuteTimeInterval}, revision.Config.AlertmanagerConfig.MuteTimeIntervals) }) }) @@ -647,7 +648,7 @@ func TestUpdateMuteTimings(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() expectedErr := errors.New("test-err") prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(expectedProvenance, nil) - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return nil, expectedErr } _, err := sut.UpdateMuteTiming(context.Background(), timing, orgID) @@ -656,8 +657,8 @@ func TestUpdateMuteTimings(t *testing.T) { t.Run("when provenance fails to save", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } expectedErr := fmt.Errorf("failed to save provenance") sut.provenanceStore.(*MockProvisioningStore).EXPECT(). @@ -680,14 +681,14 @@ func TestUpdateMuteTimings(t *testing.T) { t.Run("when AM config fails to save", func(t *testing.T) { sut, store, _ := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } sut.provenanceStore.(*MockProvisioningStore).EXPECT(). GetProvenance(mock.Anything, mock.Anything, mock.Anything). Return(expectedProvenance, nil) expectedErr := errors.New("test-err") - store.SaveFn = func(ctx context.Context, revision *cfgRevision) error { + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { return expectedErr } @@ -736,8 +737,8 @@ func TestDeleteMuteTimings(t *testing.T) { sut.validator = func(from, to models.Provenance) error { return expectedErr } - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) @@ -747,8 +748,8 @@ func TestDeleteMuteTimings(t *testing.T) { t.Run("returns ErrTimeIntervalInUse if mute timing is used by a route", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) @@ -774,8 +775,8 @@ func TestDeleteMuteTimings(t *testing.T) { }, } sut.ruleNotificationsStore = &ruleNsStore - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) @@ -791,8 +792,8 @@ func TestDeleteMuteTimings(t *testing.T) { t.Run("returns ErrVersionConflict if provided version does not match", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceAPI, nil) @@ -806,10 +807,10 @@ func TestDeleteMuteTimings(t *testing.T) { t.Run("deletes mute timing and provenance in transaction if passes optimistic concurrency check", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } - store.SaveFn = func(ctx context.Context, revision *cfgRevision) error { + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { assertInTransaction(t, ctx) return nil } @@ -829,12 +830,12 @@ func TestDeleteMuteTimings(t *testing.T) { require.Equal(t, "Save", store.Calls[1].Method) require.Equal(t, orgID, store.Calls[1].Args[2]) - revision := store.Calls[1].Args[1].(*cfgRevision) + revision := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) expectedMuteTimings := slices.DeleteFunc(initialConfig().AlertmanagerConfig.MuteTimeIntervals, func(interval config.MuteTimeInterval) bool { return interval.Name == timingToDelete.Name }) - require.EqualValues(t, expectedMuteTimings, revision.cfg.AlertmanagerConfig.MuteTimeIntervals) + require.EqualValues(t, expectedMuteTimings, revision.Config.AlertmanagerConfig.MuteTimeIntervals) prov.AssertCalled(t, "DeleteProvenance", mock.Anything, &definitions.MuteTimeInterval{MuteTimeInterval: timingToDelete}, orgID) @@ -845,12 +846,12 @@ func TestDeleteMuteTimings(t *testing.T) { require.Equal(t, "Save", store.Calls[1].Method) require.Equal(t, orgID, store.Calls[1].Args[2]) - revision := store.Calls[1].Args[1].(*cfgRevision) + revision := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) expectedMuteTimings := slices.DeleteFunc(initialConfig().AlertmanagerConfig.MuteTimeIntervals, func(interval config.MuteTimeInterval) bool { return interval.Name == timingToDelete.Name }) - require.EqualValues(t, expectedMuteTimings, revision.cfg.AlertmanagerConfig.MuteTimeIntervals) + require.EqualValues(t, expectedMuteTimings, revision.Config.AlertmanagerConfig.MuteTimeIntervals) prov.AssertCalled(t, "DeleteProvenance", mock.Anything, &definitions.MuteTimeInterval{MuteTimeInterval: timingToDelete}, orgID) }) @@ -858,10 +859,10 @@ func TestDeleteMuteTimings(t *testing.T) { t.Run("deletes mute timing and provenance by UID", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } - store.SaveFn = func(ctx context.Context, revision *cfgRevision) error { + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { assertInTransaction(t, ctx) return nil } @@ -872,7 +873,7 @@ func TestDeleteMuteTimings(t *testing.T) { return nil }) - uid := getIntervalUID(timingToDelete) + uid := legacy_storage.NameToUid(timingToDelete.Name) err := sut.DeleteMuteTiming(context.Background(), uid, orgID, "", correctVersion) require.NoError(t, err) @@ -883,12 +884,12 @@ func TestDeleteMuteTimings(t *testing.T) { require.Equal(t, "Save", store.Calls[1].Method) require.Equal(t, orgID, store.Calls[1].Args[2]) - revision := store.Calls[1].Args[1].(*cfgRevision) + revision := store.Calls[1].Args[1].(*legacy_storage.ConfigRevision) expectedMuteTimings := slices.DeleteFunc(initialConfig().AlertmanagerConfig.MuteTimeIntervals, func(interval config.MuteTimeInterval) bool { return interval.Name == timingToDelete.Name }) - require.EqualValues(t, expectedMuteTimings, revision.cfg.AlertmanagerConfig.MuteTimeIntervals) + require.EqualValues(t, expectedMuteTimings, revision.Config.AlertmanagerConfig.MuteTimeIntervals) prov.AssertCalled(t, "DeleteProvenance", mock.Anything, &definitions.MuteTimeInterval{MuteTimeInterval: timingToDelete}, orgID) }) @@ -898,7 +899,7 @@ func TestDeleteMuteTimings(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() expectedErr := errors.New("test-err") prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { return nil, expectedErr } err := sut.DeleteMuteTiming(context.Background(), timingToDelete.Name, orgID, "", "") @@ -908,8 +909,8 @@ func TestDeleteMuteTimings(t *testing.T) { t.Run("when provenance fails to save", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } expectedErr := fmt.Errorf("failed to save provenance") sut.provenanceStore.(*MockProvisioningStore).EXPECT(). @@ -930,11 +931,11 @@ func TestDeleteMuteTimings(t *testing.T) { t.Run("when AM config fails to save", func(t *testing.T) { sut, store, prov := createMuteTimingSvcSut() prov.EXPECT().GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(models.ProvenanceNone, nil) - store.GetFn = func(ctx context.Context, orgID int64) (*cfgRevision, error) { - return &cfgRevision{cfg: initialConfig()}, nil + store.GetFn = func(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) { + return &legacy_storage.ConfigRevision{Config: initialConfig()}, nil } expectedErr := errors.New("test-err") - store.SaveFn = func(ctx context.Context, revision *cfgRevision) error { + store.SaveFn = func(ctx context.Context, revision *legacy_storage.ConfigRevision) error { return expectedErr } @@ -951,8 +952,8 @@ func TestDeleteMuteTimings(t *testing.T) { }) } -func createMuteTimingSvcSut() (*MuteTimingService, *alertmanagerConfigStoreFake, *MockProvisioningStore) { - store := &alertmanagerConfigStoreFake{} +func createMuteTimingSvcSut() (*MuteTimingService, *legacy_storage.AlertmanagerConfigStoreFake, *MockProvisioningStore) { + store := &legacy_storage.AlertmanagerConfigStoreFake{} prov := &MockProvisioningStore{} return &MuteTimingService{ configStore: store, diff --git a/pkg/services/ngalert/provisioning/notification_policies.go b/pkg/services/ngalert/provisioning/notification_policies.go index eeea63c4f29..f8a64396917 100644 --- a/pkg/services/ngalert/provisioning/notification_policies.go +++ b/pkg/services/ngalert/provisioning/notification_policies.go @@ -7,21 +7,22 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/setting" ) type NotificationPolicyService struct { - configStore *alertmanagerConfigStoreImpl + configStore alertmanagerConfigStore provenanceStore ProvisioningStore xact TransactionManager log log.Logger settings setting.UnifiedAlertingSettings } -func NewNotificationPolicyService(am AMConfigStore, prov ProvisioningStore, +func NewNotificationPolicyService(am alertmanagerConfigStore, prov ProvisioningStore, xact TransactionManager, settings setting.UnifiedAlertingSettings, log log.Logger) *NotificationPolicyService { return &NotificationPolicyService{ - configStore: &alertmanagerConfigStoreImpl{store: am}, + configStore: am, provenanceStore: prov, xact: xact, log: log, @@ -29,26 +30,22 @@ func NewNotificationPolicyService(am AMConfigStore, prov ProvisioningStore, } } -func (nps *NotificationPolicyService) GetAMConfigStore() AMConfigStore { - return nps.configStore.store -} - func (nps *NotificationPolicyService) GetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { rev, err := nps.configStore.Get(ctx, orgID) if err != nil { return definitions.Route{}, err } - if rev.cfg.AlertmanagerConfig.Config.Route == nil { + if rev.Config.AlertmanagerConfig.Config.Route == nil { return definitions.Route{}, fmt.Errorf("no route present in current alertmanager config") } - provenance, err := nps.provenanceStore.GetProvenance(ctx, rev.cfg.AlertmanagerConfig.Route, orgID) + provenance, err := nps.provenanceStore.GetProvenance(ctx, rev.Config.AlertmanagerConfig.Route, orgID) if err != nil { return definitions.Route{}, err } - result := *rev.cfg.AlertmanagerConfig.Route + result := *rev.Config.AlertmanagerConfig.Route result.Provenance = definitions.Provenance(provenance) return result, nil @@ -65,7 +62,7 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI return err } - receivers, err := nps.receiversToMap(revision.cfg.AlertmanagerConfig.Receivers) + receivers, err := nps.receiversToMap(revision.Config.AlertmanagerConfig.Receivers) if err != nil { return err } @@ -77,7 +74,7 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI } muteTimes := map[string]struct{}{} - for _, mt := range revision.cfg.AlertmanagerConfig.MuteTimeIntervals { + for _, mt := range revision.Config.AlertmanagerConfig.MuteTimeIntervals { muteTimes[mt.Name] = struct{}{} } err = tree.ValidateMuteTimes(muteTimes) @@ -85,7 +82,7 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI return fmt.Errorf("%w: %s", ErrValidation, err.Error()) } - revision.cfg.AlertmanagerConfig.Config.Route = &tree + revision.Config.AlertmanagerConfig.Config.Route = &tree return nps.xact.InTransaction(ctx, func(ctx context.Context) error { if err := nps.configStore.Save(ctx, revision, orgID); err != nil { @@ -96,7 +93,7 @@ func (nps *NotificationPolicyService) UpdatePolicyTree(ctx context.Context, orgI } func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID int64) (definitions.Route, error) { - defaultCfg, err := deserializeAlertmanagerConfig([]byte(nps.settings.DefaultConfiguration)) + defaultCfg, err := legacy_storage.DeserializeAlertmanagerConfig([]byte(nps.settings.DefaultConfiguration)) if err != nil { nps.log.Error("Failed to parse default alertmanager config: %w", err) return definitions.Route{}, fmt.Errorf("failed to parse default alertmanager config: %w", err) @@ -107,8 +104,8 @@ func (nps *NotificationPolicyService) ResetPolicyTree(ctx context.Context, orgID if err != nil { return definitions.Route{}, err } - revision.cfg.AlertmanagerConfig.Config.Route = route - err = nps.ensureDefaultReceiverExists(revision.cfg, defaultCfg) + revision.Config.AlertmanagerConfig.Config.Route = route + err = nps.ensureDefaultReceiverExists(revision.Config, defaultCfg) if err != nil { return definitions.Route{}, err } diff --git a/pkg/services/ngalert/provisioning/notification_policies_test.go b/pkg/services/ngalert/provisioning/notification_policies_test.go index 95b319e5b2d..fb79b5d5df7 100644 --- a/pkg/services/ngalert/provisioning/notification_policies_test.go +++ b/pkg/services/ngalert/provisioning/notification_policies_test.go @@ -13,6 +13,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/tests/fakes" "github.com/grafana/grafana/pkg/setting" ) @@ -29,7 +30,8 @@ func TestNotificationPolicyService(t *testing.T) { t.Run("error if referenced mute time interval is not existing", func(t *testing.T) { sut := createNotificationPolicyServiceSut() - sut.configStore.store = &MockAMConfigStore{} + mockStore := &legacy_storage.MockAMConfigStore{} + sut.configStore = legacy_storage.NewAlertmanagerConfigStore(mockStore) cfg := createTestAlertingConfig() cfg.AlertmanagerConfig.MuteTimeIntervals = []config.MuteTimeInterval{ { @@ -37,10 +39,10 @@ func TestNotificationPolicyService(t *testing.T) { TimeIntervals: []timeinterval.TimeInterval{}, }, } - data, _ := serializeAlertmanagerConfig(*cfg) - sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). + data, _ := legacy_storage.SerializeAlertmanagerConfig(*cfg) + mockStore.On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil) - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil) newRoute := createTestRoutingTree() @@ -55,7 +57,8 @@ func TestNotificationPolicyService(t *testing.T) { t.Run("pass if referenced mute time interval is existing", func(t *testing.T) { sut := createNotificationPolicyServiceSut() - sut.configStore.store = &MockAMConfigStore{} + mockStore := &legacy_storage.MockAMConfigStore{} + sut.configStore = legacy_storage.NewAlertmanagerConfigStore(mockStore) cfg := createTestAlertingConfig() cfg.AlertmanagerConfig.MuteTimeIntervals = []config.MuteTimeInterval{ { @@ -63,10 +66,10 @@ func TestNotificationPolicyService(t *testing.T) { TimeIntervals: []timeinterval.TimeInterval{}, }, } - data, _ := serializeAlertmanagerConfig(*cfg) - sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). + data, _ := legacy_storage.SerializeAlertmanagerConfig(*cfg) + mockStore.On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil) - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil) newRoute := createTestRoutingTree() @@ -131,12 +134,13 @@ func TestNotificationPolicyService(t *testing.T) { t.Run("existing receiver reference will pass", func(t *testing.T) { sut := createNotificationPolicyServiceSut() - sut.configStore.store = &MockAMConfigStore{} + mockStore := &legacy_storage.MockAMConfigStore{} + sut.configStore = legacy_storage.NewAlertmanagerConfigStore(mockStore) cfg := createTestAlertingConfig() - data, _ := serializeAlertmanagerConfig(*cfg) - sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). + data, _ := legacy_storage.SerializeAlertmanagerConfig(*cfg) + mockStore.On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil) - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil) newRoute := createTestRoutingTree() @@ -171,15 +175,16 @@ func TestNotificationPolicyService(t *testing.T) { t.Run("service respects concurrency token when updating", func(t *testing.T) { sut := createNotificationPolicyServiceSut() + fake := fakes.NewFakeAlertmanagerConfigStore(defaultAlertmanagerConfigJSON) + sut.configStore = legacy_storage.NewAlertmanagerConfigStore(fake) newRoute := createTestRoutingTree() - config, err := sut.GetAMConfigStore().GetLatestAlertmanagerConfiguration(context.Background(), 1) + config, err := sut.configStore.Get(context.Background(), 1) require.NoError(t, err) - expectedConcurrencyToken := config.ConfigurationHash + expectedConcurrencyToken := config.ConcurrencyToken err = sut.UpdatePolicyTree(context.Background(), 1, newRoute, models.ProvenanceAPI) require.NoError(t, err) - fake := sut.GetAMConfigStore().(*fakes.FakeAlertmanagerConfigStore) intercepted := fake.LastSaveCommand require.Equal(t, expectedConcurrencyToken, intercepted.FetchedConfigurationHash) }) @@ -209,7 +214,8 @@ func TestNotificationPolicyService(t *testing.T) { t.Run("deleting route with missing default receiver restores receiver", func(t *testing.T) { sut := createNotificationPolicyServiceSut() - sut.configStore.store = &MockAMConfigStore{} + mockStore := &legacy_storage.MockAMConfigStore{} + sut.configStore = legacy_storage.NewAlertmanagerConfigStore(mockStore) cfg := createTestAlertingConfig() cfg.AlertmanagerConfig.Route = &definitions.Route{ Receiver: "slack receiver", @@ -222,11 +228,11 @@ func TestNotificationPolicyService(t *testing.T) { }, // No default receiver! Only our custom one. } - data, _ := serializeAlertmanagerConfig(*cfg) - sut.configStore.store.(*MockAMConfigStore).On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). + data, _ := legacy_storage.SerializeAlertmanagerConfig(*cfg) + mockStore.On("GetLatestAlertmanagerConfiguration", mock.Anything, mock.Anything). Return(&models.AlertConfiguration{AlertmanagerConfiguration: string(data)}, nil) var interceptedSave = models.SaveAlertmanagerConfigurationCmd{} - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceedsIntercept(&interceptedSave) + mockStore.EXPECT().SaveSucceedsIntercept(&interceptedSave) tree, err := sut.ResetPolicyTree(context.Background(), 1) @@ -234,7 +240,7 @@ func TestNotificationPolicyService(t *testing.T) { require.Equal(t, "grafana-default-email", tree.Receiver) require.NotEmpty(t, interceptedSave.AlertmanagerConfiguration) // Deserializing with no error asserts that the saved configStore is semantically valid. - newCfg, err := deserializeAlertmanagerConfig([]byte(interceptedSave.AlertmanagerConfiguration)) + newCfg, err := legacy_storage.DeserializeAlertmanagerConfig([]byte(interceptedSave.AlertmanagerConfiguration)) require.NoError(t, err) require.Len(t, newCfg.AlertmanagerConfig.Receivers, 2) }) @@ -242,7 +248,7 @@ func TestNotificationPolicyService(t *testing.T) { func createNotificationPolicyServiceSut() *NotificationPolicyService { return &NotificationPolicyService{ - configStore: &alertmanagerConfigStoreImpl{store: fakes.NewFakeAlertmanagerConfigStore(defaultAlertmanagerConfigJSON)}, + configStore: legacy_storage.NewAlertmanagerConfigStore(fakes.NewFakeAlertmanagerConfigStore(defaultAlertmanagerConfigJSON)), provenanceStore: fakes.NewFakeProvisioningStore(), xact: newNopTransactionManager(), log: log.NewNopLogger(), @@ -259,7 +265,7 @@ func createTestRoutingTree() definitions.Route { } func createTestAlertingConfig() *definitions.PostableUserConfig { - cfg, _ := deserializeAlertmanagerConfig([]byte(defaultConfig)) + cfg, _ := legacy_storage.DeserializeAlertmanagerConfig([]byte(defaultConfig)) cfg.AlertmanagerConfig.Receivers = append(cfg.AlertmanagerConfig.Receivers, &definitions.PostableApiReceiver{ Receiver: config.Receiver{ diff --git a/pkg/services/ngalert/provisioning/persist.go b/pkg/services/ngalert/provisioning/persist.go index 80af4f24eb1..286652b07a4 100644 --- a/pkg/services/ngalert/provisioning/persist.go +++ b/pkg/services/ngalert/provisioning/persist.go @@ -2,20 +2,15 @@ package provisioning import ( "context" - "encoding/json" - "fmt" - "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/quota" ) -// AMStore is a store of Alertmanager configurations. -// -//go:generate mockery --name AMConfigStore --structname MockAMConfigStore --inpackage --filename persist_mock.go --with-expecter -type AMConfigStore interface { - GetLatestAlertmanagerConfiguration(ctx context.Context, orgID int64) (*models.AlertConfiguration, error) - UpdateAlertmanagerConfiguration(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) error +type alertmanagerConfigStore interface { + Get(ctx context.Context, orgID int64) (*legacy_storage.ConfigRevision, error) + Save(ctx context.Context, revision *legacy_storage.ConfigRevision, orgID int64) error } // ProvisioningStore is a store of provisioning data for arbitrary objects. @@ -50,12 +45,3 @@ type RuleStore interface { type QuotaChecker interface { CheckQuotaReached(ctx context.Context, target quota.TargetSrv, scopeParams *quota.ScopeParameters) (bool, error) } - -// PersistConfig validates to config before eventually persisting it if no error occurs -func PersistConfig(ctx context.Context, store AMConfigStore, cmd *models.SaveAlertmanagerConfigurationCmd) error { - cfg := &definitions.PostableUserConfig{} - if err := json.Unmarshal([]byte(cmd.AlertmanagerConfiguration), cfg); err != nil { - return fmt.Errorf("change would result in an invalid configuration state: %w", err) - } - return store.UpdateAlertmanagerConfiguration(ctx, cmd) -} diff --git a/pkg/services/ngalert/provisioning/templates.go b/pkg/services/ngalert/provisioning/templates.go index 074934af988..024fae594d0 100644 --- a/pkg/services/ngalert/provisioning/templates.go +++ b/pkg/services/ngalert/provisioning/templates.go @@ -10,15 +10,15 @@ import ( ) type TemplateService struct { - configStore *alertmanagerConfigStoreImpl + configStore alertmanagerConfigStore provenanceStore ProvisioningStore xact TransactionManager log log.Logger } -func NewTemplateService(config AMConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *TemplateService { +func NewTemplateService(config alertmanagerConfigStore, prov ProvisioningStore, xact TransactionManager, log log.Logger) *TemplateService { return &TemplateService{ - configStore: &alertmanagerConfigStoreImpl{store: config}, + configStore: config, provenanceStore: prov, xact: xact, log: log, @@ -31,8 +31,8 @@ func (t *TemplateService) GetTemplates(ctx context.Context, orgID int64) ([]defi return nil, err } - templates := make([]definitions.NotificationTemplate, 0, len(revision.cfg.TemplateFiles)) - for name, tmpl := range revision.cfg.TemplateFiles { + templates := make([]definitions.NotificationTemplate, 0, len(revision.Config.TemplateFiles)) + for name, tmpl := range revision.Config.TemplateFiles { tmpl := definitions.NotificationTemplate{ Name: name, Template: tmpl, @@ -61,10 +61,10 @@ func (t *TemplateService) SetTemplate(ctx context.Context, orgID int64, tmpl def return definitions.NotificationTemplate{}, err } - if revision.cfg.TemplateFiles == nil { - revision.cfg.TemplateFiles = map[string]string{} + if revision.Config.TemplateFiles == nil { + revision.Config.TemplateFiles = map[string]string{} } - revision.cfg.TemplateFiles[tmpl.Name] = tmpl.Template + revision.Config.TemplateFiles[tmpl.Name] = tmpl.Template err = t.xact.InTransaction(ctx, func(ctx context.Context) error { if err := t.configStore.Save(ctx, revision, orgID); err != nil { @@ -85,7 +85,7 @@ func (t *TemplateService) DeleteTemplate(ctx context.Context, orgID int64, name return err } - delete(revision.cfg.TemplateFiles, name) + delete(revision.Config.TemplateFiles, name) return t.xact.InTransaction(ctx, func(ctx context.Context) error { if err := t.configStore.Save(ctx, revision, orgID); err != nil { diff --git a/pkg/services/ngalert/provisioning/templates_test.go b/pkg/services/ngalert/provisioning/templates_test.go index 6929d32c88c..e54ab4e68e0 100644 --- a/pkg/services/ngalert/provisioning/templates_test.go +++ b/pkg/services/ngalert/provisioning/templates_test.go @@ -11,13 +11,15 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/setting" ) func TestTemplateService(t *testing.T) { t.Run("service returns templates from config file", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) @@ -30,8 +32,9 @@ func TestTemplateService(t *testing.T) { }) t.Run("service returns empty map when config file contains no templates", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) @@ -44,8 +47,9 @@ func TestTemplateService(t *testing.T) { t.Run("service propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -55,32 +59,35 @@ func TestTemplateService(t *testing.T) { }) t.Run("when config is invalid", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) _, err := sut.GetTemplates(context.Background(), 1) - require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) + require.Truef(t, legacy_storage.ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) _, err := sut.GetTemplates(context.Background(), 1) - require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) + require.Truef(t, legacy_storage.ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) }) t.Run("setting templates", func(t *testing.T) { t.Run("rejects templates that fail validation", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := definitions.NotificationTemplate{ Name: "", Template: "", @@ -93,9 +100,10 @@ func TestTemplateService(t *testing.T) { t.Run("propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := createNotificationTemplate() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -105,38 +113,41 @@ func TestTemplateService(t *testing.T) { }) t.Run("when config is invalid", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := createNotificationTemplate() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) _, err := sut.SetTemplate(context.Background(), 1, tmpl) - require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) + require.Truef(t, legacy_storage.ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := createNotificationTemplate() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) _, err := sut.SetTemplate(context.Background(), 1, tmpl) - require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) + require.Truef(t, legacy_storage.ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when provenance fails to save", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := createNotificationTemplate() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT(). SetProvenance(mock.Anything, mock.Anything, mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save provenance")) @@ -147,13 +158,14 @@ func TestTemplateService(t *testing.T) { }) t.Run("when AM config fails to save", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := createNotificationTemplate() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save config")) sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() @@ -165,13 +177,14 @@ func TestTemplateService(t *testing.T) { }) t.Run("adds new template to config file on success", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := createNotificationTemplate() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -180,13 +193,14 @@ func TestTemplateService(t *testing.T) { }) t.Run("succeeds when stitching config file with no templates", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := createNotificationTemplate() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -195,16 +209,17 @@ func TestTemplateService(t *testing.T) { }) t.Run("normalizes template content with no define", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := definitions.NotificationTemplate{ Name: "name", Template: "content", } - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() result, _ := sut.SetTemplate(context.Background(), 1, tmpl) @@ -214,16 +229,17 @@ func TestTemplateService(t *testing.T) { }) t.Run("avoids normalizing template content with define", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := definitions.NotificationTemplate{ Name: "name", Template: "{{define \"name\"}}content{{end}}", } - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() result, _ := sut.SetTemplate(context.Background(), 1, tmpl) @@ -232,16 +248,17 @@ func TestTemplateService(t *testing.T) { }) t.Run("rejects syntactically invalid template", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := definitions.NotificationTemplate{ Name: "name", Template: "{{ .MyField }", } - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -250,16 +267,17 @@ func TestTemplateService(t *testing.T) { }) t.Run("does not reject template with unknown field", func(t *testing.T) { - sut := createTemplateServiceSut() + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) tmpl := definitions.NotificationTemplate{ Name: "name", Template: "{{ .NotAField }}", } - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() _, err := sut.SetTemplate(context.Background(), 1, tmpl) @@ -271,8 +289,9 @@ func TestTemplateService(t *testing.T) { t.Run("deleting templates", func(t *testing.T) { t.Run("propagates errors", func(t *testing.T) { t.Run("when unable to read config", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, fmt.Errorf("failed")) @@ -282,35 +301,38 @@ func TestTemplateService(t *testing.T) { }) t.Run("when config is invalid", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: brokenConfig, }) err := sut.DeleteTemplate(context.Background(), 1, "template") - require.Truef(t, ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) + require.Truef(t, legacy_storage.ErrBadAlertmanagerConfiguration.Base.Is(err), "expected ErrBadAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when no AM config in current org", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(nil, nil) err := sut.DeleteTemplate(context.Background(), 1, "template") - require.Truef(t, ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) + require.Truef(t, legacy_storage.ErrNoAlertmanagerConfiguration.Is(err), "expected ErrNoAlertmanagerConfiguration but got %s", err.Error()) }) t.Run("when provenance fails to save", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT(). DeleteProvenance(mock.Anything, mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save provenance")) @@ -321,12 +343,13 @@ func TestTemplateService(t *testing.T) { }) t.Run("when AM config fails to save", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore.EXPECT(). UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). Return(fmt.Errorf("failed to save config")) sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() @@ -338,12 +361,13 @@ func TestTemplateService(t *testing.T) { }) t.Run("deletes template from config file on success", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "a") @@ -352,12 +376,13 @@ func TestTemplateService(t *testing.T) { }) t.Run("does not error when deleting templates that do not exist", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: configWithTemplates, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "does not exist") @@ -366,12 +391,13 @@ func TestTemplateService(t *testing.T) { }) t.Run("succeeds when deleting from config file with no template section", func(t *testing.T) { - sut := createTemplateServiceSut() - sut.configStore.store.(*MockAMConfigStore).EXPECT(). + mockStore := &legacy_storage.MockAMConfigStore{} + sut := createTemplateServiceSut(legacy_storage.NewAlertmanagerConfigStore(mockStore)) + mockStore.EXPECT(). GetsConfig(models.AlertConfiguration{ AlertmanagerConfiguration: defaultConfig, }) - sut.configStore.store.(*MockAMConfigStore).EXPECT().SaveSucceeds() + mockStore.EXPECT().SaveSucceeds() sut.provenanceStore.(*MockProvisioningStore).EXPECT().SaveSucceeds() err := sut.DeleteTemplate(context.Background(), 1, "a") @@ -381,9 +407,9 @@ func TestTemplateService(t *testing.T) { }) } -func createTemplateServiceSut() *TemplateService { +func createTemplateServiceSut(configStore alertmanagerConfigStore) *TemplateService { return &TemplateService{ - configStore: &alertmanagerConfigStoreImpl{store: &MockAMConfigStore{}}, + configStore: configStore, provenanceStore: &MockProvisioningStore{}, xact: newNopTransactionManager(), log: log.NewNopLogger(), diff --git a/pkg/services/ngalert/provisioning/testing.go b/pkg/services/ngalert/provisioning/testing.go index 58d305b0cc3..60519abf2a4 100644 --- a/pkg/services/ngalert/provisioning/testing.go +++ b/pkg/services/ngalert/provisioning/testing.go @@ -70,25 +70,6 @@ func (n *NopTransactionManager) InTransaction(ctx context.Context, work func(ctx return work(context.WithValue(ctx, NopTransactionManager{}, struct{}{})) } -func (m *MockAMConfigStore_Expecter) GetsConfig(ac models.AlertConfiguration) *MockAMConfigStore_Expecter { - m.GetLatestAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(&ac, nil) - return m -} - -func (m *MockAMConfigStore_Expecter) SaveSucceeds() *MockAMConfigStore_Expecter { - m.UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything).Return(nil) - return m -} - -func (m *MockAMConfigStore_Expecter) SaveSucceedsIntercept(intercepted *models.SaveAlertmanagerConfigurationCmd) *MockAMConfigStore_Expecter { - m.UpdateAlertmanagerConfiguration(mock.Anything, mock.Anything). - Return(nil). - Run(func(ctx context.Context, cmd *models.SaveAlertmanagerConfigurationCmd) { - *intercepted = *cmd - }) - return m -} - func (m *MockProvisioningStore_Expecter) GetReturns(p models.Provenance) *MockProvisioningStore_Expecter { m.GetProvenance(mock.Anything, mock.Anything, mock.Anything).Return(p, nil) m.GetProvenances(mock.Anything, mock.Anything, mock.Anything).Return(nil, nil) @@ -111,39 +92,6 @@ func (m *MockQuotaChecker_Expecter) LimitExceeded() *MockQuotaChecker_Expecter { return m } -type methodCall struct { - Method string - Args []interface{} -} - -type alertmanagerConfigStoreFake struct { - Calls []methodCall - GetFn func(ctx context.Context, orgID int64) (*cfgRevision, error) - SaveFn func(ctx context.Context, revision *cfgRevision) error -} - -func (a *alertmanagerConfigStoreFake) Get(ctx context.Context, orgID int64) (*cfgRevision, error) { - a.Calls = append(a.Calls, methodCall{ - Method: "Get", - Args: []interface{}{ctx, orgID}, - }) - if a.GetFn != nil { - return a.GetFn(ctx, orgID) - } - return nil, nil -} - -func (a *alertmanagerConfigStoreFake) Save(ctx context.Context, revision *cfgRevision, orgID int64) error { - a.Calls = append(a.Calls, methodCall{ - Method: "Save", - Args: []interface{}{ctx, revision, orgID}, - }) - if a.SaveFn != nil { - return a.SaveFn(ctx, revision) - } - return nil -} - type NotificationSettingsValidatorProviderFake struct { } diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go index f3a49c726ff..7dd48c52359 100644 --- a/pkg/services/provisioning/provisioning.go +++ b/pkg/services/provisioning/provisioning.go @@ -17,6 +17,7 @@ import ( "github.com/grafana/grafana/pkg/services/folder" alertingauthz "github.com/grafana/grafana/pkg/services/ngalert/accesscontrol" "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/services/ngalert/notifier/legacy_storage" "github.com/grafana/grafana/pkg/services/ngalert/provisioning" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/notifications" @@ -270,13 +271,21 @@ func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error notifier.NewCachedNotificationSettingsValidationService(&st), alertingauthz.NewRuleService(ps.ac), ) - receiverSvc := notifier.NewReceiverService(ps.ac, &st, st, ps.secretService, ps.SQLStore, ps.log) - contactPointService := provisioning.NewContactPointService(&st, ps.secretService, + configStore := legacy_storage.NewAlertmanagerConfigStore(&st) + receiverSvc := notifier.NewReceiverService( + ps.ac, + configStore, + st, + ps.secretService, + ps.SQLStore, + ps.log, + ) + contactPointService := provisioning.NewContactPointService(configStore, ps.secretService, st, ps.SQLStore, receiverSvc, ps.log, &st) - notificationPolicyService := provisioning.NewNotificationPolicyService(&st, + notificationPolicyService := provisioning.NewNotificationPolicyService(configStore, st, ps.SQLStore, ps.Cfg.UnifiedAlerting, ps.log) - mutetimingsService := provisioning.NewMuteTimingService(&st, st, &st, ps.log, &st) - templateService := provisioning.NewTemplateService(&st, st, &st, ps.log) + mutetimingsService := provisioning.NewMuteTimingService(configStore, st, &st, ps.log, &st) + templateService := provisioning.NewTemplateService(configStore, st, &st, ps.log) cfg := prov_alerting.ProvisionerConfig{ Path: alertingPath, RuleService: *ruleService,