[Alerting]: Extend quota service to optionally set limits on alerts (#33283)

* Quota: Extend service to set limit on alerts

* Add test for applying quota to alert rules

* Apply suggestions from code review

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Get used alert quota only if naglert is enabled

* Set alert limit to zero if nglalert is not enabled
Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
This commit is contained in:
Sofia Papagiannaki 2021-05-04 19:16:28 +03:00 committed by GitHub
parent 985331e813
commit 540f110220
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 464 additions and 89 deletions

View File

@ -651,6 +651,9 @@ org_data_source = 10
# limit number of api_keys per Org.
org_api_key = 10
# limit number of alerts per Org.
org_alert_rule = 100
# limit number of orgs a user can create.
user_org = 10
@ -669,6 +672,9 @@ global_api_key = -1
# global limit on number of logged in users.
global_session = -1
# global limit of alerts
global_alert_rule = -1
#################################### Alerting ############################
[alerting]
# Disable alerting engine & UI features

View File

@ -640,6 +640,9 @@
# limit number of api_keys per Org.
; org_api_key = 10
# limit number of alerts per Org.
;org_alert_rule = 100
# limit number of orgs a user can create.
; user_org = 10
@ -658,6 +661,9 @@
# global limit on number of logged in users.
; global_session = -1
# global limit of alerts
;global_alert_rule = -1
#################################### Alerting ############################
[alerting]
# Disable alerting engine & UI features

View File

@ -1021,6 +1021,10 @@ Limit the number of data sources allowed per organization. Default is 10.
Limit the number of API keys that can be entered per organization. Default is 10.
### org_alert_rule
Limit the number of alert rules that can be entered per organization. Default is 100.
### user_org
Limit the number of organizations a user can create. Default is 10.
@ -1045,6 +1049,10 @@ Sets global limit of API keys that can be entered. Default is -1 (unlimited).
Sets a global limit on number of users that can be logged in at one time. Default is -1 (unlimited).
### global_alert_rule
Sets a global limit on number of alert rules that can be created. Default is -1 (unlimited).
<hr>
## [alerting]

View File

@ -208,6 +208,61 @@ func TestMiddlewareQuota(t *testing.T) {
cfg.Quota.Org.Dashboard = quotaUsed
cfg.Quota.Enabled = false
})
middlewareScenario(t, "org alert quota reached and ngalert enabled", func(t *testing.T, sc *scenarioContext) {
setUp(sc)
quotaHandler := getQuotaHandler(sc, "alert_rule")
sc.m.Get("/alert_rule", quotaHandler, sc.defaultHandler)
sc.fakeReq("GET", "/alert_rule").exec()
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.FeatureToggles = map[string]bool{"ngalert": true}
cfg.Quota.Org.AlertRule = quotaUsed
})
middlewareScenario(t, "org alert quota not reached and ngalert enabled", func(t *testing.T, sc *scenarioContext) {
setUp(sc)
quotaHandler := getQuotaHandler(sc, "alert_rule")
sc.m.Get("/alert_rule", quotaHandler, sc.defaultHandler)
sc.fakeReq("GET", "/alert_rule").exec()
assert.Equal(t, 200, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.FeatureToggles = map[string]bool{"ngalert": true}
cfg.Quota.Org.AlertRule = quotaUsed + 1
})
middlewareScenario(t, "org alert quota reached but ngalert disabled", func(t *testing.T, sc *scenarioContext) {
// this scenario can only happen if the feature was enabled and later disabled
setUp(sc)
quotaHandler := getQuotaHandler(sc, "alert_rule")
sc.m.Get("/alert_rule", quotaHandler, sc.defaultHandler)
sc.fakeReq("GET", "/alert_rule").exec()
assert.Equal(t, 403, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Org.AlertRule = quotaUsed
})
middlewareScenario(t, "org alert quota not reached but ngalert disabled", func(t *testing.T, sc *scenarioContext) {
setUp(sc)
quotaHandler := getQuotaHandler(sc, "alert_rule")
sc.m.Get("/alert_rule", quotaHandler, sc.defaultHandler)
sc.fakeReq("GET", "/alert_rule").exec()
assert.Equal(t, 200, sc.resp.Code)
}, func(cfg *setting.Cfg) {
configure(cfg)
cfg.Quota.Org.AlertRule = quotaUsed + 1
})
})
}
@ -230,6 +285,7 @@ func configure(cfg *setting.Cfg) {
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
AlertRule: 5,
},
User: &setting.UserQuota{
Org: 5,
@ -241,6 +297,7 @@ func configure(cfg *setting.Cfg) {
DataSource: 5,
ApiKey: 5,
Session: 5,
AlertRule: 5,
},
}
}

View File

@ -47,11 +47,13 @@ type GetOrgQuotaByTargetQuery struct {
Target string
OrgId int64
Default int64
IsNgAlertEnabled bool
Result *OrgQuotaDTO
}
type GetOrgQuotasQuery struct {
OrgId int64
IsNgAlertEnabled bool
Result []*OrgQuotaDTO
}
@ -59,17 +61,20 @@ type GetUserQuotaByTargetQuery struct {
Target string
UserId int64
Default int64
IsNgAlertEnabled bool
Result *UserQuotaDTO
}
type GetUserQuotasQuery struct {
UserId int64
IsNgAlertEnabled bool
Result []*UserQuotaDTO
}
type GetGlobalQuotaByTargetQuery struct {
Target string
Default int64
IsNgAlertEnabled bool
Result *GlobalQuotaDTO
}

View File

@ -3,6 +3,8 @@ package api
import (
"time"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/state"
@ -41,6 +43,7 @@ type API struct {
DatasourceCache datasources.CacheService
RouteRegister routing.RouteRegister
DataService *tsdb.Service
QuotaService *quota.QuotaService
Schedule schedule.ScheduleService
RuleStore store.RuleStore
InstanceStore store.InstanceStore
@ -73,7 +76,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.Metrics) {
api.RegisterRulerApiEndpoints(NewForkedRuler(
api.DatasourceCache,
NewLotexRuler(proxy, logger),
RulerSrv{DatasourceCache: api.DatasourceCache, manager: api.StateManager, store: api.RuleStore, log: logger},
RulerSrv{DatasourceCache: api.DatasourceCache, QuotaService: api.QuotaService, manager: api.StateManager, store: api.RuleStore, log: logger},
), m)
api.RegisterTestingApiEndpoints(TestingApiSrv{
AlertingProxy: proxy,

View File

@ -9,6 +9,7 @@ import (
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/ngalert/state"
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota"
coreapi "github.com/grafana/grafana/pkg/api"
"github.com/grafana/grafana/pkg/api/response"
@ -23,6 +24,7 @@ import (
type RulerSrv struct {
store store.RuleStore
DatasourceCache datasources.CacheService
QuotaService *quota.QuotaService
manager *state.Manager
log log.Logger
}
@ -209,8 +211,18 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf
return toNamespaceErrorResponse(err)
}
// TODO check permissions
// TODO check quota
// quotas are checked in advanced
// that is acceptable under the assumption that there will be only one alert rule under the rule group
// alternatively we should check the quotas after the rule group update
// and rollback the transaction in case of violation
limitReached, err := srv.QuotaService.QuotaReached(c, "alert_rule")
if err != nil {
return response.Error(http.StatusInternalServerError, "failed to get quota", err)
}
if limitReached {
return response.Error(http.StatusForbidden, "quota reached", nil)
}
// TODO validate UID uniqueness in the payload
//TODO: Should this belong in alerting-api?

View File

@ -4,6 +4,8 @@ import (
"context"
"time"
"github.com/grafana/grafana/pkg/services/quota"
"github.com/grafana/grafana/pkg/services/ngalert/metrics"
"github.com/grafana/grafana/pkg/services/ngalert/state"
@ -46,6 +48,7 @@ type AlertNG struct {
DataService *tsdb.Service `inject:""`
Alertmanager *notifier.Alertmanager `inject:""`
DataProxy *datasourceproxy.DatasourceProxyService `inject:""`
QuotaService *quota.QuotaService `inject:""`
Metrics *metrics.Metrics `inject:""`
Log log.Logger
schedule schedule.ScheduleService
@ -83,6 +86,7 @@ func (ng *AlertNG) Init() error {
DataService: ng.DataService,
Schedule: ng.schedule,
DataProxy: ng.DataProxy,
QuotaService: ng.QuotaService,
InstanceStore: store,
RuleStore: store,
AlertingStore: store,

View File

@ -64,7 +64,7 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool,
}
continue
}
query := models.GetGlobalQuotaByTargetQuery{Target: scope.Target}
query := models.GetGlobalQuotaByTargetQuery{Target: scope.Target, IsNgAlertEnabled: qs.Cfg.IsNgAlertEnabled()}
if err := bus.Dispatch(&query); err != nil {
return true, err
}
@ -75,7 +75,7 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool,
if !c.IsSignedIn {
continue
}
query := models.GetOrgQuotaByTargetQuery{OrgId: c.OrgId, Target: scope.Target, Default: scope.DefaultLimit}
query := models.GetOrgQuotaByTargetQuery{OrgId: c.OrgId, Target: scope.Target, Default: scope.DefaultLimit, IsNgAlertEnabled: qs.Cfg.IsNgAlertEnabled()}
if err := bus.Dispatch(&query); err != nil {
return true, err
}
@ -93,7 +93,7 @@ func (qs *QuotaService) QuotaReached(c *models.ReqContext, target string) (bool,
if !c.IsSignedIn || c.UserId == 0 {
continue
}
query := models.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit}
query := models.GetUserQuotaByTargetQuery{UserId: c.UserId, Target: scope.Target, Default: scope.DefaultLimit, IsNgAlertEnabled: qs.Cfg.IsNgAlertEnabled()}
if err := bus.Dispatch(&query); err != nil {
return true, err
}
@ -151,6 +151,12 @@ func (qs *QuotaService) getQuotaScopes(target string) ([]models.QuotaScope, erro
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.Session},
)
return scopes, nil
case "alert_rule": // target need to match the respective database name
scopes = append(scopes,
models.QuotaScope{Name: "global", Target: target, DefaultLimit: qs.Cfg.Quota.Global.AlertRule},
models.QuotaScope{Name: "org", Target: target, DefaultLimit: qs.Cfg.Quota.Org.AlertRule},
)
return scopes, nil
default:
return scopes, ErrInvalidQuotaTarget
}

View File

@ -9,6 +9,8 @@ import (
"github.com/grafana/grafana/pkg/setting"
)
const ALERT_RULE_TARGET = "alert_rule"
func init() {
bus.AddHandler("sql", GetOrgQuotaByTarget)
bus.AddHandler("sql", GetOrgQuotas)
@ -35,18 +37,22 @@ func GetOrgQuotaByTarget(query *models.GetOrgQuotaByTargetQuery) error {
quota.Limit = query.Default
}
var used int64
if query.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(query.Target))
resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL, query.OrgId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
query.Result = &models.OrgQuotaDTO{
Target: query.Target,
Limit: quota.Limit,
OrgId: query.OrgId,
Used: resp[0].Count,
Used: used,
}
return nil
@ -78,17 +84,21 @@ func GetOrgQuotas(query *models.GetOrgQuotasQuery) error {
result := make([]*models.OrgQuotaDTO, len(quotas))
for i, q := range quotas {
var used int64
if q.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where org_id=?", dialect.Quote(q.Target))
resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL, q.OrgId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
result[i] = &models.OrgQuotaDTO{
Target: q.Target,
Limit: q.Limit,
OrgId: q.OrgId,
Used: resp[0].Count,
Used: used,
}
}
query.Result = result
@ -138,18 +148,22 @@ func GetUserQuotaByTarget(query *models.GetUserQuotaByTargetQuery) error {
quota.Limit = query.Default
}
var used int64
if query.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(query.Target))
resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL, query.UserId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
query.Result = &models.UserQuotaDTO{
Target: query.Target,
Limit: quota.Limit,
UserId: query.UserId,
Used: resp[0].Count,
Used: used,
}
return nil
@ -181,17 +195,21 @@ func GetUserQuotas(query *models.GetUserQuotasQuery) error {
result := make([]*models.UserQuotaDTO, len(quotas))
for i, q := range quotas {
var used int64
if q.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s where user_id=?", dialect.Quote(q.Target))
resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL, q.UserId).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
result[i] = &models.UserQuotaDTO{
Target: q.Target,
Limit: q.Limit,
UserId: q.UserId,
Used: resp[0].Count,
Used: used,
}
}
query.Result = result
@ -230,17 +248,22 @@ func UpdateUserQuota(cmd *models.UpdateUserQuotaCmd) error {
}
func GetGlobalQuotaByTarget(query *models.GetGlobalQuotaByTargetQuery) error {
var used int64
if query.Target != ALERT_RULE_TARGET || query.IsNgAlertEnabled {
// get quota used.
rawSQL := fmt.Sprintf("SELECT COUNT(*) as count from %s", dialect.Quote(query.Target))
resp := make([]*targetCount, 0)
if err := x.SQL(rawSQL).Find(&resp); err != nil {
return err
}
used = resp[0].Count
}
query.Result = &models.GlobalQuotaDTO{
Target: query.Target,
Limit: query.Default,
Used: resp[0].Count,
Used: used,
}
return nil

View File

@ -24,6 +24,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
Dashboard: 5,
DataSource: 5,
ApiKey: 5,
AlertRule: 5,
},
User: &setting.UserQuota{
Org: 5,
@ -35,6 +36,7 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
DataSource: 5,
ApiKey: 5,
Session: 5,
AlertRule: 5,
},
}
@ -87,12 +89,19 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
So(err, ShouldBeNil)
So(query.Result.Used, ShouldEqual, 0)
})
Convey("Should be able to get zero used org alert quota when table does not exist (ngalert is not enabled - default case)", func() {
query := models.GetOrgQuotaByTargetQuery{OrgId: 2, Target: "alert", Default: 11}
err = GetOrgQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Used, ShouldEqual, 0)
})
Convey("Should be able to quota list for org", func() {
query := models.GetOrgQuotasQuery{OrgId: orgId}
err = GetOrgQuotas(&query)
So(err, ShouldBeNil)
So(len(query.Result), ShouldEqual, 4)
So(len(query.Result), ShouldEqual, 5)
for _, res := range query.Result {
limit := 5 // default quota limit
used := 0
@ -169,6 +178,14 @@ func TestQuotaCommandsAndQueries(t *testing.T) {
So(query.Result.Limit, ShouldEqual, 5)
So(query.Result.Used, ShouldEqual, 1)
})
Convey("Should be able to get zero used global alert quota when table does not exist (ngalert is not enabled - default case)", func() {
query := models.GetGlobalQuotaByTargetQuery{Target: "alert_rule", Default: 5}
err = GetGlobalQuotaByTarget(&query)
So(err, ShouldBeNil)
So(query.Result.Limit, ShouldEqual, 5)
So(query.Result.Used, ShouldEqual, 0)
})
// related: https://github.com/grafana/grafana/issues/14342
Convey("Should org quota updating is successful even if it called multiple time", func() {

View File

@ -158,7 +158,6 @@ func (ss *SQLStore) ensureMainOrgAndAdminUser() error {
// ensure admin user
if !ss.Cfg.DisableInitAdminCreation {
ss.log.Debug("Creating default admin user")
ss.log.Debug("Creating default admin user")
if _, err := ss.createUser(ctx, sess, userCreationArgs{
Login: ss.Cfg.AdminUser,

View File

@ -9,6 +9,7 @@ type OrgQuota struct {
DataSource int64 `target:"data_source"`
Dashboard int64 `target:"dashboard"`
ApiKey int64 `target:"api_key"`
AlertRule int64 `target:"alert_rule"`
}
type UserQuota struct {
@ -22,6 +23,7 @@ type GlobalQuota struct {
Dashboard int64 `target:"dashboard"`
ApiKey int64 `target:"api_key"`
Session int64 `target:"-"`
AlertRule int64 `target:"alert_rule"`
}
func (q *OrgQuota) ToMap() map[string]int64 {
@ -64,12 +66,19 @@ func (cfg *Cfg) readQuotaSettings() {
quota := cfg.Raw.Section("quota")
Quota.Enabled = quota.Key("enabled").MustBool(false)
var alertOrgQuota int64
var alertGlobalQuota int64
if cfg.IsNgAlertEnabled() {
alertOrgQuota = quota.Key("org_alert_rule").MustInt64(100)
alertGlobalQuota = quota.Key("global_alert_rule").MustInt64(-1)
}
// per ORG Limits
Quota.Org = &OrgQuota{
User: quota.Key("org_user").MustInt64(10),
DataSource: quota.Key("org_data_source").MustInt64(10),
Dashboard: quota.Key("org_dashboard").MustInt64(10),
ApiKey: quota.Key("org_api_key").MustInt64(10),
AlertRule: alertOrgQuota,
}
// per User limits
@ -85,6 +94,7 @@ func (cfg *Cfg) readQuotaSettings() {
Dashboard: quota.Key("global_dashboard").MustInt64(-1),
ApiKey: quota.Key("global_api_key").MustInt64(-1),
Session: quota.Key("global_session").MustInt64(-1),
AlertRule: alertGlobalQuota,
}
cfg.Quota = Quota

View File

@ -2,6 +2,7 @@ package alerting
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io/ioutil"
@ -10,6 +11,8 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/prometheus/common/model"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
@ -26,15 +29,52 @@ import (
func TestAlertAndGroupsQuery(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR,
DisableAnonymous: true,
})
store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// unauthenticated request to get the alerts should fail
{
alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(alertsURL)
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.StatusUnauthorized, resp.StatusCode)
require.JSONEq(t, `{"message": "Unauthorized"}`, string(b))
}
// Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
// invalid credentials request to get the alerts should fail
{
alertsURL := fmt.Sprintf("http://grafana:invalid@%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(alertsURL)
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.StatusUnauthorized, resp.StatusCode)
require.JSONEq(t, `{"error": "invalid username or password","message": "invalid username or password"}`, string(b))
}
// When there are no alerts available, it returns an empty list.
{
alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(alertsURL)
require.NoError(t, err)
@ -50,7 +90,7 @@ func TestAlertAndGroupsQuery(t *testing.T) {
// When are there no alerts available, it returns an empty list of groups.
{
alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts/groups", grafanaListedAddr)
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts/groups", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(alertsURL)
require.NoError(t, err)
@ -106,7 +146,7 @@ func TestAlertAndGroupsQuery(t *testing.T) {
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
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)
t.Cleanup(func() {
@ -119,7 +159,7 @@ func TestAlertAndGroupsQuery(t *testing.T) {
// Eventually, we'll get an alert with its state being active.
{
alertsURL := fmt.Sprintf("http://%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
alertsURL := fmt.Sprintf("http://grafana:password@%s/api/alertmanager/grafana/api/v2/alerts", grafanaListedAddr)
// nolint:gosec
require.Eventually(t, func() bool {
resp, err := http.Get(alertsURL)
@ -150,11 +190,18 @@ func TestAlertRuleCRUD(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR,
EnableQuota: true,
DisableAnonymous: true,
})
store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
err := createUser(t, store, models.ROLE_EDITOR, "grafana", "password")
require.NoError(t, err)
// Create the namespace we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "default"))
@ -229,7 +276,7 @@ func TestAlertRuleCRUD(t *testing.T) {
Annotations: map[string]string{"annotation1": "val1"},
},
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: getLongString(ngstore.AlertRuleMaxTitleLength + 1),
Title: getLongString(t, ngstore.AlertRuleMaxTitleLength+1),
Condition: "A",
Data: []ngmodels.AlertQuery{
{
@ -251,7 +298,7 @@ func TestAlertRuleCRUD(t *testing.T) {
},
{
desc: "alert rule with too long rulegroup",
rulegroup: getLongString(ngstore.AlertRuleMaxTitleLength + 1),
rulegroup: getLongString(t, ngstore.AlertRuleMaxTitleLength+1),
rule: apimodels.PostableExtendedRuleNode{
ApiRuleNode: &apimodels.ApiRuleNode{
For: interval,
@ -386,7 +433,7 @@ func TestAlertRuleCRUD(t *testing.T) {
err := enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
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)
@ -466,7 +513,7 @@ func TestAlertRuleCRUD(t *testing.T) {
err := enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
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)
@ -483,7 +530,7 @@ func TestAlertRuleCRUD(t *testing.T) {
// 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)
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)
@ -523,7 +570,7 @@ func TestAlertRuleCRUD(t *testing.T) {
},
"grafana_alert":{
"id":1,
"orgId":2,
"orgId":1,
"title":"AlwaysFiring",
"condition":"A",
"data":[
@ -558,7 +605,7 @@ func TestAlertRuleCRUD(t *testing.T) {
"expr":"",
"grafana_alert":{
"id":2,
"orgId":2,
"orgId":1,
"title":"AlwaysFiringButSilenced",
"condition":"A",
"data":[
@ -645,7 +692,7 @@ func TestAlertRuleCRUD(t *testing.T) {
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
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)
@ -660,7 +707,7 @@ func TestAlertRuleCRUD(t *testing.T) {
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)
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)
@ -729,7 +776,7 @@ func TestAlertRuleCRUD(t *testing.T) {
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
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)
@ -744,7 +791,7 @@ func TestAlertRuleCRUD(t *testing.T) {
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)
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)
@ -782,7 +829,7 @@ func TestAlertRuleCRUD(t *testing.T) {
},
"grafana_alert":{
"id":1,
"orgId":2,
"orgId":1,
"title":"AlwaysNormal",
"condition":"A",
"data":[
@ -823,7 +870,7 @@ func TestAlertRuleCRUD(t *testing.T) {
// Finally, make sure we can delete it.
{
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)
u := fmt.Sprintf("http://grafana:password@%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)
@ -840,7 +887,7 @@ func TestAlertRuleCRUD(t *testing.T) {
})
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)
u := fmt.Sprintf("http://grafana:password@%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)
@ -856,6 +903,124 @@ func TestAlertRuleCRUD(t *testing.T) {
require.JSONEq(t, `{"message":"rule group deleted"}`, string(b))
})
}
}
func TestQuota(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
EnableQuota: true,
DisableAnonymous: true,
})
store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create the namespace we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "default"))
// Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
interval, err := model.ParseDuration("1m")
require.NoError(t, err)
// check quota limits
t.Run("when quota limit exceed", func(t *testing.T) {
// get existing org quota
query := models.GetOrgQuotaByTargetQuery{OrgId: 1, Target: "alert_rule"}
err = sqlstore.GetOrgQuotaByTarget(&query)
require.NoError(t, err)
used := query.Result.Used
limit := query.Result.Limit
// set org quota limit to equal used
orgCmd := models.UpdateOrgQuotaCmd{
OrgId: 1,
Target: "alert_rule",
Limit: used,
}
err := sqlstore.UpdateOrgQuota(&orgCmd)
require.NoError(t, err)
t.Cleanup(func() {
// reset org quota to original value
orgCmd := models.UpdateOrgQuotaCmd{
OrgId: 1,
Target: "alert_rule",
Limit: limit,
}
err := sqlstore.UpdateOrgQuota(&orgCmd)
require.NoError(t, err)
})
// try to create an alert rule
rules := apimodels.PostableRuleGroupConfig{
Name: "arulegroup",
Interval: interval,
Rules: []apimodels.PostableExtendedRuleNode{
{
GrafanaManagedAlert: &apimodels.PostableGrafanaRule{
Title: "One more alert rule",
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"
}`),
},
},
},
},
},
}
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, http.StatusForbidden, resp.StatusCode)
require.JSONEq(t, `{"message":"quota reached"}`, string(b))
})
}
func TestEval(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
EnableQuota: true,
DisableAnonymous: true,
})
store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
// Create the namespace we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "default"))
// test eval conditions
testCases := []struct {
@ -1022,7 +1187,7 @@ func TestAlertRuleCRUD(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
u := fmt.Sprintf("http://%s/api/v1/rule/test/grafana", grafanaListedAddr)
u := fmt.Sprintf("http://grafana:password@%s/api/v1/rule/test/grafana", grafanaListedAddr)
r := strings.NewReader(tc.payload)
// nolint:gosec
resp, err := http.Post(u, "application/json", r)
@ -1178,7 +1343,7 @@ func TestAlertRuleCRUD(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.desc, func(t *testing.T) {
u := fmt.Sprintf("http://%s/api/v1/eval", grafanaListedAddr)
u := fmt.Sprintf("http://grafana:password@%s/api/v1/eval", grafanaListedAddr)
r := strings.NewReader(tc.payload)
// nolint:gosec
resp, err := http.Post(u, "application/json", r)
@ -1202,7 +1367,7 @@ func createFolder(t *testing.T, store *sqlstore.SQLStore, folderID int64, folder
t.Helper()
cmd := models.SaveDashboardCommand{
OrgId: 2, // This is the orgID of the anonymous user.
OrgId: 1, // default organisation
FolderId: folderID,
IsFolder: true,
Dashboard: simplejson.NewFromAny(map[string]interface{}{
@ -1244,7 +1409,21 @@ func rulesNamespaceWithoutVariableValues(t *testing.T, b []byte) (string, map[st
return string(json), m
}
func getLongString(n int) string {
func createUser(t *testing.T, store *sqlstore.SQLStore, role models.RoleType, username, password string) error {
t.Helper()
cmd := models.CreateUserCommand{
Login: username,
Password: password,
DefaultOrgRole: string(role),
}
_, err := store.CreateUser(context.Background(), cmd)
return err
}
func getLongString(t *testing.T, n int) string {
t.Helper()
b := make([]rune, n)
for i := range b {
b[i] = 'a'

View File

@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
@ -21,18 +22,23 @@ import (
func TestPrometheusRules(t *testing.T) {
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR,
DisableAnonymous: true,
})
store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create the namespace we'll save our alerts to.
// Create the namespace under default organisation (orgID = 1) where we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "default"))
// Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
interval, err := model.ParseDuration("10s")
require.NoError(t, err)
// When we have no alerting rules, it returns an empty list.
// an unauthenticated request to get rules should fail
{
promRulesURL := fmt.Sprintf("http://%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec
@ -42,6 +48,20 @@ func TestPrometheusRules(t *testing.T) {
err := resp.Body.Close()
require.NoError(t, err)
})
require.NoError(t, err)
assert.Equal(t, 401, resp.StatusCode)
}
// When we have no alerting rules, it returns an empty list.
{
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(promRulesURL)
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, 200, resp.StatusCode)
@ -109,7 +129,7 @@ func TestPrometheusRules(t *testing.T) {
err := enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/default", grafanaListedAddr)
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)
@ -126,7 +146,7 @@ func TestPrometheusRules(t *testing.T) {
// Now, let's see how this looks like.
{
promRulesURL := fmt.Sprintf("http://%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(promRulesURL)
require.NoError(t, err)
@ -181,7 +201,7 @@ func TestPrometheusRules(t *testing.T) {
}
{
promRulesURL := fmt.Sprintf("http://%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
promRulesURL := fmt.Sprintf("http://grafana:password@%s/api/prometheus/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec
require.Eventually(t, func() bool {
resp, err := http.Get(promRulesURL)

View File

@ -9,6 +9,7 @@ import (
"testing"
"time"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/models"
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
@ -22,11 +23,16 @@ func TestAlertRulePermissions(t *testing.T) {
// Setup Grafana and its Database
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
EnableFeatureToggles: []string{"ngalert"},
AnonymousUserRole: models.ROLE_EDITOR,
DisableAnonymous: true,
})
store := testinfra.SetUpDatabase(t, dir)
// override bus to get the GetSignedInUserQuery handler
store.Bus = bus.GetBus()
grafanaListedAddr := testinfra.StartGrafana(t, dir, path, store)
// Create a user to make authenticated requests
require.NoError(t, createUser(t, store, models.ROLE_EDITOR, "grafana", "password"))
// Create the namespace we'll save our alerts to.
require.NoError(t, createFolder(t, store, 0, "folder1"))
@ -41,7 +47,7 @@ func TestAlertRulePermissions(t *testing.T) {
// With the rules created, let's make sure that rule definitions are stored.
{
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules", grafanaListedAddr)
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules", grafanaListedAddr)
// nolint:gosec
resp, err := http.Get(u)
require.NoError(t, err)
@ -73,7 +79,7 @@ func TestAlertRulePermissions(t *testing.T) {
},
"grafana_alert":{
"id":1,
"orgId":2,
"orgId":1,
"title":"rule under folder folder1",
"condition":"A",
"data":[
@ -123,7 +129,7 @@ func TestAlertRulePermissions(t *testing.T) {
},
"grafana_alert":{
"id":2,
"orgId":2,
"orgId":1,
"title":"rule under folder folder2",
"condition":"A",
"data":[
@ -195,7 +201,7 @@ func TestAlertRulePermissions(t *testing.T) {
},
"grafana_alert":{
"id":1,
"orgId":2,
"orgId":1,
"title":"rule under folder folder1",
"condition":"A",
"data":[
@ -276,7 +282,7 @@ func createRule(t *testing.T, grafanaListedAddr string, folder string) {
err = enc.Encode(&rules)
require.NoError(t, err)
u := fmt.Sprintf("http://%s/api/ruler/grafana/api/v1/rules/%s", grafanaListedAddr, folder)
u := fmt.Sprintf("http://grafana:password@%s/api/ruler/grafana/api/v1/rules/%s", grafanaListedAddr, folder)
// nolint:gosec
resp, err := http.Post(u, "application/json", &buf)
require.NoError(t, err)

View File

@ -219,6 +219,18 @@ func CreateGrafDir(t *testing.T, opts ...GrafanaOpts) (string, string) {
_, err = anonSect.NewKey("org_role", string(o.AnonymousUserRole))
require.NoError(t, err)
}
if o.EnableQuota {
quotaSection, err := cfg.NewSection("quota")
require.NoError(t, err)
_, err = quotaSection.NewKey("enabled", "true")
require.NoError(t, err)
}
if o.DisableAnonymous {
anonSect, err := cfg.GetSection("auth.anonymous")
require.NoError(t, err)
_, err = anonSect.NewKey("enabled", "false")
require.NoError(t, err)
}
}
cfgPath := filepath.Join(cfgDir, "test.ini")
@ -235,4 +247,6 @@ type GrafanaOpts struct {
EnableCSP bool
EnableFeatureToggles []string
AnonymousUserRole models.RoleType
EnableQuota bool
DisableAnonymous bool
}