This commit adds a number of limits to the Grafana flavor of the Prometheus Rules API: 1. `limit` limits the maximum number of Rule Groups returned 2. `limit_rules` limits the maximum number of rules per Rule Group 3. `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.
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{}
|
|
}
|