mirror of
https://github.com/grafana/grafana.git
synced 2024-11-29 12:14:08 -06:00
6220872633
* Add fix * Add tests Co-authored-by: Yuriy Tseretyan <yuriy.tseretyan@grafana.com> Co-authored-by: Armand Grillet <2117580+armandgrillet@users.noreply.github.com> Co-authored-by: Jean-Philippe Quéméner <JohnnyQQQQ@users.noreply.github.com> Co-authored-by: George Robinson <george.robinson@grafana.com>
333 lines
12 KiB
Go
333 lines
12 KiB
Go
package api
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/api/apierrors"
|
|
"github.com/grafana/grafana/pkg/api/response"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/models"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
|
|
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/state"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
"github.com/grafana/grafana/pkg/services/quota"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
"github.com/grafana/grafana/pkg/web"
|
|
"github.com/prometheus/common/model"
|
|
)
|
|
|
|
type RulerSrv struct {
|
|
store store.RuleStore
|
|
DatasourceCache datasources.CacheService
|
|
QuotaService *quota.QuotaService
|
|
manager *state.Manager
|
|
log log.Logger
|
|
}
|
|
|
|
func (srv RulerSrv) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response {
|
|
namespaceTitle := web.Params(c.Req)[":Namespace"]
|
|
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
|
|
if err != nil {
|
|
return toNamespaceErrorResponse(err)
|
|
}
|
|
|
|
uids, err := srv.store.DeleteNamespaceAlertRules(c.SignedInUser.OrgId, namespace.Uid)
|
|
if err != nil {
|
|
return ErrResp(http.StatusInternalServerError, err, "failed to delete namespace alert rules")
|
|
}
|
|
|
|
for _, uid := range uids {
|
|
srv.manager.RemoveByRuleUID(c.SignedInUser.OrgId, uid)
|
|
}
|
|
|
|
return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules deleted"})
|
|
}
|
|
|
|
func (srv RulerSrv) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Response {
|
|
namespaceTitle := web.Params(c.Req)[":Namespace"]
|
|
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
|
|
if err != nil {
|
|
return toNamespaceErrorResponse(err)
|
|
}
|
|
ruleGroup := web.Params(c.Req)[":Groupname"]
|
|
uids, err := srv.store.DeleteRuleGroupAlertRules(c.SignedInUser.OrgId, namespace.Uid, ruleGroup)
|
|
|
|
if err != nil {
|
|
if errors.Is(err, ngmodels.ErrRuleGroupNamespaceNotFound) {
|
|
return ErrResp(http.StatusNotFound, err, "failed to delete rule group")
|
|
}
|
|
return ErrResp(http.StatusInternalServerError, err, "failed to delete rule group")
|
|
}
|
|
|
|
for _, uid := range uids {
|
|
srv.manager.RemoveByRuleUID(c.SignedInUser.OrgId, uid)
|
|
}
|
|
|
|
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"})
|
|
}
|
|
|
|
func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response {
|
|
namespaceTitle := web.Params(c.Req)[":Namespace"]
|
|
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false)
|
|
if err != nil {
|
|
return toNamespaceErrorResponse(err)
|
|
}
|
|
|
|
q := ngmodels.ListNamespaceAlertRulesQuery{
|
|
OrgID: c.SignedInUser.OrgId,
|
|
NamespaceUID: namespace.Uid,
|
|
}
|
|
if err := srv.store.GetNamespaceAlertRules(&q); err != nil {
|
|
return ErrResp(http.StatusInternalServerError, err, "failed to update rule group")
|
|
}
|
|
|
|
result := apimodels.NamespaceConfigResponse{}
|
|
ruleGroupConfigs := make(map[string]apimodels.GettableRuleGroupConfig)
|
|
for _, r := range q.Result {
|
|
ruleGroupConfig, ok := ruleGroupConfigs[r.RuleGroup]
|
|
if !ok {
|
|
ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second)
|
|
ruleGroupConfigs[r.RuleGroup] = apimodels.GettableRuleGroupConfig{
|
|
Name: r.RuleGroup,
|
|
Interval: ruleGroupInterval,
|
|
Rules: []apimodels.GettableExtendedRuleNode{
|
|
toGettableExtendedRuleNode(*r, namespace.Id),
|
|
},
|
|
}
|
|
} else {
|
|
ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, namespace.Id))
|
|
ruleGroupConfigs[r.RuleGroup] = ruleGroupConfig
|
|
}
|
|
}
|
|
|
|
for _, ruleGroupConfig := range ruleGroupConfigs {
|
|
result[namespaceTitle] = append(result[namespaceTitle], ruleGroupConfig)
|
|
}
|
|
|
|
return response.JSON(http.StatusAccepted, result)
|
|
}
|
|
|
|
func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response {
|
|
namespaceTitle := web.Params(c.Req)[":Namespace"]
|
|
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false)
|
|
if err != nil {
|
|
return toNamespaceErrorResponse(err)
|
|
}
|
|
|
|
ruleGroup := web.Params(c.Req)[":Groupname"]
|
|
q := ngmodels.ListRuleGroupAlertRulesQuery{
|
|
OrgID: c.SignedInUser.OrgId,
|
|
NamespaceUID: namespace.Uid,
|
|
RuleGroup: ruleGroup,
|
|
}
|
|
if err := srv.store.GetRuleGroupAlertRules(&q); err != nil {
|
|
return ErrResp(http.StatusInternalServerError, err, "failed to get group alert rules")
|
|
}
|
|
|
|
var ruleGroupInterval model.Duration
|
|
ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(q.Result))
|
|
for _, r := range q.Result {
|
|
ruleGroupInterval = model.Duration(time.Duration(r.IntervalSeconds) * time.Second)
|
|
ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, namespace.Id))
|
|
}
|
|
|
|
result := apimodels.RuleGroupConfigResponse{
|
|
GettableRuleGroupConfig: apimodels.GettableRuleGroupConfig{
|
|
Name: ruleGroup,
|
|
Interval: ruleGroupInterval,
|
|
Rules: ruleNodes,
|
|
},
|
|
}
|
|
return response.JSON(http.StatusAccepted, result)
|
|
}
|
|
|
|
func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response {
|
|
namespaceMap, err := srv.store.GetNamespaces(c.Req.Context(), c.OrgId, c.SignedInUser)
|
|
if err != nil {
|
|
return ErrResp(http.StatusInternalServerError, err, "failed to get namespaces visible to the user")
|
|
}
|
|
result := apimodels.NamespaceConfigResponse{}
|
|
|
|
if len(namespaceMap) == 0 {
|
|
srv.log.Debug("User has no access to any namespaces")
|
|
return response.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
namespaceUIDs := make([]string, len(namespaceMap))
|
|
for k := range namespaceMap {
|
|
namespaceUIDs = append(namespaceUIDs, k)
|
|
}
|
|
|
|
dashboardUID := c.Query("dashboard_uid")
|
|
panelID, err := getPanelIDFromRequest(c.Req)
|
|
if err != nil {
|
|
return ErrResp(http.StatusBadRequest, err, "invalid panel_id")
|
|
}
|
|
if dashboardUID == "" && panelID != 0 {
|
|
return ErrResp(http.StatusBadRequest, errors.New("panel_id must be set with dashboard_uid"), "")
|
|
}
|
|
|
|
q := ngmodels.ListAlertRulesQuery{
|
|
OrgID: c.SignedInUser.OrgId,
|
|
NamespaceUIDs: namespaceUIDs,
|
|
DashboardUID: dashboardUID,
|
|
PanelID: panelID,
|
|
}
|
|
|
|
if err := srv.store.GetOrgAlertRules(&q); err != nil {
|
|
return ErrResp(http.StatusInternalServerError, err, "failed to get alert rules")
|
|
}
|
|
|
|
configs := make(map[string]map[string]apimodels.GettableRuleGroupConfig)
|
|
for _, r := range q.Result {
|
|
folder, ok := namespaceMap[r.NamespaceUID]
|
|
if !ok {
|
|
srv.log.Error("namespace not visible to the user", "user", c.SignedInUser.UserId, "namespace", r.NamespaceUID, "rule", r.UID)
|
|
continue
|
|
}
|
|
namespace := folder.Title
|
|
_, ok = configs[namespace]
|
|
if !ok {
|
|
ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second)
|
|
configs[namespace] = make(map[string]apimodels.GettableRuleGroupConfig)
|
|
configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{
|
|
Name: r.RuleGroup,
|
|
Interval: ruleGroupInterval,
|
|
Rules: []apimodels.GettableExtendedRuleNode{
|
|
toGettableExtendedRuleNode(*r, folder.Id),
|
|
},
|
|
}
|
|
} else {
|
|
ruleGroupConfig, ok := configs[namespace][r.RuleGroup]
|
|
if !ok {
|
|
ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second)
|
|
configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{
|
|
Name: r.RuleGroup,
|
|
Interval: ruleGroupInterval,
|
|
Rules: []apimodels.GettableExtendedRuleNode{
|
|
toGettableExtendedRuleNode(*r, folder.Id),
|
|
},
|
|
}
|
|
} else {
|
|
ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, folder.Id))
|
|
configs[namespace][r.RuleGroup] = ruleGroupConfig
|
|
}
|
|
}
|
|
}
|
|
|
|
for namespace, m := range configs {
|
|
for _, ruleGroupConfig := range m {
|
|
result[namespace] = append(result[namespace], ruleGroupConfig)
|
|
}
|
|
}
|
|
return response.JSON(http.StatusOK, result)
|
|
}
|
|
|
|
func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig) response.Response {
|
|
namespaceTitle := web.Params(c.Req)[":Namespace"]
|
|
namespace, err := srv.store.GetNamespaceByTitle(c.Req.Context(), namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true)
|
|
if err != nil {
|
|
return toNamespaceErrorResponse(err)
|
|
}
|
|
|
|
//TODO: Should this belong in alerting-api?
|
|
if ruleGroupConfig.Name == "" {
|
|
return ErrResp(http.StatusBadRequest, errors.New("rule group name is not valid"), "")
|
|
}
|
|
|
|
alertRuleUIDs := make(map[string]struct{})
|
|
for _, r := range ruleGroupConfig.Rules {
|
|
cond := ngmodels.Condition{
|
|
Condition: r.GrafanaManagedAlert.Condition,
|
|
OrgID: c.SignedInUser.OrgId,
|
|
Data: r.GrafanaManagedAlert.Data,
|
|
}
|
|
if err := validateCondition(cond, c.SignedInUser, c.SkipCache, srv.DatasourceCache); err != nil {
|
|
return ErrResp(http.StatusBadRequest, err, "failed to validate alert rule %q", r.GrafanaManagedAlert.Title)
|
|
}
|
|
if r.GrafanaManagedAlert.UID != "" {
|
|
_, ok := alertRuleUIDs[r.GrafanaManagedAlert.UID]
|
|
if ok {
|
|
return ErrResp(http.StatusBadRequest, fmt.Errorf("conflicting UID %q found", r.GrafanaManagedAlert.UID), "failed to validate alert rule %q", r.GrafanaManagedAlert.Title)
|
|
}
|
|
alertRuleUIDs[r.GrafanaManagedAlert.UID] = struct{}{}
|
|
}
|
|
}
|
|
|
|
numOfNewRules := len(ruleGroupConfig.Rules) - len(alertRuleUIDs)
|
|
if numOfNewRules > 0 {
|
|
// quotas are checked in advanced
|
|
// that is acceptable under the assumption that there will be only one alert rule under the rule group
|
|
// alternatively we should check the quotas after the rule group update
|
|
// and rollback the transaction in case of violation
|
|
limitReached, err := srv.QuotaService.QuotaReached(c, "alert_rule")
|
|
if err != nil {
|
|
return ErrResp(http.StatusInternalServerError, err, "failed to get quota")
|
|
}
|
|
if limitReached {
|
|
return ErrResp(http.StatusForbidden, errors.New("quota reached"), "")
|
|
}
|
|
}
|
|
|
|
if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{
|
|
OrgID: c.SignedInUser.OrgId,
|
|
NamespaceUID: namespace.Uid,
|
|
RuleGroupConfig: ruleGroupConfig,
|
|
}); err != nil {
|
|
if errors.Is(err, ngmodels.ErrAlertRuleNotFound) {
|
|
return ErrResp(http.StatusNotFound, err, "failed to update rule group")
|
|
} else if errors.Is(err, ngmodels.ErrAlertRuleFailedValidation) {
|
|
return ErrResp(http.StatusBadRequest, err, "failed to update rule group")
|
|
}
|
|
return ErrResp(http.StatusInternalServerError, err, "failed to update rule group")
|
|
}
|
|
|
|
for uid := range alertRuleUIDs {
|
|
srv.manager.RemoveByRuleUID(c.OrgId, uid)
|
|
}
|
|
|
|
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group updated successfully"})
|
|
}
|
|
|
|
func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimodels.GettableExtendedRuleNode {
|
|
gettableExtendedRuleNode := apimodels.GettableExtendedRuleNode{
|
|
GrafanaManagedAlert: &apimodels.GettableGrafanaRule{
|
|
ID: r.ID,
|
|
OrgID: r.OrgID,
|
|
Title: r.Title,
|
|
Condition: r.Condition,
|
|
Data: r.Data,
|
|
Updated: r.Updated,
|
|
IntervalSeconds: r.IntervalSeconds,
|
|
Version: r.Version,
|
|
UID: r.UID,
|
|
NamespaceUID: r.NamespaceUID,
|
|
NamespaceID: namespaceID,
|
|
RuleGroup: r.RuleGroup,
|
|
NoDataState: apimodels.NoDataState(r.NoDataState),
|
|
ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState),
|
|
},
|
|
}
|
|
gettableExtendedRuleNode.ApiRuleNode = &apimodels.ApiRuleNode{
|
|
For: model.Duration(r.For),
|
|
Annotations: r.Annotations,
|
|
Labels: r.Labels,
|
|
}
|
|
return gettableExtendedRuleNode
|
|
}
|
|
|
|
func toNamespaceErrorResponse(err error) response.Response {
|
|
if errors.Is(err, ngmodels.ErrCannotEditNamespace) {
|
|
return ErrResp(http.StatusForbidden, err, err.Error())
|
|
}
|
|
if errors.Is(err, models.ErrDashboardIdentifierNotSet) {
|
|
return ErrResp(http.StatusBadRequest, err, err.Error())
|
|
}
|
|
return apierrors.ToFolderErrorResponse(err)
|
|
}
|