mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
[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:
parent
b929822d72
commit
87a70af7eb
@ -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,
|
||||
|
@ -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"`
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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 {
|
||||
|
@ -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"),
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
},
|
||||
},
|
||||
|
@ -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)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user