Alerting: Support for simplified notification settings in rule API (#81011)

* Add notification settings to storage\domain and API models. Settings are a slice to workaround XORM mapping
* Support validation of notification settings when rules are updated

* Implement route generator for Alertmanager configuration. That fetches all notification settings.
* Update multi-tenant Alertmanager to run the generator before applying the configuration.

* Add notification settings labels to state calculation
* update the Multi-tenant Alertmanager to provide validation for notification settings

* update GET API so only admins can see auto-gen
This commit is contained in:
Yuri Tseretyan
2024-02-15 09:45:10 -05:00
committed by GitHub
parent ff916d9c15
commit 1eebd2a4de
60 changed files with 3466 additions and 304 deletions
@@ -10,8 +10,11 @@ import (
"time"
"github.com/go-openapi/strfmt"
"github.com/google/go-cmp/cmp"
"github.com/google/go-cmp/cmp/cmpopts"
alertingNotify "github.com/grafana/alerting/notify"
amv2 "github.com/prometheus/alertmanager/api/v2/models"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/client_golang/prometheus"
"github.com/stretchr/testify/require"
@@ -20,6 +23,7 @@ import (
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
@@ -325,6 +329,84 @@ func TestAlertmanagerConfig(t *testing.T) {
})
}
func TestAlertmanagerAutogenConfig(t *testing.T) {
createSutForAutogen := func(t *testing.T) (AlertmanagerSrv, map[int64]*ngmodels.AlertConfiguration) {
sut := createSut(t)
configs := map[int64]*ngmodels.AlertConfiguration{
1: {AlertmanagerConfiguration: validConfig, OrgID: 1},
2: {AlertmanagerConfiguration: validConfigWithoutAutogen, OrgID: 2},
}
sut.mam = createMultiOrgAlertmanager(t, configs)
return sut, configs
}
compare := func(t *testing.T, expectedAm string, testAm string) {
test, err := notifier.Load([]byte(testAm))
require.NoError(t, err)
exp, err := notifier.Load([]byte(expectedAm))
require.NoError(t, err)
cOpt := []cmp.Option{
cmpopts.IgnoreUnexported(apimodels.PostableUserConfig{}, apimodels.Route{}, labels.Matcher{}),
cmpopts.IgnoreFields(apimodels.PostableGrafanaReceiver{}, "UID", "Settings"),
}
if !cmp.Equal(test, exp, cOpt...) {
t.Errorf("Unexpected AM Config: %v", cmp.Diff(test, exp, cOpt...))
}
}
t.Run("route POST config", func(t *testing.T) {
t.Run("does not save autogen routes", func(t *testing.T) {
sut, configs := createSutForAutogen(t)
rc := createRequestCtxInOrg(1)
request := createAmConfigRequest(t, validConfigWithAutogen)
response := sut.RoutePostAlertingConfig(rc, request)
require.Equal(t, 202, response.Status())
compare(t, validConfigWithoutAutogen, configs[1].AlertmanagerConfiguration)
})
t.Run("provenance guard ignores autogen routes", func(t *testing.T) {
sut := createSut(t)
rc := createRequestCtxInOrg(1)
request := createAmConfigRequest(t, validConfigWithoutAutogen)
_ = sut.RoutePostAlertingConfig(rc, request)
setRouteProvenance(t, 1, sut.mam.ProvStore)
request = createAmConfigRequest(t, validConfigWithAutogen)
request.AlertmanagerConfig.Route.Provenance = apimodels.Provenance(ngmodels.ProvenanceAPI)
response := sut.RoutePostAlertingConfig(rc, request)
require.Equal(t, 202, response.Status())
})
})
t.Run("route GET config", func(t *testing.T) {
t.Run("when admin return autogen routes", func(t *testing.T) {
sut, _ := createSutForAutogen(t)
rc := createRequestCtxInOrg(2)
rc.SignedInUser.OrgRole = org.RoleAdmin
response := sut.RouteGetAlertingConfig(rc)
require.Equal(t, 200, response.Status())
compare(t, validConfigWithAutogen, string(response.Body()))
})
t.Run("when not admin return no autogen routes", func(t *testing.T) {
sut, _ := createSutForAutogen(t)
rc := createRequestCtxInOrg(2)
response := sut.RouteGetAlertingConfig(rc)
require.Equal(t, 200, response.Status())
compare(t, validConfigWithoutAutogen, string(response.Body()))
})
})
}
func TestRouteGetAlertingConfigHistory(t *testing.T) {
sut := createSut(t)
@@ -633,7 +715,12 @@ func TestRouteCreateSilence(t *testing.T) {
func createSut(t *testing.T) AlertmanagerSrv {
t.Helper()
mam := createMultiOrgAlertmanager(t)
configs := map[int64]*ngmodels.AlertConfiguration{
1: {AlertmanagerConfiguration: validConfig, OrgID: 1},
2: {AlertmanagerConfiguration: validConfig, OrgID: 2},
3: {AlertmanagerConfiguration: brokenConfig, OrgID: 3},
}
mam := createMultiOrgAlertmanager(t, configs)
log := log.NewNopLogger()
return AlertmanagerSrv{
mam: mam,
@@ -653,14 +740,9 @@ func createAmConfigRequest(t *testing.T, config string) apimodels.PostableUserCo
return request
}
func createMultiOrgAlertmanager(t *testing.T) *notifier.MultiOrgAlertmanager {
func createMultiOrgAlertmanager(t *testing.T, configs map[int64]*ngmodels.AlertConfiguration) *notifier.MultiOrgAlertmanager {
t.Helper()
configs := map[int64]*ngmodels.AlertConfiguration{
1: {AlertmanagerConfiguration: validConfig, OrgID: 1},
2: {AlertmanagerConfiguration: validConfig, OrgID: 2},
3: {AlertmanagerConfiguration: brokenConfig, OrgID: 3},
}
configStore := notifier.NewFakeConfigStore(t, configs)
orgStore := notifier.NewFakeOrgStore(t, []int64{1, 2, 3})
provStore := ngfakes.NewFakeProvisioningStore()
@@ -679,7 +761,7 @@ func createMultiOrgAlertmanager(t *testing.T) *notifier.MultiOrgAlertmanager {
}, // do not poll in tests.
}
mam, err := notifier.NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, provStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService)
mam, err := notifier.NewMultiOrgAlertmanager(cfg, configStore, orgStore, kvStore, provStore, decryptFn, m.GetMultiOrgAlertmanagerMetrics(), nil, log.New("testlogger"), secretsService, featuremgmt.WithManager(featuremgmt.FlagAlertingSimplifiedRouting))
require.NoError(t, err)
err = mam.LoadAndSyncAlertmanagersForOrgs(context.Background())
require.NoError(t, err)
@@ -710,6 +792,90 @@ var validConfig = `{
}
`
var validConfigWithoutAutogen = `{
"template_files": {
"a": "template"
},
"alertmanager_config": {
"route": {
"receiver": "some email",
"routes": [{
"receiver": "other email",
"object_matchers": [["a", "=", "b"]]
}]
},
"receivers": [{
"name": "some email",
"grafana_managed_receiver_configs": [{
"name": "some email",
"type": "email",
"settings": {
"addresses": "<some@email.com>"
}
}]
},{
"name": "other email",
"grafana_managed_receiver_configs": [{
"name": "other email",
"type": "email",
"settings": {
"addresses": "<other@email.com>"
}
}]
}]
}
}
`
var validConfigWithAutogen = `{
"template_files": {
"a": "template"
},
"alertmanager_config": {
"route": {
"receiver": "some email",
"routes": [{
"receiver": "some email",
"object_matchers": [["__grafana_autogenerated__", "=", "true"]],
"routes": [{
"receiver": "some email",
"group_by": ["grafana_folder", "alertname"],
"object_matchers": [["__grafana_receiver__", "=", "some email"]],
"continue": false
},{
"receiver": "other email",
"group_by": ["grafana_folder", "alertname"],
"object_matchers": [["__grafana_receiver__", "=", "other email"]],
"continue": false
}]
},{
"receiver": "other email",
"object_matchers": [["a", "=", "b"]]
}]
},
"receivers": [{
"name": "some email",
"grafana_managed_receiver_configs": [{
"name": "some email",
"type": "email",
"settings": {
"addresses": "<some@email.com>"
}
}]
},{
"name": "other email",
"grafana_managed_receiver_configs": [{
"name": "other email",
"type": "email",
"settings": {
"addresses": "<other@email.com>"
}
}]
}]
}
}
`
var validConfigWithSecureSetting = `{
"template_files": {
"a": "template"