Files
grafana/pkg/tests/api/alerting/api_provisioning_test.go
Moustafa Baiou fb0d6be79e Alerting: Allow specifying uid for new rules added to groups (#99858)
When modifying rule groups the `uid` can be specified but only if the rule already existed in the DB. If the rule is new the update would be rejected.

This updates the RuleGroup provisioning apis to allow specifying the `uid` when creating/updating rule groups.

Additionally, the RuleGroupIdx was not being updated when rules were reordered in the group.

Context: https://github.com/grafana/terraform-provider-grafana/pull/1971#issuecomment-2599223897
Relates to: https://github.com/grafana/terraform-provider-grafana/issues/1928

Fixes: #98283
(cherry picked from commit 7dee4d1808)
2025-02-12 09:10:39 -05:00

1051 lines
33 KiB
Go

package alerting
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"path"
"path/filepath"
"slices"
"sort"
"strings"
"testing"
"time"
"github.com/prometheus/alertmanager/config"
"github.com/prometheus/alertmanager/timeinterval"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v3"
"github.com/grafana/grafana/pkg/apimachinery/errutil"
"github.com/grafana/grafana/pkg/expr"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/org"
"github.com/grafana/grafana/pkg/services/user"
"github.com/grafana/grafana/pkg/tests/testinfra"
)
func createRuleWithNotificationSettings(t *testing.T, client apiClient, folder string, nfSettings *definitions.AlertRuleNotificationSettings) (definitions.PostableRuleGroupConfig, string) {
t.Helper()
interval, err := model.ParseDuration("1m")
require.NoError(t, err)
doubleInterval := 2 * interval
rules := definitions.PostableRuleGroupConfig{
Name: "arulegroup",
Interval: interval,
Rules: []definitions.PostableExtendedRuleNode{
{
ApiRuleNode: &definitions.ApiRuleNode{
For: &doubleInterval,
Labels: map[string]string{"label1": "val1"},
Annotations: map[string]string{"annotation1": "val1"},
},
GrafanaManagedAlert: &definitions.PostableGrafanaRule{
Title: fmt.Sprintf("rule under folder %s", folder),
Condition: "A",
Data: []definitions.AlertQuery{
{
RefID: "A",
RelativeTimeRange: definitions.RelativeTimeRange{
From: definitions.Duration(time.Duration(5) * time.Hour),
To: definitions.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage(`{
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
NotificationSettings: nfSettings,
},
},
},
}
resp, status, _ := client.PostRulesGroupWithStatus(t, folder, &rules)
assert.Equal(t, http.StatusAccepted, status)
require.Len(t, resp.Created, 1)
return rules, resp.Created[0]
}
func TestIntegrationProvisioning(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create a users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "viewer",
Login: "viewer",
})
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor",
Login: "editor",
})
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor")
// Create the namespace we'll save our alerts to.
namespaceUID := "default"
apiClient.CreateFolder(t, namespaceUID, namespaceUID)
t.Run("when provisioning notification policies", func(t *testing.T) {
url := fmt.Sprintf("http://%s/api/v1/provisioning/policies", grafanaListedAddr)
body := `
{
"receiver": "test-receiver",
"group_by": [
"..."
],
"routes": []
}`
// As we check if the receiver exists that is referenced in the policy,
// we first need to create it, so the tests passes correctly.
urlReceiver := fmt.Sprintf("http://%s/api/v1/provisioning/contact-points", grafanaListedAddr)
bodyReceiver := `
{
"name": "test-receiver",
"type": "slack",
"settings": {
"recipient": "value_recipient",
"token": "value_token"
}
}`
req := createTestRequest("POST", urlReceiver, "admin", bodyReceiver)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 202, resp.StatusCode)
t.Run("un-authenticated GET should 401", func(t *testing.T) {
req := createTestRequest("GET", url, "", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 401, resp.StatusCode)
})
t.Run("viewer GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "viewer", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("editor GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "editor", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("admin GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "admin", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("un-authenticated PUT should 401", func(t *testing.T) {
req := createTestRequest("PUT", url, "", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 401, resp.StatusCode)
})
t.Run("viewer PUT should 403", func(t *testing.T) {
req := createTestRequest("PUT", url, "viewer", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 403, resp.StatusCode)
})
t.Run("editor PUT should succeed", func(t *testing.T) {
req := createTestRequest("PUT", url, "editor", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 202, resp.StatusCode)
})
t.Run("admin PUT should succeed", func(t *testing.T) {
req := createTestRequest("PUT", url, "admin", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 202, resp.StatusCode)
})
})
t.Run("when provisioning contactpoints", func(t *testing.T) {
url := fmt.Sprintf("http://%s/api/v1/provisioning/contact-points", grafanaListedAddr)
body := `
{
"name": "my-contact-point",
"type": "slack",
"settings": {
"recipient": "value_recipient",
"token": "value_token"
}
}`
t.Run("un-authenticated GET should 401", func(t *testing.T) {
req := createTestRequest("GET", url, "", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 401, resp.StatusCode)
})
t.Run("viewer GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "viewer", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("editor GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "editor", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("admin GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "admin", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("un-authenticated POST should 401", func(t *testing.T) {
req := createTestRequest("POST", url, "", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 401, resp.StatusCode)
})
t.Run("viewer POST should 403", func(t *testing.T) {
req := createTestRequest("POST", url, "viewer", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 403, resp.StatusCode)
})
t.Run("editor POST should succeed", func(t *testing.T) {
req := createTestRequest("POST", url, "editor", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 202, resp.StatusCode)
})
t.Run("admin POST should succeed", func(t *testing.T) {
req := createTestRequest("POST", url, "admin", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 202, resp.StatusCode)
})
createContactPoint := func(t *testing.T, name string) definitions.EmbeddedContactPoint {
cpBody := fmt.Sprintf(`
{
"name": "%s",
"type": "slack",
"settings": {
"recipient": "value_recipient",
"token": "value_token"
}
}`, name)
req := createTestRequest("POST", url, "admin", cpBody)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.Equal(t, 202, resp.StatusCode)
ecp := definitions.EmbeddedContactPoint{}
require.NoError(t, json.NewDecoder(resp.Body).Decode(&ecp))
require.NoError(t, resp.Body.Close())
return ecp
}
createPolicyForContactPoint := func(t *testing.T, receiver string) {
url := fmt.Sprintf("http://%s/api/v1/provisioning/policies", grafanaListedAddr)
body := fmt.Sprintf(`
{
"receiver": "%s",
"group_by": [
"..."
],
"routes": []
}`, receiver)
req := createTestRequest("PUT", url, "admin", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 202, resp.StatusCode)
}
t.Run("viewer DELETE should 403", func(t *testing.T) {
ecp := createContactPoint(t, "my-contact-point")
deleteURL := fmt.Sprintf("http://%s/api/v1/provisioning/contact-points/%s", grafanaListedAddr, ecp.UID)
req := createTestRequest("DELETE", deleteURL, "viewer", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 403, resp.StatusCode)
})
t.Run("admin DELETE should succeed", func(t *testing.T) {
ecp := createContactPoint(t, "my-contact-point")
deleteURL := fmt.Sprintf("http://%s/api/v1/provisioning/contact-points/%s", grafanaListedAddr, ecp.UID)
req := createTestRequest("DELETE", deleteURL, "admin", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 202, resp.StatusCode)
})
t.Run("admin DELETE should 409 when contact point used by notification policy", func(t *testing.T) {
ecp := createContactPoint(t, "my-cp-used-by-policy")
createPolicyForContactPoint(t, "my-cp-used-by-policy")
deleteURL := fmt.Sprintf("http://%s/api/v1/provisioning/contact-points/%s", grafanaListedAddr, ecp.UID)
deleteReq := createTestRequest("DELETE", deleteURL, "admin", "")
resp, err := http.DefaultClient.Do(deleteReq)
require.NoError(t, err)
require.Equal(t, 409, resp.StatusCode)
var validationError errutil.PublicError
assert.NoError(t, json.NewDecoder(resp.Body).Decode(&validationError))
require.NoError(t, resp.Body.Close())
assert.NotEmpty(t, validationError, validationError.Message)
assert.Equal(t, "alerting.notifications.contact-points.referenced", validationError.MessageID)
})
t.Run("admin DELETE should 409 when contact point used by rule", func(t *testing.T) {
ecp := createContactPoint(t, "my-cp-used-by-rule")
nfSettings := &definitions.AlertRuleNotificationSettings{
Receiver: "my-cp-used-by-rule",
}
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
createRuleWithNotificationSettings(t, apiClient, namespaceUID, nfSettings)
deleteURL := fmt.Sprintf("http://%s/api/v1/provisioning/contact-points/%s", grafanaListedAddr, ecp.UID)
deleteReq := createTestRequest("DELETE", deleteURL, "admin", "")
resp, err := http.DefaultClient.Do(deleteReq)
require.NoError(t, err)
require.Equal(t, 409, resp.StatusCode)
var validationError errutil.PublicError
assert.NoError(t, json.NewDecoder(resp.Body).Decode(&validationError))
require.NoError(t, resp.Body.Close())
assert.NotEmpty(t, validationError, validationError.Message)
assert.Equal(t, "alerting.notifications.contact-points.used-by-rule", validationError.MessageID)
})
})
t.Run("when provisioning templates", func(t *testing.T) {
url := fmt.Sprintf("http://%s/api/v1/provisioning/templates", grafanaListedAddr)
t.Run("un-authenticated GET should 401", func(t *testing.T) {
req := createTestRequest("GET", url, "", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 401, resp.StatusCode)
})
t.Run("viewer GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "viewer", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("editor GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "editor", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("admin GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "admin", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
})
t.Run("when provisioning mute timings", func(t *testing.T) {
url := fmt.Sprintf("http://%s/api/v1/provisioning/mute-timings", grafanaListedAddr)
t.Run("un-authenticated GET should 401", func(t *testing.T) {
req := createTestRequest("GET", url, "", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 401, resp.StatusCode)
})
t.Run("viewer GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "viewer", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("editor GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "editor", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
t.Run("admin GET should succeed", func(t *testing.T) {
req := createTestRequest("GET", url, "admin", "")
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 200, resp.StatusCode)
})
})
t.Run("when provisioning alert rules", func(t *testing.T) {
url := fmt.Sprintf("http://%s/api/v1/provisioning/alert-rules", grafanaListedAddr)
body := `
{
"orgID":1,
"folderUID":"default",
"ruleGroup":"Test Group",
"title":"Provisioned",
"condition":"A",
"data":[{
"refId":"A",
"queryType":"",
"relativeTimeRange":{"from":600,"to":0},
"datasourceUid":"f558c85f-66ad-4fd1-b31d-7979e6c93db4",
"model":{
"editorMode":"code",
"exemplar":false,
"expr":"sum(rate(low_card[5m])) \u003e 0",
"format":"time_series",
"instant":true,
"intervalMs":1000,
"legendFormat":"__auto",
"maxDataPoints":43200,
"range":false,"refId":"A"
}
}],
"noDataState":"NoData",
"execErrState":"Error",
"for":"0s"
}`
req := createTestRequest("POST", url, "admin", body)
resp, err := http.DefaultClient.Do(req)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, 201, resp.StatusCode)
// We want to check the provenances of both provisioned and non-provisioned rules
createRule(t, apiClient, namespaceUID)
req = createTestRequest("GET", url, "admin", "")
resp, err = http.DefaultClient.Do(req)
require.NoError(t, err)
var rules definitions.ProvisionedAlertRules
require.NoError(t, json.NewDecoder(resp.Body).Decode(&rules))
require.NoError(t, resp.Body.Close())
require.Len(t, rules, 2)
sort.Slice(rules, func(i, j int) bool {
return rules[i].ID < rules[j].ID
})
require.Equal(t, definitions.Provenance("api"), rules[0].Provenance)
require.Equal(t, definitions.Provenance(""), rules[1].Provenance)
})
}
func TestIntegrationProvisioningRules(t *testing.T) {
testinfra.SQLiteIntegrationTest(t)
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
// Create a users to make authenticated requests
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleViewer),
Password: "viewer",
Login: "viewer",
})
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleEditor),
Password: "editor",
Login: "editor",
})
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "editor", "editor")
// Create the namespace we'll save our alerts to.
namespaceUID := "default"
apiClient.CreateFolder(t, namespaceUID, namespaceUID)
t.Run("when provisioning alert rules", func(t *testing.T) {
originalRuleGroup := definitions.AlertRuleGroup{
Title: "TestGroup",
Interval: 60,
FolderUID: "default",
Rules: []definitions.ProvisionedAlertRule{
{
UID: "rule1",
Title: "Rule1",
OrgID: 1,
RuleGroup: "TestGroup",
Condition: "A",
NoDataState: definitions.Alerting,
ExecErrState: definitions.AlertingErrState,
For: model.Duration(time.Duration(60) * time.Second),
Data: []definitions.AlertQuery{
{
RefID: "A",
RelativeTimeRange: definitions.RelativeTimeRange{
From: definitions.Duration(time.Duration(5) * time.Hour),
To: definitions.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)),
},
},
},
{
UID: "rule2",
Title: "Rule2",
OrgID: 1,
RuleGroup: "TestGroup",
Condition: "A",
NoDataState: definitions.Alerting,
ExecErrState: definitions.AlertingErrState,
For: model.Duration(time.Duration(60) * time.Second),
Data: []definitions.AlertQuery{
{
RefID: "A",
RelativeTimeRange: definitions.RelativeTimeRange{
From: definitions.Duration(time.Duration(5) * time.Hour),
To: definitions.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)),
},
},
},
{
UID: "rule3",
Title: "Rule3",
OrgID: 1,
RuleGroup: "TestGroup",
Condition: "A",
NoDataState: definitions.Alerting,
ExecErrState: definitions.AlertingErrState,
For: model.Duration(time.Duration(60) * time.Second),
Data: []definitions.AlertQuery{
{
RefID: "A",
RelativeTimeRange: definitions.RelativeTimeRange{
From: definitions.Duration(time.Duration(5) * time.Hour),
To: definitions.Duration(time.Duration(3) * time.Hour),
},
DatasourceUID: expr.DatasourceUID,
Model: json.RawMessage([]byte(`{"type":"math","expression":"2 + 3 \u003e 1"}`)),
},
},
},
},
}
result, status, raw := apiClient.CreateOrUpdateRuleGroupProvisioning(t, originalRuleGroup)
t.Run("should create a new rule group with UIDs specified", func(t *testing.T) {
requireStatusCode(t, http.StatusOK, status, raw)
require.Equal(t, originalRuleGroup, result)
})
t.Run("should remove a rule when updating group with a rule removed", func(t *testing.T) {
existingRuleGroup, status, raw := apiClient.GetRuleGroupProvisioning(t, "default", "TestGroup")
requireStatusCode(t, http.StatusOK, status, raw)
require.Len(t, existingRuleGroup.Rules, 3)
updatedRuleGroup := existingRuleGroup
updatedRuleGroup.Rules = updatedRuleGroup.Rules[:2]
result, status, raw := apiClient.CreateOrUpdateRuleGroupProvisioning(t, updatedRuleGroup)
requireStatusCode(t, http.StatusOK, status, raw)
require.Equal(t, updatedRuleGroup, result)
// Check that the rule was removed
rules, status, raw := apiClient.GetRuleGroupProvisioning(t, existingRuleGroup.FolderUID, existingRuleGroup.Title)
requireStatusCode(t, http.StatusOK, status, raw)
require.Len(t, rules.Rules, 2)
})
t.Run("should recreate a rule when updating group with the rule added back", func(t *testing.T) {
result, status, raw := apiClient.CreateOrUpdateRuleGroupProvisioning(t, originalRuleGroup)
requireStatusCode(t, http.StatusOK, status, raw)
require.Equal(t, originalRuleGroup, result)
require.Len(t, result.Rules, 3)
// Check that the rule was re-added
rules, status, raw := apiClient.GetRuleGroupProvisioning(t, originalRuleGroup.FolderUID, originalRuleGroup.Title)
requireStatusCode(t, http.StatusOK, status, raw)
require.Len(t, rules.Rules, 3)
})
})
}
func TestMuteTimings(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: 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",
})
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
t.Run("default config should return empty list", func(t *testing.T) {
mt, status, body := apiClient.GetAllMuteTimingsWithStatus(t)
requireStatusCode(t, http.StatusOK, status, body)
require.Empty(t, mt)
})
emptyMuteTiming := definitions.MuteTimeInterval{
MuteTimeInterval: config.MuteTimeInterval{
Name: "Empty Mute Timing",
TimeIntervals: []timeinterval.TimeInterval{},
},
}
t.Run("should create a new mute timing without any intervals", func(t *testing.T) {
mt, status, body := apiClient.CreateMuteTimingWithStatus(t, emptyMuteTiming)
requireStatusCode(t, http.StatusCreated, status, body)
require.Equal(t, emptyMuteTiming.MuteTimeInterval, mt.MuteTimeInterval)
require.EqualValues(t, models.ProvenanceAPI, mt.Provenance)
})
anotherMuteTiming := definitions.MuteTimeInterval{
MuteTimeInterval: config.MuteTimeInterval{
Name: "Not Empty Mute Timing",
TimeIntervals: []timeinterval.TimeInterval{
{
Times: []timeinterval.TimeRange{
{
StartMinute: 10,
EndMinute: 45,
},
},
Weekdays: []timeinterval.WeekdayRange{
{
InclusiveRange: timeinterval.InclusiveRange{
Begin: 0,
End: 2,
},
},
{
InclusiveRange: timeinterval.InclusiveRange{
Begin: 4,
End: 5,
},
},
},
},
},
},
}
t.Run("should create a new mute timing with some settings", func(t *testing.T) {
mt, status, body := apiClient.CreateMuteTimingWithStatus(t, anotherMuteTiming)
requireStatusCode(t, http.StatusCreated, status, body)
require.Equal(t, anotherMuteTiming.MuteTimeInterval, mt.MuteTimeInterval)
require.EqualValues(t, models.ProvenanceAPI, mt.Provenance)
})
t.Run("should return mute timing by name", func(t *testing.T) {
mt, status, body := apiClient.GetMuteTimingByNameWithStatus(t, emptyMuteTiming.Name)
requireStatusCode(t, http.StatusOK, status, body)
require.Equal(t, emptyMuteTiming.MuteTimeInterval, mt.MuteTimeInterval)
require.EqualValues(t, models.ProvenanceAPI, mt.Provenance)
mt, status, body = apiClient.GetMuteTimingByNameWithStatus(t, anotherMuteTiming.Name)
requireStatusCode(t, http.StatusOK, status, body)
require.Equal(t, anotherMuteTiming.MuteTimeInterval, mt.MuteTimeInterval)
require.EqualValues(t, models.ProvenanceAPI, mt.Provenance)
})
t.Run("should return NotFound if mute timing does not exist", func(t *testing.T) {
_, status, body := apiClient.GetMuteTimingByNameWithStatus(t, "some-missing-timing")
requireStatusCode(t, http.StatusNotFound, status, body)
})
t.Run("should return all mute timings", func(t *testing.T) {
mt, status, body := apiClient.GetAllMuteTimingsWithStatus(t)
requireStatusCode(t, http.StatusOK, status, body)
require.Len(t, mt, 2)
slices.SortFunc(mt, func(a, b definitions.MuteTimeInterval) int {
return strings.Compare(a.Name, b.Name)
})
require.Equal(t, emptyMuteTiming.MuteTimeInterval, mt[0].MuteTimeInterval)
require.EqualValues(t, models.ProvenanceAPI, mt[0].Provenance)
require.Equal(t, anotherMuteTiming.MuteTimeInterval, mt[1].MuteTimeInterval)
require.EqualValues(t, models.ProvenanceAPI, mt[1].Provenance)
})
t.Run("should get BadRequest if creates a new mute timing with the same name", func(t *testing.T) {
m := anotherMuteTiming
m.TimeIntervals = nil
_, status, body := apiClient.CreateMuteTimingWithStatus(t, m)
t.Log(body)
requireStatusCode(t, http.StatusBadRequest, status, body)
var validationError errutil.PublicError
assert.NoError(t, json.Unmarshal([]byte(body), &validationError))
assert.NotEmpty(t, validationError, validationError.Message)
assert.Equal(t, "alerting.notifications.time-intervals.nameExists", validationError.MessageID)
if t.Failed() {
t.Fatalf("response: %s", body)
}
})
t.Run("should get BadRequest if creates an invalid mute timing", func(t *testing.T) {
m := definitions.MuteTimeInterval{
MuteTimeInterval: config.MuteTimeInterval{
Name: "Invalid",
TimeIntervals: []timeinterval.TimeInterval{
{
Times: []timeinterval.TimeRange{
{
StartMinute: 20000,
EndMinute: 90000,
},
},
},
},
},
}
_, status, body := apiClient.CreateMuteTimingWithStatus(t, m)
t.Log(body)
requireStatusCode(t, http.StatusBadRequest, status, body)
var validationError map[string]any
assert.NoError(t, json.Unmarshal([]byte(body), &validationError))
assert.Contains(t, validationError, "message")
if t.Failed() {
t.Fatalf("response: %s", body)
}
})
t.Run("should fail to update mute timing if version does not match", func(t *testing.T) {
tm := anotherMuteTiming
tm.Version = "wrong-version"
tm.TimeIntervals = []timeinterval.TimeInterval{
{
Times: []timeinterval.TimeRange{
{
StartMinute: 36,
EndMinute: 49,
},
},
},
}
_, status, body := apiClient.UpdateMuteTimingWithStatus(t, tm)
requireStatusCode(t, http.StatusConflict, status, body)
var validationError errutil.PublicError
assert.NoError(t, json.Unmarshal([]byte(body), &validationError))
assert.NotEmpty(t, validationError, validationError.Message)
assert.Equal(t, "alerting.notifications.conflict", validationError.MessageID)
if t.Failed() {
t.Fatalf("response: %s", body)
}
})
t.Run("should update existing mute timing", func(t *testing.T) {
mt, _, _ := apiClient.GetMuteTimingByNameWithStatus(t, anotherMuteTiming.Name)
anotherMuteTiming.TimeIntervals = []timeinterval.TimeInterval{
{
Times: []timeinterval.TimeRange{
{
StartMinute: 36,
EndMinute: 49,
},
},
},
}
anotherMuteTiming.Version = mt.Version
mt, status, body := apiClient.UpdateMuteTimingWithStatus(t, anotherMuteTiming)
requireStatusCode(t, http.StatusAccepted, status, body)
require.Equal(t, anotherMuteTiming.MuteTimeInterval, mt.MuteTimeInterval)
})
t.Run("should fail to update existing mute timing with invalid one", func(t *testing.T) {
mt := anotherMuteTiming
mt.TimeIntervals = []timeinterval.TimeInterval{
{
Times: []timeinterval.TimeRange{
{
StartMinute: 360000,
EndMinute: 490000,
},
},
},
}
_, status, body := apiClient.UpdateMuteTimingWithStatus(t, mt)
requireStatusCode(t, http.StatusBadRequest, status, body)
var validationError map[string]any
assert.NoError(t, json.Unmarshal([]byte(body), &validationError))
assert.Contains(t, validationError, "message")
if t.Failed() {
t.Fatalf("response: %s", body)
}
})
t.Run("should get NotFound if updates mute timing that does not exist", func(t *testing.T) {
mt := definitions.MuteTimeInterval{
MuteTimeInterval: config.MuteTimeInterval{
Name: "Missing Mute Timing",
},
}
_, status, body := apiClient.UpdateMuteTimingWithStatus(t, mt)
requireStatusCode(t, http.StatusNotFound, status, body)
})
t.Run("should delete unused mute timing", func(t *testing.T) {
status, body := apiClient.DeleteMuteTimingWithStatus(t, emptyMuteTiming.Name)
requireStatusCode(t, http.StatusNoContent, status, body)
_, status, body = apiClient.GetMuteTimingByNameWithStatus(t, emptyMuteTiming.Name)
requireStatusCode(t, http.StatusNotFound, status, body)
})
t.Run("should get 409 Conflict if deletes used mute-timing", func(t *testing.T) {
route, status, response := apiClient.GetRouteWithStatus(t)
requireStatusCode(t, http.StatusOK, status, response)
route.Routes = append(route.Routes, &definitions.Route{
Receiver: route.Receiver,
ObjectMatchers: definitions.ObjectMatchers{
{
Name: "a",
Value: "b",
},
},
MuteTimeIntervals: []string{anotherMuteTiming.Name},
})
status, response = apiClient.UpdateRouteWithStatus(t, route, false)
requireStatusCode(t, http.StatusAccepted, status, response)
status, response = apiClient.DeleteMuteTimingWithStatus(t, anotherMuteTiming.Name)
requireStatusCode(t, http.StatusConflict, status, response)
var validationError errutil.PublicError
assert.NoError(t, json.Unmarshal([]byte(response), &validationError))
assert.NotEmpty(t, validationError, validationError.Message)
assert.Equal(t, "alerting.notifications.time-intervals.used", validationError.MessageID)
if t.Failed() {
t.Fatalf("response: %s", response)
}
})
}
func createTestRequest(method string, url string, user string, body string) *http.Request {
var bodyBuf io.Reader
if body != "" {
bodyBuf = bytes.NewReader([]byte(body))
}
req, _ := http.NewRequest(method, url, bodyBuf)
if bodyBuf != nil {
req.Header.Set("Content-Type", "application/json")
}
if user != "" {
req.SetBasicAuth(user, user)
}
return req
}
func TestIntegrationExportFileProvision(t *testing.T) {
dir, p := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
DisableLegacyAlerting: true,
EnableUnifiedAlerting: true,
DisableAnonymous: true,
AppModeProduction: true,
})
provisioningDir := filepath.Join(dir, "conf", "provisioning")
alertingDir := filepath.Join(provisioningDir, "alerting")
err := os.MkdirAll(alertingDir, 0750)
require.NoError(t, err)
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, p)
apiClient := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
DefaultOrgRole: string(org.RoleAdmin),
Password: "admin",
Login: "admin",
IsAdmin: true,
})
apiClient.ReloadCachedPermissions(t)
t.Run("when provisioning alert rules from files", func(t *testing.T) {
// add file provisioned alert rules
fileProvisionedAlertRules, err := testData.ReadFile(path.Join("test-data", "provisioning-rules.yaml"))
require.NoError(t, err)
var expected definitions.AlertingFileExport
require.NoError(t, yaml.Unmarshal(fileProvisionedAlertRules, &expected))
expectedYaml, err := yaml.Marshal(expected)
require.NoError(t, err)
// create folder
folderUID := "my_first_folder_uid"
apiClient.CreateFolder(t, folderUID, "my_first_folder_with_$escaped_symbols")
err = os.WriteFile(filepath.Join(alertingDir, "provisioning-rules.yaml"), fileProvisionedAlertRules, 0750)
require.NoError(t, err)
apiClient.ReloadAlertingFileProvisioning(t)
data, status, _ := apiClient.GetAllRulesWithStatus(t)
require.Equal(t, http.StatusOK, status)
require.Greater(t, len(data), 0)
t.Run("exported alert rules should escape $ characters", func(t *testing.T) {
// call export endpoint
status, exportRaw := apiClient.ExportRulesWithStatus(t, &definitions.AlertRulesExportParameters{
ExportQueryParams: definitions.ExportQueryParams{Format: "yaml"},
FolderUID: []string{folderUID},
GroupName: "my_rule_group",
})
require.Equal(t, http.StatusOK, status)
var export definitions.AlertingFileExport
require.NoError(t, yaml.Unmarshal([]byte(exportRaw), &export))
// verify the file exported matches the file provisioned thing
require.Len(t, export.Groups, 1)
require.YAMLEq(t, string(expectedYaml), exportRaw)
})
})
}