mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Alerting: Fix rule storage to filter by group names using case-sensitive comparison (#88992)
* add test for the bug * remove unused struct * update db store to post process filters by group using go-lang's case-sensitive string comparison -------- Co-authored-by: Alexander Weaver <weaver.alex.d@gmail.com>
This commit is contained in:
parent
ee8a549fdd
commit
d4b0ac5973
@ -692,18 +692,6 @@ type ListNamespaceAlertRulesQuery struct {
|
||||
NamespaceUID string
|
||||
}
|
||||
|
||||
// ListOrgRuleGroupsQuery is the query for listing unique rule groups
|
||||
// for an organization
|
||||
type ListOrgRuleGroupsQuery struct {
|
||||
OrgID int64
|
||||
NamespaceUIDs []string
|
||||
|
||||
// DashboardUID and PanelID are optional and allow filtering rules
|
||||
// to return just those for a dashboard and panel.
|
||||
DashboardUID string
|
||||
PanelID int64
|
||||
}
|
||||
|
||||
type UpdateRule struct {
|
||||
Existing *AlertRule
|
||||
New AlertRule
|
||||
|
@ -114,7 +114,23 @@ func (st DBstore) GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmode
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
result = rules
|
||||
// MySQL by default compares strings without case-sensitivity, make sure we keep the case-sensitive comparison.
|
||||
var groupKey ngmodels.AlertRuleGroupKey
|
||||
// find the rule, which group we fetch
|
||||
for _, rule := range rules {
|
||||
if rule.UID == query.UID {
|
||||
groupKey = rule.GetGroupKey()
|
||||
break
|
||||
}
|
||||
}
|
||||
result = make([]*ngmodels.AlertRule, 0, len(rules))
|
||||
// MySQL (and potentially other databases) can use case-insensitive comparison.
|
||||
// This code makes sure we return groups that only exactly match the filter.
|
||||
for _, rule := range rules {
|
||||
if rule.GetGroupKey() == groupKey {
|
||||
result = append(result, rule)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return result, err
|
||||
@ -372,9 +388,14 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
|
||||
q = q.Where(fmt.Sprintf("uid IN (%s)", strings.Join(in, ",")), args...)
|
||||
}
|
||||
|
||||
var groupsMap map[string]struct{}
|
||||
if len(query.RuleGroups) > 0 {
|
||||
groupsMap = make(map[string]struct{})
|
||||
args, in := getINSubQueryArgs(query.RuleGroups)
|
||||
q = q.Where(fmt.Sprintf("rule_group IN (%s)", strings.Join(in, ",")), args...)
|
||||
for _, group := range query.RuleGroups {
|
||||
groupsMap[group] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
if query.ReceiverName != "" {
|
||||
@ -411,6 +432,13 @@ func (st DBstore) ListAlertRules(ctx context.Context, query *ngmodels.ListAlertR
|
||||
continue
|
||||
}
|
||||
}
|
||||
// MySQL (and potentially other databases) can use case-insensitive comparison.
|
||||
// This code makes sure we return groups that only exactly match the filter.
|
||||
if groupsMap != nil {
|
||||
if _, ok := groupsMap[rule.RuleGroup]; !ok {
|
||||
continue
|
||||
}
|
||||
}
|
||||
alertRules = append(alertRules, rule)
|
||||
}
|
||||
|
||||
@ -526,8 +554,13 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel
|
||||
alertRulesSql.NotIn("org_id", disabledOrgs)
|
||||
}
|
||||
|
||||
var groupsMap map[string]struct{}
|
||||
if len(query.RuleGroups) > 0 {
|
||||
alertRulesSql.In("rule_group", query.RuleGroups)
|
||||
groupsMap = make(map[string]struct{}, len(query.RuleGroups))
|
||||
for _, group := range query.RuleGroups {
|
||||
groupsMap[group] = struct{}{}
|
||||
}
|
||||
}
|
||||
|
||||
rule := new(ngmodels.AlertRule)
|
||||
@ -548,6 +581,13 @@ func (st DBstore) GetAlertRulesForScheduling(ctx context.Context, query *ngmodel
|
||||
st.Logger.Error("Invalid rule found in DB store, ignoring it", "func", "GetAlertRulesForScheduling", "error", err)
|
||||
continue
|
||||
}
|
||||
// MySQL (and potentially other databases) uses case-insensitive comparison.
|
||||
// This code makes sure we return groups that only exactly match the filter
|
||||
if groupsMap != nil {
|
||||
if _, ok := groupsMap[rule.RuleGroup]; !ok { // compare groups using case-sensitive logic.
|
||||
continue
|
||||
}
|
||||
}
|
||||
if st.FeatureToggles.IsEnabled(ctx, featuremgmt.FlagAlertingQueryOptimization) {
|
||||
if optimizations, err := OptimizeAlertQueries(rule.Data); err != nil {
|
||||
st.Logger.Error("Could not migrate rule from range to instant query", "rule", rule.UID, "err", err)
|
||||
|
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@ -422,6 +423,11 @@ func TestIntegration_GetAlertRulesForScheduling(t *testing.T) {
|
||||
ruleGroups: []string{rule1.RuleGroup},
|
||||
rules: []string{rule1.Title},
|
||||
},
|
||||
{
|
||||
name: "with a rule group filter, should be case sensitive",
|
||||
ruleGroups: []string{strings.ToUpper(rule1.RuleGroup)},
|
||||
rules: []string{},
|
||||
},
|
||||
{
|
||||
name: "with a filter on orgs, it returns rules that do not belong to that org",
|
||||
rules: []string{rule1.Title},
|
||||
@ -958,6 +964,114 @@ func TestIntegrationGetNamespacesByRuleUID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIntegrationRuleGroupsCaseSensitive(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("skipping integration test")
|
||||
}
|
||||
|
||||
sqlStore := db.InitTestDB(t)
|
||||
cfg := setting.NewCfg()
|
||||
cfg.UnifiedAlerting.BaseInterval = 1 * time.Second
|
||||
store := &DBstore{
|
||||
SQLStore: sqlStore,
|
||||
FolderService: setupFolderService(t, sqlStore, cfg, featuremgmt.WithFeatures()),
|
||||
Logger: log.New("test-dbstore"),
|
||||
Cfg: cfg.UnifiedAlerting,
|
||||
FeatureToggles: featuremgmt.WithFeatures(),
|
||||
}
|
||||
|
||||
gen := models.RuleGen.With(models.RuleMuts.WithOrgID(1))
|
||||
misc := gen.GenerateMany(5, 10)
|
||||
groupKey1 := models.GenerateGroupKey(1)
|
||||
groupKey1.RuleGroup = strings.ToLower(groupKey1.RuleGroup)
|
||||
groupKey2 := groupKey1
|
||||
groupKey2.RuleGroup = strings.ToUpper(groupKey2.RuleGroup)
|
||||
groupKey3 := groupKey1
|
||||
groupKey3.OrgID = 2
|
||||
|
||||
group1 := gen.With(gen.WithGroupKey(groupKey1)).GenerateMany(3)
|
||||
group2 := gen.With(gen.WithGroupKey(groupKey2)).GenerateMany(1, 3)
|
||||
group3 := gen.With(gen.WithGroupKey(groupKey3)).GenerateMany(1, 3)
|
||||
|
||||
_, err := store.InsertAlertRules(context.Background(), append(append(append(misc, group1...), group2...), group3...))
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("GetAlertRulesGroupByRuleUID", func(t *testing.T) {
|
||||
t.Run("should return rules that belong to only that group", func(t *testing.T) {
|
||||
result, err := store.GetAlertRulesGroupByRuleUID(context.Background(), &models.GetAlertRulesGroupByRuleUIDQuery{
|
||||
UID: group1[rand.Intn(len(group1))].UID,
|
||||
OrgID: groupKey1.OrgID,
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, len(group1))
|
||||
for _, rule := range result {
|
||||
assert.Equal(t, groupKey1, rule.GetGroupKey())
|
||||
assert.Truef(t, slices.ContainsFunc(group1, func(r models.AlertRule) bool {
|
||||
return r.UID == rule.UID
|
||||
}), "rule with group key [%v] should not be in group [%v]", rule.GetGroupKey(), group1)
|
||||
}
|
||||
if t.Failed() {
|
||||
deref := make([]models.AlertRule, 0, len(result))
|
||||
for _, rule := range result {
|
||||
deref = append(deref, *rule)
|
||||
}
|
||||
t.Logf("expected rules in group %v: %v\ngot:%v", groupKey1, group1, deref)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("ListAlertRules", func(t *testing.T) {
|
||||
t.Run("should find only group with exact case", func(t *testing.T) {
|
||||
result, err := store.ListAlertRules(context.Background(), &models.ListAlertRulesQuery{
|
||||
OrgID: 1,
|
||||
RuleGroups: []string{groupKey1.RuleGroup},
|
||||
})
|
||||
require.NoError(t, err)
|
||||
assert.Len(t, result, len(group1))
|
||||
for _, rule := range result {
|
||||
assert.Equal(t, groupKey1, rule.GetGroupKey())
|
||||
assert.Truef(t, slices.ContainsFunc(group1, func(r models.AlertRule) bool {
|
||||
return r.UID == rule.UID
|
||||
}), "rule with group key [%v] should not be in group [%v]", rule.GetGroupKey(), group1)
|
||||
}
|
||||
if t.Failed() {
|
||||
deref := make([]models.AlertRule, 0, len(result))
|
||||
for _, rule := range result {
|
||||
deref = append(deref, *rule)
|
||||
}
|
||||
t.Logf("expected rules in group %v: %v\ngot:%v", groupKey1, group1, deref)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("GetAlertRulesForScheduling", func(t *testing.T) {
|
||||
t.Run("should find only group with exact case", func(t *testing.T) {
|
||||
q := &models.GetAlertRulesForSchedulingQuery{
|
||||
PopulateFolders: false,
|
||||
RuleGroups: []string{groupKey1.RuleGroup},
|
||||
}
|
||||
err := store.GetAlertRulesForScheduling(context.Background(), q)
|
||||
require.NoError(t, err)
|
||||
result := q.ResultRules
|
||||
expected := append(group1, group3...)
|
||||
assert.Len(t, result, len(expected)) // query fetches all orgs
|
||||
for _, rule := range result {
|
||||
assert.Equal(t, groupKey1.RuleGroup, rule.RuleGroup)
|
||||
assert.Truef(t, slices.ContainsFunc(expected, func(r models.AlertRule) bool {
|
||||
return r.UID == rule.UID
|
||||
}), "rule with group key [%v] should not be in group [%v]", rule.GetGroupKey(), group1)
|
||||
}
|
||||
if t.Failed() {
|
||||
deref := make([]models.AlertRule, 0, len(result))
|
||||
for _, rule := range result {
|
||||
deref = append(deref, *rule)
|
||||
}
|
||||
t.Logf("expected rules in group %v: %v\ngot:%v", groupKey1.RuleGroup, expected, deref)
|
||||
}
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
// createAlertRule creates an alert rule in the database and returns it.
|
||||
// If a generator is not specified, uniqueness of primary key is not guaranteed.
|
||||
func createRule(t *testing.T, store *DBstore, generator *models.AlertRuleGenerator) *models.AlertRule {
|
||||
|
@ -2100,3 +2100,59 @@ func TestIntegrationRuleNotificationSettings(t *testing.T) {
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestIntegrationRuleUpdateAllDatabases(t *testing.T) {
|
||||
// Setup Grafana and its Database
|
||||
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
||||
DisableLegacyAlerting: true,
|
||||
EnableUnifiedAlerting: true,
|
||||
DisableAnonymous: true,
|
||||
AppModeProduction: true,
|
||||
})
|
||||
grafanaListedAddr, env := testinfra.StartGrafanaEnv(t, dir, path)
|
||||
|
||||
// Create a user to make authenticated requests
|
||||
createUser(t, env.SQLStore, env.Cfg, user.CreateUserCommand{
|
||||
DefaultOrgRole: string(org.RoleAdmin),
|
||||
Password: "admin",
|
||||
Login: "admin",
|
||||
})
|
||||
|
||||
client := newAlertingApiClient(grafanaListedAddr, "admin", "admin")
|
||||
|
||||
folderUID := util.GenerateShortUID()
|
||||
client.CreateFolder(t, folderUID, "folder1")
|
||||
|
||||
t.Run("group renamed followed by delete for case-only changes should not delete both groups", func(t *testing.T) { // Regression test.
|
||||
group := generateAlertRuleGroup(3, alertRuleGen())
|
||||
groupName := group.Name
|
||||
|
||||
_, status, body := client.PostRulesGroupWithStatus(t, folderUID, &group)
|
||||
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
|
||||
getGroup := client.GetRulesGroup(t, folderUID, group.Name)
|
||||
require.Lenf(t, getGroup.Rules, 3, "expected 3 rules in group")
|
||||
require.Equal(t, groupName, getGroup.Rules[0].GrafanaManagedAlert.RuleGroup)
|
||||
|
||||
group = convertGettableRuleGroupToPostable(getGroup.GettableRuleGroupConfig)
|
||||
newGroup := strings.ToUpper(group.Name)
|
||||
group.Name = newGroup
|
||||
_, status, body = client.PostRulesGroupWithStatus(t, folderUID, &group)
|
||||
require.Equalf(t, http.StatusAccepted, status, "failed to post rule group. Response: %s", body)
|
||||
|
||||
getGroup = client.GetRulesGroup(t, folderUID, group.Name)
|
||||
require.Lenf(t, getGroup.Rules, 3, "expected 3 rules in group")
|
||||
require.Equal(t, newGroup, getGroup.Rules[0].GrafanaManagedAlert.RuleGroup)
|
||||
|
||||
status, body = client.DeleteRulesGroup(t, folderUID, groupName)
|
||||
require.Equalf(t, http.StatusAccepted, status, "failed to post noop rule group. Response: %s", body)
|
||||
|
||||
// Old group is gone.
|
||||
getGroup = client.GetRulesGroup(t, folderUID, groupName)
|
||||
require.Lenf(t, getGroup.Rules, 0, "expected no rules")
|
||||
|
||||
// New group still exists.
|
||||
getGroup = client.GetRulesGroup(t, folderUID, newGroup)
|
||||
require.Lenf(t, getGroup.Rules, 3, "expected 3 rules in group")
|
||||
require.Equal(t, newGroup, getGroup.Rules[0].GrafanaManagedAlert.RuleGroup)
|
||||
})
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user