mirror of
https://github.com/grafana/grafana.git
synced 2025-02-14 17:43:35 -06:00
This commit adds support for limits and filters to the Prometheus Rules API. Limits: It adds a number of limits to the Grafana flavour of the Prometheus Rules API: - `limit` limits the maximum number of Rule Groups returned - `limit_rules` limits the maximum number of rules per Rule Group - `limit_alerts` limits the maximum number of alerts per rule It sorts Rule Groups and rules within Rule Groups such that data in the response is stable across requests. It also returns summaries (totals) for all Rule Groups, individual Rule Groups and rules. Filters: Alerts can be filtered by state with the `state` query string. An example of an HTTP request asking for just firing alerts might be `/api/prometheus/grafana/api/v1/rules?state=alerting`. A request can filter by two or more states by adding additional `state` query strings to the URL. For example `?state=alerting&state=normal`. Like the alert list panel, the `firing`, `pending` and `normal` state are first compared against the state of each alert rule. All other states are ignored. If the alert rule matches then its alert instances are filtered against states once more. Alerts can also be filtered by labels using the `matcher` query string. Like `state`, multiple matchers can be provided by adding additional `matcher` query strings to the URL. The match expression should be parsed using existing regular expression and sent to the API as URL-encoded JSON in the format: { "name": "test", "value": "value1", "isRegex": false, "isEqual": true } The `isRegex` and `isEqual` options work as follows: | IsEqual | IsRegex | Operator | | ------- | -------- | -------- | | true | false | = | | true | true | =~ | | false | true | !~ | | false | false | != |
806 lines
24 KiB
Go
806 lines
24 KiB
Go
package alerting
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"sort"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/expr"
|
|
"github.com/prometheus/common/model"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol/resourcepermissions"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
ngmodels "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 TestIntegrationPrometheusRules(t *testing.T) {
|
|
testinfra.SQLiteIntegrationTest(t)
|
|
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
DisableLegacyAlerting: true,
|
|
EnableUnifiedAlerting: true,
|
|
DisableAnonymous: true,
|
|
AppModeProduction: true,
|
|
})
|
|
|
|
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
|
|
|
|
// Create a user to make authenticated requests
|
|
createUser(t, store, user.CreateUserCommand{
|
|
DefaultOrgRole: string(org.RoleEditor),
|
|
Password: "password",
|
|
Login: "grafana",
|
|
})
|
|
|
|
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
|
|
|
|
// Create the namespace we'll save our alerts to.
|
|
apiClient.CreateFolder(t, "default", "default")
|
|
|
|
interval, err := model.ParseDuration("10s")
|
|
require.NoError(t, err)
|
|
|
|
// an unauthenticated request to get rules should fail
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 401, resp.StatusCode)
|
|
}
|
|
|
|
// When we have no alerting rules, it returns an empty list.
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 200, resp.StatusCode)
|
|
require.JSONEq(t, `{"status": "success", "data": {"groups": []}}`, string(b))
|
|
}
|
|
|
|
// Now, let's create some rules
|
|
{
|
|
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: []apimodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: apimodels.RelativeTimeRange{
|
|
From: apimodels.Duration(time.Duration(5) * time.Hour),
|
|
To: apimodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: expr.DatasourceUID,
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiringButSilenced",
|
|
Condition: "A",
|
|
Data: []apimodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: apimodels.RelativeTimeRange{
|
|
From: apimodels.Duration(time.Duration(5) * time.Hour),
|
|
To: apimodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: expr.DatasourceUID,
|
|
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 := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, resp.StatusCode, 202)
|
|
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
|
|
}
|
|
|
|
// Check that we cannot create a rule that has a panel_id and no dashboard_uid
|
|
{
|
|
rules := apimodels.PostableRuleGroupConfig{
|
|
Name: "anotherrulegroup",
|
|
Rules: []apimodels.PostableExtendedRuleNode{
|
|
{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: &interval,
|
|
Labels: map[string]string{},
|
|
Annotations: map[string]string{"__panelId__": "1"},
|
|
},
|
|
// this rule does not explicitly set no data and error states
|
|
// therefore it should get the default values
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "NeverCreated",
|
|
Condition: "A",
|
|
Data: []apimodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: apimodels.RelativeTimeRange{
|
|
From: apimodels.Duration(time.Duration(5) * time.Hour),
|
|
To: apimodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: expr.DatasourceUID,
|
|
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)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, 400, resp.StatusCode)
|
|
var res map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(b, &res))
|
|
require.Equal(t, "invalid rule specification at index [0]: both annotations __dashboardUid__ and __panelId__ must be specified", res["message"])
|
|
}
|
|
|
|
// Now, let's see how this looks like.
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
require.JSONEq(t, `
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": [{
|
|
"name": "arulegroup",
|
|
"file": "default",
|
|
"rules": [{
|
|
"state": "inactive",
|
|
"name": "AlwaysFiring",
|
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
|
"duration": 10,
|
|
"annotations": {
|
|
"annotation1": "val1"
|
|
},
|
|
"labels": {
|
|
"label1": "val1"
|
|
},
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}, {
|
|
"state": "inactive",
|
|
"name": "AlwaysFiringButSilenced",
|
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}],
|
|
"totals": {
|
|
"inactive": 2
|
|
},
|
|
"interval": 60,
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}],
|
|
"totals": {
|
|
"inactive": 2
|
|
}
|
|
}
|
|
}`, string(b))
|
|
}
|
|
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
require.Eventually(t, func() bool {
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
require.JSONEq(t, `
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": [{
|
|
"name": "arulegroup",
|
|
"file": "default",
|
|
"rules": [{
|
|
"state": "inactive",
|
|
"name": "AlwaysFiring",
|
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
|
"duration": 10,
|
|
"annotations": {
|
|
"annotation1": "val1"
|
|
},
|
|
"labels": {
|
|
"label1": "val1"
|
|
},
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}, {
|
|
"state": "inactive",
|
|
"name": "AlwaysFiringButSilenced",
|
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}],
|
|
"totals": {
|
|
"inactive": 2
|
|
},
|
|
"interval": 60,
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}],
|
|
"totals": {
|
|
"inactive": 2
|
|
}
|
|
}
|
|
}`, string(b))
|
|
return true
|
|
}, 18*time.Second, 2*time.Second)
|
|
}
|
|
}
|
|
|
|
func TestIntegrationPrometheusRulesFilterByDashboard(t *testing.T) {
|
|
testinfra.SQLiteIntegrationTest(t)
|
|
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
EnableFeatureToggles: []string{"ngalert"},
|
|
DisableAnonymous: true,
|
|
AppModeProduction: true,
|
|
})
|
|
|
|
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
|
|
|
|
// Create a user to make authenticated requests
|
|
createUser(t, store, user.CreateUserCommand{
|
|
DefaultOrgRole: string(org.RoleEditor),
|
|
Password: "password",
|
|
Login: "grafana",
|
|
})
|
|
|
|
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
|
|
// Create the namespace we'll save our alerts to.
|
|
dashboardUID := "default"
|
|
apiClient.CreateFolder(t, dashboardUID, dashboardUID)
|
|
|
|
interval, err := model.ParseDuration("10s")
|
|
require.NoError(t, err)
|
|
|
|
// Now, let's create some rules
|
|
{
|
|
rules := apimodels.PostableRuleGroupConfig{
|
|
Name: "anotherrulegroup",
|
|
Rules: []apimodels.PostableExtendedRuleNode{
|
|
{
|
|
ApiRuleNode: &apimodels.ApiRuleNode{
|
|
For: &interval,
|
|
Labels: map[string]string{},
|
|
Annotations: map[string]string{
|
|
"__dashboardUid__": dashboardUID,
|
|
"__panelId__": "1",
|
|
},
|
|
},
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiring",
|
|
Condition: "A",
|
|
Data: []apimodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: apimodels.RelativeTimeRange{
|
|
From: apimodels.Duration(time.Duration(5) * time.Hour),
|
|
To: apimodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: expr.DatasourceUID,
|
|
Model: json.RawMessage(`{
|
|
"type": "math",
|
|
"expression": "2 + 3 > 1"
|
|
}`),
|
|
},
|
|
},
|
|
},
|
|
},
|
|
{
|
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
|
Title: "AlwaysFiringButSilenced",
|
|
Condition: "A",
|
|
Data: []apimodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
RelativeTimeRange: apimodels.RelativeTimeRange{
|
|
From: apimodels.Duration(time.Duration(5) * time.Hour),
|
|
To: apimodels.Duration(time.Duration(3) * time.Hour),
|
|
},
|
|
DatasourceUID: expr.DatasourceUID,
|
|
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 := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, resp.StatusCode, 202)
|
|
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
|
|
}
|
|
|
|
expectedAllJSON := fmt.Sprintf(`
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": [{
|
|
"name": "anotherrulegroup",
|
|
"file": "default",
|
|
"rules": [{
|
|
"state": "inactive",
|
|
"name": "AlwaysFiring",
|
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
|
"duration": 10,
|
|
"annotations": {
|
|
"__dashboardUid__": "%s",
|
|
"__panelId__": "1"
|
|
},
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}, {
|
|
"state": "inactive",
|
|
"name": "AlwaysFiringButSilenced",
|
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}],
|
|
"totals": {
|
|
"inactive": 2
|
|
},
|
|
"interval": 60,
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}],
|
|
"totals": {
|
|
"inactive": 2
|
|
}
|
|
}
|
|
}`, dashboardUID)
|
|
expectedFilteredByJSON := fmt.Sprintf(`
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": [{
|
|
"name": "anotherrulegroup",
|
|
"file": "default",
|
|
"rules": [{
|
|
"state": "inactive",
|
|
"name": "AlwaysFiring",
|
|
"query": "[{\"refId\":\"A\",\"queryType\":\"\",\"relativeTimeRange\":{\"from\":18000,\"to\":10800},\"datasourceUid\":\"__expr__\",\"model\":{\"expression\":\"2 + 3 \\u003e 1\",\"intervalMs\":1000,\"maxDataPoints\":43200,\"type\":\"math\"}}]",
|
|
"duration": 10,
|
|
"annotations": {
|
|
"__dashboardUid__": "%s",
|
|
"__panelId__": "1"
|
|
},
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}],
|
|
"totals": {
|
|
"inactive": 1
|
|
},
|
|
"interval": 60,
|
|
"lastEvaluation": "0001-01-01T00:00:00Z",
|
|
"evaluationTime": 0
|
|
}],
|
|
"totals": {
|
|
"inactive": 1
|
|
}
|
|
}
|
|
}`, dashboardUID)
|
|
expectedNoneJSON := `
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": []
|
|
}
|
|
}`
|
|
|
|
// Now, let's see how this looks like.
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
require.JSONEq(t, expectedAllJSON, string(b))
|
|
}
|
|
|
|
// Now, let's check we get the same rule when filtering by dashboard_uid
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s", grafanaListedAddr, dashboardUID)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
require.JSONEq(t, expectedFilteredByJSON, string(b))
|
|
}
|
|
|
|
// Now, let's check we get no rules when filtering by an unknown dashboard_uid
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s", grafanaListedAddr, "abc")
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
require.JSONEq(t, expectedNoneJSON, string(b))
|
|
}
|
|
|
|
// Now, let's check we get the same rule when filtering by dashboard_uid and panel_id
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s&panel_id=1", grafanaListedAddr, dashboardUID)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
require.JSONEq(t, expectedFilteredByJSON, string(b))
|
|
}
|
|
|
|
// Now, let's check we get no rules when filtering by dashboard_uid and unknown panel_id
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s&panel_id=2", grafanaListedAddr, dashboardUID)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
require.JSONEq(t, expectedNoneJSON, string(b))
|
|
}
|
|
|
|
// Now, let's check an invalid panel_id returns a 400 Bad Request response
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?dashboard_uid=%s&panel_id=invalid", grafanaListedAddr, dashboardUID)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
var res map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(b, &res))
|
|
require.Equal(t, `invalid panel_id: strconv.ParseInt: parsing "invalid": invalid syntax`, res["message"])
|
|
}
|
|
|
|
// Now, let's check a panel_id without dashboard_uid returns a 400 Bad Request response
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules?panel_id=1", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
var res map[string]interface{}
|
|
require.NoError(t, json.Unmarshal(b, &res))
|
|
require.Equal(t, "panel_id must be set with dashboard_uid", res["message"])
|
|
}
|
|
}
|
|
|
|
func TestIntegrationPrometheusRulesPermissions(t *testing.T) {
|
|
testinfra.SQLiteIntegrationTest(t)
|
|
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
DisableLegacyAlerting: true,
|
|
EnableUnifiedAlerting: true,
|
|
DisableAnonymous: true,
|
|
AppModeProduction: true,
|
|
})
|
|
|
|
grafanaListedAddr, store := testinfra.StartGrafana(t, dir, path)
|
|
|
|
// Create a user to make authenticated requests
|
|
userID := createUser(t, store, user.CreateUserCommand{
|
|
DefaultOrgRole: string(org.RoleEditor),
|
|
Password: "password",
|
|
Login: "grafana",
|
|
})
|
|
|
|
apiClient := newAlertingApiClient(grafanaListedAddr, "grafana", "password")
|
|
|
|
// access control permissions store
|
|
permissionsStore := resourcepermissions.NewStore(store)
|
|
|
|
// Create the namespace we'll save our alerts to.
|
|
apiClient.CreateFolder(t, "folder1", "folder1")
|
|
|
|
// Create the namespace we'll save our alerts to.
|
|
apiClient.CreateFolder(t, "folder2", "folder2")
|
|
|
|
// Create rule under folder1
|
|
createRule(t, apiClient, "folder1")
|
|
|
|
// Create rule under folder2
|
|
createRule(t, apiClient, "folder2")
|
|
|
|
// Now, let's see how this looks like.
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
body := asJson(t, b)
|
|
// Sort, for test consistency.
|
|
sort.Slice(body.Data.Groups, func(i, j int) bool { return body.Data.Groups[i].File < body.Data.Groups[j].File })
|
|
require.Equal(t, "success", body.Status)
|
|
// The request should see both groups, and all rules underneath.
|
|
require.Len(t, body.Data.Groups, 2)
|
|
require.Len(t, body.Data.Groups[0].Rules, 1)
|
|
require.Len(t, body.Data.Groups[1].Rules, 1)
|
|
require.Equal(t, "folder1", body.Data.Groups[0].File)
|
|
require.Equal(t, "folder2", body.Data.Groups[1].File)
|
|
}
|
|
|
|
// remove permissions from folder2org.ROLE
|
|
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder2")
|
|
apiClient.ReloadCachedPermissions(t)
|
|
|
|
// make sure that folder2 is not included in the response
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
body := asJson(t, b)
|
|
require.Equal(t, "success", body.Status)
|
|
require.Len(t, body.Data.Groups, 1)
|
|
require.Len(t, body.Data.Groups[0].Rules, 1)
|
|
require.Equal(t, "folder1", body.Data.Groups[0].File)
|
|
}
|
|
|
|
// remove permissions from folder1org.ROLE
|
|
removeFolderPermission(t, permissionsStore, 1, userID, org.RoleEditor, "folder1")
|
|
apiClient.ReloadCachedPermissions(t)
|
|
|
|
// make sure that no folders are included in the response
|
|
{
|
|
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
|
|
// nolint:gosec
|
|
resp, err := http.Get(promRulesURL)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, 200, resp.StatusCode)
|
|
|
|
require.JSONEq(t, `
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": []
|
|
}
|
|
}`, string(b))
|
|
}
|
|
}
|
|
|
|
func removeFolderPermission(t *testing.T, store resourcepermissions.Store, orgID, userID int64, role org.RoleType, uid string) {
|
|
t.Helper()
|
|
// remove user permissions on folder
|
|
_, _ = store.SetUserResourcePermission(context.Background(), orgID, accesscontrol.User{ID: userID}, resourcepermissions.SetResourcePermissionCommand{
|
|
Resource: "folders",
|
|
ResourceID: uid,
|
|
ResourceAttribute: "uid",
|
|
}, nil)
|
|
|
|
// remove org role permissions from folder
|
|
_, _ = store.SetBuiltInResourcePermission(context.Background(), orgID, string(role), resourcepermissions.SetResourcePermissionCommand{
|
|
Resource: "folders",
|
|
ResourceID: uid,
|
|
ResourceAttribute: "uid",
|
|
}, nil)
|
|
|
|
// remove org role children permissions from folder
|
|
for _, c := range role.Children() {
|
|
_, _ = store.SetBuiltInResourcePermission(context.Background(), orgID, string(c), resourcepermissions.SetResourcePermissionCommand{
|
|
Resource: "folders",
|
|
ResourceID: uid,
|
|
ResourceAttribute: "uid",
|
|
}, nil)
|
|
}
|
|
}
|
|
|
|
func asJson(t *testing.T, blob []byte) rulesResponse {
|
|
t.Helper()
|
|
var r rulesResponse
|
|
require.NoError(t, json.Unmarshal(blob, &r))
|
|
return r
|
|
}
|
|
|
|
type rulesResponse struct {
|
|
Status string
|
|
Data rulesData
|
|
}
|
|
|
|
type rulesData struct {
|
|
Groups []groupData
|
|
}
|
|
|
|
type groupData struct {
|
|
Name string
|
|
File string
|
|
Rules []interface{}
|
|
}
|