diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 8851dc79881..7d86922c827 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -249,26 +249,18 @@ func (st DBstore) UpsertAlertRules(rules []UpsertRule) error { r.New.Data = r.Existing.Data } - if r.New.IntervalSeconds == 0 { - r.New.IntervalSeconds = r.Existing.IntervalSeconds - } - r.New.ID = r.Existing.ID r.New.OrgID = r.Existing.OrgID r.New.NamespaceUID = r.Existing.NamespaceUID r.New.RuleGroup = r.Existing.RuleGroup r.New.Version = r.Existing.Version + 1 - if r.New.For == 0 { - r.New.For = r.Existing.For + if r.New.ExecErrState == "" { + r.New.ExecErrState = r.Existing.ExecErrState } - 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 r.New.NoDataState == "" { + r.New.NoDataState = r.Existing.NoDataState } if err := st.validateAlertRule(r.New); err != nil { @@ -280,7 +272,7 @@ func (st DBstore) UpsertAlertRules(rules []UpsertRule) error { } // no way to update multiple rules at once - if _, err := sess.ID(r.Existing.ID).Update(r.New); err != nil { + if _, err := sess.ID(r.Existing.ID).AllCols().Update(r.New); err != nil { return fmt.Errorf("failed to update rule %s: %w", r.New.Title, err) } diff --git a/pkg/tests/api/alerting/api_alertmanager_test.go b/pkg/tests/api/alerting/api_alertmanager_test.go index a66c8559dd0..9903cd6d538 100644 --- a/pkg/tests/api/alerting/api_alertmanager_test.go +++ b/pkg/tests/api/alerting/api_alertmanager_test.go @@ -1258,7 +1258,7 @@ func TestAlertRuleCRUD(t *testing.T) { // update the first rule and completely remove the other { - interval, err := model.ParseDuration("30s") + forValue, err := model.ParseDuration("30s") require.NoError(t, err) rules := apimodels.PostableRuleGroupConfig{ @@ -1266,14 +1266,16 @@ func TestAlertRuleCRUD(t *testing.T) { Rules: []apimodels.PostableExtendedRuleNode{ { ApiRuleNode: &apimodels.ApiRuleNode{ - For: interval, + For: forValue, Labels: map[string]string{ - "label1": "val42", - "foo": "bar", + // delete foo label + "label1": "val1", // update label value + "label2": "val2", // new label }, Annotations: map[string]string{ - "annotation1": "val42", - "foo": "bar", + // delete foo annotation + "annotation1": "val1", // update annotation value + "annotation2": "val2", // new annotation }, }, GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ @@ -1299,6 +1301,7 @@ func TestAlertRuleCRUD(t *testing.T) { }, }, }, + Interval: interval, } buf := bytes.Buffer{} enc := json.NewEncoder(&buf) @@ -1347,14 +1350,14 @@ func TestAlertRuleCRUD(t *testing.T) { "rules":[ { "annotations": { - "annotation1": "val42", - "foo": "bar" + "annotation1": "val1", + "annotation2": "val2" }, "expr":"", "for": "30s", "labels": { - "foo": "bar", - "label1": "val42" + "label1": "val1", + "label2": "val2" }, "grafana_alert":{ "id":1, @@ -1395,6 +1398,229 @@ func TestAlertRuleCRUD(t *testing.T) { }`, body) } + // update the rule; delete labels and annotations + { + forValue, err := model.ParseDuration("30s") + require.NoError(t, err) + + rules := apimodels.PostableRuleGroupConfig{ + Name: "arulegroup", + Rules: []apimodels.PostableExtendedRuleNode{ + { + ApiRuleNode: &apimodels.ApiRuleNode{ + For: forValue, + }, + 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), + }, + DatasourceUID: "-100", + Model: json.RawMessage(`{ + "type": "math", + "expression": "2 + 3 < 1" + }`), + }, + }, + NoDataState: apimodels.NoDataState(ngmodels.Alerting), + ExecErrState: apimodels.ExecutionErrorState(ngmodels.AlertingErrState), + }, + }, + }, + Interval: interval, + } + 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 := 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://grafana:password@%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":[ + { + "expr":"", + "for": "30s", + "grafana_alert":{ + "id":1, + "orgId":1, + "title":"AlwaysNormal", + "condition":"A", + "data":[ + { + "refId":"A", + "queryType":"", + "relativeTimeRange":{ + "from":18000, + "to":10800 + }, + "datasourceUid":"-100", + "model":{ + "expression":"2 + 3 \u003C 1", + "intervalMs":1000, + "maxDataPoints":43200, + "type":"math" + } + } + ], + "updated":"2021-02-21T01:10:30Z", + "intervalSeconds":60, + "version":3, + "uid":"uid", + "namespace_uid":"nsuid", + "namespace_id":1, + "rule_group":"arulegroup", + "no_data_state":"Alerting", + "exec_err_state":"Alerting" + } + } + ] + } + ] + }`, body) + } + + // update the rule; keep title, condition, no data state, error state, queries and expressions if not provided + { + rules := apimodels.PostableRuleGroupConfig{ + Name: "arulegroup", + Rules: []apimodels.PostableExtendedRuleNode{ + { + GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ + UID: ruleUID, // Including the UID in the payload makes the endpoint update the existing rule. + }, + }, + }, + Interval: interval, + } + 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 := 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://grafana:password@%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":[ + { + "expr":"", + "grafana_alert":{ + "id":1, + "orgId":1, + "title":"AlwaysNormal", + "condition":"A", + "data":[ + { + "refId":"A", + "queryType":"", + "relativeTimeRange":{ + "from":18000, + "to":10800 + }, + "datasourceUid":"-100", + "model":{ + "expression":"2 + 3 \u003C 1", + "intervalMs":1000, + "maxDataPoints":43200, + "type":"math" + } + } + ], + "updated":"2021-02-21T01:10:30Z", + "intervalSeconds":60, + "version":4, + "uid":"uid", + "namespace_uid":"nsuid", + "namespace_id":1, + "rule_group":"arulegroup", + "no_data_state":"Alerting", + "exec_err_state":"Alerting" + } + } + ] + } + ] + }`, body) + } + client := &http.Client{} // Finally, make sure we can delete it. {