From 6bbb2fd4ba96e9fd55d7fa65d87e2ef73af7b128 Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Thu, 15 Apr 2021 15:54:37 +0300 Subject: [PATCH] [Alerting]: Several modifications in alert rules (#32983) * [Alerting]: Use common properties for all rules * Add Labels in rules * Fix update ruleGroup API Return 400 Bad Request response when the request contains a UID that does not exist * Check permissions and return namespace id * Apply suggestions from code review Co-authored-by: gotjosh --- go.mod | 2 +- go.sum | 2 + pkg/api/folder.go | 17 ++-- pkg/api/folder_permission.go | 10 +- pkg/services/ngalert/api/api_prometheus.go | 2 +- pkg/services/ngalert/api/api_ruler.go | 99 ++++++++++++------- pkg/services/ngalert/api/legacy_trans_dev.go | 2 +- .../api/test-data/post-rulegroup-42.json | 11 ++- pkg/services/ngalert/api/test-data/prom.http | 7 ++ .../test-data/ruler-grafana-recipient.http | 83 ++++++++++++++++ pkg/services/ngalert/models/alert_rule.go | 16 ++- pkg/services/ngalert/store/alert_rule.go | 68 ++++++++----- pkg/services/ngalert/store/database_mig.go | 6 ++ 13 files changed, 239 insertions(+), 86 deletions(-) create mode 100644 pkg/services/ngalert/api/test-data/prom.http diff --git a/go.mod b/go.mod index 35e072d6275..3a5ed745aad 100644 --- a/go.mod +++ b/go.mod @@ -41,7 +41,7 @@ require ( github.com/google/go-cmp v0.5.5 github.com/google/uuid v1.2.0 github.com/gosimple/slug v1.9.0 - github.com/grafana/alerting-api v0.0.0-20210412090350-fcb11bfbb6a4 + github.com/grafana/alerting-api v0.0.0-20210414165752-6625e7a4f9a9 github.com/grafana/grafana-aws-sdk v0.4.0 github.com/grafana/grafana-live-sdk v0.0.4 github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 diff --git a/go.sum b/go.sum index 10f9e14f2e4..e82c1edf596 100644 --- a/go.sum +++ b/go.sum @@ -820,6 +820,8 @@ github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= github.com/grafana/alerting-api v0.0.0-20210412090350-fcb11bfbb6a4 h1:S4nnWhH40AIWCkk3F7pUYVr67rqqangwm8a8cskYGyc= github.com/grafana/alerting-api v0.0.0-20210412090350-fcb11bfbb6a4/go.mod h1:Ce2PwraBlFMa+P0ArBzubfB/BXZV35mfYWQjM8C/BSE= +github.com/grafana/alerting-api v0.0.0-20210414165752-6625e7a4f9a9 h1:kPlrt7kss4NDk2w5G4pbvmdkQCiiJNmuORabWi3F2Ko= +github.com/grafana/alerting-api v0.0.0-20210414165752-6625e7a4f9a9/go.mod h1:Ce2PwraBlFMa+P0ArBzubfB/BXZV35mfYWQjM8C/BSE= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5JIhUUrggPcPBhOn+eT8+WsHiebvq7GgA= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/grafana/grafana v1.9.2-0.20210308201921-4ce0a49eac03/go.mod h1:AHRRvd4utJGY25J5nW8aL7wZzn/LcJ0z2za9oOp14j4= diff --git a/pkg/api/folder.go b/pkg/api/folder.go index 5c3b3f5b283..761b17bd11a 100644 --- a/pkg/api/folder.go +++ b/pkg/api/folder.go @@ -19,7 +19,7 @@ func (hs *HTTPServer) GetFolders(c *models.ReqContext) response.Response { folders, err := s.GetFolders(c.QueryInt64("limit")) if err != nil { - return toFolderError(err) + return ToFolderErrorResponse(err) } result := make([]dtos.FolderSearchHit, 0) @@ -39,7 +39,7 @@ func (hs *HTTPServer) GetFolderByUID(c *models.ReqContext) response.Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) folder, err := s.GetFolderByUID(c.Params(":uid")) if err != nil { - return toFolderError(err) + return ToFolderErrorResponse(err) } g := guardian.New(folder.Id, c.OrgId, c.SignedInUser) @@ -50,7 +50,7 @@ func (hs *HTTPServer) GetFolderByID(c *models.ReqContext) response.Response { s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) folder, err := s.GetFolderByID(c.ParamsInt64(":id")) if err != nil { - return toFolderError(err) + return ToFolderErrorResponse(err) } g := guardian.New(folder.Id, c.OrgId, c.SignedInUser) @@ -61,7 +61,7 @@ func (hs *HTTPServer) CreateFolder(c *models.ReqContext, cmd models.CreateFolder s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) folder, err := s.CreateFolder(cmd.Title, cmd.Uid) if err != nil { - return toFolderError(err) + return ToFolderErrorResponse(err) } if hs.Cfg.EditorsCanAdmin { @@ -79,7 +79,7 @@ func (hs *HTTPServer) UpdateFolder(c *models.ReqContext, cmd models.UpdateFolder s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) err := s.UpdateFolder(c.Params(":uid"), &cmd) if err != nil { - return toFolderError(err) + return ToFolderErrorResponse(err) } g := guardian.New(cmd.Result.Id, c.OrgId, c.SignedInUser) @@ -94,13 +94,13 @@ func (hs *HTTPServer) DeleteFolder(c *models.ReqContext) response.Response { // if errors.Is(err, librarypanels.ErrFolderHasConnectedLibraryPanels) { return response.Error(403, "Folder could not be deleted because it contains linked library panels", err) } - return toFolderError(err) + return ToFolderErrorResponse(err) } } f, err := s.DeleteFolder(c.Params(":uid")) if err != nil { - return toFolderError(err) + return ToFolderErrorResponse(err) } return response.JSON(200, util.DynMap{ @@ -141,7 +141,8 @@ func toFolderDto(g guardian.DashboardGuardian, folder *models.Folder) dtos.Folde } } -func toFolderError(err error) response.Response { +// ToFolderErrorResponse returns a different response status according to the folder error type +func ToFolderErrorResponse(err error) response.Response { var dashboardErr models.DashboardErr if ok := errors.As(err, &dashboardErr); ok { return response.Error(dashboardErr.StatusCode, err.Error(), err) diff --git a/pkg/api/folder_permission.go b/pkg/api/folder_permission.go index 0b0ca4b31c1..4fdb6a69dba 100644 --- a/pkg/api/folder_permission.go +++ b/pkg/api/folder_permission.go @@ -17,13 +17,13 @@ func (hs *HTTPServer) GetFolderPermissionList(c *models.ReqContext) response.Res folder, err := s.GetFolderByUID(c.Params(":uid")) if err != nil { - return toFolderError(err) + return ToFolderErrorResponse(err) } g := guardian.New(folder.Id, c.OrgId, c.SignedInUser) if canAdmin, err := g.CanAdmin(); err != nil || !canAdmin { - return toFolderError(models.ErrFolderAccessDenied) + return ToFolderErrorResponse(models.ErrFolderAccessDenied) } acl, err := g.GetAcl() @@ -64,17 +64,17 @@ func (hs *HTTPServer) UpdateFolderPermissions(c *models.ReqContext, apiCmd dtos. s := dashboards.NewFolderService(c.OrgId, c.SignedInUser, hs.SQLStore) folder, err := s.GetFolderByUID(c.Params(":uid")) if err != nil { - return toFolderError(err) + return ToFolderErrorResponse(err) } g := guardian.New(folder.Id, c.OrgId, c.SignedInUser) canAdmin, err := g.CanAdmin() if err != nil { - return toFolderError(err) + return ToFolderErrorResponse(err) } if !canAdmin { - return toFolderError(models.ErrFolderAccessDenied) + return ToFolderErrorResponse(models.ErrFolderAccessDenied) } var items []*models.DashboardAcl diff --git a/pkg/services/ngalert/api/api_prometheus.go b/pkg/services/ngalert/api/api_prometheus.go index 066273a38fa..442ab658c23 100644 --- a/pkg/services/ngalert/api/api_prometheus.go +++ b/pkg/services/ngalert/api/api_prometheus.go @@ -100,7 +100,7 @@ func (srv PrometheusSrv) RouteGetRuleStatuses(c *models.ReqContext) response.Res State: "inactive", Name: rule.Title, Query: "", // TODO: get this from parsing AlertRule.Data - Duration: time.Duration(rule.For).Seconds(), + Duration: rule.For.Seconds(), Annotations: rule.Annotations, } diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 96d01ba5774..4942bcc1fd3 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -1,13 +1,14 @@ package api import ( - "fmt" + "errors" "net/http" "time" "github.com/grafana/grafana/pkg/services/ngalert/store" apimodels "github.com/grafana/alerting-api/pkg/api" + coreapi "github.com/grafana/grafana/pkg/api" "github.com/grafana/grafana/pkg/api/response" "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/models" @@ -22,40 +23,41 @@ type RulerSrv struct { } func (srv RulerSrv) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response { - namespace := c.Params(":Namespace") - namespaceUID, err := srv.store.GetNamespaceUIDByTitle(namespace, c.SignedInUser.OrgId, c.SignedInUser) + namespaceTitle := c.Params(":Namespace") + namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true) if err != nil { - return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + return toNamespaceErrorResponse(err) } - if err := srv.store.DeleteNamespaceAlertRules(c.SignedInUser.OrgId, namespaceUID); err != nil { + + if err := srv.store.DeleteNamespaceAlertRules(c.SignedInUser.OrgId, namespace.Uid); err != nil { return response.Error(http.StatusInternalServerError, "failed to delete namespace alert rules", err) } return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules deleted"}) } func (srv RulerSrv) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Response { - namespace := c.Params(":Namespace") - namespaceUID, err := srv.store.GetNamespaceUIDByTitle(namespace, c.SignedInUser.OrgId, c.SignedInUser) + namespaceTitle := c.Params(":Namespace") + namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true) if err != nil { - return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + return toNamespaceErrorResponse(err) } ruleGroup := c.Params(":Groupname") - if err := srv.store.DeleteRuleGroupAlertRules(c.SignedInUser.OrgId, namespaceUID, ruleGroup); err != nil { + if err := srv.store.DeleteRuleGroupAlertRules(c.SignedInUser.OrgId, namespace.Uid, ruleGroup); err != nil { return response.Error(http.StatusInternalServerError, "failed to delete group alert rules", err) } return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"}) } func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response { - namespace := c.Params(":Namespace") - namespaceUID, err := srv.store.GetNamespaceUIDByTitle(namespace, c.SignedInUser.OrgId, c.SignedInUser) + namespaceTitle := c.Params(":Namespace") + namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false) if err != nil { - return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + return toNamespaceErrorResponse(err) } q := ngmodels.ListNamespaceAlertRulesQuery{ OrgID: c.SignedInUser.OrgId, - NamespaceUID: namespaceUID, + NamespaceUID: namespace.Uid, } if err := srv.store.GetNamespaceAlertRules(&q); err != nil { return response.Error(http.StatusInternalServerError, "failed to update rule group", err) @@ -71,33 +73,33 @@ func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response. Name: r.RuleGroup, Interval: ruleGroupInterval, Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r), + toGettableExtendedRuleNode(*r, namespace.Id), }, } } else { - ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r)) + ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, namespace.Id)) ruleGroupConfigs[r.RuleGroup] = ruleGroupConfig } } for _, ruleGroupConfig := range ruleGroupConfigs { - result[namespace] = append(result[namespace], ruleGroupConfig) + result[namespaceTitle] = append(result[namespaceTitle], ruleGroupConfig) } return response.JSON(http.StatusAccepted, result) } func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response { - namespace := c.Params(":Namespace") - namespaceUID, err := srv.store.GetNamespaceUIDByTitle(namespace, c.SignedInUser.OrgId, c.SignedInUser) + namespaceTitle := c.Params(":Namespace") + namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, false) if err != nil { - return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + return toNamespaceErrorResponse(err) } ruleGroup := c.Params(":Groupname") q := ngmodels.ListRuleGroupAlertRulesQuery{ OrgID: c.SignedInUser.OrgId, - NamespaceUID: namespaceUID, + NamespaceUID: namespace.Uid, RuleGroup: ruleGroup, } if err := srv.store.GetRuleGroupAlertRules(&q); err != nil { @@ -108,7 +110,7 @@ func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Resp 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)) + ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r, namespace.Id)) } result := apimodels.RuleGroupConfigResponse{ @@ -131,10 +133,11 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response configs := make(map[string]map[string]apimodels.GettableRuleGroupConfig) for _, r := range q.Result { - namespace, err := srv.store.GetNamespaceByUID(r.NamespaceUID, c.SignedInUser.OrgId, c.SignedInUser) + folder, err := srv.store.GetNamespaceByUID(r.NamespaceUID, c.SignedInUser.OrgId, c.SignedInUser) if err != nil { - return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", r.NamespaceUID), err) + return toNamespaceErrorResponse(err) } + namespace := folder.Title _, ok := configs[namespace] if !ok { ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second) @@ -143,7 +146,7 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response Name: r.RuleGroup, Interval: ruleGroupInterval, Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r), + toGettableExtendedRuleNode(*r, folder.Id), }, } } else { @@ -154,11 +157,11 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response Name: r.RuleGroup, Interval: ruleGroupInterval, Rules: []apimodels.GettableExtendedRuleNode{ - toGettableExtendedRuleNode(*r), + toGettableExtendedRuleNode(*r, folder.Id), }, } } else { - ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r)) + ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r, folder.Id)) configs[namespace][r.RuleGroup] = ruleGroupConfig } } @@ -174,10 +177,10 @@ func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response } func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig) response.Response { - namespace := c.Params(":Namespace") - namespaceUID, err := srv.store.GetNamespaceUIDByTitle(namespace, c.SignedInUser.OrgId, c.SignedInUser) + namespaceTitle := c.Params(":Namespace") + namespace, err := srv.store.GetNamespaceByTitle(namespaceTitle, c.SignedInUser.OrgId, c.SignedInUser, true) if err != nil { - return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err) + return toNamespaceErrorResponse(err) } // TODO check permissions @@ -186,17 +189,20 @@ func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConf if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{ OrgID: c.SignedInUser.OrgId, - NamespaceUID: namespaceUID, + NamespaceUID: namespace.Uid, RuleGroupConfig: ruleGroupConfig, }); err != nil { + if errors.Is(err, ngmodels.ErrAlertRuleNotFound) { + return response.Error(http.StatusNotFound, "failed to update rule group", err) + } return response.Error(http.StatusInternalServerError, "failed to update rule group", err) } return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group updated successfully"}) } -func toGettableExtendedRuleNode(r ngmodels.AlertRule) apimodels.GettableExtendedRuleNode { - return apimodels.GettableExtendedRuleNode{ +func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimodels.GettableExtendedRuleNode { + gettableExtendedRuleNode := apimodels.GettableExtendedRuleNode{ GrafanaManagedAlert: &apimodels.GettableGrafanaRule{ ID: r.ID, OrgID: r.OrgID, @@ -208,17 +214,22 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule) apimodels.GettableExtended Version: r.Version, UID: r.UID, NamespaceUID: r.NamespaceUID, + NamespaceID: namespaceID, RuleGroup: r.RuleGroup, NoDataState: apimodels.NoDataState(r.NoDataState), ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState), - For: r.For, - Annotations: r.Annotations, }, } + gettableExtendedRuleNode.ApiRuleNode = &apimodels.ApiRuleNode{ + For: model.Duration(r.For), + Annotations: r.Annotations, + Labels: r.Labels, + } + return gettableExtendedRuleNode } func toPostableExtendedRuleNode(r ngmodels.AlertRule) apimodels.PostableExtendedRuleNode { - return apimodels.PostableExtendedRuleNode{ + postableExtendedRuleNode := apimodels.PostableExtendedRuleNode{ GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ OrgID: r.OrgID, Title: r.Title, @@ -227,8 +238,22 @@ func toPostableExtendedRuleNode(r ngmodels.AlertRule) apimodels.PostableExtended UID: r.UID, NoDataState: apimodels.NoDataState(r.NoDataState), ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState), - For: r.For, - Annotations: r.Annotations, }, } + postableExtendedRuleNode.ApiRuleNode = &apimodels.ApiRuleNode{ + For: model.Duration(r.For), + Annotations: r.Annotations, + Labels: r.Labels, + } + return postableExtendedRuleNode +} + +func toNamespaceErrorResponse(err error) response.Response { + if errors.Is(err, ngmodels.ErrCannotEditNamespace) { + return response.Error(http.StatusForbidden, err.Error(), err) + } + if errors.Is(err, models.ErrDashboardIdentifierNotSet) { + return response.Error(http.StatusBadRequest, err.Error(), err) + } + return coreapi.ToFolderErrorResponse(err) } diff --git a/pkg/services/ngalert/api/legacy_trans_dev.go b/pkg/services/ngalert/api/legacy_trans_dev.go index d0a113b07ca..24461f57377 100644 --- a/pkg/services/ngalert/api/legacy_trans_dev.go +++ b/pkg/services/ngalert/api/legacy_trans_dev.go @@ -178,7 +178,7 @@ func (api *API) ruleGroupByOldID(c *models.ReqContext) response.Response { Condition: sseCond.Condition, NoDataState: *noDataSetting, ExecErrState: *execErrSetting, - For: ngmodels.Duration(oldAlert.For), + For: oldAlert.For, Annotations: ruleTags, } rgc := apimodels.PostableRuleGroupConfig{ diff --git a/pkg/services/ngalert/api/test-data/post-rulegroup-42.json b/pkg/services/ngalert/api/test-data/post-rulegroup-42.json index 737d10dbaea..50c79ec5750 100644 --- a/pkg/services/ngalert/api/test-data/post-rulegroup-42.json +++ b/pkg/services/ngalert/api/test-data/post-rulegroup-42.json @@ -3,13 +3,16 @@ "interval": "10s", "rules": [ { + "for": "1m", + "annotations": { + "foo": "bar" + }, + "labels": { + "label1": "val1" + }, "grafana_alert": { "title": "prom query with SSE", "condition": "condition", - "for": 5, - "annotations": { - "foo": "bar" - }, "data": [ { "refId": "query", diff --git a/pkg/services/ngalert/api/test-data/prom.http b/pkg/services/ngalert/api/test-data/prom.http new file mode 100644 index 00000000000..e05a8c4edf9 --- /dev/null +++ b/pkg/services/ngalert/api/test-data/prom.http @@ -0,0 +1,7 @@ + +@grafanaRecipient = grafana + +GET http://admin:admin@localhost:3000/api/prometheus/{{grafanaRecipient}}/api/v1/rules + +### +GET http://admin:admin@localhost:3000/api/prometheus/{{grafanaRecipient}}/api/v1/alerts \ No newline at end of file diff --git a/pkg/services/ngalert/api/test-data/ruler-grafana-recipient.http b/pkg/services/ngalert/api/test-data/ruler-grafana-recipient.http index 3de6a29458f..dd213243d1c 100644 --- a/pkg/services/ngalert/api/test-data/ruler-grafana-recipient.http +++ b/pkg/services/ngalert/api/test-data/ruler-grafana-recipient.http @@ -31,6 +31,10 @@ GET http://admin:admin@localhost:3000/api/ruler/{{grafanaRecipient}}/api/v1/rule // get group101 rules GET http://admin:admin@localhost:3000/api/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}}/group101 +### +// get group101 rules - empty namespace +GET http://admin:admin@localhost:3000/api/ruler/{{grafanaRecipient}}/api/v1/rules//group101 + ### // get namespace rules GET http://admin:admin@localhost:3000/api/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} @@ -244,3 +248,82 @@ GET http://admin:admin@localhost:3000/api/ruler/{{grafanaRecipient}}/api/v1/rule ### // get namespace rules GET http://admin:admin@localhost:3000/api/ruler/{{grafanaRecipient}}/api/v1/rules/{{namespace1}} + +### +// update rulegroup; Bad Request; not existing UID +POST http://admin:admin@localhost:3000/api/ruler/grafana/api/v1/rules/{{namespace1}} +Content-Type: application/json + +{ + "name": "group42", + "interval": "20s", + "rules": [ + { + "grafana_alert": { + "title": "prom query with SSE", + "condition": "condition", + "uid": "unknown", + "data": [ + { + "refId": "query", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "gdev-prometheus", + "datasourceUid": "000000002", + "expr": "http_request_duration_microseconds_count", + "hide": false, + "interval": "", + "intervalMs": 1000, + "legendFormat": "", + "maxDataPoints": 100, + "refId": "query" + } + }, + { + "refId": "reduced", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "query", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "reducer": "mean", + "refId": "reduced", + "type": "reduce" + } + }, + { + "refId": "condition", + "queryType": "", + "relativeTimeRange": { + "from": 18000, + "to": 10800 + }, + "model": { + "datasource": "__expr__", + "datasourceUid": "-100", + "expression": "$reduced > 42", + "hide": false, + "intervalMs": 1000, + "maxDataPoints": 100, + "refId": "condition", + "type": "math" + } + } + ], + "no_data_state": "NoData", + "exec_err_state": "Alerting" + } + } + ] +} \ No newline at end of file diff --git a/pkg/services/ngalert/models/alert_rule.go b/pkg/services/ngalert/models/alert_rule.go index b931d4631a1..6b8d4a7e579 100644 --- a/pkg/services/ngalert/models/alert_rule.go +++ b/pkg/services/ngalert/models/alert_rule.go @@ -11,6 +11,8 @@ var ( ErrAlertRuleNotFound = fmt.Errorf("could not find alert rule") // ErrAlertRuleFailedGenerateUniqueUID is an error for failure to generate alert rule UID ErrAlertRuleFailedGenerateUniqueUID = errors.New("failed to generate alert rule UID") + // ErrCannotEditNamespace is an error returned if the user does not have permissions to edit the namespace + ErrCannotEditNamespace = errors.New("user does not have permissions to edit the namespace") ) type NoDataState string @@ -52,8 +54,11 @@ type AlertRule struct { RuleGroup string NoDataState NoDataState ExecErrState ExecutionErrorState - For Duration - Annotations map[string]string + // ideally this field should have been apimodels.ApiDuration + // but this is currently not possible because of circular dependencies + For time.Duration + Annotations map[string]string + Labels map[string]string } // AlertRuleKey is the alert definition identifier @@ -102,8 +107,11 @@ type AlertRuleVersion struct { IntervalSeconds int64 NoDataState NoDataState ExecErrState ExecutionErrorState - For Duration - Annotations map[string]string + // ideally this field should have been apimodels.ApiDuration + // but this is currently not possible because of circular dependencies + For time.Duration + Annotations map[string]string + Labels map[string]string } // GetAlertRuleByUIDQuery is the query for retrieving/deleting an alert rule by UID and organisation ID. diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index ae69cb8b8b1..54309a81137 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -6,6 +6,8 @@ import ( "fmt" "time" + "github.com/grafana/grafana/pkg/services/guardian" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/dashboards" @@ -43,8 +45,8 @@ type RuleStore interface { GetOrgAlertRules(query *ngmodels.ListAlertRulesQuery) error GetNamespaceAlertRules(query *ngmodels.ListNamespaceAlertRulesQuery) error GetRuleGroupAlertRules(query *ngmodels.ListRuleGroupAlertRulesQuery) error - GetNamespaceUIDByTitle(string, int64, *models.SignedInUser) (string, error) - GetNamespaceByUID(string, int64, *models.SignedInUser) (string, error) + GetNamespaceByTitle(string, int64, *models.SignedInUser, bool) (*models.Folder, error) + GetNamespaceByUID(string, int64, *models.SignedInUser) (*models.Folder, error) GetOrgRuleGroups(query *ngmodels.ListOrgRuleGroupsQuery) error UpsertAlertRules([]UpsertRule) error UpdateRuleGroup(UpdateRuleGroupCmd) error @@ -68,7 +70,6 @@ func getAlertRuleByUID(sess *sqlstore.DBSession, alertRuleUID string, orgID int6 } // DeleteAlertRuleByUID is a handler for deleting an alert rule. -// It returns ngmodels.ErrAlertRuleNotFound if no alert rule is found for the provided ID. func (st DBstore) DeleteAlertRuleByUID(orgID int64, ruleUID string) error { return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { _, err := sess.Exec("DELETE FROM alert_rule WHERE org_id = ? AND uid = ?", orgID, ruleUID) @@ -156,7 +157,7 @@ func (st DBstore) UpsertAlertRules(rules []UpsertRule) error { existingAlertRule, err := getAlertRuleByUID(sess, r.New.UID, r.New.OrgID) if err != nil { if errors.Is(err, ngmodels.ErrAlertRuleNotFound) { - return nil + return fmt.Errorf("failed to get alert rule %s: %w", r.New.UID, err) } return err } @@ -214,6 +215,7 @@ func (st DBstore) UpsertAlertRules(rules []UpsertRule) error { r.New.For = r.Existing.For r.New.Annotations = r.Existing.Annotations + r.New.Labels = r.Existing.Labels if err := st.ValidateAlertRule(r.New, true); err != nil { return err @@ -247,6 +249,7 @@ func (st DBstore) UpsertAlertRules(rules []UpsertRule) error { ExecErrState: r.New.ExecErrState, For: r.New.For, Annotations: r.New.Annotations, + Labels: r.New.Labels, }) } @@ -310,24 +313,33 @@ func (st DBstore) GetRuleGroupAlertRules(query *ngmodels.ListRuleGroupAlertRules }) } -// GetNamespaceUIDByTitle is a handler for retrieving a namespace UID by its title. -func (st DBstore) GetNamespaceUIDByTitle(namespace string, orgID int64, user *models.SignedInUser) (string, error) { +// GetNamespaceByTitle is a handler for retrieving a namespace by its title. Alerting rules follow a Grafana folder-like structure which we call namespaces. +func (st DBstore) GetNamespaceByTitle(namespace string, orgID int64, user *models.SignedInUser, withEdit bool) (*models.Folder, error) { s := dashboards.NewFolderService(orgID, user, st.SQLStore) folder, err := s.GetFolderByTitle(namespace) if err != nil { - return "", err + return nil, err } - return folder.Uid, nil + + if withEdit { + g := guardian.New(folder.Id, orgID, user) + if canAdmin, err := g.CanEdit(); err != nil || !canAdmin { + return nil, ngmodels.ErrCannotEditNamespace + } + } + + return folder, nil } // GetNamespaceByUID is a handler for retrieving namespace by its UID. -func (st DBstore) GetNamespaceByUID(UID string, orgID int64, user *models.SignedInUser) (string, error) { +func (st DBstore) GetNamespaceByUID(UID string, orgID int64, user *models.SignedInUser) (*models.Folder, error) { s := dashboards.NewFolderService(orgID, user, st.SQLStore) folder, err := s.GetFolderByUID(UID) if err != nil { - return "", err + return nil, err } - return folder.Title, nil + + return folder, nil } // GetAlertRulesForScheduling returns alert rule info (identifier, interval, version state) @@ -419,21 +431,27 @@ func (st DBstore) UpdateRuleGroup(cmd UpdateRuleGroupCmd) error { continue } + new := ngmodels.AlertRule{ + OrgID: cmd.OrgID, + Title: r.GrafanaManagedAlert.Title, + Condition: r.GrafanaManagedAlert.Condition, + Data: r.GrafanaManagedAlert.Data, + UID: r.GrafanaManagedAlert.UID, + IntervalSeconds: int64(time.Duration(cmd.RuleGroupConfig.Interval).Seconds()), + NamespaceUID: cmd.NamespaceUID, + RuleGroup: ruleGroup, + NoDataState: ngmodels.NoDataState(r.GrafanaManagedAlert.NoDataState), + ExecErrState: ngmodels.ExecutionErrorState(r.GrafanaManagedAlert.ExecErrState), + } + + if r.ApiRuleNode != nil { + new.For = time.Duration(r.ApiRuleNode.For) + new.Annotations = r.ApiRuleNode.Annotations + new.Labels = r.ApiRuleNode.Labels + } + upsertRule := UpsertRule{ - New: ngmodels.AlertRule{ - OrgID: cmd.OrgID, - Title: r.GrafanaManagedAlert.Title, - Condition: r.GrafanaManagedAlert.Condition, - Data: r.GrafanaManagedAlert.Data, - UID: r.GrafanaManagedAlert.UID, - IntervalSeconds: int64(time.Duration(cmd.RuleGroupConfig.Interval).Seconds()), - NamespaceUID: cmd.NamespaceUID, - RuleGroup: ruleGroup, - For: r.GrafanaManagedAlert.For, - Annotations: r.GrafanaManagedAlert.Annotations, - NoDataState: ngmodels.NoDataState(r.GrafanaManagedAlert.NoDataState), - ExecErrState: ngmodels.ExecutionErrorState(r.GrafanaManagedAlert.ExecErrState), - }, + New: new, } if existingGroupRule, ok := existingGroupRulesUIDs[r.GrafanaManagedAlert.UID]; ok { diff --git a/pkg/services/ngalert/store/database_mig.go b/pkg/services/ngalert/store/database_mig.go index 341b9a40211..70368a26a4d 100644 --- a/pkg/services/ngalert/store/database_mig.go +++ b/pkg/services/ngalert/store/database_mig.go @@ -153,6 +153,9 @@ func AddAlertRuleMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) // add annotations column mg.AddMigration("add column annotations to alert_rule", migrator.NewAddColumnMigration(alertRule, &migrator.Column{Name: "annotations", Type: migrator.DB_Text, Nullable: true})) + + // add labels column + mg.AddMigration("add column labels to alert_rule", migrator.NewAddColumnMigration(alertRule, &migrator.Column{Name: "labels", Type: migrator.DB_Text, Nullable: true})) } func AddAlertRuleVersionMigrations(mg *migrator.Migrator) { @@ -193,4 +196,7 @@ func AddAlertRuleVersionMigrations(mg *migrator.Migrator) { // add annotations column mg.AddMigration("add column annotations to alert_rule_version", migrator.NewAddColumnMigration(alertRuleVersion, &migrator.Column{Name: "annotations", Type: migrator.DB_Text, Nullable: true})) + + // add labels column + mg.AddMigration("add column labels to alert_rule_version", migrator.NewAddColumnMigration(alertRuleVersion, &migrator.Column{Name: "labels", Type: migrator.DB_Text, Nullable: true})) }