mirror of
https://github.com/grafana/grafana.git
synced 2024-12-02 05:29:42 -06:00
af9353caec
* add check for access to rule's data source in GET APIs * use more general method GetAlertRules instead of GetNamespaceAlertRules. * remove unused GetNamespaceAlertRules. Tests: * create a method to generate permissions for rules * extract method to create RuleSrv * add tests for RouteGetNamespaceRulesConfig
578 lines
16 KiB
Go
578 lines
16 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"math/rand"
|
|
"net/http"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/eval"
|
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/state"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
)
|
|
|
|
func Test_FormatValues(t *testing.T) {
|
|
val1 := 1.1
|
|
val2 := 1.4
|
|
|
|
tc := []struct {
|
|
name string
|
|
alertState *state.State
|
|
expected string
|
|
}{
|
|
{
|
|
name: "with no value, it renders the evaluation string",
|
|
alertState: &state.State{
|
|
LastEvaluationString: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
|
Results: []state.Evaluation{
|
|
{Condition: "A", Values: map[string]*float64{}},
|
|
},
|
|
},
|
|
expected: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
|
},
|
|
{
|
|
name: "with one value, it renders the single value",
|
|
alertState: &state.State{
|
|
LastEvaluationString: "[ var='A' metric='vector(10) + time() % 50' labels={} value=1.1 ]",
|
|
Results: []state.Evaluation{
|
|
{Condition: "A", Values: map[string]*float64{"A": &val1}},
|
|
},
|
|
},
|
|
expected: "1.1e+00",
|
|
},
|
|
{
|
|
name: "with two values, it renders the value based on their refID and position",
|
|
alertState: &state.State{
|
|
LastEvaluationString: "[ var='B0' metric='vector(10) + time() % 50' labels={} value=1.1 ], [ var='B1' metric='vector(10) + time() % 50' labels={} value=1.4 ]",
|
|
Results: []state.Evaluation{
|
|
{Condition: "B", Values: map[string]*float64{"B0": &val1, "B1": &val2}},
|
|
},
|
|
},
|
|
expected: "B0: 1.1e+00, B1: 1.4e+00",
|
|
},
|
|
{
|
|
name: "with a high number of values, it renders the value based on their refID and position using a natural order",
|
|
alertState: &state.State{
|
|
LastEvaluationString: "[ var='B0' metric='vector(10) + time() % 50' labels={} value=1.1 ], [ var='B1' metric='vector(10) + time() % 50' labels={} value=1.4 ]",
|
|
Results: []state.Evaluation{
|
|
{Condition: "B", Values: map[string]*float64{"B0": &val1, "B1": &val2, "B2": &val1, "B10": &val2, "B11": &val1}},
|
|
},
|
|
},
|
|
expected: "B0: 1.1e+00, B10: 1.4e+00, B11: 1.1e+00, B1: 1.4e+00, B2: 1.1e+00",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tc {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
require.Equal(t, tt.expected, formatValues(tt.alertState))
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRouteGetAlertStatuses(t *testing.T) {
|
|
orgID := int64(1)
|
|
|
|
t.Run("with no alerts", func(t *testing.T) {
|
|
_, _, _, api := setupAPI(t)
|
|
req, err := http.NewRequest("GET", "/api/v1/alerts", nil)
|
|
require.NoError(t, err)
|
|
c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgId: orgID}}
|
|
|
|
r := api.RouteGetAlertStatuses(c)
|
|
require.Equal(t, http.StatusOK, r.Status())
|
|
require.JSONEq(t, `
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"alerts": []
|
|
}
|
|
}
|
|
`, string(r.Body()))
|
|
})
|
|
|
|
t.Run("with two alerts", func(t *testing.T) {
|
|
_, fakeAIM, _, api := setupAPI(t)
|
|
fakeAIM.GenerateAlertInstances(1, util.GenerateShortUID(), 2)
|
|
req, err := http.NewRequest("GET", "/api/v1/alerts", nil)
|
|
require.NoError(t, err)
|
|
c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgId: orgID}}
|
|
|
|
r := api.RouteGetAlertStatuses(c)
|
|
require.Equal(t, http.StatusOK, r.Status())
|
|
require.JSONEq(t, `
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"alerts": [{
|
|
"labels": {
|
|
"alertname": "test_title_0",
|
|
"instance_label": "test",
|
|
"label": "test"
|
|
},
|
|
"annotations": {
|
|
"annotation": "test"
|
|
},
|
|
"state": "Normal",
|
|
"activeAt": "0001-01-01T00:00:00Z",
|
|
"value": ""
|
|
}, {
|
|
"labels": {
|
|
"alertname": "test_title_1",
|
|
"instance_label": "test",
|
|
"label": "test"
|
|
},
|
|
"annotations": {
|
|
"annotation": "test"
|
|
},
|
|
"state": "Normal",
|
|
"activeAt": "0001-01-01T00:00:00Z",
|
|
"value": ""
|
|
}]
|
|
}
|
|
}`, string(r.Body()))
|
|
})
|
|
|
|
t.Run("with two firing alerts", func(t *testing.T) {
|
|
_, fakeAIM, _, api := setupAPI(t)
|
|
fakeAIM.GenerateAlertInstances(1, util.GenerateShortUID(), 2, withAlertingState())
|
|
req, err := http.NewRequest("GET", "/api/v1/alerts", nil)
|
|
require.NoError(t, err)
|
|
c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgId: orgID}}
|
|
|
|
r := api.RouteGetAlertStatuses(c)
|
|
require.Equal(t, http.StatusOK, r.Status())
|
|
require.JSONEq(t, `
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"alerts": [{
|
|
"labels": {
|
|
"alertname": "test_title_0",
|
|
"instance_label": "test",
|
|
"label": "test"
|
|
},
|
|
"annotations": {
|
|
"annotation": "test"
|
|
},
|
|
"state": "Alerting",
|
|
"activeAt": "0001-01-01T00:00:00Z",
|
|
"value": "1.1e+00"
|
|
}, {
|
|
"labels": {
|
|
"alertname": "test_title_1",
|
|
"instance_label": "test",
|
|
"label": "test"
|
|
},
|
|
"annotations": {
|
|
"annotation": "test"
|
|
},
|
|
"state": "Alerting",
|
|
"activeAt": "0001-01-01T00:00:00Z",
|
|
"value": "1.1e+00"
|
|
}]
|
|
}
|
|
}`, string(r.Body()))
|
|
})
|
|
|
|
t.Run("with the inclusion of internal labels", func(t *testing.T) {
|
|
_, fakeAIM, _, api := setupAPI(t)
|
|
fakeAIM.GenerateAlertInstances(orgID, util.GenerateShortUID(), 2)
|
|
req, err := http.NewRequest("GET", "/api/v1/alerts?includeInternalLabels=true", nil)
|
|
require.NoError(t, err)
|
|
c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgId: orgID}}
|
|
|
|
r := api.RouteGetAlertStatuses(c)
|
|
require.Equal(t, http.StatusOK, r.Status())
|
|
require.JSONEq(t, `
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"alerts": [{
|
|
"labels": {
|
|
"__alert_rule_namespace_uid__": "test_namespace_uid",
|
|
"__alert_rule_uid__": "test_alert_rule_uid_0",
|
|
"alertname": "test_title_0",
|
|
"instance_label": "test",
|
|
"label": "test"
|
|
},
|
|
"annotations": {
|
|
"annotation": "test"
|
|
},
|
|
"state": "Normal",
|
|
"activeAt": "0001-01-01T00:00:00Z",
|
|
"value": ""
|
|
}, {
|
|
"labels": {
|
|
"__alert_rule_namespace_uid__": "test_namespace_uid",
|
|
"__alert_rule_uid__": "test_alert_rule_uid_1",
|
|
"alertname": "test_title_1",
|
|
"instance_label": "test",
|
|
"label": "test"
|
|
},
|
|
"annotations": {
|
|
"annotation": "test"
|
|
},
|
|
"state": "Normal",
|
|
"activeAt": "0001-01-01T00:00:00Z",
|
|
"value": ""
|
|
}]
|
|
}
|
|
}`, string(r.Body()))
|
|
})
|
|
}
|
|
|
|
func withAlertingState() forEachState {
|
|
return func(s *state.State) *state.State {
|
|
s.State = eval.Alerting
|
|
value := float64(1.1)
|
|
s.Results = append(s.Results, state.Evaluation{
|
|
EvaluationState: eval.Alerting,
|
|
EvaluationTime: timeNow(),
|
|
Values: map[string]*float64{"B": &value},
|
|
Condition: "B",
|
|
})
|
|
return s
|
|
}
|
|
}
|
|
|
|
func TestRouteGetRuleStatuses(t *testing.T) {
|
|
timeNow = func() time.Time { return time.Date(2022, 3, 10, 14, 0, 0, 0, time.UTC) }
|
|
orgID := int64(1)
|
|
|
|
req, err := http.NewRequest("GET", "/api/v1/rules", nil)
|
|
require.NoError(t, err)
|
|
c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgId: orgID}, IsSignedIn: true}
|
|
|
|
t.Run("with no rules", func(t *testing.T) {
|
|
_, _, _, api := setupAPI(t)
|
|
r := api.RouteGetRuleStatuses(c)
|
|
|
|
require.JSONEq(t, `
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": []
|
|
}
|
|
}
|
|
`, string(r.Body()))
|
|
})
|
|
|
|
t.Run("with a rule that only has one query", func(t *testing.T) {
|
|
fakeStore, fakeAIM, _, api := setupAPI(t)
|
|
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery())
|
|
folder := fakeStore.Folders[orgID][0]
|
|
|
|
r := api.RouteGetRuleStatuses(c)
|
|
require.Equal(t, http.StatusOK, r.Status())
|
|
require.JSONEq(t, fmt.Sprintf(`
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": [{
|
|
"name": "rule-group",
|
|
"file": "%s",
|
|
"rules": [{
|
|
"state": "inactive",
|
|
"name": "AlwaysFiring",
|
|
"query": "vector(1)",
|
|
"alerts": [{
|
|
"labels": {
|
|
"job": "prometheus"
|
|
},
|
|
"annotations": {
|
|
"severity": "critical"
|
|
},
|
|
"state": "Normal",
|
|
"activeAt": "0001-01-01T00:00:00Z",
|
|
"value": ""
|
|
}],
|
|
"labels": {
|
|
"__a_private_label_on_the_rule__": "a_value"
|
|
},
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "2022-03-10T14:01:00Z",
|
|
"duration": 180,
|
|
"evaluationTime": 60
|
|
}],
|
|
"interval": 60,
|
|
"lastEvaluation": "2022-03-10T14:01:00Z",
|
|
"evaluationTime": 60
|
|
}]
|
|
}
|
|
}
|
|
`, folder.Title), string(r.Body()))
|
|
})
|
|
|
|
t.Run("with the inclusion of internal Labels", func(t *testing.T) {
|
|
fakeStore, fakeAIM, _, api := setupAPI(t)
|
|
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withClassicConditionSingleQuery())
|
|
folder := fakeStore.Folders[orgID][0]
|
|
|
|
req, err := http.NewRequest("GET", "/api/v1/rules?includeInternalLabels=true", nil)
|
|
require.NoError(t, err)
|
|
c := &models.ReqContext{Context: &web.Context{Req: req}, SignedInUser: &models.SignedInUser{OrgId: orgID}, IsSignedIn: true}
|
|
|
|
r := api.RouteGetRuleStatuses(c)
|
|
require.Equal(t, http.StatusOK, r.Status())
|
|
require.JSONEq(t, fmt.Sprintf(`
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": [{
|
|
"name": "rule-group",
|
|
"file": "%s",
|
|
"rules": [{
|
|
"state": "inactive",
|
|
"name": "AlwaysFiring",
|
|
"query": "vector(1)",
|
|
"alerts": [{
|
|
"labels": {
|
|
"job": "prometheus",
|
|
"__alert_rule_namespace_uid__": "test_namespace_uid",
|
|
"__alert_rule_uid__": "test_alert_rule_uid_0"
|
|
},
|
|
"annotations": {
|
|
"severity": "critical"
|
|
},
|
|
"state": "Normal",
|
|
"activeAt": "0001-01-01T00:00:00Z",
|
|
"value": ""
|
|
}],
|
|
"labels": {
|
|
"__a_private_label_on_the_rule__": "a_value",
|
|
"__alert_rule_uid__": "RuleUID"
|
|
},
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "2022-03-10T14:01:00Z",
|
|
"duration": 180,
|
|
"evaluationTime": 60
|
|
}],
|
|
"interval": 60,
|
|
"lastEvaluation": "2022-03-10T14:01:00Z",
|
|
"evaluationTime": 60
|
|
}]
|
|
}
|
|
}
|
|
`, folder.Title), string(r.Body()))
|
|
})
|
|
|
|
t.Run("with a rule that has multiple queries", func(t *testing.T) {
|
|
fakeStore, fakeAIM, _, api := setupAPI(t)
|
|
generateRuleAndInstanceWithQuery(t, orgID, fakeAIM, fakeStore, withExpressionsMultiQuery())
|
|
folder := fakeStore.Folders[orgID][0]
|
|
|
|
r := api.RouteGetRuleStatuses(c)
|
|
require.Equal(t, http.StatusOK, r.Status())
|
|
require.JSONEq(t, fmt.Sprintf(`
|
|
{
|
|
"status": "success",
|
|
"data": {
|
|
"groups": [{
|
|
"name": "rule-group",
|
|
"file": "%s",
|
|
"rules": [{
|
|
"state": "inactive",
|
|
"name": "AlwaysFiring",
|
|
"query": "vector(1) | vector(1)",
|
|
"alerts": [{
|
|
"labels": {
|
|
"job": "prometheus"
|
|
},
|
|
"annotations": {
|
|
"severity": "critical"
|
|
},
|
|
"state": "Normal",
|
|
"activeAt": "0001-01-01T00:00:00Z",
|
|
"value": ""
|
|
}],
|
|
"labels": {
|
|
"__a_private_label_on_the_rule__": "a_value"
|
|
},
|
|
"health": "ok",
|
|
"type": "alerting",
|
|
"lastEvaluation": "2022-03-10T14:01:00Z",
|
|
"duration": 180,
|
|
"evaluationTime": 60
|
|
}],
|
|
"interval": 60,
|
|
"lastEvaluation": "2022-03-10T14:01:00Z",
|
|
"evaluationTime": 60
|
|
}]
|
|
}
|
|
}
|
|
`, folder.Title), string(r.Body()))
|
|
})
|
|
|
|
t.Run("when fine-grained access is enabled", func(t *testing.T) {
|
|
t.Run("should return only rules if the user can query all data sources", func(t *testing.T) {
|
|
ruleStore := store.NewFakeRuleStore(t)
|
|
fakeAIM := NewFakeAlertInstanceManager(t)
|
|
|
|
rules := ngmodels.GenerateAlertRules(rand.Intn(4)+2, ngmodels.AlertRuleGen(withOrgID(orgID)))
|
|
ruleStore.PutRule(context.Background(), rules...)
|
|
ruleStore.PutRule(context.Background(), ngmodels.GenerateAlertRules(rand.Intn(4)+2, ngmodels.AlertRuleGen(withOrgID(orgID)))...)
|
|
|
|
acMock := acmock.New().WithPermissions(createPermissionsForRules(rules))
|
|
|
|
api := PrometheusSrv{
|
|
log: log.NewNopLogger(),
|
|
manager: fakeAIM,
|
|
store: ruleStore,
|
|
ac: acMock,
|
|
}
|
|
|
|
response := api.RouteGetRuleStatuses(c)
|
|
require.Equal(t, http.StatusOK, response.Status())
|
|
result := &apimodels.RuleResponse{}
|
|
require.NoError(t, json.Unmarshal(response.Body(), result))
|
|
for _, group := range result.Data.RuleGroups {
|
|
grouploop:
|
|
for _, rule := range group.Rules {
|
|
for i, expected := range rules {
|
|
if rule.Name == expected.Title && group.Name == expected.RuleGroup {
|
|
rules = append(rules[:i], rules[i+1:]...)
|
|
continue grouploop
|
|
}
|
|
}
|
|
assert.Failf(t, "rule %s in a group %s was not found in expected", rule.Name, group.Name)
|
|
}
|
|
}
|
|
assert.Emptyf(t, rules, "not all expected rules were returned")
|
|
})
|
|
})
|
|
}
|
|
|
|
func setupAPI(t *testing.T) (*store.FakeRuleStore, *fakeAlertInstanceManager, *acmock.Mock, PrometheusSrv) {
|
|
fakeStore := store.NewFakeRuleStore(t)
|
|
fakeAIM := NewFakeAlertInstanceManager(t)
|
|
acMock := acmock.New().WithDisabled()
|
|
|
|
api := PrometheusSrv{
|
|
log: log.NewNopLogger(),
|
|
manager: fakeAIM,
|
|
store: fakeStore,
|
|
ac: acMock,
|
|
}
|
|
|
|
return fakeStore, fakeAIM, acMock, api
|
|
}
|
|
|
|
func generateRuleAndInstanceWithQuery(t *testing.T, orgID int64, fakeAIM *fakeAlertInstanceManager, fakeStore *store.FakeRuleStore, query func(r *ngmodels.AlertRule)) {
|
|
t.Helper()
|
|
|
|
rules := ngmodels.GenerateAlertRules(1, ngmodels.AlertRuleGen(withOrgID(orgID), asFixture(), query))
|
|
|
|
fakeAIM.GenerateAlertInstances(orgID, rules[0].UID, 1, func(s *state.State) *state.State {
|
|
s.Labels = data.Labels{
|
|
"job": "prometheus",
|
|
ngmodels.NamespaceUIDLabel: "test_namespace_uid",
|
|
ngmodels.RuleUIDLabel: "test_alert_rule_uid_0",
|
|
}
|
|
s.Annotations = data.Labels{"severity": "critical"}
|
|
return s
|
|
})
|
|
|
|
for _, r := range rules {
|
|
fakeStore.PutRule(context.Background(), r)
|
|
}
|
|
}
|
|
|
|
// asFixture removes variable values of the alert rule.
|
|
// we're not too interested in variability of the rule in this scenario.
|
|
func asFixture() func(r *ngmodels.AlertRule) {
|
|
return func(r *ngmodels.AlertRule) {
|
|
r.Title = "AlwaysFiring"
|
|
r.NamespaceUID = "namespaceUID"
|
|
r.RuleGroup = "rule-group"
|
|
r.UID = "RuleUID"
|
|
r.Labels = map[string]string{
|
|
"__a_private_label_on_the_rule__": "a_value",
|
|
ngmodels.RuleUIDLabel: "RuleUID",
|
|
}
|
|
r.Annotations = nil
|
|
r.IntervalSeconds = 60
|
|
r.For = 180 * time.Second
|
|
}
|
|
}
|
|
|
|
func withClassicConditionSingleQuery() func(r *ngmodels.AlertRule) {
|
|
return func(r *ngmodels.AlertRule) {
|
|
queries := []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
QueryType: "",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
|
DatasourceUID: "AUID",
|
|
Model: json.RawMessage(fmt.Sprintf(prometheusQueryModel, "A")),
|
|
},
|
|
{
|
|
RefID: "B",
|
|
QueryType: "",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(fmt.Sprintf(classicConditionsModel, "A", "B")),
|
|
},
|
|
}
|
|
r.Data = queries
|
|
}
|
|
}
|
|
|
|
func withExpressionsMultiQuery() func(r *ngmodels.AlertRule) {
|
|
return func(r *ngmodels.AlertRule) {
|
|
queries := []ngmodels.AlertQuery{
|
|
{
|
|
RefID: "A",
|
|
QueryType: "",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
|
DatasourceUID: "AUID",
|
|
Model: json.RawMessage(fmt.Sprintf(prometheusQueryModel, "A")),
|
|
},
|
|
{
|
|
RefID: "B",
|
|
QueryType: "",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
|
DatasourceUID: "BUID",
|
|
Model: json.RawMessage(fmt.Sprintf(prometheusQueryModel, "B")),
|
|
},
|
|
{
|
|
RefID: "C",
|
|
QueryType: "",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(fmt.Sprintf(reduceLastExpressionModel, "A", "C")),
|
|
},
|
|
{
|
|
RefID: "D",
|
|
QueryType: "",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(fmt.Sprintf(reduceLastExpressionModel, "B", "D")),
|
|
},
|
|
{
|
|
RefID: "E",
|
|
QueryType: "",
|
|
RelativeTimeRange: ngmodels.RelativeTimeRange{From: ngmodels.Duration(0), To: ngmodels.Duration(0)},
|
|
DatasourceUID: "-100",
|
|
Model: json.RawMessage(fmt.Sprintf(mathExpressionModel, "A", "B", "E")),
|
|
},
|
|
}
|
|
r.Data = queries
|
|
}
|
|
}
|