From b66cd7ef7994a7485b5c3c34d94140c755b67366 Mon Sep 17 00:00:00 2001 From: Fayzal Ghantiwala <114010985+fayzal-g@users.noreply.github.com> Date: Tue, 4 Jun 2024 10:57:55 +0100 Subject: [PATCH] Alerting: Add filters for RouteGetRuleStatuses (#88295) * Placeholder commit with rule_uid change * Add new filters to grafana rule state API * Revert type change * Split rule_group and rule_name params * remove debug line * Change how query params are parsed * Comment --- pkg/services/ngalert/api/api_prometheus.go | 35 ++++- .../ngalert/api/api_prometheus_test.go | 133 ++++++++++++++++++ 2 files changed, 164 insertions(+), 4 deletions(-) diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index d2601fa7e96..ad3981e2fe6 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -235,6 +235,9 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) } +// TODO: Refactor this function to reduce the cylomatic complexity +// +//nolint:gocyclo func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse { ruleResponse := apimodels.RuleResponse{ DiscoveryBase: apimodels.DiscoveryBase{ @@ -292,16 +295,26 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager return ruleResponse } - namespaceUIDs := make([]string, len(opts.Namespaces)) - for k := range opts.Namespaces { - namespaceUIDs = append(namespaceUIDs, k) + namespaceUIDs := make([]string, 0, len(opts.Namespaces)) + + folderUID := opts.Query.Get("folder_uid") + _, exists := opts.Namespaces[folderUID] + if folderUID != "" && exists { + namespaceUIDs = append(namespaceUIDs, folderUID) + } else { + for k := range opts.Namespaces { + namespaceUIDs = append(namespaceUIDs, k) + } } + ruleGroups := opts.Query["rule_group"] + alertRuleQuery := ngmodels.ListAlertRulesQuery{ OrgID: opts.OrgID, NamespaceUIDs: namespaceUIDs, DashboardUID: dashboardUID, PanelID: panelID, + RuleGroups: ruleGroups, } ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery) if err != nil { @@ -311,10 +324,23 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager return ruleResponse } + ruleNames := opts.Query["rule_name"] + ruleNamesSet := make(map[string]struct{}, len(ruleNames)) + for _, rn := range ruleNames { + ruleNamesSet[rn] = struct{}{} + } + // Group rules together by Namespace and Rule Group. Rules are also grouped by Org ID, - // but in this API all rules belong to the same organization. + // but in this API all rules belong to the same organization. Also filter by rule name if + // it was provided as a query param. groupedRules := make(map[ngmodels.AlertRuleGroupKey][]*ngmodels.AlertRule) for _, rule := range ruleList { + if len(ruleNamesSet) > 0 { + if _, exists := ruleNamesSet[rule.Title]; !exists { + continue + } + } + groupKey := rule.GetGroupKey() ruleGroup := groupedRules[groupKey] ruleGroup = append(ruleGroup, rule) @@ -343,6 +369,7 @@ func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager if !ok { continue } + ruleGroup, totals := toRuleGroup(log, manager, groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions) ruleGroup.Totals = totals for k, v := range totals { diff --git a/pkg/services/ngalert/api/api_prometheus_test.go b/pkg/services/ngalert/api/api_prometheus_test.go index b7a93cd4239..1f28c685ef7 100644 --- a/pkg/services/ngalert/api/api_prometheus_test.go +++ b/pkg/services/ngalert/api/api_prometheus_test.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "net/http" + "slices" "testing" "time" @@ -527,6 +528,138 @@ func TestRouteGetRuleStatuses(t *testing.T) { }) }) + t.Run("test folder, group and rule name query params", func(t *testing.T) { + ruleStore := fakes.NewRuleStore(t) + fakeAIM := NewFakeAlertInstanceManager(t) + + rulesInGroup1 := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + RuleGroup: "rule-group-1", + NamespaceUID: "folder-1", + OrgID: orgID, + })).GenerateManyRef(1) + + rulesInGroup2 := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + RuleGroup: "rule-group-2", + NamespaceUID: "folder-2", + OrgID: orgID, + })).GenerateManyRef(2) + + rulesInGroup3 := gen.With(gen.WithGroupKey(ngmodels.AlertRuleGroupKey{ + RuleGroup: "rule-group-3", + NamespaceUID: "folder-1", + OrgID: orgID, + })).GenerateManyRef(3) + + ruleStore.PutRule(context.Background(), rulesInGroup1...) + ruleStore.PutRule(context.Background(), rulesInGroup2...) + ruleStore.PutRule(context.Background(), rulesInGroup3...) + + api := PrometheusSrv{ + log: log.NewNopLogger(), + manager: fakeAIM, + store: ruleStore, + authz: accesscontrol.NewRuleService(acimpl.ProvideAccessControl(featuremgmt.WithFeatures())), + } + + permissions := createPermissionsForRules(slices.Concat(rulesInGroup1, rulesInGroup2, rulesInGroup3), orgID) + user := &user.SignedInUser{ + OrgID: orgID, + Permissions: permissions, + } + c := &contextmodel.ReqContext{ + SignedInUser: user, + } + t.Run("should only return rule groups under given folder_uid", func(t *testing.T) { + r, err := http.NewRequest("GET", "/api/v1/rules?folder_uid=folder-1", nil) + require.NoError(t, err) + + c.Context = &web.Context{Req: r} + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + result := &apimodels.RuleResponse{} + require.NoError(t, json.Unmarshal(resp.Body(), result)) + + require.Len(t, result.Data.RuleGroups, 2) + require.Equal(t, "rule-group-1", result.Data.RuleGroups[0].Name) + require.Equal(t, "rule-group-3", result.Data.RuleGroups[1].Name) + }) + + t.Run("should only return rule groups under given rule_group list", func(t *testing.T) { + r, err := http.NewRequest("GET", "/api/v1/rules?rule_group=rule-group-1&rule_group=rule-group-2", nil) + require.NoError(t, err) + + c.Context = &web.Context{Req: r} + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + result := &apimodels.RuleResponse{} + require.NoError(t, json.Unmarshal(resp.Body(), result)) + + require.Len(t, result.Data.RuleGroups, 2) + require.True(t, true, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool { + return rg.Name == "rule-group-1" + })) + require.True(t, true, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool { + return rg.Name == "rule-group-2" + })) + }) + + t.Run("should only return rule under given rule_name list", func(t *testing.T) { + expectedRuleInGroup2 := rulesInGroup2[0] + expectedRuleInGroup3 := rulesInGroup3[0] + + r, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?rule_name=%s&rule_name=%s", expectedRuleInGroup2.Title, expectedRuleInGroup3.Title), nil) + require.NoError(t, err) + + c.Context = &web.Context{Req: r} + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + result := &apimodels.RuleResponse{} + require.NoError(t, json.Unmarshal(resp.Body(), result)) + + require.Len(t, result.Data.RuleGroups, 2) + require.True(t, true, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool { + return rg.Name == "rule-group-2" + })) + require.True(t, true, slices.ContainsFunc(result.Data.RuleGroups, func(rg apimodels.RuleGroup) bool { + return rg.Name == "rule-group-3" + })) + require.Len(t, result.Data.RuleGroups[0].Rules, 1) + require.Len(t, result.Data.RuleGroups[1].Rules, 1) + + if result.Data.RuleGroups[0].Name == "rule-group-2" { + require.Equal(t, expectedRuleInGroup2.Title, result.Data.RuleGroups[0].Rules[0].Name) + require.Equal(t, expectedRuleInGroup3.Title, result.Data.RuleGroups[1].Rules[0].Name) + } else { + require.Equal(t, expectedRuleInGroup2.Title, result.Data.RuleGroups[1].Rules[0].Name) + require.Equal(t, expectedRuleInGroup3.Title, result.Data.RuleGroups[0].Rules[0].Name) + } + }) + + t.Run("should only return rule with given folder_uid, rule_group and rule_name", func(t *testing.T) { + expectedRule := rulesInGroup3[2] + r, err := http.NewRequest("GET", fmt.Sprintf("/api/v1/rules?folder_uid=folder-1&rule_group=rule-group-3&rule_name=%s", expectedRule.Title), nil) + require.NoError(t, err) + + c.Context = &web.Context{Req: r} + + resp := api.RouteGetRuleStatuses(c) + require.Equal(t, http.StatusOK, resp.Status()) + result := &apimodels.RuleResponse{} + require.NoError(t, json.Unmarshal(resp.Body(), result)) + + require.Len(t, result.Data.RuleGroups, 1) + folder, err := api.store.GetNamespaceByUID(context.Background(), "folder-1", orgID, user) + require.NoError(t, err) + require.Equal(t, folder.Fullpath, result.Data.RuleGroups[0].File) + require.Equal(t, "rule-group-3", result.Data.RuleGroups[0].Name) + require.Len(t, result.Data.RuleGroups[0].Rules, 1) + require.Equal(t, expectedRule.Title, result.Data.RuleGroups[0].Rules[0].Name) + }) + }) + t.Run("when fine-grained access is enabled", func(t *testing.T) { t.Run("should return only rules if the user can query all data sources", func(t *testing.T) { ruleStore := fakes.NewRuleStore(t)