diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index dae93551bea..d2601fa7e96 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -1,10 +1,10 @@ package api import ( + "context" "encoding/json" "errors" "fmt" - "net/http" "net/url" "sort" "strconv" @@ -17,7 +17,6 @@ import ( "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" - "github.com/grafana/grafana/pkg/services/folder" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" "github.com/grafana/grafana/pkg/services/ngalert/eval" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" @@ -61,6 +60,20 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo // As we are using req.Form directly, this triggers a call to ParseForm() if needed. c.Query("") + resp := PrepareAlertStatuses(srv.manager, AlertStatusesOptions{ + OrgID: c.SignedInUser.GetOrgID(), + Query: c.Req.Form, + }) + + return response.JSON(resp.HTTPStatusCode(), resp) +} + +type AlertStatusesOptions struct { + OrgID int64 + Query url.Values +} + +func PrepareAlertStatuses(manager state.AlertInstanceManager, opts AlertStatusesOptions) apimodels.AlertResponse { alertResponse := apimodels.AlertResponse{ DiscoveryBase: apimodels.DiscoveryBase{ Status: "success", @@ -71,11 +84,11 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo } var labelOptions []ngmodels.LabelOption - if !getBoolWithDefault(c.Req.Form, queryIncludeInternalLabels, false) { + if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) { labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) } - for _, alertState := range srv.manager.GetAll(c.SignedInUser.GetOrgID()) { + for _, alertState := range manager.GetAll(opts.OrgID) { startsAt := alertState.StartsAt valString := "" @@ -95,7 +108,7 @@ func (srv PrometheusSrv) RouteGetAlertStatuses(c *contextmodel.ReqContext) respo }) } - return response.JSON(alertResponse.HTTPStatusCode(), alertResponse) + return alertResponse } func formatValues(alertState *state.State) string { @@ -126,16 +139,16 @@ func formatValues(alertState *state.State) string { return fv } -func getPanelIDFromRequest(r *http.Request) (int64, error) { - if s := strings.TrimSpace(r.URL.Query().Get("panel_id")); s != "" { +func getPanelIDFromQuery(v url.Values) (int64, error) { + if s := strings.TrimSpace(v.Get("panel_id")); s != "" { return strconv.ParseInt(s, 10, 64) } return 0, nil } -func getMatchersFromRequest(r *http.Request) (labels.Matchers, error) { +func getMatchersFromQuery(v url.Values) (labels.Matchers, error) { var matchers labels.Matchers - for _, s := range r.URL.Query()["matcher"] { + for _, s := range v["matcher"] { var m labels.Matcher if err := json.Unmarshal([]byte(s), &m); err != nil { return nil, err @@ -148,9 +161,9 @@ func getMatchersFromRequest(r *http.Request) (labels.Matchers, error) { return matchers, nil } -func getStatesFromRequest(r *http.Request) ([]eval.State, error) { +func getStatesFromQuery(v url.Values) ([]eval.State, error) { var states []eval.State - for _, s := range r.URL.Query()["state"] { + for _, s := range v["state"] { s = strings.ToLower(s) switch s { case "normal", "inactive": @@ -171,6 +184,18 @@ func getStatesFromRequest(r *http.Request) ([]eval.State, error) { return states, nil } +type RuleGroupStatusesOptions struct { + Ctx context.Context + OrgID int64 + Query url.Values + Namespaces map[string]string + AuthorizeRuleGroup func(rules []*ngmodels.AlertRule) (bool, error) +} + +type ListAlertRulesStore interface { + ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) (ngmodels.RulesGroup, error) +} + func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) response.Response { // As we are using req.Form directly, this triggers a call to ParseForm() if needed. c.Query("") @@ -184,48 +209,6 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon }, } - dashboardUID := c.Query("dashboard_uid") - panelID, err := getPanelIDFromRequest(c.Req) - if err != nil { - ruleResponse.DiscoveryBase.Status = "error" - ruleResponse.DiscoveryBase.Error = fmt.Sprintf("invalid panel_id: %s", err.Error()) - ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) - } - if dashboardUID == "" && panelID != 0 { - ruleResponse.DiscoveryBase.Status = "error" - ruleResponse.DiscoveryBase.Error = "panel_id must be set with dashboard_uid" - ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) - } - - limitGroups := getInt64WithDefault(c.Req.Form, "limit", -1) - limitRulesPerGroup := getInt64WithDefault(c.Req.Form, "limit_rules", -1) - limitAlertsPerRule := getInt64WithDefault(c.Req.Form, "limit_alerts", -1) - matchers, err := getMatchersFromRequest(c.Req) - if err != nil { - ruleResponse.DiscoveryBase.Status = "error" - ruleResponse.DiscoveryBase.Error = err.Error() - ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) - } - withStates, err := getStatesFromRequest(c.Req) - if err != nil { - ruleResponse.DiscoveryBase.Status = "error" - ruleResponse.DiscoveryBase.Error = err.Error() - ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) - } - withStatesFast := make(map[eval.State]struct{}) - for _, state := range withStates { - withStatesFast[state] = struct{}{} - } - - var labelOptions []ngmodels.LabelOption - if !getBoolWithDefault(c.Req.Form, queryIncludeInternalLabels, false) { - labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) - } - namespaceMap, err := srv.store.GetUserVisibleNamespaces(c.Req.Context(), c.SignedInUser.GetOrgID(), c.SignedInUser) if err != nil { ruleResponse.DiscoveryBase.Status = "error" @@ -234,28 +217,98 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) } - if len(namespaceMap) == 0 { - srv.log.Debug("User does not have access to any namespaces") - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) + namespaces := map[string]string{} + for namespaceUID, folder := range namespaceMap { + namespaces[namespaceUID] = folder.Fullpath } - namespaceUIDs := make([]string, len(namespaceMap)) - for k := range namespaceMap { + ruleResponse = PrepareRuleGroupStatuses(srv.log, srv.manager, srv.store, RuleGroupStatusesOptions{ + Ctx: c.Req.Context(), + OrgID: c.OrgID, + Query: c.Req.Form, + Namespaces: namespaces, + AuthorizeRuleGroup: func(rules []*ngmodels.AlertRule) (bool, error) { + return srv.authz.HasAccessToRuleGroup(c.Req.Context(), c.SignedInUser, rules) + }, + }) + + return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) +} + +func PrepareRuleGroupStatuses(log log.Logger, manager state.AlertInstanceManager, store ListAlertRulesStore, opts RuleGroupStatusesOptions) apimodels.RuleResponse { + ruleResponse := apimodels.RuleResponse{ + DiscoveryBase: apimodels.DiscoveryBase{ + Status: "success", + }, + Data: apimodels.RuleDiscovery{ + RuleGroups: []apimodels.RuleGroup{}, + }, + } + + dashboardUID := opts.Query.Get("dashboard_uid") + panelID, err := getPanelIDFromQuery(opts.Query) + if err != nil { + ruleResponse.DiscoveryBase.Status = "error" + ruleResponse.DiscoveryBase.Error = fmt.Sprintf("invalid panel_id: %s", err.Error()) + ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData + return ruleResponse + } + if dashboardUID == "" && panelID != 0 { + ruleResponse.DiscoveryBase.Status = "error" + ruleResponse.DiscoveryBase.Error = "panel_id must be set with dashboard_uid" + ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData + return ruleResponse + } + + limitGroups := getInt64WithDefault(opts.Query, "limit", -1) + limitRulesPerGroup := getInt64WithDefault(opts.Query, "limit_rules", -1) + limitAlertsPerRule := getInt64WithDefault(opts.Query, "limit_alerts", -1) + matchers, err := getMatchersFromQuery(opts.Query) + if err != nil { + ruleResponse.DiscoveryBase.Status = "error" + ruleResponse.DiscoveryBase.Error = err.Error() + ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData + return ruleResponse + } + withStates, err := getStatesFromQuery(opts.Query) + if err != nil { + ruleResponse.DiscoveryBase.Status = "error" + ruleResponse.DiscoveryBase.Error = err.Error() + ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrBadData + return ruleResponse + } + withStatesFast := make(map[eval.State]struct{}) + for _, state := range withStates { + withStatesFast[state] = struct{}{} + } + + var labelOptions []ngmodels.LabelOption + if !getBoolWithDefault(opts.Query, queryIncludeInternalLabels, false) { + labelOptions = append(labelOptions, ngmodels.WithoutInternalLabels()) + } + + if len(opts.Namespaces) == 0 { + log.Debug("User does not have access to any namespaces") + return ruleResponse + } + + namespaceUIDs := make([]string, len(opts.Namespaces)) + for k := range opts.Namespaces { namespaceUIDs = append(namespaceUIDs, k) } alertRuleQuery := ngmodels.ListAlertRulesQuery{ - OrgID: c.SignedInUser.GetOrgID(), + OrgID: opts.OrgID, NamespaceUIDs: namespaceUIDs, DashboardUID: dashboardUID, PanelID: panelID, } - ruleList, err := srv.store.ListAlertRules(c.Req.Context(), &alertRuleQuery) + ruleList, err := store.ListAlertRules(opts.Ctx, &alertRuleQuery) if err != nil { ruleResponse.DiscoveryBase.Status = "error" ruleResponse.DiscoveryBase.Error = fmt.Sprintf("failure getting rules: %s", err.Error()) ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) + return ruleResponse } // Group rules together by Namespace and Rule Group. Rules are also grouped by Org ID, @@ -275,22 +328,22 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon rulesTotals := make(map[string]int64, len(groupedRules)) for groupKey, rules := range groupedRules { - folder := namespaceMap[groupKey.NamespaceUID] - if folder == nil { - srv.log.Warn("Query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID) + folder, ok := opts.Namespaces[groupKey.NamespaceUID] + if !ok { + log.Warn("Query returned rules that belong to folder the user does not have access to. All rules that belong to that namespace will not be added to the response", "folder_uid", groupKey.NamespaceUID) continue } - ok, err := srv.authz.HasAccessToRuleGroup(c.Req.Context(), c.SignedInUser, rules) + ok, err := opts.AuthorizeRuleGroup(rules) if err != nil { ruleResponse.DiscoveryBase.Status = "error" ruleResponse.DiscoveryBase.Error = fmt.Sprintf("cannot authorize access to rule group: %s", err.Error()) ruleResponse.DiscoveryBase.ErrorType = apiv1.ErrServer - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) + return ruleResponse } if !ok { continue } - ruleGroup, totals := srv.toRuleGroup(groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions) + ruleGroup, totals := toRuleGroup(log, manager, groupKey, folder, rules, limitAlertsPerRule, withStatesFast, matchers, labelOptions) ruleGroup.Totals = totals for k, v := range totals { rulesTotals[k] += v @@ -335,7 +388,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *contextmodel.ReqContext) respon ruleResponse.Data.RuleGroups = ruleResponse.Data.RuleGroups[0:limitGroups] } - return response.JSON(ruleResponse.HTTPStatusCode(), ruleResponse) + return ruleResponse } // This is the same as matchers.Matches but avoids the need to create a LabelSet @@ -348,11 +401,11 @@ func matchersMatch(matchers []*labels.Matcher, labels map[string]string) bool { return true } -func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder *folder.Folder, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) { +func toRuleGroup(log log.Logger, manager state.AlertInstanceManager, groupKey ngmodels.AlertRuleGroupKey, folderFullPath string, rules []*ngmodels.AlertRule, limitAlerts int64, withStates map[eval.State]struct{}, matchers labels.Matchers, labelOptions []ngmodels.LabelOption) (*apimodels.RuleGroup, map[string]int64) { newGroup := &apimodels.RuleGroup{ Name: groupKey.RuleGroup, // file is what Prometheus uses for provisioning, we replace it with namespace which is the folder in Grafana. - File: folder.Fullpath, + File: folderFullPath, } rulesTotals := make(map[string]int64, len(rules)) @@ -362,7 +415,7 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder alertingRule := apimodels.AlertingRule{ State: "inactive", Name: rule.Title, - Query: ruleToQuery(srv.log, rule), + Query: ruleToQuery(log, rule), Duration: rule.For.Seconds(), Annotations: rule.Annotations, } @@ -375,7 +428,7 @@ func (srv PrometheusSrv) toRuleGroup(groupKey ngmodels.AlertRuleGroupKey, folder LastEvaluation: time.Time{}, } - states := srv.manager.GetStatesForRuleUID(rule.OrgID, rule.UID) + states := manager.GetStatesForRuleUID(rule.OrgID, rule.UID) totals := make(map[string]int64) totalsFiltered := make(map[string]int64) for _, alertState := range states { diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 8a77b72a9cf..53f1e1464cb 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -234,7 +234,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *contextmodel.ReqContext) response.Res } dashboardUID := c.Query("dashboard_uid") - panelID, err := getPanelIDFromRequest(c.Req) + panelID, err := getPanelIDFromQuery(c.Req.URL.Query()) if err != nil { return ErrResp(http.StatusBadRequest, err, "invalid panel_id") }