mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
0edb0c5c4f
commit
a1f0b599a7
@ -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)
|
||||
|
@ -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(),
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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()),
|
||||
|
17
pkg/services/ngalert/notifier/legacy_storage/compat.go
Normal file
17
pkg/services/ngalert/notifier/legacy_storage/compat.go
Normal 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
|
||||
}
|
92
pkg/services/ngalert/notifier/legacy_storage/config.go
Normal file
92
pkg/services/ngalert/notifier/legacy_storage/config.go
Normal 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)
|
||||
}
|
@ -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
|
||||
})
|
||||
|
18
pkg/services/ngalert/notifier/legacy_storage/errors.go
Normal file
18
pkg/services/ngalert/notifier/legacy_storage/errors.go
Normal 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)
|
||||
}
|
15
pkg/services/ngalert/notifier/legacy_storage/persist.go
Normal file
15
pkg/services/ngalert/notifier/legacy_storage/persist.go
Normal 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
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
// Code generated by mockery v2.34.2. DO NOT EDIT.
|
||||
|
||||
package provisioning
|
||||
package legacy_storage
|
||||
|
||||
import (
|
||||
context "context"
|
53
pkg/services/ngalert/notifier/legacy_storage/receivers.go
Normal file
53
pkg/services/ngalert/notifier/legacy_storage/receivers.go
Normal 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
|
||||
}
|
@ -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)
|
||||
}
|
61
pkg/services/ngalert/notifier/legacy_storage/testing.go
Normal file
61
pkg/services/ngalert/notifier/legacy_storage/testing.go
Normal 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
|
||||
}
|
@ -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
|
||||
}
|
||||
return 0, nil
|
||||
if usedByRoutes {
|
||||
data["UsedByRoutes"] = true
|
||||
}
|
||||
|
||||
// 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,
|
||||
})
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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)
|
||||
}
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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{
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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{
|
||||
|
@ -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)
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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(),
|
||||
|
@ -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 {
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
Loading…
Reference in New Issue
Block a user