Alerting: API to delete rule groups using mimirtool

This commit is contained in:
Alexander Akhmetov 2025-02-13 23:59:41 +01:00
parent ed7c4aa96b
commit 2a6259d26b
No known key found for this signature in database
GPG Key ID: A5A8947133B1B31B
14 changed files with 12973 additions and 443 deletions

View File

@ -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",
})
}

View File

@ -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)

View File

@ -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"

View File

@ -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"

View File

@ -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",

View File

@ -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) {

View File

@ -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)

View File

@ -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.

View File

@ -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])
})
}

View File

@ -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)
}

View File

@ -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) {

View File

@ -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

File diff suppressed because it is too large Load Diff