mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: API to delete rule groups using mimirtool
This commit is contained in:
parent
ed7c4aa96b
commit
2a6259d26b
@ -84,12 +84,44 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRules(c *contextmodel.
|
||||
// RouteConvertPrometheusDeleteNamespace deletes all rule groups that were imported from a Prometheus-compatible source
|
||||
// within a specified namespace.
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteNamespace(c *contextmodel.ReqContext, namespaceTitle string) response.Response {
|
||||
return response.Error(501, "Not implemented", nil)
|
||||
logger := srv.logger.FromContext(c.Req.Context())
|
||||
|
||||
logger.Debug("Searching for the namespace", "fullpath", namespaceTitle)
|
||||
namespace, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
|
||||
if err != nil {
|
||||
return namespaceErrorResponse(err)
|
||||
}
|
||||
logger.Debug("Found namespace", "namespace_uid", namespace.UID, "fullpath", namespaceTitle)
|
||||
|
||||
filterOpts := &provisioning.FilterOptions{
|
||||
NamespaceUIDs: []string{namespace.UID},
|
||||
ImportedPrometheusRule: util.Pointer(true),
|
||||
}
|
||||
err = srv.alertRuleService.DeleteRuleGroups(c.Req.Context(), c.SignedInUser, models.ProvenanceConvertedPrometheus, filterOpts)
|
||||
if err != nil {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
return successfulResponse()
|
||||
}
|
||||
|
||||
// RouteConvertPrometheusDeleteRuleGroup deletes a specific rule group if it was imported from a Prometheus-compatible source.
|
||||
func (srv *ConvertPrometheusSrv) RouteConvertPrometheusDeleteRuleGroup(c *contextmodel.ReqContext, namespaceTitle string, group string) response.Response {
|
||||
return response.Error(501, "Not implemented", nil)
|
||||
logger := srv.logger.FromContext(c.Req.Context())
|
||||
|
||||
logger.Debug("Searching for the namespace", "fullpath", namespaceTitle)
|
||||
folder, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
|
||||
if err != nil {
|
||||
return namespaceErrorResponse(err)
|
||||
}
|
||||
logger.Debug("Found namespace", "namespace", folder.UID)
|
||||
|
||||
err = srv.alertRuleService.DeleteRuleGroup(c.Req.Context(), c.SignedInUser, folder.UID, group, models.ProvenanceConvertedPrometheus)
|
||||
if err != nil {
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
return successfulResponse()
|
||||
}
|
||||
|
||||
// RouteConvertPrometheusGetNamespace returns the Grafana-managed alert rules for a specified namespace (folder).
|
||||
@ -100,13 +132,9 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetNamespace(c *contextmo
|
||||
logger.Debug("Searching for the namespace", "fullpath", namespaceTitle)
|
||||
namespace, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
|
||||
if err != nil {
|
||||
return toNamespaceErrorResponse(err)
|
||||
}
|
||||
if errors.Is(err, dashboards.ErrFolderAccessDenied) {
|
||||
// If there is no such folder, GetNamespaceByUID returns ErrFolderAccessDenied.
|
||||
// We should return 404 in this case, otherwise mimirtool does not work correctly.
|
||||
return response.Empty(http.StatusNotFound)
|
||||
return namespaceErrorResponse(err)
|
||||
}
|
||||
logger.Debug("Found namespace", "namespace_uid", namespace.UID, "fullpath", namespaceTitle)
|
||||
|
||||
filterOpts := &provisioning.FilterOptions{
|
||||
ImportedPrometheusRule: util.Pointer(true),
|
||||
@ -133,12 +161,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusGetRuleGroup(c *contextmo
|
||||
logger.Debug("Searching for the namespace", "fullpath", namespaceTitle)
|
||||
namespace, err := srv.ruleStore.GetNamespaceInRootByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.GetOrgID(), c.SignedInUser)
|
||||
if err != nil {
|
||||
return toNamespaceErrorResponse(err)
|
||||
}
|
||||
if errors.Is(err, dashboards.ErrFolderAccessDenied) {
|
||||
// If there is no such folder, GetNamespaceByUID returns ErrFolderAccessDenied.
|
||||
// We should return 404 in this case, otherwise mimirtool does not work correctly.
|
||||
return response.Empty(http.StatusNotFound)
|
||||
return namespaceErrorResponse(err)
|
||||
}
|
||||
|
||||
filterOpts := &provisioning.FilterOptions{
|
||||
@ -204,7 +227,7 @@ func (srv *ConvertPrometheusSrv) RouteConvertPrometheusPostRuleGroup(c *contextm
|
||||
return errorToResponse(err)
|
||||
}
|
||||
|
||||
return response.JSON(http.StatusAccepted, map[string]string{"status": "success"})
|
||||
return successfulResponse()
|
||||
}
|
||||
|
||||
func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext, title string, logger log.Logger) (*folder.Folder, response.Response) {
|
||||
@ -216,8 +239,7 @@ func (srv *ConvertPrometheusSrv) getOrCreateNamespace(c *contextmodel.ReqContext
|
||||
c.SignedInUser,
|
||||
)
|
||||
if err != nil {
|
||||
logger.Error("Failed to get or create a new namespace", "error", err)
|
||||
return nil, toNamespaceErrorResponse(err)
|
||||
return nil, namespaceErrorResponse(err)
|
||||
}
|
||||
return ns, nil
|
||||
}
|
||||
@ -309,3 +331,19 @@ func grafanaRuleGroupToPrometheus(group string, rules []models.AlertRule) (apimo
|
||||
|
||||
return promGroup, nil
|
||||
}
|
||||
|
||||
func namespaceErrorResponse(err error) response.Response {
|
||||
if errors.Is(err, dashboards.ErrFolderAccessDenied) {
|
||||
// If there is no such folder, GetNamespaceByUID returns ErrFolderAccessDenied.
|
||||
// We should return 404 in this case, otherwise mimirtool does not work correctly.
|
||||
return response.Empty(http.StatusNotFound)
|
||||
}
|
||||
|
||||
return toNamespaceErrorResponse(err)
|
||||
}
|
||||
|
||||
func successfulResponse() response.Response {
|
||||
return response.JSON(http.StatusAccepted, apimodels.ConvertPrometheusResponse{
|
||||
Status: "success",
|
||||
})
|
||||
}
|
||||
|
@ -75,6 +75,7 @@ type AlertRuleService interface {
|
||||
GetRuleGroup(ctx context.Context, user identity.Requester, folder, group string) (alerting_models.AlertRuleGroup, error)
|
||||
ReplaceRuleGroup(ctx context.Context, user identity.Requester, group alerting_models.AlertRuleGroup, provenance alerting_models.Provenance) error
|
||||
DeleteRuleGroup(ctx context.Context, user identity.Requester, folder, group string, provenance alerting_models.Provenance) error
|
||||
DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance alerting_models.Provenance, opts *provisioning.FilterOptions) error
|
||||
GetAlertRuleWithFolderFullpath(ctx context.Context, u identity.Requester, ruleUID string) (provisioning.AlertRuleWithFolderFullpath, error)
|
||||
GetAlertRuleGroupWithFolderFullpath(ctx context.Context, u identity.Requester, folder, group string) (alerting_models.AlertRuleGroupWithFolderFullpath, error)
|
||||
GetAlertGroupsWithFolderFullpath(ctx context.Context, u identity.Requester, opts *provisioning.FilterOptions) ([]alerting_models.AlertRuleGroupWithFolderFullpath, error)
|
||||
|
@ -4770,7 +4770,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup",
|
||||
"type": "object"
|
||||
@ -4932,7 +4931,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableAlert",
|
||||
"type": "object"
|
||||
@ -5057,6 +5055,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableSilences": {
|
||||
"description": "GettableSilences gettable silences",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableSilence",
|
||||
"type": "object"
|
||||
|
@ -4771,7 +4771,6 @@
|
||||
"type": "object"
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"items": {
|
||||
"$ref": "#/definitions/alertGroup",
|
||||
"type": "object"
|
||||
@ -4933,6 +4932,7 @@
|
||||
"type": "object"
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"items": {
|
||||
"$ref": "#/definitions/gettableAlert",
|
||||
"type": "object"
|
||||
|
@ -8711,7 +8711,6 @@
|
||||
}
|
||||
},
|
||||
"alertGroups": {
|
||||
"description": "AlertGroups alert groups",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
@ -8873,6 +8872,7 @@
|
||||
}
|
||||
},
|
||||
"gettableAlerts": {
|
||||
"description": "GettableAlerts gettable alerts",
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
|
@ -443,27 +443,44 @@ func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, user iden
|
||||
}
|
||||
|
||||
func (service *AlertRuleService) DeleteRuleGroup(ctx context.Context, user identity.Requester, namespaceUID, group string, provenance models.Provenance) error {
|
||||
delta, err := store.CalculateRuleGroupDelete(ctx, service.ruleStore, models.AlertRuleGroupKey{
|
||||
OrgID: user.GetOrgID(),
|
||||
NamespaceUID: namespaceUID,
|
||||
RuleGroup: group,
|
||||
return service.DeleteRuleGroups(ctx, user, provenance, &FilterOptions{
|
||||
NamespaceUIDs: []string{namespaceUID},
|
||||
RuleGroups: []string{group},
|
||||
})
|
||||
}
|
||||
|
||||
// DeleteGroups deletes alert rule groups by the specified filter options.
|
||||
func (service *AlertRuleService) DeleteRuleGroups(ctx context.Context, user identity.Requester, provenance models.Provenance, filterOpts *FilterOptions) error {
|
||||
q := models.ListAlertRulesQuery{}
|
||||
q = filterOpts.apply(q)
|
||||
q.OrgID = user.GetOrgID()
|
||||
|
||||
deltas, err := store.CalculateRuleGroupsDelete(ctx, service.ruleStore, user.GetOrgID(), &q)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// check if the current user has permissions to all rules and can bypass the regular authorization validation.
|
||||
can, err := service.authz.CanWriteAllRules(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !can {
|
||||
if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil {
|
||||
return err
|
||||
// Perform all deletions in a transaction
|
||||
return service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||
for _, delta := range deltas {
|
||||
// Here we don't use persistDelta since it would create a new transaction
|
||||
// Check if user has write permission to all rules
|
||||
can, err := service.authz.CanWriteAllRules(ctx, user)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !can {
|
||||
if err := service.authz.AuthorizeRuleGroupWrite(ctx, user, delta); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
err = service.persistDelta(ctx, user, delta, provenance)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return service.persistDelta(ctx, user, delta, provenance)
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (service *AlertRuleService) calcDelta(ctx context.Context, user identity.Requester, group models.AlertRuleGroup) (*store.GroupDelta, error) {
|
||||
|
@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"math/rand"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
@ -1575,7 +1576,7 @@ func TestDeleteRuleGroup(t *testing.T) {
|
||||
assert.Equal(t, u, user)
|
||||
assert.Equal(t, groupKey, change.GroupKey)
|
||||
assert.Contains(t, change.AffectedGroups, groupKey)
|
||||
assert.EqualValues(t, rules, change.AffectedGroups[groupKey])
|
||||
assert.ElementsMatch(t, rules, change.AffectedGroups[groupKey])
|
||||
assert.Empty(t, change.Update)
|
||||
assert.Empty(t, change.New)
|
||||
assert.Len(t, change.Delete, len(rules))
|
||||
@ -1595,6 +1596,228 @@ func TestDeleteRuleGroup(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestDeleteRuleGroups(t *testing.T) {
|
||||
orgID1 := rand.Int63()
|
||||
orgID2 := rand.Int63()
|
||||
u := &user.SignedInUser{OrgID: orgID1}
|
||||
|
||||
// Create groups across different orgs and namespaces
|
||||
groupKey1 := models.AlertRuleGroupKey{
|
||||
OrgID: orgID1,
|
||||
NamespaceUID: "namespace1",
|
||||
RuleGroup: "group1",
|
||||
}
|
||||
groupKey2 := models.AlertRuleGroupKey{
|
||||
OrgID: orgID1,
|
||||
NamespaceUID: "namespace2",
|
||||
RuleGroup: "group2",
|
||||
}
|
||||
groupKey3 := models.AlertRuleGroupKey{
|
||||
OrgID: orgID1,
|
||||
NamespaceUID: "namespace3",
|
||||
RuleGroup: "group3",
|
||||
}
|
||||
groupKey4 := models.AlertRuleGroupKey{
|
||||
OrgID: orgID2, // Different org
|
||||
NamespaceUID: "namespace1",
|
||||
RuleGroup: "group1",
|
||||
}
|
||||
|
||||
gen := models.RuleGen
|
||||
// Create rules for each group
|
||||
rules1 := gen.With(gen.WithGroupKey(groupKey1)).GenerateManyRef(2)
|
||||
rules2 := gen.With(gen.WithGroupKey(groupKey2)).GenerateManyRef(3)
|
||||
rules3 := gen.With(gen.WithGroupKey(groupKey3)).GenerateManyRef(2)
|
||||
rules4 := gen.With(gen.WithGroupKey(groupKey4)).GenerateManyRef(2)
|
||||
|
||||
org1Rules := slices.Concat(rules1, rules2, rules3)
|
||||
org2Rules := rules4
|
||||
|
||||
initServiceWithData := func(t *testing.T) (*AlertRuleService, *fakes.RuleStore, *fakes.FakeProvisioningStore, *fakeRuleAccessControlService) {
|
||||
service, ruleStore, provenanceStore, ac := initService(t)
|
||||
ruleStore.Rules = map[int64][]*models.AlertRule{
|
||||
orgID1: org1Rules,
|
||||
orgID2: org2Rules,
|
||||
}
|
||||
// Set provenance for all rules
|
||||
for _, rules := range []([]*models.AlertRule){org1Rules, org2Rules} {
|
||||
for _, rule := range rules {
|
||||
err := provenanceStore.SetProvenance(context.Background(), rule, rule.OrgID, models.ProvenanceAPI)
|
||||
require.NoError(t, err)
|
||||
}
|
||||
}
|
||||
return service, ruleStore, provenanceStore, ac
|
||||
}
|
||||
|
||||
getUIDs := func(rules []*models.AlertRule) []string {
|
||||
uids := make([]string, 0, len(rules))
|
||||
for _, rule := range rules {
|
||||
uids = append(uids, rule.UID)
|
||||
}
|
||||
return uids
|
||||
}
|
||||
|
||||
t.Run("when deleting specific groups", func(t *testing.T) {
|
||||
filterOpts := &FilterOptions{
|
||||
NamespaceUIDs: []string{"namespace1"},
|
||||
RuleGroups: []string{"group1"},
|
||||
}
|
||||
|
||||
t.Run("when user can write all rules", func(t *testing.T) {
|
||||
service, ruleStore, _, ac := initServiceWithData(t)
|
||||
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, ac.Calls, 1)
|
||||
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
||||
|
||||
// Verify only rules from group1 in org1 were deleted
|
||||
deletes := getDeletedRules(t, ruleStore)
|
||||
require.Len(t, deletes, 1)
|
||||
require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
|
||||
})
|
||||
|
||||
t.Run("when user cannot write all rules", func(t *testing.T) {
|
||||
t.Run("should not delete if not authorized", func(t *testing.T) {
|
||||
service, ruleStore, _, ac := initServiceWithData(t)
|
||||
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
expectedErr := errors.New("test error")
|
||||
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
||||
return expectedErr
|
||||
}
|
||||
|
||||
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
|
||||
require.Len(t, ac.Calls, 2)
|
||||
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
||||
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
||||
|
||||
deletes := getDeletedRules(t, ruleStore)
|
||||
require.Empty(t, deletes)
|
||||
})
|
||||
|
||||
t.Run("should delete group1 when authorized", func(t *testing.T) {
|
||||
service, ruleStore, _, ac := initServiceWithData(t)
|
||||
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
ac.AuthorizeRuleChangesFunc = func(ctx context.Context, user identity.Requester, change *store.GroupDelta) error {
|
||||
assert.Equal(t, u, user)
|
||||
assert.Equal(t, groupKey1, change.GroupKey)
|
||||
assert.ElementsMatch(t, rules1, change.AffectedGroups[groupKey1])
|
||||
assert.Empty(t, change.Update)
|
||||
assert.Empty(t, change.New)
|
||||
assert.Len(t, change.Delete, len(rules1))
|
||||
return nil
|
||||
}
|
||||
|
||||
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, ac.Calls, 2)
|
||||
assert.Equal(t, "CanWriteAllRules", ac.Calls[0].Method)
|
||||
assert.Equal(t, "AuthorizeRuleGroupWrite", ac.Calls[1].Method)
|
||||
|
||||
deletes := getDeletedRules(t, ruleStore)
|
||||
require.Len(t, deletes, 1)
|
||||
require.ElementsMatch(t, getUIDs(rules1), deletes[0].uids)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when deleting multiple groups from multiple namespaces", func(t *testing.T) {
|
||||
filterOpts := &FilterOptions{
|
||||
NamespaceUIDs: []string{"namespace1", "namespace2"},
|
||||
RuleGroups: []string{"group1", "group2"},
|
||||
}
|
||||
|
||||
t.Run("should delete all matching groups from correct org", func(t *testing.T) {
|
||||
service, ruleStore, _, ac := initServiceWithData(t)
|
||||
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
||||
require.NoError(t, err)
|
||||
|
||||
deletes := getDeletedRules(t, ruleStore)
|
||||
require.Len(t, deletes, 2)
|
||||
require.ElementsMatch(
|
||||
t,
|
||||
slices.Concat(getUIDs(rules1), getUIDs(rules2)),
|
||||
slices.Concat(deletes[0].uids, deletes[1].uids),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("when filtering by imported Prometheus rules", func(t *testing.T) {
|
||||
filterOpts := &FilterOptions{
|
||||
ImportedPrometheusRule: util.Pointer(true),
|
||||
NamespaceUIDs: []string{"namespace1"},
|
||||
}
|
||||
|
||||
t.Run("when the group is not imported", func(t *testing.T) {
|
||||
filterOpts.RuleGroups = []string{groupKey1.RuleGroup}
|
||||
service, _, _, ac := initServiceWithData(t)
|
||||
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
||||
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
|
||||
})
|
||||
|
||||
t.Run("when the group is imported", func(t *testing.T) {
|
||||
importedGroup := models.AlertRuleGroupKey{
|
||||
OrgID: orgID1,
|
||||
NamespaceUID: "namespace1",
|
||||
RuleGroup: "newgroup",
|
||||
}
|
||||
importedRules := gen.With(
|
||||
gen.WithGroupKey(importedGroup),
|
||||
gen.WithPrometheusOriginalRuleDefinition("something"),
|
||||
).GenerateManyRef(2)
|
||||
filterOpts.RuleGroups = []string{importedGroup.RuleGroup}
|
||||
service, ruleStore, _, ac := initServiceWithData(t)
|
||||
ruleStore.Rules[orgID1] = append(ruleStore.Rules[orgID1], importedRules...)
|
||||
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
||||
require.NoError(t, err)
|
||||
deletes := getDeletedRules(t, ruleStore)
|
||||
require.Len(t, deletes, 1)
|
||||
require.ElementsMatch(t, getUIDs(importedRules), deletes[0].uids)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("with no matching rule groups", func(t *testing.T) {
|
||||
filterOpts := &FilterOptions{
|
||||
NamespaceUIDs: []string{"non-existent"},
|
||||
RuleGroups: []string{"non-existent"},
|
||||
}
|
||||
|
||||
service, ruleStore, _, ac := initServiceWithData(t)
|
||||
ac.CanWriteAllRulesFunc = func(ctx context.Context, user identity.Requester) (bool, error) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
err := service.DeleteRuleGroups(context.Background(), u, models.ProvenanceAPI, filterOpts)
|
||||
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
|
||||
|
||||
deletes := getDeletedRules(t, ruleStore)
|
||||
require.Empty(t, deletes)
|
||||
})
|
||||
}
|
||||
|
||||
func TestProvisiongWithFullpath(t *testing.T) {
|
||||
tracer := tracing.InitializeTracerForTest()
|
||||
inProcBus := bus.ProvideBus(tracer)
|
||||
@ -1694,6 +1917,31 @@ func getDeleteQueries(ruleStore *fakes.RuleStore) []fakes.GenericRecordedQuery {
|
||||
return result
|
||||
}
|
||||
|
||||
type deleteRuleOperation struct {
|
||||
orgID int64
|
||||
uids []string
|
||||
}
|
||||
|
||||
func getDeletedRules(t *testing.T, ruleStore *fakes.RuleStore) []deleteRuleOperation {
|
||||
t.Helper()
|
||||
|
||||
queries := getDeleteQueries(ruleStore)
|
||||
operations := make([]deleteRuleOperation, 0, len(queries))
|
||||
for _, q := range queries {
|
||||
orgID, ok := q.Params[0].(int64)
|
||||
require.True(t, ok, "orgID parameter should be int64")
|
||||
|
||||
uids, ok := q.Params[1].([]string)
|
||||
require.True(t, ok, "uids parameter should be []string")
|
||||
|
||||
operations = append(operations, deleteRuleOperation{
|
||||
orgID: orgID,
|
||||
uids: uids,
|
||||
})
|
||||
}
|
||||
return operations
|
||||
}
|
||||
|
||||
func createAlertRuleService(t *testing.T, folderService folder.Service) AlertRuleService {
|
||||
t.Helper()
|
||||
sqlStore := db.InitTestDB(t)
|
||||
|
@ -221,15 +221,13 @@ func CalculateRuleUpdate(ctx context.Context, ruleReader RuleReader, rule *model
|
||||
return calculateChanges(ctx, ruleReader, rule.GetGroupKey(), existingGroupRules, newGroup)
|
||||
}
|
||||
|
||||
// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group
|
||||
func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) {
|
||||
// List all rules in the group.
|
||||
q := models.ListAlertRulesQuery{
|
||||
OrgID: groupKey.OrgID,
|
||||
NamespaceUIDs: []string{groupKey.NamespaceUID},
|
||||
RuleGroups: []string{groupKey.RuleGroup},
|
||||
// CalculateRuleGroupsDelete calculates []*GroupDelta that reflects an operation of removing multiple groups
|
||||
func CalculateRuleGroupsDelete(ctx context.Context, ruleReader RuleReader, orgID int64, query *models.ListAlertRulesQuery) ([]*GroupDelta, error) {
|
||||
if query == nil {
|
||||
query = &models.ListAlertRulesQuery{}
|
||||
}
|
||||
ruleList, err := ruleReader.ListAlertRules(ctx, &q)
|
||||
query.OrgID = orgID
|
||||
ruleList, err := ruleReader.ListAlertRules(ctx, query)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -237,14 +235,40 @@ func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupK
|
||||
return nil, models.ErrAlertRuleGroupNotFound.Errorf("")
|
||||
}
|
||||
|
||||
delta := &GroupDelta{
|
||||
GroupKey: groupKey,
|
||||
Delete: ruleList,
|
||||
AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{
|
||||
groupKey: ruleList,
|
||||
},
|
||||
groups := models.GroupByAlertRuleGroupKey(ruleList)
|
||||
deltas := make([]*GroupDelta, 0, len(groups))
|
||||
for groupKey := range groups {
|
||||
delta := &GroupDelta{
|
||||
GroupKey: groupKey,
|
||||
Delete: groups[groupKey],
|
||||
AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{
|
||||
groupKey: groups[groupKey],
|
||||
},
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
deltas = append(deltas, delta)
|
||||
}
|
||||
return delta, nil
|
||||
|
||||
return deltas, nil
|
||||
}
|
||||
|
||||
// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group
|
||||
func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) {
|
||||
q := &models.ListAlertRulesQuery{
|
||||
NamespaceUIDs: []string{groupKey.NamespaceUID},
|
||||
RuleGroups: []string{groupKey.RuleGroup},
|
||||
}
|
||||
deltas, err := CalculateRuleGroupsDelete(ctx, ruleReader, groupKey.OrgID, q)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(deltas) != 1 {
|
||||
return nil, fmt.Errorf("expected to get a single group delta, got %d", len(deltas))
|
||||
}
|
||||
|
||||
return deltas[0], nil
|
||||
}
|
||||
|
||||
// CalculateRuleDelete calculates GroupDelta that reflects an operation of removing a rule from the group.
|
||||
|
@ -428,6 +428,91 @@ func TestCalculateAutomaticChanges(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestCalculateRuleGroupsDelete(t *testing.T) {
|
||||
orgId := int64(rand.Int31())
|
||||
gen := models.RuleGen
|
||||
|
||||
t.Run("returns ErrAlertRuleGroupNotFound when namespace has no rules", func(t *testing.T) {
|
||||
fakeStore := fakes.NewRuleStore(t)
|
||||
otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUID("ns-1")).GenerateManyRef(3)
|
||||
fakeStore.Rules[orgId] = otherRules
|
||||
|
||||
query := &models.ListAlertRulesQuery{
|
||||
NamespaceUIDs: []string{"ns-2"},
|
||||
}
|
||||
deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query)
|
||||
require.ErrorIs(t, err, models.ErrAlertRuleGroupNotFound)
|
||||
require.Nil(t, deltas)
|
||||
})
|
||||
|
||||
t.Run("returns deltas for all affected groups in namespace", func(t *testing.T) {
|
||||
fakeStore := fakes.NewRuleStore(t)
|
||||
folder := randFolder()
|
||||
|
||||
// Create rules in two groups in target namespace
|
||||
group1Key := models.AlertRuleGroupKey{
|
||||
OrgID: orgId,
|
||||
NamespaceUID: folder.UID,
|
||||
RuleGroup: util.GenerateShortUID(),
|
||||
}
|
||||
group2Key := models.AlertRuleGroupKey{
|
||||
OrgID: orgId,
|
||||
NamespaceUID: folder.UID,
|
||||
RuleGroup: util.GenerateShortUID(),
|
||||
}
|
||||
|
||||
group1Rules := gen.With(gen.WithGroupKey(group1Key)).GenerateManyRef(3)
|
||||
group2Rules := gen.With(gen.WithGroupKey(group2Key)).GenerateManyRef(2)
|
||||
allNamespaceRules := append(group1Rules, group2Rules...)
|
||||
|
||||
// Create rules in different namespace
|
||||
otherRules := gen.With(gen.WithOrgID(orgId), gen.WithNamespaceUIDNotIn(folder.UID)).GenerateManyRef(3)
|
||||
|
||||
fakeStore.Rules[orgId] = append(allNamespaceRules, otherRules...)
|
||||
|
||||
query := &models.ListAlertRulesQuery{
|
||||
NamespaceUIDs: []string{folder.UID},
|
||||
}
|
||||
|
||||
deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, query)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.Len(t, deltas, 2, "expected deltas for two groups")
|
||||
|
||||
// Verify each group's delta
|
||||
for _, delta := range deltas {
|
||||
require.True(t, delta.GroupKey == group1Key || delta.GroupKey == group2Key)
|
||||
require.Empty(t, delta.Update)
|
||||
require.Empty(t, delta.New)
|
||||
|
||||
require.Contains(t, delta.AffectedGroups, delta.GroupKey)
|
||||
if delta.GroupKey == group1Key {
|
||||
require.ElementsMatch(t, group1Rules, delta.Delete)
|
||||
require.ElementsMatch(t, group1Rules, delta.AffectedGroups[delta.GroupKey])
|
||||
} else {
|
||||
require.ElementsMatch(t, group2Rules, delta.Delete)
|
||||
require.ElementsMatch(t, group2Rules, delta.AffectedGroups[delta.GroupKey])
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("fails if store returns error", func(t *testing.T) {
|
||||
fakeStore := fakes.NewRuleStore(t)
|
||||
expectedErr := errors.New("store error")
|
||||
fakeStore.Hook = func(cmd any) error {
|
||||
switch cmd.(type) {
|
||||
case models.ListAlertRulesQuery:
|
||||
return expectedErr
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
deltas, err := CalculateRuleGroupsDelete(context.Background(), fakeStore, orgId, nil)
|
||||
require.ErrorIs(t, err, expectedErr)
|
||||
require.Nil(t, deltas)
|
||||
})
|
||||
}
|
||||
|
||||
func TestCalculateRuleGroupDelete(t *testing.T) {
|
||||
gen := models.RuleGen
|
||||
fakeStore := fakes.NewRuleStore(t)
|
||||
@ -449,13 +534,13 @@ func TestCalculateRuleGroupDelete(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, groupKey, delta.GroupKey)
|
||||
assert.EqualValues(t, groupRules, delta.Delete)
|
||||
assert.ElementsMatch(t, groupRules, delta.Delete)
|
||||
|
||||
assert.Empty(t, delta.Update)
|
||||
assert.Empty(t, delta.New)
|
||||
|
||||
assert.Len(t, delta.AffectedGroups, 1)
|
||||
assert.Equal(t, models.RulesGroup(groupRules), delta.AffectedGroups[delta.GroupKey])
|
||||
assert.ElementsMatch(t, groupRules, delta.AffectedGroups[delta.GroupKey])
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -213,6 +213,15 @@ func (f *RuleStore) ListAlertRules(_ context.Context, q *models.ListAlertRulesQu
|
||||
if len(q.RuleUIDs) > 0 && !slices.Contains(q.RuleUIDs, r.UID) {
|
||||
continue
|
||||
}
|
||||
if q.ImportedPrometheusRule != nil {
|
||||
hasOriginalRuleDefinition := r.Metadata.PrometheusStyleRule != nil && len(r.Metadata.PrometheusStyleRule.OriginalRuleDefinition) > 0
|
||||
if *q.ImportedPrometheusRule && !hasOriginalRuleDefinition {
|
||||
continue
|
||||
}
|
||||
if !*q.ImportedPrometheusRule && hasOriginalRuleDefinition {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
ruleList = append(ruleList, r)
|
||||
}
|
||||
|
@ -169,6 +169,35 @@ func TestIntegrationConvertPrometheusEndpoints(t *testing.T) {
|
||||
_, status, raw := viewerClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
|
||||
requireStatusCode(t, http.StatusForbidden, status, raw)
|
||||
})
|
||||
|
||||
t.Run("delete one rule group", func(t *testing.T) {
|
||||
_, status, body := apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup1, nil)
|
||||
requireStatusCode(t, http.StatusAccepted, status, body)
|
||||
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace1, ds.Body.Datasource.UID, promGroup2, nil)
|
||||
requireStatusCode(t, http.StatusAccepted, status, body)
|
||||
_, status, body = apiClient.ConvertPrometheusPostRuleGroup(t, namespace2, ds.Body.Datasource.UID, promGroup3, nil)
|
||||
requireStatusCode(t, http.StatusAccepted, status, body)
|
||||
|
||||
apiClient.ConvertPrometheusDeleteRuleGroup(t, namespace1, promGroup1.Name)
|
||||
|
||||
// Check that the promGroup2 and promGroup3 are still there
|
||||
namespaces := apiClient.ConvertPrometheusGetAllRules(t)
|
||||
expectedNamespaces := map[string][]apimodels.PrometheusRuleGroup{
|
||||
namespace1: {promGroup2},
|
||||
namespace2: {promGroup3},
|
||||
}
|
||||
require.Equal(t, expectedNamespaces, namespaces)
|
||||
|
||||
// Delete the second namespace
|
||||
apiClient.ConvertPrometheusDeleteNamespace(t, namespace2)
|
||||
|
||||
// Check that only the first namespace is left
|
||||
namespaces = apiClient.ConvertPrometheusGetAllRules(t)
|
||||
expectedNamespaces = map[string][]apimodels.PrometheusRuleGroup{
|
||||
namespace1: {promGroup2},
|
||||
}
|
||||
require.Equal(t, expectedNamespaces, namespaces)
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationConvertPrometheusEndpoints_CreatePausedRules(t *testing.T) {
|
||||
|
@ -1146,6 +1146,22 @@ func (a apiClient) ConvertPrometheusGetAllRules(t *testing.T) map[string][]apimo
|
||||
return result
|
||||
}
|
||||
|
||||
func (a apiClient) ConvertPrometheusDeleteRuleGroup(t *testing.T, namespaceTitle, groupName string) {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s/%s", a.url, namespaceTitle, groupName), nil)
|
||||
require.NoError(t, err)
|
||||
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
|
||||
requireStatusCode(t, http.StatusAccepted, status, raw)
|
||||
}
|
||||
|
||||
func (a apiClient) ConvertPrometheusDeleteNamespace(t *testing.T, namespaceTitle string) {
|
||||
t.Helper()
|
||||
req, err := http.NewRequest(http.MethodDelete, fmt.Sprintf("%s/api/convert/prometheus/config/v1/rules/%s", a.url, namespaceTitle), nil)
|
||||
require.NoError(t, err)
|
||||
_, status, raw := sendRequestJSON[apimodels.ConvertPrometheusResponse](t, req, http.StatusAccepted)
|
||||
requireStatusCode(t, http.StatusAccepted, status, raw)
|
||||
}
|
||||
|
||||
func sendRequestRaw(t *testing.T, req *http.Request) ([]byte, int, error) {
|
||||
t.Helper()
|
||||
client := &http.Client{}
|
||||
|
File diff suppressed because it is too large
Load Diff
6514
public/openapi3.json
6514
public/openapi3.json
File diff suppressed because it is too large
Load Diff
Loading…
Reference in New Issue
Block a user