Alerting: Refactor receiver_svc and provisioning config store into legacy_storage package (#90856)

* Add more receivers api tests

* Move provisioning config store to new legacy_storage package
This commit is contained in:
Matthew Jacobson 2024-07-26 17:45:33 -04:00 committed by GitHub
parent 0edb0c5c4f
commit a1f0b599a7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 1013 additions and 687 deletions

View File

@ -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)

View File

@ -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(),

View File

@ -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)

View File

@ -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,

View File

@ -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()),

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
})

View File

@ -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)
}

View File

@ -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
}

View File

@ -1,6 +1,6 @@
// Code generated by mockery v2.34.2. DO NOT EDIT.
package provisioning
package legacy_storage
import (
context "context"

View File

@ -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
}

View File

@ -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)
}

View File

@ -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
}

View File

@ -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
}
cfg := definitions.PostableUserConfig{}
err = json.Unmarshal([]byte(baseCfg.AlertmanagerConfiguration), &cfg)
if err != nil {
return definitions.GettableApiReceiver{}, err
postable := revision.GetReceiver(legacy_storage.NameToUid(q.Name))
if postable == nil {
return definitions.GettableApiReceiver{}, ErrReceiverNotFound.Errorf("")
}
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)
}
storedProvenances, err := rs.provisioningStore.GetProvenances(ctx, q.OrgID, (&definitions.EmbeddedContactPoint{}).ResourceType())
if err != nil {
return definitions.GettableApiReceiver{}, err
}
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,
})
}

View File

@ -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 {

View File

@ -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)
}

View File

@ -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
}

View File

@ -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,

View File

@ -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{

View File

@ -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
}

View File

@ -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,

View File

@ -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
}

View File

@ -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{

View File

@ -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)
}

View File

@ -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 {

View File

@ -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(),

View File

@ -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 {
}

View File

@ -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,