Alerting: Provisioning API respects global rule quota (#52180)
* Inject interface for quota service and create mock * Check quota and return 403 if limit exceeded * Implement tests for quota being exceeded
This commit is contained in:
@@ -52,7 +52,7 @@ type MuteTimingService interface {
|
||||
|
||||
type AlertRuleService interface {
|
||||
GetAlertRule(ctx context.Context, orgID int64, ruleUID string) (alerting_models.AlertRule, alerting_models.Provenance, error)
|
||||
CreateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error)
|
||||
CreateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance, userID int64) (alerting_models.AlertRule, error)
|
||||
UpdateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error)
|
||||
DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance alerting_models.Provenance) error
|
||||
GetRuleGroup(ctx context.Context, orgID int64, folder, group string) (definitions.AlertRuleGroup, error)
|
||||
@@ -254,7 +254,7 @@ func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *models.ReqContext, UID str
|
||||
}
|
||||
|
||||
func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definitions.ProvisionedAlertRule) response.Response {
|
||||
createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI)
|
||||
createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI, c.UserId)
|
||||
if errors.Is(err, alerting_models.ErrAlertRuleFailedValidation) {
|
||||
return ErrResp(http.StatusBadRequest, err, "")
|
||||
}
|
||||
@@ -262,6 +262,9 @@ func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar definiti
|
||||
if errors.Is(err, store.ErrOptimisticLock) {
|
||||
return ErrResp(http.StatusConflict, err, "")
|
||||
}
|
||||
if errors.Is(err, alerting_models.ErrQuotaReached) {
|
||||
return ErrResp(http.StatusForbidden, err, "")
|
||||
}
|
||||
return ErrResp(http.StatusInternalServerError, err, "")
|
||||
}
|
||||
ar.ID = createdAlertRule.ID
|
||||
|
||||
@@ -15,7 +15,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
secrets "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/secrets"
|
||||
secrets_fakes "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
prometheus "github.com/prometheus/alertmanager/config"
|
||||
@@ -259,6 +260,20 @@ func TestProvisioningApi(t *testing.T) {
|
||||
|
||||
require.Equal(t, 404, response.Status())
|
||||
})
|
||||
|
||||
t.Run("have reached the rule quota, POST returns 403", func(t *testing.T) {
|
||||
env := createTestEnv(t)
|
||||
quotas := provisioning.MockQuotaChecker{}
|
||||
quotas.EXPECT().LimitExceeded()
|
||||
env.quotas = "as
|
||||
sut := createProvisioningSrvSutFromEnv(t, &env)
|
||||
rule := createTestAlertRule("rule", 1)
|
||||
rc := createTestRequestCtx()
|
||||
|
||||
response := sut.RoutePostAlertRule(&rc, rule)
|
||||
|
||||
require.Equal(t, 403, response.Status())
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("alert rule groups", func(t *testing.T) {
|
||||
@@ -284,9 +299,21 @@ func TestProvisioningApi(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func createProvisioningSrvSut(t *testing.T) ProvisioningSrv {
|
||||
// testEnvironment binds together common dependencies for testing alerting APIs.
|
||||
type testEnvironment struct {
|
||||
secrets secrets.Service
|
||||
log log.Logger
|
||||
store store.DBstore
|
||||
configs provisioning.AMConfigStore
|
||||
xact provisioning.TransactionManager
|
||||
quotas provisioning.QuotaChecker
|
||||
prov provisioning.ProvisioningStore
|
||||
}
|
||||
|
||||
func createTestEnv(t *testing.T) testEnvironment {
|
||||
t.Helper()
|
||||
secrets := secrets.NewFakeSecretsService()
|
||||
|
||||
secrets := secrets_fakes.NewFakeSecretsService()
|
||||
log := log.NewNopLogger()
|
||||
configs := &provisioning.MockAMConfigStore{}
|
||||
configs.EXPECT().
|
||||
@@ -298,18 +325,41 @@ func createProvisioningSrvSut(t *testing.T) ProvisioningSrv {
|
||||
SQLStore: sqlStore,
|
||||
BaseInterval: time.Second * 10,
|
||||
}
|
||||
quotas := &provisioning.MockQuotaChecker{}
|
||||
quotas.EXPECT().LimitOK()
|
||||
xact := &provisioning.NopTransactionManager{}
|
||||
prov := &provisioning.MockProvisioningStore{}
|
||||
prov.EXPECT().SaveSucceeds()
|
||||
prov.EXPECT().GetReturns(models.ProvenanceNone)
|
||||
|
||||
return testEnvironment{
|
||||
secrets: secrets,
|
||||
log: log,
|
||||
configs: configs,
|
||||
store: store,
|
||||
xact: xact,
|
||||
prov: prov,
|
||||
quotas: quotas,
|
||||
}
|
||||
}
|
||||
|
||||
func createProvisioningSrvSut(t *testing.T) ProvisioningSrv {
|
||||
t.Helper()
|
||||
|
||||
env := createTestEnv(t)
|
||||
return createProvisioningSrvSutFromEnv(t, &env)
|
||||
}
|
||||
|
||||
func createProvisioningSrvSutFromEnv(t *testing.T, env *testEnvironment) ProvisioningSrv {
|
||||
t.Helper()
|
||||
|
||||
return ProvisioningSrv{
|
||||
log: log,
|
||||
log: env.log,
|
||||
policies: newFakeNotificationPolicyService(),
|
||||
contactPointService: provisioning.NewContactPointService(configs, secrets, prov, xact, log),
|
||||
templates: provisioning.NewTemplateService(configs, prov, xact, log),
|
||||
muteTimings: provisioning.NewMuteTimingService(configs, prov, xact, log),
|
||||
alertRules: provisioning.NewAlertRuleService(store, prov, xact, 60, 10, log),
|
||||
contactPointService: provisioning.NewContactPointService(env.configs, env.secrets, env.prov, env.xact, env.log),
|
||||
templates: provisioning.NewTemplateService(env.configs, env.prov, env.xact, env.log),
|
||||
muteTimings: provisioning.NewMuteTimingService(env.configs, env.prov, env.xact, env.log),
|
||||
alertRules: provisioning.NewAlertRuleService(env.store, env.prov, env.quotas, env.xact, 60, 10, env.log),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,6 @@ type RulerSrv struct {
|
||||
}
|
||||
|
||||
var (
|
||||
errQuotaReached = errors.New("quota has been exceeded")
|
||||
errProvisionedResource = errors.New("request affects resources created via provisioning API")
|
||||
)
|
||||
|
||||
@@ -401,7 +400,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
|
||||
return fmt.Errorf("failed to get alert rules quota: %w", err)
|
||||
}
|
||||
if limitReached {
|
||||
return errQuotaReached
|
||||
return ngmodels.ErrQuotaReached
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -412,7 +411,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod
|
||||
return ErrResp(http.StatusNotFound, err, "failed to update rule group")
|
||||
} else if errors.Is(err, ngmodels.ErrAlertRuleFailedValidation) || errors.Is(err, errProvisionedResource) {
|
||||
return ErrResp(http.StatusBadRequest, err, "failed to update rule group")
|
||||
} else if errors.Is(err, errQuotaReached) {
|
||||
} else if errors.Is(err, ngmodels.ErrQuotaReached) {
|
||||
return ErrResp(http.StatusForbidden, err, "")
|
||||
} else if errors.Is(err, ErrAuthorization) {
|
||||
return ErrResp(http.StatusUnauthorized, err, "")
|
||||
|
||||
Reference in New Issue
Block a user