Skip to content

Commit ee06970

Browse files
papagiankylebrandt
andauthored
[Alerting]: Grafana managed ruler API implementation (grafana#32537)
* [Alerting]: Grafana managed ruler API impl * Apply suggestions from code review * fix lint * Add validation for ruleGroup name length * Fix MySQL migration Co-authored-by: kyle <[email protected]>
1 parent e499585 commit ee06970

19 files changed

+1433
-356
lines changed

go.mod

+1-1
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ require (
4040
github.com/google/go-cmp v0.5.5
4141
github.com/google/uuid v1.2.0
4242
github.com/gosimple/slug v1.9.0
43-
github.com/grafana/alerting-api v0.0.0-20210331130828-17c19ddf88ee
43+
github.com/grafana/alerting-api v0.0.0-20210331135037-3294563b51bb
4444
github.com/grafana/grafana-aws-sdk v0.4.0
4545
github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4
4646
github.com/grafana/grafana-plugin-sdk-go v0.90.0

go.sum

+4
Original file line numberDiff line numberDiff line change
@@ -796,6 +796,10 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U
796796
github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
797797
github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs=
798798
github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg=
799+
github.com/grafana/alerting-api v0.0.0-20210323194814-03a29a4c4c27 h1:DuyuEAHJeI+CMxIyzCVhmHcIeK+sjqberhDUfrgd3PY=
800+
github.com/grafana/alerting-api v0.0.0-20210323194814-03a29a4c4c27/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY=
801+
github.com/grafana/alerting-api v0.0.0-20210331135037-3294563b51bb h1:Hj25Whc/TRv0hSLm5VN0FJ5R4yZ6M4ycRcBgu7bsEAc=
802+
github.com/grafana/alerting-api v0.0.0-20210331135037-3294563b51bb/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY=
799803
github.com/grafana/alerting-api v0.0.0-20210330162237-0b5408c529a8 h1:okhEX26LU7AGN/3C8NDWfdjBmKclvoFvJz9o/LsNcK8=
800804
github.com/grafana/alerting-api v0.0.0-20210330162237-0b5408c529a8/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY=
801805
github.com/grafana/alerting-api v0.0.0-20210331130828-17c19ddf88ee h1:jpZdUOta4PK3CH3+2UCuzqn1SGZ+dQj+dWH45B0c1aI=

pkg/services/dashboards/folder_service.go

+19
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ type FolderService interface {
1616
GetFolders(limit int64) ([]*models.Folder, error)
1717
GetFolderByID(id int64) (*models.Folder, error)
1818
GetFolderByUID(uid string) (*models.Folder, error)
19+
GetFolderBySlug(slug string) (*models.Folder, error)
1920
CreateFolder(title, uid string) (*models.Folder, error)
2021
UpdateFolder(uid string, cmd *models.UpdateFolderCommand) error
2122
DeleteFolder(uid string) (*models.Folder, error)
@@ -96,6 +97,24 @@ func (dr *dashboardServiceImpl) GetFolderByUID(uid string) (*models.Folder, erro
9697
return dashToFolder(dashFolder), nil
9798
}
9899

100+
func (dr *dashboardServiceImpl) GetFolderBySlug(slug string) (*models.Folder, error) {
101+
query := models.GetDashboardQuery{OrgId: dr.orgId, Slug: slug}
102+
dashFolder, err := getFolder(query)
103+
if err != nil {
104+
return nil, toFolderError(err)
105+
}
106+
107+
g := guardian.New(dashFolder.Id, dr.orgId, dr.user)
108+
if canView, err := g.CanView(); err != nil || !canView {
109+
if err != nil {
110+
return nil, toFolderError(err)
111+
}
112+
return nil, models.ErrFolderAccessDenied
113+
}
114+
115+
return dashToFolder(dashFolder), nil
116+
}
117+
99118
func (dr *dashboardServiceImpl) CreateFolder(title, uid string) (*models.Folder, error) {
100119
dashFolder := models.NewDashboardFolder(title)
101120
dashFolder.OrgId = dr.orgId

pkg/services/ngalert/api/api.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ type API struct {
4444
DataService *tsdb.Service
4545
Schedule schedule.ScheduleService
4646
Store store.Store
47+
RuleStore store.RuleStore
4748
AlertingStore store.AlertingStore
4849
DataProxy *datasourceproxy.DatasourceProxyService
4950
Alertmanager Alertmanager
@@ -68,7 +69,7 @@ func (api *API) RegisterAPIEndpoints() {
6869
api.RegisterRulerApiEndpoints(NewForkedRuler(
6970
api.DatasourceCache,
7071
NewLotexRuler(proxy, logger),
71-
RulerApiMock{log: logger},
72+
RulerSrv{store: api.RuleStore, log: logger},
7273
))
7374
api.RegisterTestingApiEndpoints(TestingApiMock{log: logger})
7475

pkg/services/ngalert/api/api_ruler.go

+219
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,219 @@
1+
package api
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"time"
7+
8+
"github.com/grafana/grafana/pkg/services/ngalert/store"
9+
10+
apimodels "github.com/grafana/alerting-api/pkg/api"
11+
"github.com/grafana/grafana/pkg/api/response"
12+
"github.com/grafana/grafana/pkg/infra/log"
13+
"github.com/grafana/grafana/pkg/models"
14+
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
15+
"github.com/grafana/grafana/pkg/util"
16+
"github.com/prometheus/common/model"
17+
)
18+
19+
type RulerSrv struct {
20+
store store.RuleStore
21+
log log.Logger
22+
}
23+
24+
func (srv RulerSrv) RouteDeleteNamespaceRulesConfig(c *models.ReqContext) response.Response {
25+
namespace := c.Params(":Namespace")
26+
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser)
27+
if err != nil {
28+
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err)
29+
}
30+
if err := srv.store.DeleteNamespaceAlertRules(c.SignedInUser.OrgId, namespaceUID); err != nil {
31+
return response.Error(http.StatusInternalServerError, "failed to delete namespace alert rules", err)
32+
}
33+
return response.JSON(http.StatusAccepted, util.DynMap{"message": "namespace rules deleted"})
34+
}
35+
36+
func (srv RulerSrv) RouteDeleteRuleGroupConfig(c *models.ReqContext) response.Response {
37+
namespace := c.Params(":Namespace")
38+
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser)
39+
if err != nil {
40+
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err)
41+
}
42+
ruleGroup := c.Params(":Groupname")
43+
if err := srv.store.DeleteRuleGroupAlertRules(c.SignedInUser.OrgId, namespaceUID, ruleGroup); err != nil {
44+
return response.Error(http.StatusInternalServerError, "failed to delete group alert rules", err)
45+
}
46+
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group deleted"})
47+
}
48+
49+
func (srv RulerSrv) RouteGetNamespaceRulesConfig(c *models.ReqContext) response.Response {
50+
namespace := c.Params(":Namespace")
51+
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser)
52+
if err != nil {
53+
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err)
54+
}
55+
56+
q := ngmodels.ListNamespaceAlertRulesQuery{
57+
OrgID: c.SignedInUser.OrgId,
58+
NamespaceUID: namespaceUID,
59+
}
60+
if err := srv.store.GetNamespaceAlertRules(&q); err != nil {
61+
return response.Error(http.StatusInternalServerError, "failed to update rule group", err)
62+
}
63+
64+
result := apimodels.NamespaceConfigResponse{}
65+
ruleGroupConfigs := make(map[string]apimodels.GettableRuleGroupConfig)
66+
for _, r := range q.Result {
67+
ruleGroupConfig, ok := ruleGroupConfigs[r.RuleGroup]
68+
if !ok {
69+
ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second)
70+
ruleGroupConfigs[r.RuleGroup] = apimodels.GettableRuleGroupConfig{
71+
Name: r.RuleGroup,
72+
Interval: ruleGroupInterval,
73+
Rules: []apimodels.GettableExtendedRuleNode{
74+
toGettableExtendedRuleNode(*r),
75+
},
76+
}
77+
} else {
78+
ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r))
79+
ruleGroupConfigs[r.RuleGroup] = ruleGroupConfig
80+
}
81+
}
82+
83+
for _, ruleGroupConfig := range ruleGroupConfigs {
84+
result[namespace] = append(result[namespace], ruleGroupConfig)
85+
}
86+
87+
return response.JSON(http.StatusAccepted, result)
88+
}
89+
90+
func (srv RulerSrv) RouteGetRulegGroupConfig(c *models.ReqContext) response.Response {
91+
namespace := c.Params(":Namespace")
92+
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser)
93+
if err != nil {
94+
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err)
95+
}
96+
97+
ruleGroup := c.Params(":Groupname")
98+
q := ngmodels.ListRuleGroupAlertRulesQuery{
99+
OrgID: c.SignedInUser.OrgId,
100+
NamespaceUID: namespaceUID,
101+
RuleGroup: ruleGroup,
102+
}
103+
if err := srv.store.GetRuleGroupAlertRules(&q); err != nil {
104+
return response.Error(http.StatusInternalServerError, "failed to get group alert rules", err)
105+
}
106+
107+
var ruleGroupInterval model.Duration
108+
ruleNodes := make([]apimodels.GettableExtendedRuleNode, 0, len(q.Result))
109+
for _, r := range q.Result {
110+
ruleGroupInterval = model.Duration(time.Duration(r.IntervalSeconds) * time.Second)
111+
ruleNodes = append(ruleNodes, toGettableExtendedRuleNode(*r))
112+
}
113+
114+
result := apimodels.RuleGroupConfigResponse{
115+
GettableRuleGroupConfig: apimodels.GettableRuleGroupConfig{
116+
Name: ruleGroup,
117+
Interval: ruleGroupInterval,
118+
Rules: ruleNodes,
119+
},
120+
}
121+
return response.JSON(http.StatusAccepted, result)
122+
}
123+
124+
func (srv RulerSrv) RouteGetRulesConfig(c *models.ReqContext) response.Response {
125+
q := ngmodels.ListAlertRulesQuery{
126+
OrgID: c.SignedInUser.OrgId,
127+
}
128+
if err := srv.store.GetOrgAlertRules(&q); err != nil {
129+
return response.Error(http.StatusInternalServerError, "failed to get alert rules", err)
130+
}
131+
132+
configs := make(map[string]map[string]apimodels.GettableRuleGroupConfig)
133+
for _, r := range q.Result {
134+
namespace, err := srv.store.GetNamespaceByUID(r.NamespaceUID, c.SignedInUser.OrgId, c.SignedInUser)
135+
if err != nil {
136+
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", r.NamespaceUID), err)
137+
}
138+
_, ok := configs[namespace]
139+
if !ok {
140+
ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second)
141+
configs[namespace] = make(map[string]apimodels.GettableRuleGroupConfig)
142+
configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{
143+
Name: r.RuleGroup,
144+
Interval: ruleGroupInterval,
145+
Rules: []apimodels.GettableExtendedRuleNode{
146+
toGettableExtendedRuleNode(*r),
147+
},
148+
}
149+
} else {
150+
ruleGroupConfig, ok := configs[namespace][r.RuleGroup]
151+
if !ok {
152+
ruleGroupInterval := model.Duration(time.Duration(r.IntervalSeconds) * time.Second)
153+
configs[namespace][r.RuleGroup] = apimodels.GettableRuleGroupConfig{
154+
Name: r.RuleGroup,
155+
Interval: ruleGroupInterval,
156+
Rules: []apimodels.GettableExtendedRuleNode{
157+
toGettableExtendedRuleNode(*r),
158+
},
159+
}
160+
} else {
161+
ruleGroupConfig.Rules = append(ruleGroupConfig.Rules, toGettableExtendedRuleNode(*r))
162+
configs[namespace][r.RuleGroup] = ruleGroupConfig
163+
}
164+
}
165+
}
166+
167+
result := apimodels.NamespaceConfigResponse{}
168+
for namespace, m := range configs {
169+
for _, ruleGroupConfig := range m {
170+
result[namespace] = append(result[namespace], ruleGroupConfig)
171+
}
172+
}
173+
return response.JSON(http.StatusAccepted, result)
174+
}
175+
176+
func (srv RulerSrv) RoutePostNameRulesConfig(c *models.ReqContext, ruleGroupConfig apimodels.PostableRuleGroupConfig) response.Response {
177+
namespace := c.Params(":Namespace")
178+
namespaceUID, err := srv.store.GetNamespaceUIDBySlug(namespace, c.SignedInUser.OrgId, c.SignedInUser)
179+
if err != nil {
180+
return response.Error(http.StatusInternalServerError, fmt.Sprintf("failed to get namespace: %s", namespace), err)
181+
}
182+
183+
// TODO check permissions
184+
// TODO check quota
185+
// TODO validate UID uniqueness in the payload
186+
187+
ruleGroup := ruleGroupConfig.Name
188+
189+
if err := srv.store.UpdateRuleGroup(store.UpdateRuleGroupCmd{
190+
OrgID: c.SignedInUser.OrgId,
191+
NamespaceUID: namespaceUID,
192+
RuleGroup: ruleGroup,
193+
RuleGroupConfig: ruleGroupConfig,
194+
}); err != nil {
195+
return response.Error(http.StatusInternalServerError, "failed to update rule group", err)
196+
}
197+
198+
return response.JSON(http.StatusAccepted, util.DynMap{"message": "rule group updated successfully"})
199+
}
200+
201+
func toGettableExtendedRuleNode(r ngmodels.AlertRule) apimodels.GettableExtendedRuleNode {
202+
return apimodels.GettableExtendedRuleNode{
203+
GrafanaManagedAlert: &apimodels.GettableGrafanaRule{
204+
ID: r.ID,
205+
OrgID: r.OrgID,
206+
Title: r.Title,
207+
Condition: r.Condition,
208+
Data: r.Data,
209+
Updated: r.Updated,
210+
IntervalSeconds: r.IntervalSeconds,
211+
Version: r.Version,
212+
UID: r.UID,
213+
NamespaceUID: r.NamespaceUID,
214+
RuleGroup: r.RuleGroup,
215+
NoDataState: apimodels.NoDataState(r.NoDataState),
216+
ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState),
217+
},
218+
}
219+
}

pkg/services/ngalert/api/api_ruler_base.go

+3-3
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ type RulerApiService interface {
2323
RouteGetNamespaceRulesConfig(*models.ReqContext) response.Response
2424
RouteGetRulegGroupConfig(*models.ReqContext) response.Response
2525
RouteGetRulesConfig(*models.ReqContext) response.Response
26-
RoutePostNameRulesConfig(*models.ReqContext, apimodels.RuleGroupConfig) response.Response
26+
RoutePostNameRulesConfig(*models.ReqContext, apimodels.PostableRuleGroupConfig) response.Response
2727
}
2828

2929
type RulerApiBase struct {
@@ -37,7 +37,7 @@ func (api *API) RegisterRulerApiEndpoints(srv RulerApiService) {
3737
group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}"), routing.Wrap(srv.RouteGetNamespaceRulesConfig))
3838
group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}/{Groupname}"), routing.Wrap(srv.RouteGetRulegGroupConfig))
3939
group.Get(toMacaronPath("/ruler/{Recipient}/api/v1/rules"), routing.Wrap(srv.RouteGetRulesConfig))
40-
group.Post(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}"), binding.Bind(apimodels.RuleGroupConfig{}), routing.Wrap(srv.RoutePostNameRulesConfig))
40+
group.Post(toMacaronPath("/ruler/{Recipient}/api/v1/rules/{Namespace}"), binding.Bind(apimodels.PostableRuleGroupConfig{}), routing.Wrap(srv.RoutePostNameRulesConfig))
4141
})
4242
}
4343

@@ -83,7 +83,7 @@ func (base RulerApiBase) RouteGetRulesConfig(c *models.ReqContext) response.Resp
8383
return response.Error(http.StatusNotImplemented, "", nil)
8484
}
8585

86-
func (base RulerApiBase) RoutePostNameRulesConfig(c *models.ReqContext, body apimodels.RuleGroupConfig) response.Response {
86+
func (base RulerApiBase) RoutePostNameRulesConfig(c *models.ReqContext, body apimodels.PostableRuleGroupConfig) response.Response {
8787
recipient := c.Params(":Recipient")
8888
base.log.Info("RoutePostNameRulesConfig: ", "Recipient", recipient)
8989
namespace := c.Params(":Namespace")

0 commit comments

Comments
 (0)