Alerting: Remove the POST endpoint for the internal Grafana Alertmanager config (#103819)

* Remove POST config for Grafana Alertmanager

* Delete auth + test for removed path

* Alerting: Remove check for `alertingApiServer` toggle in UI (#103805)

* Remove check for alertingApiServer in UI

* Update tests to no longer care about alertingApiServer

* Add contact points handlers now that we use alertingApiServer all the time

* Fix test broken from removing camelCase for UIDs

---------

Co-authored-by: Tom Ratcliffe <tom.ratcliffe@grafana.com>
This commit is contained in:
William Wernert
2025-04-11 17:38:53 -04:00
committed by GitHub
co-authored by Tom Ratcliffe
parent c47ab101d1
commit a5288db624
50 changed files with 687 additions and 1928 deletions
@@ -10,8 +10,6 @@ import (
"testing"
"time"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/require"
@@ -354,49 +352,3 @@ func TestIntegrationAdminConfiguration_SendingToExternalAlertmanagers(t *testing
}, 16*time.Second, 8*time.Second) // the sync interval is 2s so after 8s all alertmanagers (if any) most probably are started
}
}
func TestIntegrationAdminConfiguration_CannotCreateInhibitionRules(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
client := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
cfg := apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
},
InhibitRules: []config.InhibitRule{{
SourceMatchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "foo",
Value: "bar",
}},
TargetMatchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "bar",
Value: "baz",
}},
}},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
}
ok, err := client.PostConfiguration(t, cfg)
require.False(t, ok)
require.EqualError(t, err, "inhibition rules are not supported")
}
@@ -1,554 +0,0 @@
package alerting
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"regexp"
"testing"
"time"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/org/orgimpl"
"github.com/grafana/grafana/pkg/services/quota/quotatest"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
func TestIntegrationAlertmanagerConfiguration(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
client := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
cases := []struct {
name string
cfg apimodels.PostableUserConfig
expErr string
}{{
name: "configuration with default route",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
name: "configuration with UTF-8 matchers",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
GroupBy: []model.LabelName{"foo🙂"},
Matchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "foo🙂",
Value: "bar",
}, {
Type: labels.MatchNotEqual,
Name: "_bar1",
Value: "baz🙂",
}, {
Type: labels.MatchRegexp,
Name: "0baz",
Value: "[a-zA-Z0-9]+,?",
}, {
Type: labels.MatchNotRegexp,
Name: "corge",
Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$",
}, {
Type: labels.MatchEqual,
Name: "Προμηθέας", // Prometheus in Greek
Value: "Prom",
}, {
Type: labels.MatchNotEqual,
Name: "犬", // Dog in Japanese
Value: "Shiba Inu",
}},
}},
},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
name: "configuration with UTF-8 object matchers",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
GroupBy: []model.LabelName{"foo🙂"},
ObjectMatchers: apimodels.ObjectMatchers{{
Type: labels.MatchEqual,
Name: "foo🙂",
Value: "bar",
}, {
Type: labels.MatchNotEqual,
Name: "_bar1",
Value: "baz🙂",
}, {
Type: labels.MatchRegexp,
Name: "0baz",
Value: "[a-zA-Z0-9]+,?",
}, {
Type: labels.MatchNotRegexp,
Name: "corge",
Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$",
}, {
Type: labels.MatchEqual,
Name: "Προμηθέας", // Prometheus in Greek
Value: "Prom",
}, {
Type: labels.MatchNotEqual,
Name: "犬", // Dog in Japanese
Value: "Shiba Inu",
}},
}},
},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
name: "configuration with UTF-8 in both matchers and object matchers",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
GroupBy: []model.LabelName{"foo🙂"},
Matchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "foo🙂",
Value: "bar",
}, {
Type: labels.MatchNotEqual,
Name: "_bar1",
Value: "baz🙂",
}, {
Type: labels.MatchRegexp,
Name: "0baz",
Value: "[a-zA-Z0-9]+,?",
}, {
Type: labels.MatchNotRegexp,
Name: "corge",
Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$",
}},
ObjectMatchers: apimodels.ObjectMatchers{{
Type: labels.MatchEqual,
Name: "Προμηθέας", // Prometheus in Greek
Value: "Prom",
}, {
Type: labels.MatchNotEqual,
Name: "犬", // Dog in Japanese
Value: "Shiba Inu",
}},
}},
},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
// TODO: Mute time intervals is deprecated in Alertmanager and scheduled to be
// removed before version 1.0. Remove this test when support for mute time
// intervals is removed.
name: "configuration with mute time intervals",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
MuteTimeIntervals: []string{"weekends"},
}},
},
MuteTimeIntervals: []config.MuteTimeInterval{{
Name: "weekends",
TimeIntervals: []timeinterval.TimeInterval{{
Weekdays: []timeinterval.WeekdayRange{{
InclusiveRange: timeinterval.InclusiveRange{
Begin: 1,
End: 5,
},
}},
}},
}},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}, {
name: "configuration with time intervals",
cfg: apimodels.PostableUserConfig{
AlertmanagerConfig: apimodels.PostableApiAlertingConfig{
Config: apimodels.Config{
Route: &apimodels.Route{
Receiver: "test",
Routes: []*apimodels.Route{{
MuteTimeIntervals: []string{"weekends"},
}},
},
TimeIntervals: []config.TimeInterval{{
Name: "weekends",
TimeIntervals: []timeinterval.TimeInterval{{
Weekdays: []timeinterval.WeekdayRange{{
InclusiveRange: timeinterval.InclusiveRange{
Begin: 1,
End: 5,
},
}},
}},
}},
},
Receivers: []*apimodels.PostableApiReceiver{{
Receiver: config.Receiver{
Name: "test",
},
}},
},
},
}}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
ok, err := client.PostConfiguration(t, tc.cfg)
if tc.expErr != "" {
require.EqualError(t, err, tc.expErr)
require.False(t, ok)
} else {
require.NoError(t, err)
require.True(t, ok)
}
})
}
}
func TestIntegrationAlertmanagerConfigurationIsTransactional(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
NGAlertAlertmanagerConfigPollInterval: 2 * time.Second,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
orgService, err := orgimpl.ProvideService(env.SQLStore, env.Cfg, quotatest.New(false, nil))
require.NoError(t, err)
// editor from main organisation requests configuration
alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
// create user under main organisation
userID := createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor",
Login: "editor",
})
// create another organisation
newOrg, err := orgService.CreateWithMember(context.Background(), &org.CreateOrgCommand{Name: "another org", UserID: userID})
require.NoError(t, err)
orgID := newOrg.ID
// create user under different organisation
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor-42",
Login: "editor-42",
OrgID: orgID,
})
// On a blank start with no configuration, it saves and delivers the default configuration.
{
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
}
// When creating new configuration, if it fails to apply - it does not save it.
{
payload := `
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"settings": {
"iconEmoji": "",
"iconUrl": "",
"mentionGroups": "",
"mentionUsers": "",
"recipient": "#unified-alerting-test",
"username": ""
},
"secureSettings": {},
"type": "slack",
"name": "slack.receiver",
"disableResolveMessage": false,
"uid": ""
}]
}]
}
}
`
resp := postRequest(t, alertConfigURL, payload, http.StatusBadRequest) // nolint
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
var res map[string]any
require.NoError(t, json.Unmarshal(b, &res))
require.Regexp(t, `^failed to save and apply Alertmanager configuration: failed to validate integration "slack.receiver" \(UID [^\)]+\) of type "slack": token must be specified when using the Slack chat API`, res["message"])
resp = getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
}
// editor42 from organisation 42 posts configuration
alertConfigURL = fmt.Sprintf("http://editor-42:editor-42@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
// Before we start operating, make sure we've synced this org.
require.Eventually(t, func() bool {
resp, err := http.Get(alertConfigURL) // nolint
require.NoError(t, err)
return resp.StatusCode == http.StatusOK
}, 10*time.Second, 2*time.Second)
// Post the alertmanager config.
{
mockChannel := newMockNotificationChannel(t, grafanaListedAddr)
amConfig := getAlertmanagerConfig(mockChannel.server.Addr)
postRequest(t, alertConfigURL, amConfig, http.StatusAccepted) // nolint
// Verifying that the new configuration is returned
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
b := getBody(t, resp.Body)
re := regexp.MustCompile(`"uid":"([\w|-]*)"`)
e := getExpAlertmanagerConfigFromAPI(mockChannel.server.Addr)
require.JSONEq(t, e, string(re.ReplaceAll([]byte(b), []byte(`"uid":""`))))
}
// verify that main organisation still gets the default configuration
alertConfigURL = fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
{
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, defaultAlertmanagerConfigJSON, getBody(t, resp.Body))
}
}
func TestIntegrationAlertmanagerConfigurationPersistSecrets(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
DisableFeatureToggles: []string{featuremgmt.FlagAlertingApiServer},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
alertConfigURL := fmt.Sprintf("http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor",
Login: "editor",
})
generatedUID := ""
// create a new configuration that has a secret
{
payload := `
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"settings": {
"recipient": "#unified-alerting-test"
},
"secureSettings": {
"url": "http://averysecureurl.com/webhook"
},
"type": "slack",
"name": "slack.receiver",
"disableResolveMessage": false
}]
}]
}
}
`
resp := postRequest(t, alertConfigURL, payload, http.StatusAccepted) // nolint
require.JSONEq(t, `{"message":"configuration created"}`, getBody(t, resp.Body))
}
// Try to update a receiver with unknown UID
{
// Then, update the recipient
payload := `
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"settings": {
"recipient": "#unified-alerting-test-but-updated"
},
"secureFields": {
"url": true
},
"type": "slack",
"name": "slack.receiver",
"disableResolveMessage": false,
"uid": "invalid"
}]
}]
}
}
`
resp := postRequest(t, alertConfigURL, payload, http.StatusBadRequest) // nolint
s := getBody(t, resp.Body)
var res map[string]any
require.NoError(t, json.Unmarshal([]byte(s), &res))
require.Equal(t, "unknown receiver: invalid", res["message"])
}
// The secure settings must be present
{
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
var c apimodels.GettableUserConfig
bb := getBody(t, resp.Body)
err := json.Unmarshal([]byte(bb), &c)
require.NoError(t, err)
m := c.GetGrafanaReceiverMap()
assert.Len(t, m, 1)
for k := range m {
generatedUID = m[k].UID
}
// Then, update the recipient
payload := fmt.Sprintf(`
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"settings": {
"recipient": "#unified-alerting-test-but-updated"
},
"secureFields": {
"url": true
},
"type": "slack",
"name": "slack.receiver",
"disableResolveMessage": false,
"uid": %q
}]
}]
}
}
`, generatedUID)
resp = postRequest(t, alertConfigURL, payload, http.StatusAccepted) // nolint
require.JSONEq(t, `{"message": "configuration created"}`, getBody(t, resp.Body))
}
// The secure settings must be present
{
resp := getRequest(t, alertConfigURL, http.StatusOK) // nolint
require.JSONEq(t, fmt.Sprintf(`
{
"template_files": {},
"alertmanager_config": {
"route": {
"receiver": "slack.receiver"
},
"receivers": [{
"name": "slack.receiver",
"grafana_managed_receiver_configs": [{
"uid": %q,
"name": "slack.receiver",
"type": "slack",
"disableResolveMessage": false,
"settings": {
"recipient": "#unified-alerting-test-but-updated"
},
"secureFields": {
"url": true
}
}]
}]
}
}
`, generatedUID), getBody(t, resp.Body))
}
}
@@ -2,6 +2,7 @@ package alerting
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
@@ -16,7 +17,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/services/featuremgmt"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
@@ -32,9 +32,6 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
DisableFeatureToggles: []string{
featuremgmt.FlagAlertingApiServer,
},
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
@@ -63,8 +60,9 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
expBody string
}
t.Run("when creating alertmanager configuration", func(t *testing.T) {
body := `
// Create alertmanager config
cfg := apimodels.PostableUserConfig{}
amConfig := `
{
"alertmanager_config": {
"route": {
@@ -85,51 +83,10 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
}
}
`
testCases := []testCase{
{
desc: "un-authenticated request should fail",
url: "http://%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusUnauthorized,
expBody: `"message":"Unauthorized"`,
},
{
desc: "viewer request should fail",
url: "http://viewer:viewer@%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusForbidden,
expBody: `"title":"Access denied"`,
},
{
desc: "editor request should succeed",
url: "http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusAccepted,
expBody: `{"message":"configuration created"}`,
},
{
desc: "admin request should succeed",
url: "http://admin:admin@%s/api/alertmanager/grafana/config/api/v1/alerts",
expStatus: http.StatusAccepted,
expBody: `{"message":"configuration created"}`,
},
}
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
url := fmt.Sprintf(tc.url, grafanaListedAddr)
buf := bytes.NewReader([]byte(body))
// nolint:gosec
resp, err := http.Post(url, "application/json", buf)
t.Cleanup(func() {
require.NoError(t, resp.Body.Close())
})
require.NoError(t, err)
require.Equal(t, tc.expStatus, resp.StatusCode)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
require.Contains(t, string(b), tc.expBody)
})
}
})
err := json.Unmarshal([]byte(amConfig), &cfg)
require.NoError(t, err)
err = env.Server.HTTPServer.AlertNG.MultiOrgAlertmanager.SaveAndApplyAlertmanagerConfiguration(context.Background(), 1, cfg)
require.NoError(t, err)
t.Run("when retrieve alertmanager configuration", func(t *testing.T) {
cfgTemplate := `
@@ -329,7 +286,7 @@ func TestIntegrationAMConfigAccess(t *testing.T) {
})
var silences apimodels.GettableSilences
err := json.Unmarshal(blob, &silences)
err = json.Unmarshal(blob, &silences)
require.NoError(t, err)
assert.Len(t, silences, 2)
silenceIDs := make([]string, 0, len(silences))
@@ -28,7 +28,6 @@ import (
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/ngalert/notifier"
"github.com/grafana/grafana/pkg/util"
"github.com/grafana/grafana/pkg/infra/db"
@@ -249,112 +248,6 @@ func TestIntegrationTestReceivers(t *testing.T) {
require.JSONEq(t, expectedJSON, string(b))
})
t.Run("assert working receiver with existing secure settings returns OK", func(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Login: "grafana",
Password: "password",
})
mockChannel := newMockNotificationChannel(t, grafanaListedAddr)
amConfig := createAlertmanagerConfig(`{
"alertmanager_config": {
"route": {
"receiver": "receiver-1"
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"uid": "",
"name": "receiver-1",
"type": "slack",
"disableResolveMessage": false,
"settings": {},
"secureSettings": {"url": "http://CHANNEL_ADDR/slack_recv1/slack_test_without_token"}
}
]
}]
}
}`, mockChannel.server.Addr)
// Set up responses
mockChannel.responses["slack_recv1"] = `{"ok": true}`
// Post config
u := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
_ = postRequest(t, u, amConfig, http.StatusAccepted) // nolint
// Get am config with UID and without secureSettings
resp := getRequest(t, u, http.StatusOK)
b, err := io.ReadAll(resp.Body)
require.NoError(t, err)
config, err := notifier.Load(b)
require.NoError(t, err)
body, err := json.Marshal(apimodels.TestReceiversConfigBodyParams{
Receivers: config.AlertmanagerConfig.Receivers,
})
require.NoError(t, err)
// Test using the am config without secureSettings
testReceiversURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/receivers/test", grafanaListedAddr)
// nolint
resp = postRequest(t, testReceiversURL, string(body), http.StatusOK)
t.Cleanup(func() {
err := resp.Body.Close()
require.NoError(t, err)
})
b, err = io.ReadAll(resp.Body)
require.NoError(t, err)
var result apimodels.TestReceiversResult
require.NoError(t, json.Unmarshal(b, &result))
require.Len(t, result.Receivers, 1)
require.Len(t, result.Receivers[0].Configs, 1)
expectedJSON := fmt.Sprintf(`{
"alert": {
"annotations": {
"summary": "Notification test",
"__dashboardUid__": "dashboard_uid",
"__orgId__": "1",
"__panelId__": "1",
"__value_string__": "[ var='B' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=22 ], [ var='C' labels={__name__=go_threads, instance=host.docker.internal:3000, job=grafana} value=1 ]",
"__values__": "{\"B\":22,\"C\":1}"
},
"labels": {
"alertname": "TestAlert",
"grafana_folder": "Test Folder",
"instance": "Grafana"
}
},
"receivers": [{
"name":"receiver-1",
"grafana_managed_receiver_configs": [
{
"name": "receiver-1",
"uid": "%s",
"status": "ok"
}
]
}],
"notified_at": "%s"
}`,
result.Receivers[0].Configs[0].UID,
result.NotifiedAt.Format(time.RFC3339Nano))
require.JSONEq(t, expectedJSON, string(b))
})
t.Run("assert invalid receiver returns 400 Bad Request", func(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
@@ -1008,8 +901,11 @@ func TestIntegrationNotificationChannels(t *testing.T) {
apiClient.CreateFolder(t, "default", "default")
// Post the alertmanager config.
u := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
_ = postRequest(t, u, amConfig, http.StatusAccepted) // nolint
cfg := apimodels.PostableUserConfig{}
err := json.Unmarshal([]byte(amConfig), &cfg)
require.NoError(t, err)
err = env.Server.HTTPServer.AlertNG.MultiOrgAlertmanager.SaveAndApplyAlertmanagerConfiguration(context.Background(), 1, cfg)
require.NoError(t, err)
// Verifying that all the receivers and routes have been registered.
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/config/api/v1/alerts", grafanaListedAddr)
@@ -1025,7 +921,7 @@ func TestIntegrationNotificationChannels(t *testing.T) {
b = getBody(t, resp.Body)
var receivers []apimodels.Receiver
err := json.Unmarshal([]byte(b), &receivers)
err = json.Unmarshal([]byte(b), &receivers)
require.NoError(t, err)
for _, rcv := range receivers {
require.NotNil(t, rcv.Name)
+1 -27
View File
@@ -3288,7 +3288,7 @@ func TestIntegrationAlertRuleCRUD(t *testing.T) {
"intervalSeconds":60,
"is_paused": false,
"version":1,
"uid":"uid",
"uid":"uid",
"guid": "guid",
"namespace_uid":"nsuid",
"rule_group":"arulegroup",
@@ -4451,32 +4451,6 @@ func TestIntegrationRuleNotificationSettings(t *testing.T) {
t.Log(body)
})
t.Run("create with '...' groupBy followed by config post should succeed", func(t *testing.T) {
var copyD testData
err = json.Unmarshal(testDataRaw, &copyD)
group := copyD.RuleGroup
ns := group.Rules[0].GrafanaManagedAlert.NotificationSettings
ns.GroupBy = []string{ngmodels.FolderTitleLabel, model.AlertNameLabel, ngmodels.GroupByAll}
_, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &group, false)
require.Equalf(t, http.StatusAccepted, status, body)
// Now update the config with no changes.
_, status, body = apiClient.GetAlertmanagerConfigWithStatus(t)
if !assert.Equalf(t, http.StatusOK, status, body) {
return
}
cfg := apimodels.PostableUserConfig{}
err = json.Unmarshal([]byte(body), &cfg)
require.NoError(t, err)
ok, err := apiClient.PostConfiguration(t, cfg)
require.NoError(t, err)
require.True(t, ok)
})
t.Run("should create rule and generate route", func(t *testing.T) {
_, status, body := apiClient.PostRulesGroupWithStatus(t, folder, &d.RuleGroup, false)
require.Equalf(t, http.StatusAccepted, status, body)
-35
View File
@@ -4,7 +4,6 @@ import (
"bytes"
"context"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
@@ -420,40 +419,6 @@ func (a apiClient) UpdateAlertRuleOrgQuota(t *testing.T, orgID int64, limit int6
assert.Equal(t, http.StatusOK, resp.StatusCode)
}
func (a apiClient) PostConfiguration(t *testing.T, c apimodels.PostableUserConfig) (bool, error) {
t.Helper()
b, err := json.Marshal(c)
require.NoError(t, err)
u := fmt.Sprintf("%s/api/alertmanager/grafana/config/api/v1/alerts", a.url)
req, err := http.NewRequest(http.MethodPost, u, bytes.NewReader(b))
require.NoError(t, err)
req.Header.Set("Content-Type", "application/json")
client := &http.Client{}
resp, err := client.Do(req)
require.NoError(t, err)
require.NotNil(t, resp)
defer func() {
_ = resp.Body.Close()
}()
b, err = io.ReadAll(resp.Body)
require.NoError(t, err)
data := struct {
Message string `json:"message"`
}{}
require.NoError(t, json.Unmarshal(b, &data))
if resp.StatusCode == http.StatusAccepted {
return true, nil
}
return false, errors.New(data.Message)
}
func (a apiClient) PostRulesGroupWithStatus(t *testing.T, folder string, group *apimodels.PostableRuleGroupConfig, permanentlyDelete bool) (apimodels.UpdateRuleGroupResponse, int, string) {
t.Helper()
buf := bytes.Buffer{}
@@ -0,0 +1,79 @@
package common
import (
"testing"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
v0alpha1_receiver "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1"
v0alpha1_routingtree "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1"
v0alpha1_templategroup "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1"
v0alpha1_timeinterval "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1"
)
func NewReceiverClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_receiver.Receiver, v0alpha1_receiver.ReceiverList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1_receiver.Receiver, v0alpha1_receiver.ReceiverList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1_receiver.Kind().Group(),
Version: v0alpha1_receiver.Kind().Version(),
Resource: v0alpha1_receiver.Kind().Plural(),
}).Namespace("default"),
}
}
func NewRoutingTreeClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_routingtree.RoutingTree, v0alpha1_routingtree.RoutingTreeList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1_routingtree.RoutingTree, v0alpha1_routingtree.RoutingTreeList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1_routingtree.Kind().Group(),
Version: v0alpha1_routingtree.Kind().Version(),
Resource: v0alpha1_routingtree.Kind().Plural(),
}).Namespace("default"),
}
}
func NewTemplateGroupClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_templategroup.TemplateGroup, v0alpha1_templategroup.TemplateGroupList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1_templategroup.TemplateGroup, v0alpha1_templategroup.TemplateGroupList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1_templategroup.Kind().Group(),
Version: v0alpha1_templategroup.Kind().Version(),
Resource: v0alpha1_templategroup.Kind().Plural(),
}).Namespace("default"),
}
}
func NewTimeIntervalClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1_timeinterval.TimeInterval, v0alpha1_timeinterval.TimeIntervalList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1_timeinterval.TimeInterval, v0alpha1_timeinterval.TimeIntervalList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1_timeinterval.Kind().Group(),
Version: v0alpha1_timeinterval.Kind().Version(),
Resource: v0alpha1_timeinterval.Kind().Plural(),
}).Namespace("default"),
}
}
@@ -14,18 +14,19 @@ import (
"testing"
"github.com/grafana/alerting/notify"
"github.com/prometheus/alertmanager/config"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/receiver/v0alpha1"
common "github.com/grafana/grafana/pkg/apimachinery/apis/common/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routingtree"
test_common "github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/tracing"
@@ -67,7 +68,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
client := newClient(t, helper.Org1.Admin)
client := test_common.NewReceiverClient(t, helper.Org1.Admin)
newResource := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@@ -145,7 +146,7 @@ func TestIntegrationResourcePermissions(t *testing.T) {
admin := org1.Admin
viewer := org1.Viewer
editor := org1.Editor
adminClient := newClient(t, admin)
adminClient := test_common.NewReceiverClient(t, admin)
writeACMetadata := []string{"canWrite", "canDelete"}
allACMetadata := []string{"canWrite", "canDelete", "canReadSecrets", "canAdmin"}
@@ -297,8 +298,8 @@ func TestIntegrationResourcePermissions(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
createClient := newClient(t, tc.creatingUser)
client := newClient(t, tc.testUser)
createClient := test_common.NewReceiverClient(t, tc.creatingUser)
client := test_common.NewReceiverClient(t, tc.testUser)
var created = &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
@@ -561,10 +562,10 @@ func TestIntegrationAccessControl(t *testing.T) {
},
}
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
client := newClient(t, tc.user)
client := test_common.NewReceiverClient(t, tc.user)
var expected = &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
@@ -739,7 +740,7 @@ func TestIntegrationInUseMetadata(t *testing.T) {
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
// Prepare environment and create notification policy and rule that use receiver
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
require.NoError(t, err)
@@ -754,7 +755,7 @@ func TestIntegrationInUseMetadata(t *testing.T) {
parentRoute.Routes = []*definitions.Route{&route1, &route2}
amConfig.AlertmanagerConfig.Route.Routes = append(amConfig.AlertmanagerConfig.Route.Routes, &parentRoute)
persistInitialConfig(t, amConfig, adminClient, legacyCli)
persistInitialConfig(t, amConfig)
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
require.NoError(t, err)
@@ -817,8 +818,11 @@ func TestIntegrationInUseMetadata(t *testing.T) {
// Removing the new extra route should leave only 1.
amConfig.AlertmanagerConfig.Route.Routes = amConfig.AlertmanagerConfig.Route.Routes[:1]
success, err := legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
require.NoError(t, err)
routeAdminClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
_, err = routeAdminClient.Update(ctx, v1Route, v1.UpdateOptions{})
require.NoError(t, err)
receiverListed, receiverGet = requestReceivers(t, "user-defined")
checkInUse(t, receiverListed, receiverGet, 1, 2)
@@ -834,11 +838,14 @@ func TestIntegrationInUseMetadata(t *testing.T) {
receiverListed, receiverGet = requestReceivers(t, "grafana-default-email")
checkInUse(t, receiverListed, receiverGet, 1, 0)
// Remove the rest.
// Remove the remaining routes.
amConfig.AlertmanagerConfig.Route.Routes = nil
success, err = legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
v1route, err := routingtree.ConvertToK8sResource(1, *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeAdminClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
// Remove the remaining rules.
ruleGroup.Rules = nil
_, status, data = legacyCli.PostRulesGroupWithStatus(t, folderUID, &ruleGroup, false)
require.Equalf(t, http.StatusAccepted, status, "Failed to post Rule: %s", data)
@@ -861,7 +868,7 @@ func TestIntegrationProvisioning(t *testing.T) {
org := helper.Org1
admin := org.Admin
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
db, err := store.ProvideDBStore(env.Cfg, env.FeatureToggles, env.SQLStore, &foldertest.FakeService{}, &dashboards.FakeDashboardService{}, ac, bus.ProvideBus(tracing.InitializeTracerForTest()))
@@ -915,7 +922,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
receiver := v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@@ -998,7 +1005,7 @@ func TestIntegrationPatch(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
receiver := v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@@ -1086,75 +1093,6 @@ func TestIntegrationPatch(t *testing.T) {
})
}
func TestIntegrationRejectConfigApiReceiverModification(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
}
helper := getTestHelper(t)
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
adminClient := newClient(t, helper.Org1.Admin)
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
require.NoError(t, err)
var amConfig definitions.PostableUserConfig
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
persistInitialConfig(t, amConfig, adminClient, legacyCli)
// We modify the receiver, this should cause the POST to be rejected.
userDefinedReceiver := amConfig.AlertmanagerConfig.Receivers[slices.IndexFunc(amConfig.AlertmanagerConfig.Receivers, func(receiver *definitions.PostableApiReceiver) bool {
return receiver.Name == "user-defined"
})]
userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage = !userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage
success, err := legacyCli.PostConfiguration(t, amConfig)
require.Falsef(t, success, "Expected receiver modification to be rejected, but got %t", success)
require.ErrorContainsf(t, err, "alertingApiServer", "Expected error message to contain 'alertingApiServer', but got %s", err)
// Revert the change.
userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage = !userDefinedReceiver.GrafanaManagedReceivers[0].DisableResolveMessage
// We add a receiver, this should be accepted.
amConfig.AlertmanagerConfig.Receivers = append(amConfig.AlertmanagerConfig.Receivers, &definitions.PostableApiReceiver{
Receiver: config.Receiver{
Name: "new receiver",
},
PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{
GrafanaManagedReceivers: []*definitions.PostableGrafanaReceiver{
{
Name: "new receiver",
Type: "email",
Settings: []byte(`{"addresses": "<some@email.com>"}`),
},
},
},
})
success, err = legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Expected receiver modification to be accepted, but got %s", err)
require.NoError(t, err)
// Sanity check.
gettable, status, body := legacyCli.GetAlertmanagerConfigWithStatus(t)
require.Equalf(t, http.StatusOK, status, body)
require.Lenf(t, gettable.AlertmanagerConfig.Receivers, 3, "Expected 3 receivers, got %d", len(gettable.AlertmanagerConfig.Receivers))
// We remove the receiver, this should be accepted.
amConfig.AlertmanagerConfig.Receivers = slices.DeleteFunc(amConfig.AlertmanagerConfig.Receivers, func(receiver *definitions.PostableApiReceiver) bool {
return receiver.GrafanaManagedReceivers[0].Name == "new receiver"
})
success, err = legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Expected receiver modification to be accepted, but got %s", err)
require.NoError(t, err)
// Sanity check.
gettable, status, body = legacyCli.GetAlertmanagerConfigWithStatus(t)
require.Equalf(t, http.StatusOK, status, body)
require.Lenf(t, gettable.AlertmanagerConfig.Receivers, 2, "Expected 2 receivers, got %d", len(gettable.AlertmanagerConfig.Receivers))
}
func TestIntegrationReferentialIntegrity(t *testing.T) {
if testing.Short() {
t.Skip("skipping integration test")
@@ -1171,14 +1109,14 @@ func TestIntegrationReferentialIntegrity(t *testing.T) {
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
// Prepare environment and create notification policy and rule that use time receiver
alertmanagerRaw, err := testData.ReadFile(path.Join("test-data", "notification-settings.json"))
require.NoError(t, err)
var amConfig definitions.PostableUserConfig
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
persistInitialConfig(t, amConfig, adminClient, legacyCli)
persistInitialConfig(t, amConfig)
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
require.NoError(t, err)
@@ -1283,7 +1221,7 @@ func TestIntegrationCRUD(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
var defaultReceiver *v0alpha1.Receiver
t.Run("should list the default receiver", func(t *testing.T) {
items, err := adminClient.List(ctx, v1.ListOptions{})
@@ -1451,7 +1389,7 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
recv1 := &v0alpha1.Receiver{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@@ -1533,13 +1471,14 @@ func TestIntegrationReceiverListSelector(t *testing.T) {
// persistInitialConfig helps create an initial config with new receivers using legacy json. Config API blocks receiver
// modifications, so we need to use k8s API to create new receivers before posting the config.
func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig, adminClient *apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList], legacyCli alerting.LegacyApiClient) {
func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig) {
ctx := context.Background()
var defaultReceiver *definitions.PostableApiReceiver
helper := getTestHelper(t)
receiverClient := test_common.NewReceiverClient(t, helper.Org1.Admin)
for _, receiver := range amConfig.AlertmanagerConfig.Receivers {
if receiver.Name == "grafana-default-email" {
defaultReceiver = receiver
continue
}
@@ -1563,7 +1502,7 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig,
})
}
created, err := adminClient.Create(ctx, &toCreate, v1.CreateOptions{})
created, err := receiverClient.Create(ctx, &toCreate, v1.CreateOptions{})
require.NoError(t, err)
for i, integration := range created.Spec.Integrations {
@@ -1571,19 +1510,13 @@ func persistInitialConfig(t *testing.T, amConfig definitions.PostableUserConfig,
}
}
success, err := legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
nsMapper := func(_ int64) string { return "default" }
gettable, status, body := legacyCli.GetAlertmanagerConfigWithStatus(t)
require.Equalf(t, http.StatusOK, status, body)
idx := slices.IndexFunc(gettable.AlertmanagerConfig.Receivers, func(recv *definitions.GettableApiReceiver) bool {
return recv.Name == "grafana-default-email"
})
gettableDefault := gettable.AlertmanagerConfig.Receivers[idx]
// Assign uid of default receiver as well.
defaultReceiver.GrafanaManagedReceivers[0].UID = gettableDefault.GrafanaManagedReceivers[0].UID
routeClient := test_common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.AdminServiceAccount.OrgId, *amConfig.AlertmanagerConfig.Route, "", nsMapper)
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
}
func createIntegration(t *testing.T, integrationType string) v0alpha1.Integration {
@@ -1609,19 +1542,3 @@ func createWildcardPermission(actions ...string) resourcepermissions.SetResource
ResourceID: "*",
}
}
func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1.Receiver, v0alpha1.ReceiverList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1.Kind().Group(),
Version: v0alpha1.Kind().Version(),
Resource: v0alpha1.Kind().Plural(),
}).Namespace("default"),
}
}
@@ -8,7 +8,6 @@ import (
"testing"
"time"
"github.com/grafana/alerting/definition"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/pkg/labels"
"github.com/prometheus/common/model"
@@ -16,10 +15,12 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/routingtree/v0alpha1"
v0alpha1_timeinterval "github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routingtree"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1/fakes"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/services/accesscontrol"
@@ -32,6 +33,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
@@ -52,7 +54,7 @@ func TestIntegrationNotAllowedMethods(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
client := newClient(t, helper.Org1.Admin)
client := common.NewRoutingTreeClient(t, helper.Org1.Admin)
route := &v0alpha1.RoutingTree{
ObjectMeta: v1.ObjectMeta{
@@ -156,11 +158,11 @@ func TestIntegrationAccessControl(t *testing.T) {
}
admin := org1.Admin
adminClient := newClient(t, admin)
adminClient := common.NewRoutingTreeClient(t, admin)
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
client := newClient(t, tc.user)
client := common.NewRoutingTreeClient(t, tc.user)
if tc.canRead {
t.Run("should be able to list routing trees", func(t *testing.T) {
@@ -291,7 +293,7 @@ func TestIntegrationProvisioning(t *testing.T) {
org := helper.Org1
admin := org.Admin
adminClient := newClient(t, admin)
adminClient := common.NewRoutingTreeClient(t, admin)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
@@ -342,7 +344,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
current, err := adminClient.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
require.NoError(t, err)
@@ -388,46 +390,30 @@ func TestIntegrationDataConsistency(t *testing.T) {
cliCfg := helper.Org1.Admin.NewRestConfig()
legacyCli := alerting.NewAlertingLegacyAPIClient(helper.GetEnv().Server.HTTPServer.Listener.Addr().String(), cliCfg.Username, cliCfg.Password)
client := newClient(t, helper.Org1.Admin)
client := common.NewRoutingTreeClient(t, helper.Org1.Admin)
receiver := "grafana-default-email"
timeInterval := "test-time-interval"
createRoute := func(t *testing.T, route definitions.Route) {
t.Helper()
cfg, _, _ := legacyCli.GetAlertmanagerConfigWithStatus(t)
var receivers []*definitions.PostableApiReceiver
for _, apiReceiver := range cfg.AlertmanagerConfig.Receivers {
var recv []*definitions.PostableGrafanaReceiver
for _, r := range apiReceiver.GrafanaManagedReceivers {
recv = append(recv, &definitions.PostableGrafanaReceiver{
UID: r.UID,
Name: r.Name,
Type: r.Type,
DisableResolveMessage: r.DisableResolveMessage,
Settings: r.Settings,
})
}
receivers = append(receivers, &definitions.PostableApiReceiver{
Receiver: config.Receiver{Name: apiReceiver.Name},
PostableGrafanaReceivers: definitions.PostableGrafanaReceivers{GrafanaManagedReceivers: recv},
})
}
_, err := legacyCli.PostConfiguration(t, definitions.PostableUserConfig{
AlertmanagerConfig: definitions.PostableApiAlertingConfig{
Config: definition.Config{
Route: &route,
TimeIntervals: []config.TimeInterval{
{
Name: timeInterval,
},
},
},
Receivers: receivers,
},
})
routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1Route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), route, "", func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1Route, v1.UpdateOptions{})
require.NoError(t, err)
}
_, err := common.NewTimeIntervalClient(t, helper.Org1.Admin).Create(ctx, &v0alpha1_timeinterval.TimeInterval{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
},
Spec: v0alpha1_timeinterval.Spec{
Name: timeInterval,
TimeIntervals: fakes.IntervalGenerator{}.GenerateMany(1),
},
}, v1.CreateOptions{})
require.NoError(t, err)
var regex config.Regexp
require.NoError(t, json.Unmarshal([]byte(`".*"`), &regex))
@@ -461,7 +447,7 @@ func TestIntegrationDataConsistency(t *testing.T) {
createRoute(t, route)
tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, []v0alpha1.Matcher{
expected := []v0alpha1.Matcher{
{
Label: "label_match",
Type: v0alpha1.MatcherTypeEqual,
@@ -482,7 +468,8 @@ func TestIntegrationDataConsistency(t *testing.T) {
Type: v0alpha1.MatcherTypeNotEqualRegex,
Value: "test-456",
},
}, tree.Spec.Routes[0].Matchers)
}
assert.ElementsMatch(t, expected, tree.Spec.Routes[0].Matchers)
})
t.Run("should save into ObjectMatchers", func(t *testing.T) {
route := definitions.Route{
@@ -623,20 +610,55 @@ func TestIntegrationDataConsistency(t *testing.T) {
require.Equalf(t, http.StatusOK, status, body)
require.Equal(t, before, after)
})
}
func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.RoutingTree, v0alpha1.RoutingTreeList] {
t.Helper()
t.Run("unicode support in groupBy and matchers", func(t *testing.T) {
route := definitions.Route{
Receiver: receiver,
Routes: []*definitions.Route{
{
GroupByStr: []string{"foo🙂"},
Matchers: config.Matchers{{
Type: labels.MatchEqual,
Name: "foo🙂",
Value: "bar",
}, {
Type: labels.MatchNotEqual,
Name: "_bar1",
Value: "baz🙂",
}, {
Type: labels.MatchRegexp,
Name: "0baz",
Value: "[a-zA-Z0-9]+,?",
}, {
Type: labels.MatchNotRegexp,
Name: "corge",
Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$",
}},
ObjectMatchers: definitions.ObjectMatchers{{
Type: labels.MatchEqual,
Name: "Προμηθέας", // Prometheus in Greek
Value: "Prom",
}, {
Type: labels.MatchNotEqual,
Name: "犬", // Dog in Japanese (inu)
Value: "Shiba Inu",
}},
},
},
}
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1.RoutingTree, v0alpha1.RoutingTreeList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1.Kind().Group(),
Version: v0alpha1.Kind().Version(),
Resource: v0alpha1.Kind().Plural(),
}).Namespace("default"),
}
createRoute(t, route)
tree, err := client.Get(ctx, v0alpha1.UserDefinedRoutingTreeName, v1.GetOptions{})
require.NoError(t, err)
assert.Equal(t, "foo🙂", tree.Spec.Routes[0].GroupBy[0])
expected := []v0alpha1.Matcher{
{Label: "foo🙂", Type: v0alpha1.MatcherTypeEqual, Value: "bar"},
{Label: "_bar1", Type: v0alpha1.MatcherTypeNotEqual, Value: "baz🙂"},
{Label: "0baz", Type: v0alpha1.MatcherTypeEqualRegex, Value: "[a-zA-Z0-9]+,?"},
{Label: "corge", Type: v0alpha1.MatcherTypeNotEqualRegex, Value: "^[0-9]+((,[0-9]{3})*(,[0-9]{0,3})?)?$"},
{Label: "Προμηθέας", Type: v0alpha1.MatcherTypeEqual, Value: "Prom"},
{Label: "犬", Type: v0alpha1.MatcherTypeNotEqual, Value: "Shiba Inu"},
}
assert.ElementsMatch(t, expected, tree.Spec.Routes[0].Matchers)
})
}
@@ -11,9 +11,7 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/templategroup/v0alpha1"
"github.com/grafana/grafana/pkg/bus"
@@ -27,6 +25,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
@@ -47,7 +46,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
client := newClient(t, helper.Org1.Admin)
client := common.NewTemplateGroupClient(t, helper.Org1.Admin)
newTemplate := &v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@@ -217,11 +216,11 @@ func TestIntegrationAccessControl(t *testing.T) {
},
}
adminClient := newClient(t, org1.Admin)
adminClient := common.NewTemplateGroupClient(t, org1.Admin)
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
client := newClient(t, tc.user)
client := common.NewTemplateGroupClient(t, tc.user)
var expected = &v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@@ -377,7 +376,7 @@ func TestIntegrationProvisioning(t *testing.T) {
org := helper.Org1
admin := org.Admin
adminClient := newClient(t, admin)
adminClient := common.NewTemplateGroupClient(t, admin)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
@@ -427,7 +426,7 @@ func TestIntegrationOptimisticConcurrency(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
template := v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@@ -511,7 +510,7 @@ func TestIntegrationPatch(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
template := v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@@ -571,7 +570,7 @@ func TestIntegrationListSelector(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTemplateGroupClient(t, helper.Org1.Admin)
template1 := &v0alpha1.TemplateGroup{
ObjectMeta: v1.ObjectMeta{
@@ -664,19 +663,3 @@ func TestIntegrationListSelector(t *testing.T) {
require.NotEqualf(t, templates.DefaultTemplateName, list.Items[1].Name, "Expected non-default template but got %s", list.Items[1].Name)
})
}
func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.TemplateGroup, v0alpha1.TemplateGroupList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1.TemplateGroup, v0alpha1.TemplateGroupList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1.Kind().Group(),
Version: v0alpha1.Kind().Version(),
Resource: v0alpha1.Kind().Plural(),
}).Namespace("default"),
}
}
@@ -15,14 +15,14 @@ import (
"github.com/stretchr/testify/require"
"k8s.io/apimachinery/pkg/api/errors"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/runtime/schema"
"k8s.io/apimachinery/pkg/types"
"k8s.io/client-go/dynamic"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1"
"github.com/grafana/grafana/apps/alerting/notifications/pkg/apis/resource/timeinterval/v0alpha1/fakes"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/infra/tracing"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/routingtree"
"github.com/grafana/grafana/pkg/registry/apis/alerting/notifications/timeinterval"
"github.com/grafana/grafana/pkg/services/accesscontrol"
"github.com/grafana/grafana/pkg/services/accesscontrol/acimpl"
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
@@ -34,6 +34,7 @@ import (
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/tests/api/alerting"
"github.com/grafana/grafana/pkg/tests/apis"
"github.com/grafana/grafana/pkg/tests/apis/alerting/notifications/common"
"github.com/grafana/grafana/pkg/tests/testinfra"
"github.com/grafana/grafana/pkg/tests/testsuite"
"github.com/grafana/grafana/pkg/util"
@@ -57,7 +58,7 @@ func TestIntegrationResourceIdentifier(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
client := newClient(t, helper.Org1.Admin)
client := common.NewTimeIntervalClient(t, helper.Org1.Admin)
newInterval := &v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
@@ -191,11 +192,11 @@ func TestIntegrationTimeIntervalAccessControl(t *testing.T) {
},
}
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
for _, tc := range testCases {
t.Run(fmt.Sprintf("user '%s'", tc.user.Identity.GetLogin()), func(t *testing.T) {
client := newClient(t, tc.user)
client := common.NewTimeIntervalClient(t, tc.user)
var expected = &v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
Namespace: "default",
@@ -349,7 +350,7 @@ func TestIntegrationTimeIntervalProvisioning(t *testing.T) {
org := helper.Org1
admin := org.Admin
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
env := helper.GetEnv()
ac := acimpl.ProvideAccessControl(env.FeatureToggles)
@@ -401,7 +402,7 @@ func TestIntegrationTimeIntervalOptimisticConcurrency(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
interval := v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
@@ -485,7 +486,7 @@ func TestIntegrationTimeIntervalPatch(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
interval := v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
@@ -550,7 +551,7 @@ func TestIntegrationTimeIntervalListSelector(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
interval1 := &v0alpha1.TimeInterval{
ObjectMeta: v1.ObjectMeta{
@@ -650,8 +651,26 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
var amConfig definitions.PostableUserConfig
require.NoError(t, json.Unmarshal(alertmanagerRaw, &amConfig))
success, err := legacyCli.PostConfiguration(t, amConfig)
require.Truef(t, success, "Failed to post Alertmanager configuration: %s", err)
mtis := []definitions.MuteTimeInterval{}
for _, interval := range amConfig.AlertmanagerConfig.MuteTimeIntervals {
mtis = append(mtis, definitions.MuteTimeInterval{
MuteTimeInterval: interval,
})
}
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
v1intervals, err := timeinterval.ConvertToK8sResources(orgID, mtis, func(int64) string { return "default" }, nil)
require.NoError(t, err)
for _, interval := range v1intervals.Items {
_, err := adminClient.Create(ctx, &interval, v1.CreateOptions{})
require.NoError(t, err)
}
routeClient := common.NewRoutingTreeClient(t, helper.Org1.Admin)
v1route, err := routingtree.ConvertToK8sResource(helper.Org1.Admin.Identity.GetOrgID(), *amConfig.AlertmanagerConfig.Route, "", func(int64) string { return "default" })
require.NoError(t, err)
_, err = routeClient.Update(ctx, v1route, v1.UpdateOptions{})
require.NoError(t, err)
postGroupRaw, err := testData.ReadFile(path.Join("test-data", "rulegroup-1.json"))
require.NoError(t, err)
@@ -667,8 +686,6 @@ func TestIntegrationTimeIntervalReferentialIntegrity(t *testing.T) {
currentRuleGroup, status := legacyCli.GetRulesGroup(t, folderUID, ruleGroup.Name)
require.Equal(t, http.StatusAccepted, status)
adminClient := newClient(t, helper.Org1.Admin)
intervals, err := adminClient.List(ctx, v1.ListOptions{})
require.NoError(t, err)
require.Len(t, intervals.Items, 2)
@@ -769,7 +786,7 @@ func TestIntegrationTimeIntervalValidation(t *testing.T) {
ctx := context.Background()
helper := getTestHelper(t)
adminClient := newClient(t, helper.Org1.Admin)
adminClient := common.NewTimeIntervalClient(t, helper.Org1.Admin)
testCases := []struct {
name string
@@ -809,19 +826,3 @@ func TestIntegrationTimeIntervalValidation(t *testing.T) {
})
}
}
func newClient(t *testing.T, user apis.User) *apis.TypedClient[v0alpha1.TimeInterval, v0alpha1.TimeIntervalList] {
t.Helper()
client, err := dynamic.NewForConfig(user.NewRestConfig())
require.NoError(t, err)
return &apis.TypedClient[v0alpha1.TimeInterval, v0alpha1.TimeIntervalList]{
Client: client.Resource(
schema.GroupVersionResource{
Group: v0alpha1.Kind().Group(),
Version: v0alpha1.Kind().Version(),
Resource: v0alpha1.Kind().Plural(),
}).Namespace("default"),
}
}