[Alerting]: Fix updating rule group and add tests (#33074)

* [Alerting]: Fix updating rule group and add test

* Fix updating rule labels
* Set default values for rule no data and error states
if they are missing
* Add test for updating rule

* Test updating annotations

* Apply suggestions from code review

Co-authored-by: gotjosh <josue@grafana.com>

* add test for posting an unknown rule UID

* Fix alert rule validation and add tests

* Remove org id from PostableGrafanaRule

This field was not used; each rule gets the organisation of the user making
the rerquest

* Update pkg/tests/api/alerting/api_alertmanager_test.go

Co-authored-by: gotjosh <josue@grafana.com>
This commit is contained in:
Sofia Papagiannaki 2021-04-21 17:22:58 +03:00 committed by GitHub
parent b929822d72
commit 87a70af7eb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 597 additions and 129 deletions

View File

@ -203,6 +203,8 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf
}); err != nil {
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
return response.Error(http.StatusNotFound, "failed to update rule group", err)
} else if errors.Is(err, ngmodels.ErrAlertRuleFailedValidation) {
return response.Error(http.StatusBadRequest, "failed to update rule group", err)
}
return response.Error(http.StatusInternalServerError, "failed to update rule group", err)
}
@ -240,7 +242,6 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimode
func toPostableExtendedRuleNode(r ngmodels.AlertRule) apimodels.PostableExtendedRuleNode {
postableExtendedRuleNode := apimodels.PostableExtendedRuleNode{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
OrgID: r.OrgID,
Title: r.Title,
Condition: r.Condition,
Data: r.Data,

View File

@ -295,7 +295,6 @@ const (
// swagger:model
type PostableGrafanaRule struct {
OrgID int64 `json:"-" yaml:"-"`
Title string `json:"title" yaml:"title"`
Condition string `json:"condition" yaml:"condition"`
Data []models.AlertQuery `json:"data" yaml:"data"`

View File

@ -15,6 +15,8 @@ var (
ErrCannotEditNamespace = errors.New("user does not have permissions to edit the namespace")
// ErrRuleGroupNamespaceNotFound
ErrRuleGroupNamespaceNotFound = errors.New("rule group not found under this namespace")
// ErrAlertRuleFailedValidation
ErrAlertRuleFailedValidation = errors.New("invalid alert rule")
)
type NoDataState string

View File

@ -53,7 +53,6 @@ type RuleStore interface {
GetAlertInstance(*ngmodels.GetAlertInstanceQuery) error
ListAlertInstances(cmd *ngmodels.ListAlertInstancesQuery) error
SaveAlertInstance(cmd *ngmodels.SaveAlertInstanceCommand) error
ValidateAlertRule(ngmodels.AlertRule, bool) error
}
func getAlertRuleByUID(sess *sqlstore.DBSession, alertRuleUID string, orgID int64) (*ngmodels.AlertRule, error) {
@ -188,7 +187,17 @@ func (st DBstore) UpsertAlertRules(rules []UpsertRule) error {
r.New.Version = 1
if err := st.ValidateAlertRule(r.New, true); err != nil {
if r.New.NoDataState == "" {
// set default no data state
r.New.NoDataState = ngmodels.NoData
}
if r.New.ExecErrState == "" {
// set default error state
r.New.ExecErrState = ngmodels.AlertingErrState
}
if err := st.validateAlertRule(r.New); err != nil {
return err
}
@ -222,11 +231,19 @@ func (st DBstore) UpsertAlertRules(rules []UpsertRule) error {
r.New.RuleGroup = r.Existing.RuleGroup
r.New.Version = r.Existing.Version + 1
if r.New.For == 0 {
r.New.For = r.Existing.For
r.New.Annotations = r.Existing.Annotations
r.New.Labels = r.Existing.Labels
}
if err := st.ValidateAlertRule(r.New, true); err != nil {
if len(r.New.Annotations) == 0 {
r.New.Annotations = r.Existing.Annotations
}
if len(r.New.Labels) == 0 {
r.New.Labels = r.Existing.Labels
}
if err := st.validateAlertRule(r.New); err != nil {
return err
}
@ -383,33 +400,32 @@ func generateNewAlertRuleUID(sess *sqlstore.DBSession, orgID int64) (string, err
return "", ngmodels.ErrAlertRuleFailedGenerateUniqueUID
}
// ValidateAlertRule validates the alert rule interval and organisation.
// If requireData is true checks that it contains at least one alert query
func (st DBstore) ValidateAlertRule(alertRule ngmodels.AlertRule, requireData bool) error {
if !requireData && len(alertRule.Data) == 0 {
return fmt.Errorf("no queries or expressions are found")
// validateAlertRule validates the alert rule interval and organisation.
func (st DBstore) validateAlertRule(alertRule ngmodels.AlertRule) error {
if len(alertRule.Data) == 0 {
return fmt.Errorf("%w: no queries or expressions are found", ngmodels.ErrAlertRuleFailedValidation)
}
if alertRule.Title == "" {
return ErrEmptyTitleError
return fmt.Errorf("%w: title is empty", ngmodels.ErrAlertRuleFailedValidation)
}
if alertRule.IntervalSeconds%int64(st.BaseInterval.Seconds()) != 0 {
return fmt.Errorf("invalid interval: %v: interval should be divided exactly by scheduler interval: %v", time.Duration(alertRule.IntervalSeconds)*time.Second, st.BaseInterval)
return fmt.Errorf("%w: interval (%v) should be divided exactly by scheduler interval: %v", ngmodels.ErrAlertRuleFailedValidation, time.Duration(alertRule.IntervalSeconds)*time.Second, st.BaseInterval)
}
// enfore max name length in SQLite
if len(alertRule.Title) > AlertRuleMaxTitleLength {
return fmt.Errorf("name length should not be greater than %d", AlertRuleMaxTitleLength)
return fmt.Errorf("%w: name length should not be greater than %d", ngmodels.ErrAlertRuleFailedValidation, AlertRuleMaxTitleLength)
}
// enfore max name length in SQLite
// enfore max rule group name length in SQLite
if len(alertRule.RuleGroup) > AlertRuleMaxRuleGroupNameLength {
return fmt.Errorf("name length should not be greater than %d", AlertRuleMaxRuleGroupNameLength)
return fmt.Errorf("%w: rule group name length should not be greater than %d", ngmodels.ErrAlertRuleFailedValidation, AlertRuleMaxRuleGroupNameLength)
}
if alertRule.OrgID == 0 {
return fmt.Errorf("no organisation is found")
return fmt.Errorf("%w: no organisation is found", ngmodels.ErrAlertRuleFailedValidation)
}
return nil

View File

@ -19,9 +19,6 @@ var TimeNow = time.Now
// AlertDefinitionMaxTitleLength is the maximum length of the alert definition title
const AlertDefinitionMaxTitleLength = 190
// ErrEmptyTitleError is an error returned if the alert definition title is empty
var ErrEmptyTitleError = errors.New("title is empty")
// Store is the interface for persisting alert definitions and instances
type Store interface {
DeleteAlertDefinitionByUID(*models.DeleteAlertDefinitionByUIDCommand) error
@ -329,7 +326,7 @@ func (st DBstore) ValidateAlertDefinition(alertDefinition *models.AlertDefinitio
}
if alertDefinition.Title == "" {
return ErrEmptyTitleError
return fmt.Errorf("title is empty")
}
if alertDefinition.IntervalSeconds%int64(st.BaseInterval.Seconds()) != 0 {

View File

@ -74,7 +74,7 @@ func TestCreatingAlertDefinition(t *testing.T) {
desc: "should fail to create an alert definition with empty title",
inputIntervalSeconds: &customIntervalSeconds,
inputTitle: "",
expectedError: store.ErrEmptyTitleError,
expectedError: fmt.Errorf("title is empty"),
},
}

View File

@ -77,7 +77,6 @@ func createTestAlertRule(t *testing.T, dbstore *store.DBstore, intervalSeconds i
Rules: []apimodels.PostableExtendedRuleNode{
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
OrgID: 1,
Title: fmt.Sprintf("an alert definition %d", d),
Condition: "A",
Data: []models.AlertQuery{
@ -126,7 +125,6 @@ func updateTestAlertRuleIntervalSeconds(t *testing.T, dbstore *store.DBstore, ex
Rules: []apimodels.PostableExtendedRuleNode{
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
OrgID: 1,
UID: existingRule.UID,
},
},

View File

@ -6,6 +6,7 @@ import (
"fmt"
"io/ioutil"
"net/http"
"strings"
"testing"
"time"
@ -17,6 +18,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
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"
)
@ -157,14 +159,207 @@ func TestAlertRuleCRUD(t *testing.T) {
// Create the namespace we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "default"))
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",
Condition: "A",
},
},
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),
},
Model: json.RawMessage(`{
"datasourceUid": "-100",
"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(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),
},
Model: json.RawMessage(`{
"datasourceUid": "-100",
"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(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),
},
Model: json.RawMessage(`{
"datasourceUid": "-100",
"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),
},
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type": "math",
"expression": "2 + 3 > 1"
}`),
},
},
},
},
expectedResponse: `{"error":"invalid alert rule: interval (1s) should be divided exactly by scheduler interval: 10s", "message":"failed to update rule group"}`,
},
}
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://%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{
OrgID: 2,
Title: "AlwaysFiring",
Condition: "A",
Data: []ngmodels.AlertQuery{
@ -185,7 +380,6 @@ func TestAlertRuleCRUD(t *testing.T) {
},
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
OrgID: 2,
Title: "AlwaysFiringButSilenced",
Condition: "A",
Data: []ngmodels.AlertQuery{
@ -202,6 +396,8 @@ func TestAlertRuleCRUD(t *testing.T) {
}`),
},
},
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
ExecErrState: apimodels.ExecutionErrorState(ngmodels.KeepLastStateErrState),
},
},
},
@ -222,7 +418,6 @@ func TestAlertRuleCRUD(t *testing.T) {
b, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
fmt.Println(string(b))
assert.Equal(t, resp.StatusCode, 202)
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
}
@ -241,15 +436,32 @@ func TestAlertRuleCRUD(t *testing.T) {
require.NoError(t, err)
assert.Equal(t, resp.StatusCode, 202)
assert.JSONEq(t, `
{
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":2,
@ -279,8 +491,8 @@ func TestAlertRuleCRUD(t *testing.T) {
"namespace_uid":"nsuid",
"namespace_id":1,
"rule_group":"arulegroup",
"no_data_state":"",
"exec_err_state":""
"no_data_state":"NoData",
"exec_err_state":"Alerting"
}
},
{
@ -314,20 +526,244 @@ func TestAlertRuleCRUD(t *testing.T) {
"namespace_uid":"nsuid",
"namespace_id":1,
"rule_group":"arulegroup",
"no_data_state":"",
"exec_err_state":""
"no_data_state":"Alerting",
"exec_err_state":"KeepLastState"
}
}
]
}
]
}`, rulesNamespaceWithoutVariableValues(t, b))
}`
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),
},
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type": "math",
"expression": "2 + 3 < 1"
}`),
},
},
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
ExecErrState: apimodels.ExecutionErrorState(ngmodels.KeepLastStateErrState),
},
},
},
}
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://%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://%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),
},
Model: json.RawMessage(`{
"datasourceUid": "-100",
"type": "math",
"expression": "2 + 3 < 1"
}`),
},
},
NoDataState: apimodels.NoDataState(ngmodels.Alerting),
ExecErrState: apimodels.ExecutionErrorState(ngmodels.KeepLastStateErrState),
},
},
},
}
buf := bytes.Buffer{}
enc := json.NewEncoder(&buf)
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://%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://%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":2,
"title":"AlwaysNormal",
"condition":"A",
"data":[
{
"refId":"A",
"queryType":"",
"relativeTimeRange":{
"from":18000,
"to":10800
},
"model":{
"datasourceUid":"-100",
"expression":"2 + 3 \u003C 1",
"intervalMs":1000,
"maxDataPoints":100,
"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":"KeepLastState"
}
}
]
}
]
}`, body)
}
client := &http.Client{}
// Finally, make sure we can delete it.
{
// If the rule group name does not exists
t.Run("fail if he rule group name does not exists", func(t *testing.T) {
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default/groupnotexist", grafanaListedAddr)
req, err := http.NewRequest(http.MethodDelete, u, nil)
require.NoError(t, err)
@ -342,22 +778,24 @@ func TestAlertRuleCRUD(t *testing.T) {
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))
})
// If the rule group name does exist
u = fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default/arulegroup", grafanaListedAddr)
req, err = http.NewRequest(http.MethodDelete, u, nil)
t.Run("succeed if the rule group name does exist", func(t *testing.T) {
u := fmt.Sprintf("http://%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)
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)
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))
})
}
}
@ -380,14 +818,23 @@ func createFolder(t *testing.T, store *sqlstore.SQLStore, folderID int64, folder
}
// rulesNamespaceWithoutVariableValues takes a apimodels.NamespaceConfigResponse JSON-based input and makes the dynamic fields static e.g. uid, dates, etc.
func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) string {
// 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))
for _, nodes := range 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)
@ -397,5 +844,13 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) string {
json, err := json.Marshal(&r)
require.NoError(t, err)
return string(json)
return string(json), m
}
func getLongString(n int) string {
b := make([]rune, n)
for i := range b {
b[i] = 'a'
}
return string(b)
}