mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 00:41:08 +08:00

When using mulit-dimensional Grafana managed alerts (e.g. SSE math) extract refIds values and labels so they can be shown in the notification and dashboards.
1988 lines
55 KiB
Go
1988 lines
55 KiB
Go
package alerting
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io/ioutil"
|
|
"net/http"
|
|
"regexp"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/bus"
|
|
|
|
"github.com/prometheus/common/model"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
ngstore "github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
|
)
|
|
|
|
func TestAMConfigAccess(t *testing.T) {
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
EnableFeatureToggles: []string{"ngalert"},
|
|
DisableAnonymous: true,
|
|
})
|
|
|
|
store := testinfra.SetUpDatabase(t, dir)
|
|
// override bus to get the GetSignedInUserQuery handler
|
|
store.Bus = bus.GetBus()
|
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
|
|
|
// Create a users to make authenticated requests
|
|
require.NoError(t, createUser(t, store, models.ROLE_VIEWER, "viewer", "viewer"))
|
|
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "editor", "editor"))
|
|
require.NoError(t, createUser(t, store, models.ROLE_ADMIN, "admin", "admin"))
|
|
|
|
type testCase struct {
|
|
desc string
|
|
url string
|
|
expStatus int
|
|
expBody string
|
|
}
|
|
|
|
t.Run("when creating alertmanager configuration", func(t *testing.T) {
|
|
body := `
|
|
{
|
|
"alertmanager_config": {
|
|
"route": {
|
|
"receiver": "grafana-default-email"
|
|
},
|
|
"receivers": [{
|
|
"name": "grafana-default-email",
|
|
"grafana_managed_receiver_configs": [{
|
|
"uid": "",
|
|
"name": "email receiver",
|
|
"type": "email",
|
|
"isDefault": true,
|
|
"settings": {
|
|
"addresses": "<example@email.com>"
|
|
}
|
|
}]
|
|
}]
|
|
}
|
|
}
|
|
`
|
|
|
|
testCases := []testCase{
|
|
{
|
|
desc: "un-authenticated request should fail",
|
|
url: "http://%s/api/alertmanager/grafana/config/api/v1/alerts",
|
|
expStatus: http.StatusUnauthorized,
|
|
expBody: `{"message": "Unauthorized"}`,
|
|
},
|
|
{
|
|
desc: "viewer request should fail",
|
|
url: "http://viewer:viewer@%s/api/alertmanager/grafana/config/api/v1/alerts",
|
|
expStatus: http.StatusForbidden,
|
|
expBody: `{"message": "Permission denied"}`,
|
|
},
|
|
{
|
|
desc: "editor request should succeed",
|
|
url: "http://editor:editor@%s/api/alertmanager/grafana/config/api/v1/alerts",
|
|
expStatus: http.StatusAccepted,
|
|
expBody: `{"message":"configuration created"}`,
|
|
},
|
|
{
|
|
desc: "admin request should succeed",
|
|
url: "http://admin:admin@%s/api/alertmanager/grafana/config/api/v1/alerts",
|
|
expStatus: http.StatusAccepted,
|
|
expBody: `{"message":"configuration created"}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
url := fmt.Sprintf(tc.url, grafanaListedAddr)
|
|
buf := bytes.NewReader([]byte(body))
|
|
// nolint:gosec
|
|
resp, err := http.Post(url, "application/json", buf)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, resp.Body.Close())
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expStatus, resp.StatusCode)
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.JSONEq(t, tc.expBody, string(b))
|
|
})
|
|
}
|
|
})
|
|
|
|
t.Run("when creating silence", func(t *testing.T) {
|
|
body := `
|
|
{
|
|
"comment": "string",
|
|
"createdBy": "string",
|
|
"endsAt": "2023-03-31T14:17:04.419Z",
|
|
"matchers": [
|
|
{
|
|
"isRegex": true,
|
|
"name": "string",
|
|
"value": "string"
|
|
}
|
|
],
|
|
"startsAt": "2021-03-31T13:17:04.419Z"
|
|
}
|
|
`
|
|
|
|
testCases := []testCase{
|
|
{
|
|
desc: "un-authenticated request should fail",
|
|
url: "http://%s/api/alertmanager/grafana/config/api/v2/silences",
|
|
expStatus: http.StatusUnauthorized,
|
|
expBody: `{"message": "Unauthorized"}`,
|
|
},
|
|
{
|
|
desc: "viewer request should fail",
|
|
url: "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/silences",
|
|
expStatus: http.StatusForbidden,
|
|
expBody: `{"message": "Permission denied"}`,
|
|
},
|
|
{
|
|
desc: "editor request should succeed",
|
|
url: "http://editor:editor@%s/api/alertmanager/grafana/api/v2/silences",
|
|
expStatus: http.StatusAccepted,
|
|
expBody: `{"id": "0", "message":"silence created"}`,
|
|
},
|
|
{
|
|
desc: "admin request should succeed",
|
|
url: "http://admin:admin@%s/api/alertmanager/grafana/api/v2/silences",
|
|
expStatus: http.StatusAccepted,
|
|
expBody: `{"id": "0", "message":"silence created"}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
url := fmt.Sprintf(tc.url, grafanaListedAddr)
|
|
buf := bytes.NewReader([]byte(body))
|
|
// nolint:gosec
|
|
resp, err := http.Post(url, "application/json", buf)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, resp.Body.Close())
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expStatus, resp.StatusCode)
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
if tc.expStatus == http.StatusAccepted {
|
|
re := regexp.MustCompile(`"id":"([\w|-]+)"`)
|
|
b = re.ReplaceAll(b, []byte(`"id":"0"`))
|
|
}
|
|
require.JSONEq(t, tc.expBody, string(b))
|
|
})
|
|
}
|
|
})
|
|
|
|
var blob []byte
|
|
t.Run("when getting silences", func(t *testing.T) {
|
|
testCases := []testCase{
|
|
{
|
|
desc: "un-authenticated request should fail",
|
|
url: "http://%s/api/alertmanager/grafana/api/v2/silences",
|
|
expStatus: http.StatusUnauthorized,
|
|
expBody: `{"message": "Unauthorized"}`,
|
|
},
|
|
{
|
|
desc: "viewer request should succeed",
|
|
url: "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/silences",
|
|
expStatus: http.StatusOK,
|
|
},
|
|
{
|
|
desc: "editor request should succeed",
|
|
url: "http://editor:editor@%s/api/alertmanager/grafana/api/v2/silences",
|
|
expStatus: http.StatusOK,
|
|
},
|
|
{
|
|
desc: "admin request should succeed",
|
|
url: "http://admin:admin@%s/api/alertmanager/grafana/api/v2/silences",
|
|
expStatus: http.StatusOK,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
url := fmt.Sprintf(tc.url, grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(url)
|
|
t.Cleanup(func() {
|
|
require.NoError(t, resp.Body.Close())
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expStatus, resp.StatusCode)
|
|
require.NoError(t, err)
|
|
if tc.expStatus == http.StatusOK {
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
blob = b
|
|
}
|
|
})
|
|
}
|
|
})
|
|
|
|
var silences apimodels.GettableSilences
|
|
err := json.Unmarshal(blob, &silences)
|
|
require.NoError(t, err)
|
|
assert.Len(t, silences, 2)
|
|
silenceIDs := make([]string, 0, len(silences))
|
|
for _, s := range silences {
|
|
silenceIDs = append(silenceIDs, *s.ID)
|
|
}
|
|
|
|
unconsumedSilenceIdx := 0
|
|
t.Run("when deleting a silence", func(t *testing.T) {
|
|
testCases := []testCase{
|
|
{
|
|
desc: "un-authenticated request should fail",
|
|
url: "http://%s/api/alertmanager/grafana/api/v2/silence/%s",
|
|
expStatus: http.StatusUnauthorized,
|
|
expBody: `{"message": "Unauthorized"}`,
|
|
},
|
|
{
|
|
desc: "viewer request should fail",
|
|
url: "http://viewer:viewer@%s/api/alertmanager/grafana/api/v2/silence/%s",
|
|
expStatus: http.StatusForbidden,
|
|
expBody: `{"message": "Permission denied"}`,
|
|
},
|
|
{
|
|
desc: "editor request should succeed",
|
|
url: "http://editor:editor@%s/api/alertmanager/grafana/api/v2/silence/%s",
|
|
expStatus: http.StatusOK,
|
|
expBody: `{"message": "silence deleted"}`,
|
|
},
|
|
{
|
|
desc: "admin request should succeed",
|
|
url: "http://admin:admin@%s/api/alertmanager/grafana/api/v2/silence/%s",
|
|
expStatus: http.StatusOK,
|
|
expBody: `{"message": "silence deleted"}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
url := fmt.Sprintf(tc.url, grafanaListedAddr, silenceIDs[unconsumedSilenceIdx])
|
|
|
|
// Create client
|
|
client := &http.Client{}
|
|
|
|
// Create request
|
|
req, err := http.NewRequest("DELETE", url, nil)
|
|
if err != nil {
|
|
fmt.Println(err)
|
|
return
|
|
}
|
|
|
|
// Fetch Request
|
|
resp, err := client.Do(req)
|
|
if err != nil {
|
|
return
|
|
}
|
|
t.Cleanup(func() {
|
|
require.NoError(t, resp.Body.Close())
|
|
})
|
|
require.NoError(t, err)
|
|
require.Equal(t, tc.expStatus, resp.StatusCode)
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
if tc.expStatus == http.StatusOK {
|
|
unconsumedSilenceIdx++
|
|
}
|
|
require.JSONEq(t, tc.expBody, string(b))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestAlertAndGroupsQuery(t *testing.T) {
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
EnableFeatureToggles: []string{"ngalert"},
|
|
DisableAnonymous: true,
|
|
})
|
|
|
|
store := testinfra.SetUpDatabase(t, dir)
|
|
// override bus to get the GetSignedInUserQuery handler
|
|
store.Bus = bus.GetBus()
|
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
|
|
|
// unauthenticated request to get the alerts should fail
|
|
{
|
|
alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(alertsURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
require.JSONEq(t, `{"message": "Unauthorized"}`, string(b))
|
|
}
|
|
|
|
// Create a user to make authenticated requests
|
|
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
|
|
|
// invalid credentials request to get the alerts should fail
|
|
{
|
|
alertsURL := fmt.Sprintf("http://grafana:invalid@%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(alertsURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
|
|
require.JSONEq(t, `{"error": "invalid username or password","message": "invalid username or password"}`, string(b))
|
|
}
|
|
|
|
// When there are no alerts available, it returns an empty list.
|
|
{
|
|
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(alertsURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
require.JSONEq(t, "[]", string(b))
|
|
}
|
|
|
|
// When are there no alerts available, it returns an empty list of groups.
|
|
{
|
|
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts/groups", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(alertsURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
require.JSONEq(t, "[]", string(b))
|
|
}
|
|
|
|
// Now, let's test the endpoint with some alerts.
|
|
{
|
|
// Create the namespace we'll save our alerts to.
|
|
_, err := createFolder(t, store, 0, "default")
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
// Create an alert that will fire as quickly as possible
|
|
{
|
|
interval, err := model.ParseDuration("10s")
|
|
require.NoError(t, err)
|
|
rules := apimodels.PostableRuleGroupConfig{
|
|
Name: "arulegroup",
|
|
Interval: interval,
|
|
Rules: []apimodels.PostableExtendedRuleNode{
|
|
{
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiring",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
buf := bytes.Buffer{}
|
|
enc := json.NewEncoder(&buf)
|
|
err = enc.Encode(&rules)
|
|
require.NoError(t, err)
|
|
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Post(u, "application/json", &buf)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, resp.StatusCode, 202)
|
|
}
|
|
|
|
// Eventually, we'll get an alert with its state being active.
|
|
{
|
|
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
|
|
// nolint:gosec
|
|
require.Eventually(t, func() bool {
|
|
resp, err := http.Get(alertsURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
var alerts apimodels.GettableAlerts
|
|
err = json.Unmarshal(b, &alerts)
|
|
require.NoError(t, err)
|
|
|
|
if len(alerts) > 0 {
|
|
status := alerts[0].Status
|
|
return status != nil && status.State != nil && *status.State == "active"
|
|
}
|
|
|
|
return false
|
|
}, 18*time.Second, 2*time.Second)
|
|
}
|
|
}
|
|
|
|
func TestRulerAccess(t *testing.T) {
|
|
// Setup Grafana and its Database
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
EnableFeatureToggles: []string{"ngalert"},
|
|
EnableQuota: true,
|
|
DisableAnonymous: true,
|
|
ViewersCanEdit: true,
|
|
})
|
|
|
|
store := testinfra.SetUpDatabase(t, dir)
|
|
// override bus to get the GetSignedInUserQuery handler
|
|
store.Bus = bus.GetBus()
|
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
|
|
|
// Create the namespace we'll save our alerts to.
|
|
_, err := createFolder(t, store, 0, "default")
|
|
require.NoError(t, err)
|
|
|
|
// Create a users to make authenticated requests
|
|
require.NoError(t, createUser(t, store, models.ROLE_VIEWER, "viewer", "viewer"))
|
|
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "editor", "editor"))
|
|
require.NoError(t, createUser(t, store, models.ROLE_ADMIN, "admin", "admin"))
|
|
|
|
// Now, let's test the access policies.
|
|
testCases := []struct {
|
|
desc string
|
|
url string
|
|
expStatus int
|
|
expectedResponse string
|
|
}{
|
|
{
|
|
desc: "un-authenticated request should fail",
|
|
url: "http://%s/api/ruler/grafana/api/v1/rules/default",
|
|
expStatus: http.StatusUnauthorized,
|
|
expectedResponse: `{"message": "Unauthorized"}`,
|
|
},
|
|
{
|
|
desc: "viewer request should fail",
|
|
url: "http://viewer:viewer@%s/api/ruler/grafana/api/v1/rules/default",
|
|
expStatus: http.StatusForbidden,
|
|
expectedResponse: `{"error":"user does not have permissions to edit the namespace", "message":"user does not have permissions to edit the namespace"}`,
|
|
},
|
|
{
|
|
desc: "editor request should succeed",
|
|
url: "http://editor:editor@%s/api/ruler/grafana/api/v1/rules/default",
|
|
expStatus: http.StatusAccepted,
|
|
expectedResponse: `{"message":"rule group updated successfully"}`,
|
|
},
|
|
{
|
|
desc: "admin request should succeed",
|
|
url: "http://admin:admin@%s/api/ruler/grafana/api/v1/rules/default",
|
|
expStatus: http.StatusAccepted,
|
|
expectedResponse: `{"message":"rule group updated successfully"}`,
|
|
},
|
|
}
|
|
|
|
for i, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
interval, err := model.ParseDuration("1m")
|
|
require.NoError(t, err)
|
|
|
|
rules := apimodels.PostableRuleGroupConfig{
|
|
Name: "arulegroup",
|
|
Rules: []apimodels.PostableExtendedRuleNode{
|
|
{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{"label1": "val1"},
|
|
Annotations: map[string]string{"annotation1": "val1"},
|
|
},
|
|
// this rule does not explicitly set no data and error states
|
|
// therefore it should get the default values
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: fmt.Sprintf("AlwaysFiring %d", i),
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
buf := bytes.Buffer{}
|
|
enc := json.NewEncoder(&buf)
|
|
err = enc.Encode(&rules)
|
|
require.NoError(t, err)
|
|
|
|
u := fmt.Sprintf(tc.url, grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Post(u, "application/json", &buf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tc.expStatus, resp.StatusCode)
|
|
require.JSONEq(t, tc.expectedResponse, string(b))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeleteFolderWithRules(t *testing.T) {
|
|
// Setup Grafana and its Database
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
EnableFeatureToggles: []string{"ngalert"},
|
|
EnableQuota: true,
|
|
DisableAnonymous: true,
|
|
ViewersCanEdit: true,
|
|
})
|
|
|
|
store := testinfra.SetUpDatabase(t, dir)
|
|
// override bus to get the GetSignedInUserQuery handler
|
|
store.Bus = bus.GetBus()
|
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
|
|
|
// Create the namespace we'll save our alerts to.
|
|
namespaceUID, err := createFolder(t, store, 0, "default")
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, createUser(t, store, models.ROLE_VIEWER, "viewer", "viewer"))
|
|
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "editor", "editor"))
|
|
|
|
createRule(t, grafanaListedAddr, "default", "editor", "editor")
|
|
|
|
// First, let's have an editor create a rule within the folder/namespace.
|
|
{
|
|
u := fmt.Sprintf("http://editor:editor@%s/api/ruler/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(u)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 202, resp.StatusCode)
|
|
|
|
re := regexp.MustCompile(`"uid":"([\w|-]+)"`)
|
|
b = re.ReplaceAll(b, []byte(`"uid":""`))
|
|
re = regexp.MustCompile(`"updated":"(\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z)"`)
|
|
b = re.ReplaceAll(b, []byte(`"updated":"2021-05-19T19:47:55Z"`))
|
|
|
|
expectedGetRulesResponseBody := fmt.Sprintf(`{
|
|
"default": [
|
|
{
|
|
"name": "arulegroup",
|
|
"interval": "1m",
|
|
"rules": [
|
|
{
|
|
"expr": "",
|
|
"for": "2m",
|
|
"labels": {
|
|
"label1": "val1"
|
|
},
|
|
"annotations": {
|
|
"annotation1": "val1"
|
|
},
|
|
"grafana_alert": {
|
|
"id": 1,
|
|
"orgId": 1,
|
|
"title": "rule under folder default",
|
|
"condition": "A",
|
|
"data": [
|
|
{
|
|
"refId": "A",
|
|
"queryType": "",
|
|
"relativeTimeRange": {
|
|
"from": 18000,
|
|
"to": 10800
|
|
},
|
|
"datasourceUid": "-100",
|
|
"model": {
|
|
"expression": "2 + 3 > 1",
|
|
"intervalMs": 1000,
|
|
"maxDataPoints": 43200,
|
|
"type": "math"
|
|
}
|
|
}
|
|
],
|
|
"updated": "2021-05-19T19:47:55Z",
|
|
"intervalSeconds": 60,
|
|
"version": 1,
|
|
"uid": "",
|
|
"namespace_uid": %q,
|
|
"namespace_id": 1,
|
|
"rule_group": "arulegroup",
|
|
"no_data_state": "NoData",
|
|
"exec_err_state": "Alerting"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`, namespaceUID)
|
|
assert.JSONEq(t, expectedGetRulesResponseBody, string(b))
|
|
}
|
|
|
|
// Next, the editor can delete the folder.
|
|
{
|
|
u := fmt.Sprintf("http://editor:editor@%s/api/folders/%s", grafanaListedAddr, namespaceUID)
|
|
req, err := http.NewRequest(http.MethodDelete, u, nil)
|
|
require.NoError(t, err)
|
|
client := &http.Client{}
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
require.JSONEq(t, `{"id":1,"message":"Folder default deleted","title":"default"}`, string(b))
|
|
}
|
|
|
|
// Finally, we ensure the rules were deleted.
|
|
{
|
|
u := fmt.Sprintf("http://editor:editor@%s/api/ruler/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(u)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 202, resp.StatusCode)
|
|
assert.JSONEq(t, "{}", string(b))
|
|
}
|
|
}
|
|
|
|
func TestAlertRuleCRUD(t *testing.T) {
|
|
// Setup Grafana and its Database
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
EnableFeatureToggles: []string{"ngalert"},
|
|
EnableQuota: true,
|
|
DisableAnonymous: true,
|
|
})
|
|
|
|
store := testinfra.SetUpDatabase(t, dir)
|
|
// override bus to get the GetSignedInUserQuery handler
|
|
store.Bus = bus.GetBus()
|
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
|
|
|
err := createUser(t, store, models.ROLE_EDITOR, "grafana", "password")
|
|
|
|
require.NoError(t, err)
|
|
|
|
// Create the namespace we'll save our alerts to.
|
|
_, err = createFolder(t, store, 0, "default")
|
|
require.NoError(t, err)
|
|
|
|
interval, err := model.ParseDuration("1m")
|
|
require.NoError(t, err)
|
|
|
|
invalidInterval, err := model.ParseDuration("1s")
|
|
require.NoError(t, err)
|
|
|
|
// Now, let's try to create some invalid alert rules.
|
|
{
|
|
testCases := []struct {
|
|
desc string
|
|
rulegroup string
|
|
interval model.Duration
|
|
rule apimodels.PostableExtendedRuleNode
|
|
expectedResponse string
|
|
}{
|
|
{
|
|
desc: "alert rule without queries and expressions",
|
|
rulegroup: "arulegroup",
|
|
rule: apimodels.PostableExtendedRuleNode{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{"label1": "val1"},
|
|
Annotations: map[string]string{"annotation1": "val1"},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiring",
|
|
Data: []ngmodels.AlertQuery{},
|
|
},
|
|
},
|
|
expectedResponse: `{"error":"invalid alert rule: no queries or expressions are found", "message":"failed to update rule group"}`,
|
|
},
|
|
{
|
|
desc: "alert rule with empty title",
|
|
rulegroup: "arulegroup",
|
|
rule: apimodels.PostableExtendedRuleNode{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{"label1": "val1"},
|
|
Annotations: map[string]string{"annotation1": "val1"},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedResponse: `{"error":"invalid alert rule: title is empty", "message":"failed to update rule group"}`,
|
|
},
|
|
{
|
|
desc: "alert rule with too long name",
|
|
rulegroup: "arulegroup",
|
|
rule: apimodels.PostableExtendedRuleNode{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{"label1": "val1"},
|
|
Annotations: map[string]string{"annotation1": "val1"},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: getLongString(t, ngstore.AlertRuleMaxTitleLength+1),
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedResponse: `{"error":"invalid alert rule: name length should not be greater than 190", "message":"failed to update rule group"}`,
|
|
},
|
|
{
|
|
desc: "alert rule with too long rulegroup",
|
|
rulegroup: getLongString(t, ngstore.AlertRuleMaxTitleLength+1),
|
|
rule: apimodels.PostableExtendedRuleNode{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{"label1": "val1"},
|
|
Annotations: map[string]string{"annotation1": "val1"},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiring",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedResponse: `{"error":"invalid alert rule: rule group name length should not be greater than 190", "message":"failed to update rule group"}`,
|
|
},
|
|
{
|
|
desc: "alert rule with invalid interval",
|
|
rulegroup: "arulegroup",
|
|
interval: invalidInterval,
|
|
rule: apimodels.PostableExtendedRuleNode{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{"label1": "val1"},
|
|
Annotations: map[string]string{"annotation1": "val1"},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiring",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedResponse: `{"error":"invalid alert rule: interval (1s) should be non-zero and divided exactly by scheduler interval: 10s", "message":"failed to update rule group"}`,
|
|
},
|
|
{
|
|
desc: "alert rule with unknown datasource",
|
|
rulegroup: "arulegroup",
|
|
rule: apimodels.PostableExtendedRuleNode{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{"label1": "val1"},
|
|
Annotations: map[string]string{"annotation1": "val1"},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiring",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "unknown",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedResponse: `{"error":"invalid query A: data source not found: unknown", "message":"failed to validate alert rule AlwaysFiring"}`,
|
|
},
|
|
{
|
|
desc: "alert rule with invalid condition",
|
|
rulegroup: "arulegroup",
|
|
rule: apimodels.PostableExtendedRuleNode{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{"label1": "val1"},
|
|
Annotations: map[string]string{"annotation1": "val1"},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiring",
|
|
Condition: "B",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
expectedResponse: `{"error":"condition B not found in any query or expression: it should be one of: [A]", "message":"failed to validate alert rule AlwaysFiring"}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
rules := apimodels.PostableRuleGroupConfig{
|
|
Name: tc.rulegroup,
|
|
Interval: tc.interval,
|
|
Rules: []apimodels.PostableExtendedRuleNode{
|
|
tc.rule,
|
|
},
|
|
}
|
|
buf := bytes.Buffer{}
|
|
enc := json.NewEncoder(&buf)
|
|
err := enc.Encode(&rules)
|
|
require.NoError(t, err)
|
|
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Post(u, "application/json", &buf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, resp.StatusCode, http.StatusBadRequest)
|
|
require.JSONEq(t, tc.expectedResponse, string(b))
|
|
})
|
|
}
|
|
}
|
|
|
|
var ruleUID string
|
|
var expectedGetNamespaceResponseBody string
|
|
// Now, let's create two alerts.
|
|
{
|
|
rules := apimodels.PostableRuleGroupConfig{
|
|
Name: "arulegroup",
|
|
Rules: []apimodels.PostableExtendedRuleNode{
|
|
{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{"label1": "val1"},
|
|
Annotations: map[string]string{"annotation1": "val1"},
|
|
},
|
|
// this rule does not explicitly set no data and error states
|
|
// therefore it should get the default values
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiring",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiringButSilenced",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
|
|
ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
buf := bytes.Buffer{}
|
|
enc := json.NewEncoder(&buf)
|
|
err := enc.Encode(&rules)
|
|
require.NoError(t, err)
|
|
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Post(u, "application/json", &buf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, resp.StatusCode, 202)
|
|
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
|
|
}
|
|
|
|
// With the rules created, let's make sure that rule definition is stored correctly.
|
|
{
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(u)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, resp.StatusCode, 202)
|
|
|
|
body, m := rulesNamespaceWithoutVariableValues(t, b)
|
|
generatedUIDs, ok := m["default,arulegroup"]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, 2, len(generatedUIDs))
|
|
// assert that generated UIDs are unique
|
|
assert.NotEqual(t, generatedUIDs[0], generatedUIDs[1])
|
|
// copy result to a variable with a wider scope
|
|
// to be used by the next test
|
|
ruleUID = generatedUIDs[0]
|
|
expectedGetNamespaceResponseBody = `
|
|
{
|
|
"default":[
|
|
{
|
|
"name":"arulegroup",
|
|
"interval":"1m",
|
|
"rules":[
|
|
{
|
|
"annotations": {
|
|
"annotation1": "val1"
|
|
},
|
|
"expr":"",
|
|
"for": "1m",
|
|
"labels": {
|
|
"label1": "val1"
|
|
},
|
|
"grafana_alert":{
|
|
"id":1,
|
|
"orgId":1,
|
|
"title":"AlwaysFiring",
|
|
"condition":"A",
|
|
"data":[
|
|
{
|
|
"refId":"A",
|
|
"queryType":"",
|
|
"relativeTimeRange":{
|
|
"from":18000,
|
|
"to":10800
|
|
},
|
|
"datasourceUid":"-100",
|
|
"model":{
|
|
"expression":"2 + 3 \u003e 1",
|
|
"intervalMs":1000,
|
|
"maxDataPoints":43200,
|
|
"type":"math"
|
|
}
|
|
}
|
|
],
|
|
"updated":"2021-02-21T01:10:30Z",
|
|
"intervalSeconds":60,
|
|
"version":1,
|
|
"uid":"uid",
|
|
"namespace_uid":"nsuid",
|
|
"namespace_id":1,
|
|
"rule_group":"arulegroup",
|
|
"no_data_state":"NoData",
|
|
"exec_err_state":"Alerting"
|
|
}
|
|
},
|
|
{
|
|
"expr":"",
|
|
"grafana_alert":{
|
|
"id":2,
|
|
"orgId":1,
|
|
"title":"AlwaysFiringButSilenced",
|
|
"condition":"A",
|
|
"data":[
|
|
{
|
|
"refId":"A",
|
|
"queryType":"",
|
|
"relativeTimeRange":{
|
|
"from":18000,
|
|
"to":10800
|
|
},
|
|
"datasourceUid":"-100",
|
|
"model":{
|
|
"expression":"2 + 3 \u003e 1",
|
|
"intervalMs":1000,
|
|
"maxDataPoints":43200,
|
|
"type":"math"
|
|
}
|
|
}
|
|
],
|
|
"updated":"2021-02-21T01:10:30Z",
|
|
"intervalSeconds":60,
|
|
"version":1,
|
|
"uid":"uid",
|
|
"namespace_uid":"nsuid",
|
|
"namespace_id":1,
|
|
"rule_group":"arulegroup",
|
|
"no_data_state":"Alerting",
|
|
"exec_err_state":"Alerting"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`
|
|
assert.JSONEq(t, expectedGetNamespaceResponseBody, body)
|
|
}
|
|
|
|
// try to update by pass an invalid UID
|
|
{
|
|
interval, err := model.ParseDuration("30s")
|
|
require.NoError(t, err)
|
|
|
|
rules := apimodels.PostableRuleGroupConfig{
|
|
Name: "arulegroup",
|
|
Rules: []apimodels.PostableExtendedRuleNode{
|
|
{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{
|
|
"label1": "val42",
|
|
"foo": "bar",
|
|
},
|
|
Annotations: map[string]string{
|
|
"annotation1": "val42",
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
UID: "unknown",
|
|
Title: "AlwaysNormal",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 < 1"
|
|
}`),
|
|
},
|
|
},
|
|
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
|
|
ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
buf := bytes.Buffer{}
|
|
enc := json.NewEncoder(&buf)
|
|
err = enc.Encode(&rules)
|
|
require.NoError(t, err)
|
|
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Post(u, "application/json", &buf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
require.JSONEq(t, `{"error":"failed to get alert rule unknown: could not find alert rule", "message": "failed to update rule group"}`, string(b))
|
|
|
|
// let's make sure that rule definitions are not affected by the failed POST request.
|
|
u = fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err = http.Get(u)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err = ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, resp.StatusCode, 202)
|
|
|
|
body, m := rulesNamespaceWithoutVariableValues(t, b)
|
|
returnedUIDs, ok := m["default,arulegroup"]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, 2, len(returnedUIDs))
|
|
assert.JSONEq(t, expectedGetNamespaceResponseBody, body)
|
|
}
|
|
|
|
// update the first rule and completely remove the other
|
|
{
|
|
interval, err := model.ParseDuration("30s")
|
|
require.NoError(t, err)
|
|
|
|
rules := apimodels.PostableRuleGroupConfig{
|
|
Name: "arulegroup",
|
|
Rules: []apimodels.PostableExtendedRuleNode{
|
|
{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: interval,
|
|
Labels: map[string]string{
|
|
"label1": "val42",
|
|
"foo": "bar",
|
|
},
|
|
Annotations: map[string]string{
|
|
"annotation1": "val42",
|
|
"foo": "bar",
|
|
},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
UID: ruleUID, // Including the UID in the payload makes the endpoint update the existing rule.
|
|
Title: "AlwaysNormal",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 < 1"
|
|
}`),
|
|
},
|
|
},
|
|
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
|
|
ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState),
|
|
},
|
|
},
|
|
},
|
|
}
|
|
buf := bytes.Buffer{}
|
|
enc := json.NewEncoder(&buf)
|
|
err = enc.Encode(&rules)
|
|
require.NoError(t, err)
|
|
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Post(u, "application/json", &buf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, resp.StatusCode, 202)
|
|
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
|
|
|
|
// let's make sure that rule definitions are updated correctly.
|
|
u = fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err = http.Get(u)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err = ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, resp.StatusCode, 202)
|
|
|
|
body, m := rulesNamespaceWithoutVariableValues(t, b)
|
|
returnedUIDs, ok := m["default,arulegroup"]
|
|
assert.True(t, ok)
|
|
assert.Equal(t, 1, len(returnedUIDs))
|
|
assert.Equal(t, ruleUID, returnedUIDs[0])
|
|
assert.JSONEq(t, `
|
|
{
|
|
"default":[
|
|
{
|
|
"name":"arulegroup",
|
|
"interval":"1m",
|
|
"rules":[
|
|
{
|
|
"annotations": {
|
|
"annotation1": "val42",
|
|
"foo": "bar"
|
|
},
|
|
"expr":"",
|
|
"for": "30s",
|
|
"labels": {
|
|
"foo": "bar",
|
|
"label1": "val42"
|
|
},
|
|
"grafana_alert":{
|
|
"id":1,
|
|
"orgId":1,
|
|
"title":"AlwaysNormal",
|
|
"condition":"A",
|
|
"data":[
|
|
{
|
|
"refId":"A",
|
|
"queryType":"",
|
|
"relativeTimeRange":{
|
|
"from":18000,
|
|
"to":10800
|
|
},
|
|
"datasourceUid":"-100",
|
|
"model":{
|
|
"expression":"2 + 3 \u003C 1",
|
|
"intervalMs":1000,
|
|
"maxDataPoints":43200,
|
|
"type":"math"
|
|
}
|
|
}
|
|
],
|
|
"updated":"2021-02-21T01:10:30Z",
|
|
"intervalSeconds":60,
|
|
"version":2,
|
|
"uid":"uid",
|
|
"namespace_uid":"nsuid",
|
|
"namespace_id":1,
|
|
"rule_group":"arulegroup",
|
|
"no_data_state":"Alerting",
|
|
"exec_err_state":"Alerting"
|
|
}
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}`, body)
|
|
}
|
|
|
|
client := &http.Client{}
|
|
// Finally, make sure we can delete it.
|
|
{
|
|
t.Run("fail if he rule group name does not exists", func(t *testing.T) {
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default/groupnotexist", grafanaListedAddr)
|
|
req, err := http.NewRequest(http.MethodDelete, u, nil)
|
|
require.NoError(t, err)
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
|
require.JSONEq(t, `{"error":"rule group not found under this namespace", "message": "failed to delete rule group"}`, string(b))
|
|
})
|
|
|
|
t.Run("succeed if the rule group name does exist", func(t *testing.T) {
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default/arulegroup", grafanaListedAddr)
|
|
req, err := http.NewRequest(http.MethodDelete, u, nil)
|
|
require.NoError(t, err)
|
|
resp, err := client.Do(req)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, http.StatusAccepted, resp.StatusCode)
|
|
require.JSONEq(t, `{"message":"rule group deleted"}`, string(b))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestQuota(t *testing.T) {
|
|
// Setup Grafana and its Database
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
EnableFeatureToggles: []string{"ngalert"},
|
|
EnableQuota: true,
|
|
DisableAnonymous: true,
|
|
})
|
|
|
|
store := testinfra.SetUpDatabase(t, dir)
|
|
// override bus to get the GetSignedInUserQuery handler
|
|
store.Bus = bus.GetBus()
|
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
|
|
|
// Create the namespace we'll save our alerts to.
|
|
_, err := createFolder(t, store, 0, "default")
|
|
require.NoError(t, err)
|
|
|
|
// Create a user to make authenticated requests
|
|
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
|
|
|
interval, err := model.ParseDuration("1m")
|
|
require.NoError(t, err)
|
|
|
|
// check quota limits
|
|
t.Run("when quota limit exceed", func(t *testing.T) {
|
|
// get existing org quota
|
|
query := models.GetOrgQuotaByTargetQuery{OrgId: 1, Target: "alert_rule"}
|
|
err = sqlstore.GetOrgQuotaByTarget(&query)
|
|
require.NoError(t, err)
|
|
used := query.Result.Used
|
|
limit := query.Result.Limit
|
|
|
|
// set org quota limit to equal used
|
|
orgCmd := models.UpdateOrgQuotaCmd{
|
|
OrgId: 1,
|
|
Target: "alert_rule",
|
|
Limit: used,
|
|
}
|
|
err := sqlstore.UpdateOrgQuota(&orgCmd)
|
|
require.NoError(t, err)
|
|
|
|
t.Cleanup(func() {
|
|
// reset org quota to original value
|
|
orgCmd := models.UpdateOrgQuotaCmd{
|
|
OrgId: 1,
|
|
Target: "alert_rule",
|
|
Limit: limit,
|
|
}
|
|
err := sqlstore.UpdateOrgQuota(&orgCmd)
|
|
require.NoError(t, err)
|
|
})
|
|
|
|
// try to create an alert rule
|
|
rules := apimodels.PostableRuleGroupConfig{
|
|
Name: "arulegroup",
|
|
Interval: interval,
|
|
Rules: []apimodels.PostableExtendedRuleNode{
|
|
{
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "One more alert rule",
|
|
Condition: "A",
|
|
Data: []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{
|
|
From: ngmodels.Duration(time.Duration(5) * time.Hour),
|
|
To: ngmodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
Model: json.RawMessage(`{
|
|
"datasourceUid": "-100",
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
buf := bytes.Buffer{}
|
|
enc := json.NewEncoder(&buf)
|
|
err = enc.Encode(&rules)
|
|
require.NoError(t, err)
|
|
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Post(u, "application/json", &buf)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, http.StatusForbidden, resp.StatusCode)
|
|
require.JSONEq(t, `{"message":"quota reached"}`, string(b))
|
|
})
|
|
}
|
|
|
|
func TestEval(t *testing.T) {
|
|
// Setup Grafana and its Database
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
EnableFeatureToggles: []string{"ngalert"},
|
|
EnableQuota: true,
|
|
DisableAnonymous: true,
|
|
})
|
|
|
|
store := testinfra.SetUpDatabase(t, dir)
|
|
// override bus to get the GetSignedInUserQuery handler
|
|
store.Bus = bus.GetBus()
|
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
|
|
|
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
|
|
|
|
// Create the namespace we'll save our alerts to.
|
|
_, err := createFolder(t, store, 0, "default")
|
|
require.NoError(t, err)
|
|
|
|
// test eval conditions
|
|
testCases := []struct {
|
|
desc string
|
|
payload string
|
|
expectedStatusCode int
|
|
expectedResponse string
|
|
}{
|
|
{
|
|
desc: "alerting condition",
|
|
payload: `
|
|
{
|
|
"grafana_condition": {
|
|
"condition": "A",
|
|
"data": [
|
|
{
|
|
"refId": "A",
|
|
"relativeTimeRange": {
|
|
"from": 18000,
|
|
"to": 10800
|
|
},
|
|
"datasourceUid":"-100",
|
|
"model": {
|
|
"type":"math",
|
|
"expression":"1 < 2"
|
|
}
|
|
}
|
|
],
|
|
"now": "2021-04-11T14:38:14Z"
|
|
}
|
|
}
|
|
`,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedResponse: `{
|
|
"instances": [
|
|
{
|
|
"schema": {
|
|
"name": "evaluation results",
|
|
"fields": [
|
|
{
|
|
"name": "State",
|
|
"type": "string",
|
|
"typeInfo": {
|
|
"frame": "string"
|
|
}
|
|
},
|
|
{
|
|
"name": "Info",
|
|
"type": "string",
|
|
"typeInfo": {
|
|
"frame": "string"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"data": {
|
|
"values": [
|
|
[
|
|
"Alerting"
|
|
],
|
|
[
|
|
"[ var='A' labels={} value=1 ]"
|
|
]
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}`,
|
|
},
|
|
{
|
|
desc: "normal condition",
|
|
payload: `
|
|
{
|
|
"grafana_condition": {
|
|
"condition": "A",
|
|
"data": [
|
|
{
|
|
"refId": "A",
|
|
"relativeTimeRange": {
|
|
"from": 18000,
|
|
"to": 10800
|
|
},
|
|
"datasourceUid": "-100",
|
|
"model": {
|
|
"type":"math",
|
|
"expression":"1 > 2"
|
|
}
|
|
}
|
|
],
|
|
"now": "2021-04-11T14:38:14Z"
|
|
}
|
|
}
|
|
`,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedResponse: `{
|
|
"instances": [
|
|
{
|
|
"schema": {
|
|
"name": "evaluation results",
|
|
"fields": [
|
|
{
|
|
"name": "State",
|
|
"type": "string",
|
|
"typeInfo": {
|
|
"frame": "string"
|
|
}
|
|
},
|
|
{
|
|
"name": "Info",
|
|
"type": "string",
|
|
"typeInfo": {
|
|
"frame": "string"
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"data": {
|
|
"values": [
|
|
[
|
|
"Normal"
|
|
],
|
|
[
|
|
"[ var='A' labels={} value=0 ]"
|
|
]
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}`,
|
|
},
|
|
{
|
|
desc: "condition not found in any query or expression",
|
|
payload: `
|
|
{
|
|
"grafana_condition": {
|
|
"condition": "B",
|
|
"data": [
|
|
{
|
|
"refId": "A",
|
|
"relativeTimeRange": {
|
|
"from": 18000,
|
|
"to": 10800
|
|
},
|
|
"datasourceUid": "-100",
|
|
"model": {
|
|
"type":"math",
|
|
"expression":"1 > 2"
|
|
}
|
|
}
|
|
],
|
|
"now": "2021-04-11T14:38:14Z"
|
|
}
|
|
}
|
|
`,
|
|
expectedStatusCode: http.StatusBadRequest,
|
|
expectedResponse: `{"error":"condition B not found in any query or expression: it should be one of: [A]","message":"invalid condition"}`,
|
|
},
|
|
{
|
|
desc: "unknown query datasource",
|
|
payload: `
|
|
{
|
|
"grafana_condition": {
|
|
"condition": "A",
|
|
"data": [
|
|
{
|
|
"refId": "A",
|
|
"relativeTimeRange": {
|
|
"from": 18000,
|
|
"to": 10800
|
|
},
|
|
"datasourceUid": "unknown",
|
|
"model": {
|
|
}
|
|
}
|
|
],
|
|
"now": "2021-04-11T14:38:14Z"
|
|
}
|
|
}
|
|
`,
|
|
expectedStatusCode: http.StatusBadRequest,
|
|
expectedResponse: `{"error":"invalid query A: data source not found: unknown","message":"invalid condition"}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/v1/rule/test/grafana", grafanaListedAddr)
|
|
r := strings.NewReader(tc.payload)
|
|
// nolint:gosec
|
|
resp, err := http.Post(u, "application/json", r)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
|
|
require.JSONEq(t, tc.expectedResponse, string(b))
|
|
})
|
|
}
|
|
|
|
// test eval queries and expressions
|
|
testCases = []struct {
|
|
desc string
|
|
payload string
|
|
expectedStatusCode int
|
|
expectedResponse string
|
|
}{
|
|
{
|
|
desc: "alerting condition",
|
|
payload: `
|
|
{
|
|
"data": [
|
|
{
|
|
"refId": "A",
|
|
"relativeTimeRange": {
|
|
"from": 18000,
|
|
"to": 10800
|
|
},
|
|
"datasourceUid": "-100",
|
|
"model": {
|
|
"type":"math",
|
|
"expression":"1 < 2"
|
|
}
|
|
}
|
|
],
|
|
"now": "2021-04-11T14:38:14Z"
|
|
}
|
|
`,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedResponse: `{
|
|
"results": {
|
|
"A": {
|
|
"frames": [
|
|
{
|
|
"schema": {
|
|
"refId": "A",
|
|
"fields": [
|
|
{
|
|
"name": "A",
|
|
"type": "number",
|
|
"typeInfo": {
|
|
"frame": "float64",
|
|
"nullable": true
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"data": {
|
|
"values": [
|
|
[
|
|
1
|
|
]
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
desc: "normal condition",
|
|
payload: `
|
|
{
|
|
"data": [
|
|
{
|
|
"refId": "A",
|
|
"relativeTimeRange": {
|
|
"from": 18000,
|
|
"to": 10800
|
|
},
|
|
"datasourceUid": "-100",
|
|
"model": {
|
|
"type":"math",
|
|
"expression":"1 > 2"
|
|
}
|
|
}
|
|
],
|
|
"now": "2021-04-11T14:38:14Z"
|
|
}
|
|
`,
|
|
expectedStatusCode: http.StatusOK,
|
|
expectedResponse: `{
|
|
"results": {
|
|
"A": {
|
|
"frames": [
|
|
{
|
|
"schema": {
|
|
"refId": "A",
|
|
"fields": [
|
|
{
|
|
"name": "A",
|
|
"type": "number",
|
|
"typeInfo": {
|
|
"frame": "float64",
|
|
"nullable": true
|
|
}
|
|
}
|
|
]
|
|
},
|
|
"data": {
|
|
"values": [
|
|
[
|
|
0
|
|
]
|
|
]
|
|
}
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`,
|
|
},
|
|
{
|
|
desc: "unknown query datasource",
|
|
payload: `
|
|
{
|
|
"data": [
|
|
{
|
|
"refId": "A",
|
|
"relativeTimeRange": {
|
|
"from": 18000,
|
|
"to": 10800
|
|
},
|
|
"datasourceUid": "unknown",
|
|
"model": {
|
|
}
|
|
}
|
|
],
|
|
"now": "2021-04-11T14:38:14Z"
|
|
}
|
|
`,
|
|
expectedStatusCode: http.StatusBadRequest,
|
|
expectedResponse: `{"error":"invalid query A: data source not found: unknown","message":"invalid queries or expressions"}`,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.desc, func(t *testing.T) {
|
|
u := fmt.Sprintf("http://grafana:password@%s/api/v1/eval", grafanaListedAddr)
|
|
r := strings.NewReader(tc.payload)
|
|
// nolint:gosec
|
|
resp, err := http.Post(u, "application/json", r)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := ioutil.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, tc.expectedStatusCode, resp.StatusCode)
|
|
require.JSONEq(t, tc.expectedResponse, string(b))
|
|
})
|
|
}
|
|
}
|
|
|
|
// createFolder creates a folder for storing our alerts under. Grafana uses folders as a replacement for alert namespaces to match its permission model.
|
|
// We use the dashboard command using IsFolder = true to tell it's a folder, it takes the dashboard as the name of the folder.
|
|
func createFolder(t *testing.T, store *sqlstore.SQLStore, folderID int64, folderName string) (string, error) {
|
|
t.Helper()
|
|
|
|
cmd := models.SaveDashboardCommand{
|
|
OrgId: 1, // default organisation
|
|
FolderId: folderID,
|
|
IsFolder: true,
|
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
|
"title": folderName,
|
|
}),
|
|
}
|
|
f, err := store.SaveDashboard(cmd)
|
|
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return f.Uid, nil
|
|
}
|
|
|
|
// rulesNamespaceWithoutVariableValues takes a apimodels.NamespaceConfigResponse JSON-based input and makes the dynamic fields static e.g. uid, dates, etc.
|
|
// it returns a map of the modified rule UIDs with the namespace,rule_group as a key
|
|
func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[string][]string) {
|
|
t.Helper()
|
|
|
|
var r apimodels.NamespaceConfigResponse
|
|
require.NoError(t, json.Unmarshal(b, &r))
|
|
// create a map holding the created rule UIDs per namespace/group
|
|
m := make(map[string][]string)
|
|
for namespace, nodes := range r {
|
|
for _, node := range nodes {
|
|
compositeKey := strings.Join([]string{namespace, node.Name}, ",")
|
|
_, ok := m[compositeKey]
|
|
if !ok {
|
|
m[compositeKey] = make([]string, 0, len(node.Rules))
|
|
}
|
|
for _, rule := range node.Rules {
|
|
m[compositeKey] = append(m[compositeKey], rule.GrafanaManagedAlert.UID)
|
|
rule.GrafanaManagedAlert.UID = "uid"
|
|
rule.GrafanaManagedAlert.NamespaceUID = "nsuid"
|
|
rule.GrafanaManagedAlert.Updated = time.Date(2021, time.Month(2), 21, 1, 10, 30, 0, time.UTC)
|
|
}
|
|
}
|
|
}
|
|
|
|
json, err := json.Marshal(&r)
|
|
require.NoError(t, err)
|
|
return string(json), m
|
|
}
|
|
|
|
func createUser(t *testing.T, store *sqlstore.SQLStore, role models.RoleType, username, password string) error {
|
|
t.Helper()
|
|
|
|
cmd := models.CreateUserCommand{
|
|
Login: username,
|
|
Password: password,
|
|
DefaultOrgRole: string(role),
|
|
}
|
|
_, err := store.CreateUser(context.Background(), cmd)
|
|
return err
|
|
}
|
|
|
|
func getLongString(t *testing.T, n int) string {
|
|
t.Helper()
|
|
|
|
b := make([]rune, n)
|
|
for i := range b {
|
|
b[i] = 'a'
|
|
}
|
|
return string(b)
|
|
}
|