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
1197 lines
38 KiB
Go
1197 lines
38 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)
|
|
})
|
|
})
|
|
t.Run("when provisioning mute times from files", func(t *testing.T) {
|
|
// add file provisioned mute times
|
|
fileProvisionedMuteTimings, err := testData.ReadFile(path.Join("test-data", "provisioning-mute-times.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
var expected definitions.AlertingFileExport
|
|
require.NoError(t, yaml.Unmarshal(fileProvisionedMuteTimings, &expected))
|
|
expected.MuteTimings[0].OrgID = 1 // HACK to deal with weird goyaml behavior
|
|
expectedYamlRaw, err := yaml.Marshal(expected)
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(filepath.Join(alertingDir, "provisioning-mute-times.yaml"), fileProvisionedMuteTimings, 0750)
|
|
require.NoError(t, err)
|
|
|
|
apiClient.ReloadAlertingFileProvisioning(t)
|
|
|
|
t.Run("exported mute times shouldn't escape $ characters", func(t *testing.T) {
|
|
// call export endpoint
|
|
exportRaw := apiClient.ExportMuteTiming(t, "$mute_time_a", "yaml")
|
|
var export definitions.AlertingFileExport
|
|
require.NoError(t, yaml.Unmarshal([]byte(exportRaw), &export))
|
|
expectedYaml := string(expectedYamlRaw)
|
|
// verify the file exported matches the file provisioned thing
|
|
require.Len(t, export.MuteTimings, 1)
|
|
require.YAMLEq(t, expectedYaml, exportRaw)
|
|
})
|
|
t.Run("reloading provisioning should not fail", func(t *testing.T) {
|
|
apiClient.ReloadAlertingFileProvisioning(t)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestIntegrationExportFileProvisionMixed(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 mixed set of alerting configurations from files", func(t *testing.T) {
|
|
// add file provisioned mixed set of alerting configurations
|
|
fileProvisionedResources, err := testData.ReadFile(path.Join("test-data", "provisioning-mixed-set.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
var expected definitions.AlertingFileExport
|
|
require.NoError(t, yaml.Unmarshal(fileProvisionedResources, &expected))
|
|
expected.MuteTimings[0].OrgID = 1 // HACK to deal with weird goyaml behavior
|
|
|
|
err = os.WriteFile(filepath.Join(alertingDir, "provisioning-mixed-set.yaml"), fileProvisionedResources, 0750)
|
|
require.NoError(t, err)
|
|
|
|
apiClient.ReloadAlertingFileProvisioning(t)
|
|
|
|
t.Run("exported notification policy matches imported", func(t *testing.T) {
|
|
notificationPolicyExpected := expected
|
|
notificationPolicyExpected.MuteTimings = nil
|
|
notificationPolicyExpected.ContactPoints = nil
|
|
notificationPolicyExpected.Groups = nil
|
|
serializedExpected, err := yaml.Marshal(notificationPolicyExpected)
|
|
require.NoError(t, err)
|
|
|
|
actual := apiClient.ExportNotificationPolicy(t, "yaml")
|
|
|
|
require.YAMLEq(t, string(serializedExpected), actual)
|
|
})
|
|
})
|
|
}
|
|
|
|
func TestIntegrationExportFileProvisionContactPoints(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 contact points from files", func(t *testing.T) {
|
|
// add file provisioned contact points
|
|
fileProvisionedContactPoints, err := testData.ReadFile(path.Join("test-data", "provisioning-contact-points.yaml"))
|
|
require.NoError(t, err)
|
|
|
|
var expected definitions.AlertingFileExport
|
|
require.NoError(t, yaml.Unmarshal(fileProvisionedContactPoints, &expected))
|
|
expectedYaml, err := yaml.Marshal(expected)
|
|
require.NoError(t, err)
|
|
|
|
err = os.WriteFile(filepath.Join(alertingDir, "provisioning-contact-points.yaml"), fileProvisionedContactPoints, 0750)
|
|
require.NoError(t, err)
|
|
|
|
apiClient.ReloadAlertingFileProvisioning(t)
|
|
|
|
t.Run("exported contact points should escape $ characters", func(t *testing.T) {
|
|
// call export endpoint
|
|
exportRaw := apiClient.ExportReceiver(t, "cp_1_$escaped", "yaml", true)
|
|
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.ContactPoints, 1)
|
|
require.YAMLEq(t, string(expectedYaml), exportRaw)
|
|
})
|
|
t.Run("reloading provisioning should not change things", func(t *testing.T) {
|
|
apiClient.ReloadAlertingFileProvisioning(t)
|
|
|
|
exportRaw := apiClient.ExportReceiver(t, "cp_1_$escaped", "yaml", true)
|
|
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.ContactPoints, 1)
|
|
require.YAMLEq(t, string(expectedYaml), exportRaw)
|
|
})
|
|
})
|
|
}
|