mirror of https://github.com/grafana/grafana
prometheushacktoberfestmetricsmonitoringalertinggrafanagoinfluxdbmysqlpostgresanalyticsdata-visualizationdashboardbusiness-intelligenceelasticsearch
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1026 lines
33 KiB
1026 lines
33 KiB
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 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)
|
|
})
|
|
})
|
|
}
|
|
|