mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 02:10:45 -06:00
[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:
parent
985331e813
commit
540f110220
@ -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
|
||||
|
@ -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
|
||||
|
@ -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]
|
||||
|
@ -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,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
|
@ -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?
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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() {
|
||||
|
@ -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,
|
||||
|
@ -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
|
||||
|
@ -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'
|
||||
|
@ -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)
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user