mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: rules delete API to check data source authorization (#46906)
* merge RuleSrv rule delete methods * remove unused store methods * implement delete by uid for fake store * add scheduler mock * implement tests for RouteDeleteAlertRules
This commit is contained in:
parent
a06329d988
commit
e20d157a9b
@ -42,52 +42,82 @@ var (
|
||||
errQuotaReached = errors.New("quota has been exceeded")
|
||||
)
|
||||
|
||||
func (srv RulerSrv) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response {
|
||||
// RouteDeleteAlertRules deletes all alert rules user is authorized to access in the namespace (request parameter :Namespace)
|
||||
// or, if specified, a group of rules (request parameter :Groupname) in the namespace
|
||||
func (srv RulerSrv) RouteDeleteAlertRules(c *models.ReqContext) response.Response {
|
||||
namespaceTitle := web.Params(c.Req)[":Namespace"]
|
||||
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
|
||||
if err != nil {
|
||||
return toNamespaceErrorResponse(err)
|
||||
}
|
||||
var loggerCtx = []interface{}{
|
||||
"namespace",
|
||||
namespace.Title,
|
||||
}
|
||||
var ruleGroup *string
|
||||
if group, ok := web.Params(c.Req)[":Groupname"]; ok {
|
||||
ruleGroup = &group
|
||||
loggerCtx = append(loggerCtx, "group", group)
|
||||
}
|
||||
logger := srv.log.New(loggerCtx...)
|
||||
|
||||
uids, err := srv.store.DeleteNamespaceAlertRules(c.Req.Context(), c.SignedInUser.OrgId, namespace.Uid)
|
||||
if err != nil {
|
||||
return ErrResp(http.StatusInternalServerError, err, "failed to delete namespace alert rules")
|
||||
hasAccess := func(evaluator accesscontrol.Evaluator) bool {
|
||||
return accesscontrol.HasAccess(srv.ac, c)(accesscontrol.ReqOrgAdminOrEditor, evaluator)
|
||||
}
|
||||
|
||||
for _, uid := range uids {
|
||||
srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
UID: uid,
|
||||
})
|
||||
}
|
||||
var canDelete, cannotDelete []string
|
||||
err = srv.xactManager.InTransaction(c.Req.Context(), func(ctx context.Context) error {
|
||||
q := ngmodels.GetAlertRulesQuery{
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
NamespaceUID: namespace.Uid,
|
||||
RuleGroup: ruleGroup,
|
||||
}
|
||||
if err = srv.store.GetAlertRules(ctx, &q); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules deleted"})
|
||||
}
|
||||
if len(q.Result) == 0 {
|
||||
logger.Debug("no alert rules to delete from namespace/group")
|
||||
return nil
|
||||
}
|
||||
|
||||
func (srv RulerSrv) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Response {
|
||||
namespaceTitle := web.Params(c.Req)[":Namespace"]
|
||||
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
|
||||
if err != nil {
|
||||
return toNamespaceErrorResponse(err)
|
||||
}
|
||||
ruleGroup := web.Params(c.Req)[":Groupname"]
|
||||
uids, err := srv.store.DeleteRuleGroupAlertRules(c.Req.Context(), c.SignedInUser.OrgId, namespace.Uid, ruleGroup)
|
||||
canDelete = make([]string, 0, len(q.Result))
|
||||
for _, rule := range q.Result {
|
||||
if authorizeDatasourceAccessForRule(rule, hasAccess) {
|
||||
canDelete = append(canDelete, rule.UID)
|
||||
continue
|
||||
}
|
||||
cannotDelete = append(cannotDelete, rule.UID)
|
||||
}
|
||||
|
||||
if len(canDelete) == 0 {
|
||||
return fmt.Errorf("%w to delete rules because user is not authorized to access data sources used by the rules", ErrAuthorization)
|
||||
}
|
||||
|
||||
if len(cannotDelete) > 0 {
|
||||
logger.Info("user cannot delete one or many alert rules because it does not have access to data sources. Those rules will be skipped", "expected", len(q.Result), "authorized", len(canDelete), "unauthorized", cannotDelete)
|
||||
}
|
||||
|
||||
return srv.store.DeleteAlertRulesByUID(ctx, c.SignedInUser.OrgId, canDelete...)
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, ngmodels.ErrRuleGroupNamespaceNotFound) {
|
||||
return ErrResp(http.StatusNotFound, err, "failed to delete rule group")
|
||||
if errors.Is(err, ErrAuthorization) {
|
||||
return ErrResp(http.StatusUnauthorized, err, "")
|
||||
}
|
||||
return ErrResp(http.StatusInternalServerError, err, "failed to delete rule group")
|
||||
}
|
||||
|
||||
for _, uid := range uids {
|
||||
logger.Debug("rules have been deleted from the store. updating scheduler")
|
||||
|
||||
for _, uid := range canDelete {
|
||||
srv.scheduleService.DeleteAlertRule(ngmodels.AlertRuleKey{
|
||||
OrgID: c.SignedInUser.OrgId,
|
||||
UID: uid,
|
||||
})
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"})
|
||||
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rules deleted"})
|
||||
}
|
||||
|
||||
func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response {
|
||||
|
@ -4,15 +4,24 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/mock"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
models2 "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||
acMock "github.com/grafana/grafana/pkg/services/accesscontrol/mock"
|
||||
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/datasources"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
)
|
||||
|
||||
func TestCalculateChanges(t *testing.T) {
|
||||
@ -255,6 +264,266 @@ func TestCalculateChanges(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestRouteDeleteAlertRules(t *testing.T) {
|
||||
createService := func(ac *acMock.Mock, store *store.FakeRuleStore, scheduler schedule.ScheduleService) *RulerSrv {
|
||||
return &RulerSrv{
|
||||
xactManager: store,
|
||||
store: store,
|
||||
DatasourceCache: nil,
|
||||
QuotaService: nil,
|
||||
scheduleService: scheduler,
|
||||
log: log.New("test"),
|
||||
cfg: nil,
|
||||
ac: ac,
|
||||
}
|
||||
}
|
||||
|
||||
getRecordedCommand := func(ruleStore *store.FakeRuleStore) []store.GenericRecordedQuery {
|
||||
results := ruleStore.GetRecordedCommands(func(cmd interface{}) (interface{}, bool) {
|
||||
c, ok := cmd.(store.GenericRecordedQuery)
|
||||
if !ok || c.Name != "DeleteAlertRulesByUID" {
|
||||
return nil, false
|
||||
}
|
||||
return c, ok
|
||||
})
|
||||
var result []store.GenericRecordedQuery
|
||||
for _, cmd := range results {
|
||||
result = append(result, cmd.(store.GenericRecordedQuery))
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
assertRulesDeleted := func(t *testing.T, expectedRules []*models.AlertRule, ruleStore *store.FakeRuleStore, scheduler *schedule.FakeScheduleService) {
|
||||
deleteCommands := getRecordedCommand(ruleStore)
|
||||
require.Len(t, deleteCommands, 1)
|
||||
cmd := deleteCommands[0]
|
||||
actualUIDs := cmd.Params[1].([]string)
|
||||
require.Len(t, actualUIDs, len(expectedRules))
|
||||
for _, rule := range expectedRules {
|
||||
require.Containsf(t, actualUIDs, rule.UID, "Rule %s was expected to be deleted but it wasn't", rule.UID)
|
||||
}
|
||||
|
||||
require.Len(t, scheduler.Calls, len(expectedRules))
|
||||
for _, call := range scheduler.Calls {
|
||||
require.Equal(t, "DeleteAlertRule", call.Method)
|
||||
key, ok := call.Arguments.Get(0).(models.AlertRuleKey)
|
||||
require.Truef(t, ok, "Expected AlertRuleKey but got something else")
|
||||
found := false
|
||||
for _, rule := range expectedRules {
|
||||
if rule.GetKey() == key {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
require.Truef(t, found, "Key %v was not expected to be submitted to scheduler", key)
|
||||
}
|
||||
}
|
||||
|
||||
t.Run("when fine-grained access is disabled", func(t *testing.T) {
|
||||
t.Run("viewer should not be authorized", func(t *testing.T) {
|
||||
ruleStore := store.NewFakeRuleStore(t)
|
||||
orgID := rand.Int63()
|
||||
folder := randFolder()
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
|
||||
|
||||
scheduler := &schedule.FakeScheduleService{}
|
||||
scheduler.On("DeleteAlertRule", mock.Anything).Panic("should not be called")
|
||||
|
||||
ac := acMock.New().WithDisabled()
|
||||
request := createRequestContext(orgID, models2.ROLE_VIEWER, map[string]string{
|
||||
":Namespace": folder.Title,
|
||||
})
|
||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
|
||||
require.Equalf(t, 401, response.Status(), "Expected 403 but got %d: %v", response.Status(), string(response.Body()))
|
||||
|
||||
scheduler.AssertNotCalled(t, "DeleteAlertRule")
|
||||
require.Empty(t, getRecordedCommand(ruleStore))
|
||||
})
|
||||
t.Run("editor should be able to delete all rules in folder", func(t *testing.T) {
|
||||
ruleStore := store.NewFakeRuleStore(t)
|
||||
orgID := rand.Int63()
|
||||
folder := randFolder()
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
rulesInFolder := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))
|
||||
ruleStore.PutRule(context.Background(), rulesInFolder...)
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
|
||||
|
||||
scheduler := &schedule.FakeScheduleService{}
|
||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
||||
|
||||
ac := acMock.New().WithDisabled()
|
||||
request := createRequestContext(orgID, models2.ROLE_EDITOR, map[string]string{
|
||||
":Namespace": folder.Title,
|
||||
})
|
||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
|
||||
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
||||
assertRulesDeleted(t, rulesInFolder, ruleStore, scheduler)
|
||||
})
|
||||
t.Run("editor should be able to delete rules in a group in a folder", func(t *testing.T) {
|
||||
ruleStore := store.NewFakeRuleStore(t)
|
||||
orgID := rand.Int63()
|
||||
groupName := util.GenerateShortUID()
|
||||
folder := randFolder()
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
rulesInFolderInGroup := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))
|
||||
ruleStore.PutRule(context.Background(), rulesInFolderInGroup...)
|
||||
// rules in different groups but in the same namespace
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
|
||||
// rules in the same group but different folder
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withGroup(groupName)))...)
|
||||
|
||||
scheduler := &schedule.FakeScheduleService{}
|
||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
||||
|
||||
ac := acMock.New().WithDisabled()
|
||||
request := createRequestContext(orgID, models2.ROLE_EDITOR, map[string]string{
|
||||
":Namespace": folder.Title,
|
||||
":Groupname": groupName,
|
||||
})
|
||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
|
||||
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
||||
assertRulesDeleted(t, rulesInFolderInGroup, ruleStore, scheduler)
|
||||
})
|
||||
})
|
||||
t.Run("when fine-grained access is enabled", func(t *testing.T) {
|
||||
t.Run("and user does not have access to any of data sources used by alert rules", func(t *testing.T) {
|
||||
ruleStore := store.NewFakeRuleStore(t)
|
||||
orgID := rand.Int63()
|
||||
folder := randFolder()
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
|
||||
|
||||
scheduler := &schedule.FakeScheduleService{}
|
||||
scheduler.On("DeleteAlertRule", mock.Anything).Panic("should not be called")
|
||||
|
||||
ac := acMock.New()
|
||||
request := createRequestContext(orgID, "None", map[string]string{
|
||||
":Namespace": folder.Title,
|
||||
})
|
||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
|
||||
require.Equalf(t, 401, response.Status(), "Expected 403 but got %d: %v", response.Status(), string(response.Body()))
|
||||
|
||||
scheduler.AssertNotCalled(t, "DeleteAlertRule")
|
||||
require.Empty(t, getRecordedCommand(ruleStore))
|
||||
})
|
||||
t.Run("and user has access to all alert rules", func(t *testing.T) {
|
||||
t.Run("should delete all rules", func(t *testing.T) {
|
||||
ruleStore := store.NewFakeRuleStore(t)
|
||||
orgID := rand.Int63()
|
||||
folder := randFolder()
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
rulesInFolder := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))
|
||||
ruleStore.PutRule(context.Background(), rulesInFolder...)
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
|
||||
|
||||
scheduler := &schedule.FakeScheduleService{}
|
||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
||||
|
||||
var permissions []*accesscontrol.Permission
|
||||
for _, rule := range rulesInFolder {
|
||||
for _, query := range rule.Data {
|
||||
permissions = append(permissions, &accesscontrol.Permission{
|
||||
Action: datasources.ActionQuery, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.DatasourceUID),
|
||||
})
|
||||
}
|
||||
}
|
||||
ac := acMock.New().WithPermissions(permissions)
|
||||
request := createRequestContext(orgID, "None", map[string]string{
|
||||
":Namespace": folder.Title,
|
||||
})
|
||||
|
||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
|
||||
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
||||
assertRulesDeleted(t, rulesInFolder, ruleStore, scheduler)
|
||||
})
|
||||
})
|
||||
t.Run("and user has access to data sources of some of alert rules", func(t *testing.T) {
|
||||
t.Run("should delete only those that are accessible in folder", func(t *testing.T) {
|
||||
ruleStore := store.NewFakeRuleStore(t)
|
||||
orgID := rand.Int63()
|
||||
folder := randFolder()
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
authorizedRulesInFolder := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))
|
||||
ruleStore.PutRule(context.Background(), authorizedRulesInFolder...)
|
||||
// more rules in the same namespace but user does not have access to them
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID)))...)
|
||||
|
||||
scheduler := &schedule.FakeScheduleService{}
|
||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
||||
|
||||
var permissions []*accesscontrol.Permission
|
||||
for _, rule := range authorizedRulesInFolder {
|
||||
for _, query := range rule.Data {
|
||||
permissions = append(permissions, &accesscontrol.Permission{
|
||||
Action: datasources.ActionQuery, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.DatasourceUID),
|
||||
})
|
||||
}
|
||||
}
|
||||
ac := acMock.New().WithPermissions(permissions)
|
||||
request := createRequestContext(orgID, "None", map[string]string{
|
||||
":Namespace": folder.Title,
|
||||
})
|
||||
|
||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
|
||||
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
||||
assertRulesDeleted(t, authorizedRulesInFolder, ruleStore, scheduler)
|
||||
})
|
||||
t.Run("should delete only rules in a group that are authorized", func(t *testing.T) {
|
||||
ruleStore := store.NewFakeRuleStore(t)
|
||||
orgID := rand.Int63()
|
||||
groupName := util.GenerateShortUID()
|
||||
folder := randFolder()
|
||||
ruleStore.Folders[orgID] = append(ruleStore.Folders[orgID], folder)
|
||||
authorizedRulesInGroup := models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))
|
||||
ruleStore.PutRule(context.Background(), authorizedRulesInGroup...)
|
||||
// more rules in the same group but user is not authorized to access them
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder), withGroup(groupName)))...)
|
||||
// rules in different groups but in the same namespace
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withNamespace(folder)))...)
|
||||
// rules in the same group but different folder
|
||||
ruleStore.PutRule(context.Background(), models.GenerateAlertRules(rand.Intn(4)+2, models.AlertRuleGen(withOrgID(orgID), withGroup(groupName)))...)
|
||||
|
||||
scheduler := &schedule.FakeScheduleService{}
|
||||
scheduler.On("DeleteAlertRule", mock.Anything)
|
||||
|
||||
var permissions []*accesscontrol.Permission
|
||||
for _, rule := range authorizedRulesInGroup {
|
||||
for _, query := range rule.Data {
|
||||
permissions = append(permissions, &accesscontrol.Permission{
|
||||
Action: datasources.ActionQuery, Scope: dashboards.ScopeFoldersProvider.GetResourceScopeUID(query.DatasourceUID),
|
||||
})
|
||||
}
|
||||
}
|
||||
ac := acMock.New().WithPermissions(permissions)
|
||||
request := createRequestContext(orgID, "None", map[string]string{
|
||||
":Namespace": folder.Title,
|
||||
":Groupname": groupName,
|
||||
})
|
||||
response := createService(ac, ruleStore, scheduler).RouteDeleteAlertRules(request)
|
||||
require.Equalf(t, 202, response.Status(), "Expected 202 but got %d: %v", response.Status(), string(response.Body()))
|
||||
assertRulesDeleted(t, authorizedRulesInGroup, ruleStore, scheduler)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func createRequestContext(orgID int64, role models2.RoleType, params map[string]string) *models2.ReqContext {
|
||||
ctx := web.Context{Req: &http.Request{}}
|
||||
ctx.Req = web.SetURLParams(ctx.Req, params)
|
||||
|
||||
return &models2.ReqContext{
|
||||
SignedInUser: &models2.SignedInUser{
|
||||
OrgRole: role,
|
||||
OrgId: orgID,
|
||||
},
|
||||
Context: &ctx,
|
||||
}
|
||||
}
|
||||
|
||||
func withOrgID(orgId int64) func(rule *models.AlertRule) {
|
||||
return func(rule *models.AlertRule) {
|
||||
rule.OrgID = orgId
|
||||
|
@ -110,11 +110,11 @@ func (f *ForkedRulerApi) forkRoutePostNameRulesConfig(ctx *models.ReqContext, co
|
||||
}
|
||||
|
||||
func (f *ForkedRulerApi) forkRouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
|
||||
return f.GrafanaRuler.RouteDeleteNamespaceRulesConfig(ctx)
|
||||
return f.GrafanaRuler.RouteDeleteAlertRules(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedRulerApi) forkRouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response {
|
||||
return f.GrafanaRuler.RouteDeleteRuleGroupConfig(ctx)
|
||||
return f.GrafanaRuler.RouteDeleteAlertRules(ctx)
|
||||
}
|
||||
|
||||
func (f *ForkedRulerApi) forkRouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response {
|
||||
|
@ -26,6 +26,7 @@ import (
|
||||
|
||||
// ScheduleService is an interface for a service that schedules the evaluation
|
||||
// of alert rules.
|
||||
//go:generate mockery --name ScheduleService --structname FakeScheduleService --inpackage --filename schedule_mock.go
|
||||
type ScheduleService interface {
|
||||
// Run the scheduler until the context is canceled or the scheduler returns
|
||||
// an error. The scheduler is terminated when this function returns.
|
||||
|
118
pkg/services/ngalert/schedule/schedule_mock.go
Normal file
118
pkg/services/ngalert/schedule/schedule_mock.go
Normal file
@ -0,0 +1,118 @@
|
||||
// Code generated by mockery v2.10.0. DO NOT EDIT.
|
||||
|
||||
package schedule
|
||||
|
||||
import (
|
||||
context "context"
|
||||
|
||||
models "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
mock "github.com/stretchr/testify/mock"
|
||||
|
||||
time "time"
|
||||
|
||||
url "net/url"
|
||||
)
|
||||
|
||||
// FakeScheduleService is an autogenerated mock type for the ScheduleService type
|
||||
type FakeScheduleService struct {
|
||||
mock.Mock
|
||||
}
|
||||
|
||||
// AlertmanagersFor provides a mock function with given fields: orgID
|
||||
func (_m *FakeScheduleService) AlertmanagersFor(orgID int64) []*url.URL {
|
||||
ret := _m.Called(orgID)
|
||||
|
||||
var r0 []*url.URL
|
||||
if rf, ok := ret.Get(0).(func(int64) []*url.URL); ok {
|
||||
r0 = rf(orgID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*url.URL)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// DeleteAlertRule provides a mock function with given fields: key
|
||||
func (_m *FakeScheduleService) DeleteAlertRule(key models.AlertRuleKey) {
|
||||
_m.Called(key)
|
||||
}
|
||||
|
||||
// DroppedAlertmanagersFor provides a mock function with given fields: orgID
|
||||
func (_m *FakeScheduleService) DroppedAlertmanagersFor(orgID int64) []*url.URL {
|
||||
ret := _m.Called(orgID)
|
||||
|
||||
var r0 []*url.URL
|
||||
if rf, ok := ret.Get(0).(func(int64) []*url.URL); ok {
|
||||
r0 = rf(orgID)
|
||||
} else {
|
||||
if ret.Get(0) != nil {
|
||||
r0 = ret.Get(0).([]*url.URL)
|
||||
}
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Pause provides a mock function with given fields:
|
||||
func (_m *FakeScheduleService) Pause() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Run provides a mock function with given fields: _a0
|
||||
func (_m *FakeScheduleService) Run(_a0 context.Context) error {
|
||||
ret := _m.Called(_a0)
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func(context.Context) error); ok {
|
||||
r0 = rf(_a0)
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// Unpause provides a mock function with given fields:
|
||||
func (_m *FakeScheduleService) Unpause() error {
|
||||
ret := _m.Called()
|
||||
|
||||
var r0 error
|
||||
if rf, ok := ret.Get(0).(func() error); ok {
|
||||
r0 = rf()
|
||||
} else {
|
||||
r0 = ret.Error(0)
|
||||
}
|
||||
|
||||
return r0
|
||||
}
|
||||
|
||||
// UpdateAlertRule provides a mock function with given fields: key
|
||||
func (_m *FakeScheduleService) UpdateAlertRule(key models.AlertRuleKey) {
|
||||
_m.Called(key)
|
||||
}
|
||||
|
||||
// evalApplied provides a mock function with given fields: _a0, _a1
|
||||
func (_m *FakeScheduleService) evalApplied(_a0 models.AlertRuleKey, _a1 time.Time) {
|
||||
_m.Called(_a0, _a1)
|
||||
}
|
||||
|
||||
// overrideCfg provides a mock function with given fields: cfg
|
||||
func (_m *FakeScheduleService) overrideCfg(cfg SchedulerCfg) {
|
||||
_m.Called(cfg)
|
||||
}
|
||||
|
||||
// stopApplied provides a mock function with given fields: _a0
|
||||
func (_m *FakeScheduleService) stopApplied(_a0 models.AlertRuleKey) {
|
||||
_m.Called(_a0)
|
||||
}
|
@ -36,8 +36,6 @@ type UpsertRule struct {
|
||||
// Store is the interface for persisting alert rules and instances
|
||||
type RuleStore interface {
|
||||
DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error
|
||||
DeleteNamespaceAlertRules(ctx context.Context, orgID int64, namespaceUID string) ([]string, error)
|
||||
DeleteRuleGroupAlertRules(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) ([]string, error)
|
||||
DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error
|
||||
GetAlertRuleByUID(ctx context.Context, query *ngmodels.GetAlertRuleByUIDQuery) error
|
||||
GetAlertRulesForScheduling(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error
|
||||
@ -88,76 +86,6 @@ func (st DBstore) DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUI
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteNamespaceAlertRules is a handler for deleting namespace alert rules. A list of deleted rule UIDs are returned.
|
||||
func (st DBstore) DeleteNamespaceAlertRules(ctx context.Context, orgID int64, namespaceUID string) ([]string, error) {
|
||||
ruleUIDs := []string{}
|
||||
|
||||
err := st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
if err := sess.SQL("SELECT uid FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID).Find(&ruleUIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? and namespace_uid = ?", orgID, namespaceUID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ?", orgID, namespaceUID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec(`DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid NOT IN (
|
||||
SELECT uid FROM alert_rule where org_id = ?
|
||||
)`, orgID, orgID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
return ruleUIDs, err
|
||||
}
|
||||
|
||||
// DeleteRuleGroupAlertRules is a handler for deleting rule group alert rules. A list of deleted rule UIDs are returned.
|
||||
func (st DBstore) DeleteRuleGroupAlertRules(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) ([]string, error) {
|
||||
ruleUIDs := []string{}
|
||||
|
||||
err := st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
if err := sess.SQL("SELECT uid FROM alert_rule WHERE org_id = ? and namespace_uid = ? and rule_group = ?",
|
||||
orgID, namespaceUID, ruleGroup).Find(&ruleUIDs); err != nil {
|
||||
return err
|
||||
}
|
||||
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 {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec("DELETE FROM alert_rule_version WHERE rule_org_id = ? and rule_namespace_uid = ? and rule_group = ?", orgID, namespaceUID, ruleGroup); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := sess.Exec(`DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid NOT IN (
|
||||
SELECT uid FROM alert_rule where org_id = ?
|
||||
)`, orgID, orgID); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
return ruleUIDs, err
|
||||
}
|
||||
|
||||
// DeleteAlertInstanceByRuleUID is a handler for deleting alert instances by alert rule UID when a rule has been updated
|
||||
func (st DBstore) DeleteAlertInstancesByRuleUID(ctx context.Context, orgID int64, ruleUID string) error {
|
||||
return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error {
|
||||
|
@ -3,13 +3,16 @@ package store
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/services/annotations"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
|
||||
models2 "github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
@ -27,6 +30,7 @@ func NewFakeRuleStore(t *testing.T) *FakeRuleStore {
|
||||
Hook: func(interface{}) error {
|
||||
return nil
|
||||
},
|
||||
Folders: map[int64][]*models2.Folder{},
|
||||
}
|
||||
}
|
||||
|
||||
@ -38,6 +42,12 @@ type FakeRuleStore struct {
|
||||
Rules map[int64][]*models.AlertRule
|
||||
Hook func(cmd interface{}) error // use Hook if you need to intercept some query and return an error
|
||||
RecordedOps []interface{}
|
||||
Folders map[int64][]*models2.Folder
|
||||
}
|
||||
|
||||
type GenericRecordedQuery struct {
|
||||
Name string
|
||||
Params []interface{}
|
||||
}
|
||||
|
||||
// PutRule puts the rule in the Rules map. If there are existing rule in the same namespace, they will be overwritten
|
||||
@ -55,6 +65,23 @@ mainloop:
|
||||
}
|
||||
rgs = append(rgs, r)
|
||||
f.Rules[r.OrgID] = rgs
|
||||
|
||||
var existing *models2.Folder
|
||||
folders := f.Folders[r.OrgID]
|
||||
for _, folder := range folders {
|
||||
if folder.Uid == r.NamespaceUID {
|
||||
existing = folder
|
||||
break
|
||||
}
|
||||
}
|
||||
if existing == nil {
|
||||
folders = append(folders, &models2.Folder{
|
||||
Id: rand.Int63(),
|
||||
Uid: r.NamespaceUID,
|
||||
Title: "TEST-FOLDER-" + util.GenerateShortUID(),
|
||||
})
|
||||
f.Folders[r.OrgID] = folders
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -74,15 +101,33 @@ func (f *FakeRuleStore) GetRecordedCommands(predicate func(cmd interface{}) (int
|
||||
return result
|
||||
}
|
||||
|
||||
func (f *FakeRuleStore) DeleteAlertRulesByUID(_ context.Context, _ int64, _ ...string) error {
|
||||
func (f *FakeRuleStore) DeleteAlertRulesByUID(_ context.Context, orgID int64, UIDs ...string) error {
|
||||
f.RecordedOps = append(f.RecordedOps, GenericRecordedQuery{
|
||||
Name: "DeleteAlertRulesByUID",
|
||||
Params: []interface{}{orgID, UIDs},
|
||||
})
|
||||
|
||||
rules := f.Rules[orgID]
|
||||
|
||||
var result = make([]*models.AlertRule, 0, len(rules))
|
||||
|
||||
for _, rule := range rules {
|
||||
add := true
|
||||
for _, UID := range UIDs {
|
||||
if rule.UID == UID {
|
||||
add = false
|
||||
break
|
||||
}
|
||||
}
|
||||
if add {
|
||||
result = append(result, rule)
|
||||
}
|
||||
}
|
||||
|
||||
f.Rules[orgID] = result
|
||||
return nil
|
||||
}
|
||||
func (f *FakeRuleStore) DeleteNamespaceAlertRules(_ context.Context, _ int64, _ string) ([]string, error) {
|
||||
return []string{}, nil
|
||||
}
|
||||
func (f *FakeRuleStore) DeleteRuleGroupAlertRules(_ context.Context, _ int64, _ string, _ string) ([]string, error) {
|
||||
return []string{}, nil
|
||||
}
|
||||
|
||||
func (f *FakeRuleStore) DeleteAlertInstancesByRuleUID(_ context.Context, _ int64, _ string) error {
|
||||
return nil
|
||||
}
|
||||
@ -179,8 +224,14 @@ func (f *FakeRuleStore) GetNamespaces(_ context.Context, orgID int64, _ *models2
|
||||
}
|
||||
return namespacesMap, nil
|
||||
}
|
||||
func (f *FakeRuleStore) GetNamespaceByTitle(_ context.Context, _ string, _ int64, _ *models2.SignedInUser, _ bool) (*models2.Folder, error) {
|
||||
return nil, nil
|
||||
func (f *FakeRuleStore) GetNamespaceByTitle(_ context.Context, title string, orgID int64, _ *models2.SignedInUser, _ bool) (*models2.Folder, error) {
|
||||
folders := f.Folders[orgID]
|
||||
for _, folder := range folders {
|
||||
if folder.Title == title {
|
||||
return folder, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not found")
|
||||
}
|
||||
func (f *FakeRuleStore) GetOrgRuleGroups(_ context.Context, q *models.ListOrgRuleGroupsQuery) error {
|
||||
f.mtx.Lock()
|
||||
|
@ -1890,7 +1890,7 @@ func TestAlertRuleCRUD(t *testing.T) {
|
||||
client := &http.Client{}
|
||||
// Finally, make sure we can delete it.
|
||||
{
|
||||
t.Run("fail if he rule group name does not exists", func(t *testing.T) {
|
||||
t.Run("succeed if the rule group name does not exists", func(t *testing.T) {
|
||||
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)
|
||||
@ -1903,8 +1903,8 @@ func TestAlertRuleCRUD(t *testing.T) {
|
||||
b, err := ioutil.ReadAll(resp.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, http.StatusNotFound, resp.StatusCode)
|
||||
require.JSONEq(t, `{"message": "failed to delete rule group: rule group not found under this namespace"}`, string(b))
|
||||
require.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||
require.JSONEq(t, `{"message":"rules deleted"}`, string(b))
|
||||
})
|
||||
|
||||
t.Run("succeed if the rule group name does exist", func(t *testing.T) {
|
||||
@ -1921,7 +1921,7 @@ func TestAlertRuleCRUD(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Equal(t, http.StatusAccepted, resp.StatusCode)
|
||||
require.JSONEq(t, `{"message":"rule group deleted"}`, string(b))
|
||||
require.JSONEq(t, `{"message":"rules deleted"}`, string(b))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user