mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Integration test rule creation (#33047)
* Alerting: Integration test rule creation * Appease the linter * Cleanup * Make anonymous user role a parameter
This commit is contained in:
parent
0491fe0a5c
commit
362c4d4276
@ -43,8 +43,12 @@ func (srv RulerSrv) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Re
|
|||||||
}
|
}
|
||||||
ruleGroup := c.Params(":Groupname")
|
ruleGroup := c.Params(":Groupname")
|
||||||
if err := srv.store.DeleteRuleGroupAlertRules(c.SignedInUser.OrgId, namespace.Uid, ruleGroup); err != nil {
|
if err := srv.store.DeleteRuleGroupAlertRules(c.SignedInUser.OrgId, namespace.Uid, ruleGroup); err != nil {
|
||||||
return response.Error(http.StatusInternalServerError, "failed to delete group alert rules", err)
|
if errors.Is(err, ngmodels.ErrRuleGroupNamespaceNotFound) {
|
||||||
|
return response.Error(http.StatusNotFound, "failed to delete rule group", err)
|
||||||
|
}
|
||||||
|
return response.Error(http.StatusInternalServerError, "failed to delete rule group", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"})
|
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -187,6 +191,11 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf
|
|||||||
// TODO check quota
|
// TODO check quota
|
||||||
// TODO validate UID uniqueness in the payload
|
// TODO validate UID uniqueness in the payload
|
||||||
|
|
||||||
|
//TODO: Should this belong in alerting-api?
|
||||||
|
if ruleGroupConfig.Name == "" {
|
||||||
|
return response.Error(http.StatusBadRequest, "rule group name is not valid", nil)
|
||||||
|
}
|
||||||
|
|
||||||
if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{
|
if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{
|
||||||
OrgID: c.SignedInUser.OrgId,
|
OrgID: c.SignedInUser.OrgId,
|
||||||
NamespaceUID: namespace.Uid,
|
NamespaceUID: namespace.Uid,
|
||||||
|
@ -13,6 +13,8 @@ var (
|
|||||||
ErrAlertRuleFailedGenerateUniqueUID = errors.New("failed to generate alert rule UID")
|
ErrAlertRuleFailedGenerateUniqueUID = errors.New("failed to generate alert rule UID")
|
||||||
// ErrCannotEditNamespace is an error returned if the user does not have permissions to edit the namespace
|
// ErrCannotEditNamespace is an error returned if the user does not have permissions to edit the namespace
|
||||||
ErrCannotEditNamespace = errors.New("user does not have permissions to edit the namespace")
|
ErrCannotEditNamespace = errors.New("user does not have permissions to edit the namespace")
|
||||||
|
// ErrRuleGroupNamespaceNotFound
|
||||||
|
ErrRuleGroupNamespaceNotFound = errors.New("rule group not found under this namespace")
|
||||||
)
|
)
|
||||||
|
|
||||||
type NoDataState string
|
type NoDataState string
|
||||||
|
@ -115,6 +115,15 @@ func (st DBstore) DeleteNamespaceAlertRules(orgID int64, namespaceUID string) er
|
|||||||
// DeleteRuleGroupAlertRules is a handler for deleting rule group alert rules.
|
// DeleteRuleGroupAlertRules is a handler for deleting rule group alert rules.
|
||||||
func (st DBstore) DeleteRuleGroupAlertRules(orgID int64, namespaceUID string, ruleGroup string) error {
|
func (st DBstore) DeleteRuleGroupAlertRules(orgID int64, namespaceUID string, ruleGroup string) error {
|
||||||
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error {
|
||||||
|
exist, err := sess.Exist(&ngmodels.AlertRule{OrgID: orgID, NamespaceUID: namespaceUID, RuleGroup: ruleGroup})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exist {
|
||||||
|
return ngmodels.ErrRuleGroupNamespaceNotFound
|
||||||
|
}
|
||||||
|
|
||||||
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil {
|
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,23 @@
|
|||||||
package alerting
|
package alerting
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"testing"
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/tests/testinfra"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
apimodels "github.com/grafana/alerting-api/pkg/api"
|
||||||
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestAlertAndGroupsQuery(t *testing.T) {
|
func TestAlertAndGroupsQuery(t *testing.T) {
|
||||||
@ -50,3 +60,260 @@ func TestAlertAndGroupsQuery(t *testing.T) {
|
|||||||
require.JSONEq(t, "[]", string(b))
|
require.JSONEq(t, "[]", string(b))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAlertRuleCRUD(t *testing.T) {
|
||||||
|
// Setup Grafana and its Database
|
||||||
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||||
|
EnableFeatureToggles: []string{"ngalert"},
|
||||||
|
AnonymousUserRole: models.ROLE_EDITOR,
|
||||||
|
})
|
||||||
|
store := testinfra.SetUpDatabase(t, dir)
|
||||||
|
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
|
||||||
|
|
||||||
|
// Create the namespace we'll save our alerts to.
|
||||||
|
require.NoError(t, createFolder(t, store, 0, "default"))
|
||||||
|
|
||||||
|
// Now, let's create two alerts.
|
||||||
|
{
|
||||||
|
rules := apimodels.PostableRuleGroupConfig{
|
||||||
|
Name: "arulegroup",
|
||||||
|
Rules: []apimodels.PostableExtendedRuleNode{
|
||||||
|
{
|
||||||
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
||||||
|
OrgID: 2,
|
||||||
|
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(`{
|
||||||
|
"datasource": "__expr__",
|
||||||
|
"type": "math",
|
||||||
|
"expression": "2 + 3 > 1"
|
||||||
|
}`),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
|
||||||
|
OrgID: 2,
|
||||||
|
Title: "AlwaysFiringButSilenced",
|
||||||
|
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(`{
|
||||||
|
"datasource": "__expr__",
|
||||||
|
"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://%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)
|
||||||
|
|
||||||
|
fmt.Println(string(b))
|
||||||
|
assert.Equal(t, resp.StatusCode, 202)
|
||||||
|
require.JSONEq(t, `{"message":"rule group updated successfully"}`, string(b))
|
||||||
|
}
|
||||||
|
|
||||||
|
// With the rules created, let's make sure that rule definition is stored 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)
|
||||||
|
assert.JSONEq(t, `
|
||||||
|
{
|
||||||
|
"default":[
|
||||||
|
{
|
||||||
|
"name":"arulegroup",
|
||||||
|
"interval":"1m",
|
||||||
|
"rules":[
|
||||||
|
{
|
||||||
|
"expr":"",
|
||||||
|
"grafana_alert":{
|
||||||
|
"id":1,
|
||||||
|
"orgId":2,
|
||||||
|
"title":"AlwaysFiring",
|
||||||
|
"condition":"A",
|
||||||
|
"data":[
|
||||||
|
{
|
||||||
|
"refId":"A",
|
||||||
|
"queryType":"",
|
||||||
|
"relativeTimeRange":{
|
||||||
|
"from":18000,
|
||||||
|
"to":10800
|
||||||
|
},
|
||||||
|
"model":{
|
||||||
|
"datasource":"__expr__",
|
||||||
|
"datasourceUid":"-100",
|
||||||
|
"expression":"2 + 3 \u003e 1",
|
||||||
|
"intervalMs":1000,
|
||||||
|
"maxDataPoints":100,
|
||||||
|
"type":"math"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated":"2021-02-21T01:10:30Z",
|
||||||
|
"intervalSeconds":60,
|
||||||
|
"version":1,
|
||||||
|
"uid":"uid",
|
||||||
|
"namespace_uid":"nsuid",
|
||||||
|
"namespace_id":1,
|
||||||
|
"rule_group":"arulegroup",
|
||||||
|
"no_data_state":"",
|
||||||
|
"exec_err_state":""
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"expr":"",
|
||||||
|
"grafana_alert":{
|
||||||
|
"id":2,
|
||||||
|
"orgId":2,
|
||||||
|
"title":"AlwaysFiringButSilenced",
|
||||||
|
"condition":"A",
|
||||||
|
"data":[
|
||||||
|
{
|
||||||
|
"refId":"A",
|
||||||
|
"queryType":"",
|
||||||
|
"relativeTimeRange":{
|
||||||
|
"from":18000,
|
||||||
|
"to":10800
|
||||||
|
},
|
||||||
|
"model":{
|
||||||
|
"datasource":"__expr__",
|
||||||
|
"datasourceUid":"-100",
|
||||||
|
"expression":"2 + 3 \u003e 1",
|
||||||
|
"intervalMs":1000,
|
||||||
|
"maxDataPoints":100,
|
||||||
|
"type":"math"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"updated":"2021-02-21T01:10:30Z",
|
||||||
|
"intervalSeconds":60,
|
||||||
|
"version":1,
|
||||||
|
"uid":"uid",
|
||||||
|
"namespace_uid":"nsuid",
|
||||||
|
"namespace_id":1,
|
||||||
|
"rule_group":"arulegroup",
|
||||||
|
"no_data_state":"",
|
||||||
|
"exec_err_state":""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`, rulesNamespaceWithoutVariableValues(t, b))
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
// Finally, make sure we can delete it.
|
||||||
|
{
|
||||||
|
// If the rule group name does not exists
|
||||||
|
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)
|
||||||
|
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)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
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)
|
||||||
|
require.NoError(t, err)
|
||||||
|
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)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||||
|
require.JSONEq(t, `{"message":"rule group deleted"}`, string(b))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// createFolder creates a folder for storing our alerts under. Grafana uses folders as a replacement for alert namespaces to match its permission model.
|
||||||
|
// We use the dashboard command using IsFolder = true to tell it's a folder, it takes the dashboard as the name of the folder.
|
||||||
|
func createFolder(t *testing.T, store *sqlstore.SQLStore, folderID int64, folderName string) error {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
cmd := models.SaveDashboardCommand{
|
||||||
|
OrgId: 2, // This is the orgID of the anonymous user.
|
||||||
|
FolderId: folderID,
|
||||||
|
IsFolder: true,
|
||||||
|
Dashboard: simplejson.NewFromAny(map[string]interface{}{
|
||||||
|
"title": folderName,
|
||||||
|
}),
|
||||||
|
}
|
||||||
|
_, err := store.SaveDashboard(cmd)
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
var r apimodels.NamespaceConfigResponse
|
||||||
|
require.NoError(t, json.Unmarshal(b, &r))
|
||||||
|
for _, nodes := range r {
|
||||||
|
for _, node := range nodes {
|
||||||
|
for _, rule := range node.Rules {
|
||||||
|
rule.GrafanaManagedAlert.UID = "uid"
|
||||||
|
rule.GrafanaManagedAlert.NamespaceUID = "nsuid"
|
||||||
|
rule.GrafanaManagedAlert.Updated = time.Date(2021, time.Month(2), 21, 1, 10, 30, 0, time.UTC)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
json, err := json.Marshal(&r)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return string(json)
|
||||||
|
}
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/fs"
|
"github.com/grafana/grafana/pkg/infra/fs"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
"github.com/grafana/grafana/pkg/server"
|
"github.com/grafana/grafana/pkg/server"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
@ -202,6 +203,10 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
|
|||||||
_, err = featureSection.NewKey("enable", strings.Join(o.EnableFeatureToggles, " "))
|
_, err = featureSection.NewKey("enable", strings.Join(o.EnableFeatureToggles, " "))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
}
|
}
|
||||||
|
if o.AnonymousUserRole != "" {
|
||||||
|
_, err = anonSect.NewKey("org_role", string(o.AnonymousUserRole))
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgPath := filepath.Join(cfgDir, "test.ini")
|
cfgPath := filepath.Join(cfgDir, "test.ini")
|
||||||
@ -217,4 +222,5 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
|
|||||||
type GrafanaOpts struct {
|
type GrafanaOpts struct {
|
||||||
EnableCSP bool
|
EnableCSP bool
|
||||||
EnableFeatureToggles []string
|
EnableFeatureToggles []string
|
||||||
|
AnonymousUserRole models.RoleType
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user