From 81d360529b18a977127a13c52250e1f82331d479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jean-Philippe=20Qu=C3=A9m=C3=A9ner?= Date: Thu, 2 Jun 2022 14:48:53 +0200 Subject: [PATCH] Alerting: Provisioning API - Alert rules (#47930) --- pkg/services/ngalert/api/api.go | 2 + pkg/services/ngalert/api/api_provisioning.go | 66 +++- pkg/services/ngalert/api/api_ruler.go | 2 +- pkg/services/ngalert/api/authorization.go | 9 +- .../ngalert/api/authorization_test.go | 2 +- .../ngalert/api/forked_provisioning.go | 20 ++ .../api/generated_base_api_alertmanager.go | 24 -- .../api/generated_base_api_configuration.go | 4 - .../api/generated_base_api_prometheus.go | 4 - .../api/generated_base_api_provisioning.go | 97 +++++- .../ngalert/api/generated_base_api_ruler.go | 12 - .../ngalert/api/generated_base_api_testing.go | 3 - pkg/services/ngalert/api/tooling/Makefile | 8 +- .../definitions/provisioning_alert_rules.go | 144 +++++++++ pkg/services/ngalert/api/tooling/post.json | 298 +++++++++++++++++- pkg/services/ngalert/api/tooling/spec.json | 288 ++++++++++++++++- pkg/services/ngalert/ngalert.go | 2 + .../ngalert/provisioning/alert_rules.go | 151 +++++++++ .../ngalert/provisioning/alert_rules_test.go | 166 ++++++++++ pkg/services/ngalert/store/alert_rule.go | 88 ++++-- pkg/services/ngalert/store/testing.go | 29 +- pkg/services/ngalert/tests/util.go | 2 +- 22 files changed, 1295 insertions(+), 126 deletions(-) create mode 100644 pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go create mode 100644 pkg/services/ngalert/provisioning/alert_rules.go create mode 100644 pkg/services/ngalert/provisioning/alert_rules_test.go diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index c163b0f2836..feff31c52ab 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -81,6 +81,7 @@ type API struct { ContactPointService *provisioning.ContactPointService Templates *provisioning.TemplateService MuteTimings *provisioning.MuteTimingService + AlertRules *provisioning.AlertRuleService } // RegisterAPIEndpoints registers API handlers @@ -142,6 +143,7 @@ func (api *API) RegisterAPIEndpoints(m *metrics.API) { contactPointService: api.ContactPointService, templates: api.Templates, muteTimings: api.MuteTimings, + alertRules: api.AlertRules, }), m) } } diff --git a/pkg/services/ngalert/api/api_provisioning.go b/pkg/services/ngalert/api/api_provisioning.go index 1edcb98d555..ce43a3c34ed 100644 --- a/pkg/services/ngalert/api/api_provisioning.go +++ b/pkg/services/ngalert/api/api_provisioning.go @@ -16,8 +16,13 @@ import ( "github.com/grafana/grafana/pkg/web" ) -const namePathParam = ":name" -const idPathParam = ":ID" +const ( + namePathParam = ":name" + idPathParam = ":ID" + uidPathParam = ":UID" + groupPathParam = ":Group" + folderUIDPathParam = ":FolderUID" +) type ProvisioningSrv struct { log log.Logger @@ -25,6 +30,7 @@ type ProvisioningSrv struct { contactPointService ContactPointService templates TemplateService muteTimings MuteTimingService + alertRules AlertRuleService } type ContactPointService interface { @@ -52,6 +58,14 @@ type MuteTimingService interface { DeleteMuteTiming(ctx context.Context, name string, orgID int64) error } +type AlertRuleService interface { + GetAlertRule(ctx context.Context, orgID int64, ruleUID string) (alerting_models.AlertRule, alerting_models.Provenance, error) + CreateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error) + UpdateAlertRule(ctx context.Context, rule alerting_models.AlertRule, provenance alerting_models.Provenance) (alerting_models.AlertRule, error) + DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance alerting_models.Provenance) error + UpdateAlertGroup(ctx context.Context, orgID int64, folderUID, rulegroup string, interval int64) error +} + func (srv *ProvisioningSrv) RouteGetPolicyTree(c *models.ReqContext) response.Response { policies, err := srv.policies.GetPolicyTree(c.Req.Context(), c.OrgId) if errors.Is(err, store.ErrNoAlertmanagerConfiguration) { @@ -223,6 +237,54 @@ func (srv *ProvisioningSrv) RouteDeleteMuteTiming(c *models.ReqContext) response return response.JSON(http.StatusNoContent, nil) } +func (srv *ProvisioningSrv) RouteRouteGetAlertRule(c *models.ReqContext) response.Response { + uid := pathParam(c, uidPathParam) + rule, provenace, err := srv.alertRules.GetAlertRule(c.Req.Context(), c.OrgId, uid) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, apimodels.NewAlertRule(rule, provenace)) +} + +func (srv *ProvisioningSrv) RoutePostAlertRule(c *models.ReqContext, ar apimodels.AlertRule) response.Response { + createdAlertRule, err := srv.alertRules.CreateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + ar.ID = createdAlertRule.ID + ar.UID = createdAlertRule.UID + ar.Updated = createdAlertRule.Updated + return response.JSON(http.StatusCreated, ar) +} + +func (srv *ProvisioningSrv) RoutePutAlertRule(c *models.ReqContext, ar apimodels.AlertRule) response.Response { + updatedAlertRule, err := srv.alertRules.UpdateAlertRule(c.Req.Context(), ar.UpstreamModel(), alerting_models.ProvenanceAPI) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + ar.Updated = updatedAlertRule.Updated + return response.JSON(http.StatusOK, ar) +} + +func (srv *ProvisioningSrv) RouteDeleteAlertRule(c *models.ReqContext) response.Response { + uid := pathParam(c, uidPathParam) + err := srv.alertRules.DeleteAlertRule(c.Req.Context(), c.OrgId, uid, alerting_models.ProvenanceAPI) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusNoContent, "") +} + +func (srv *ProvisioningSrv) RoutePutAlertRuleGroup(c *models.ReqContext, ag apimodels.AlertRuleGroup) response.Response { + rulegroup := pathParam(c, groupPathParam) + folderUID := pathParam(c, folderUIDPathParam) + err := srv.alertRules.UpdateAlertGroup(c.Req.Context(), c.OrgId, folderUID, rulegroup, ag.Interval) + if err != nil { + return ErrResp(http.StatusInternalServerError, err, "") + } + return response.JSON(http.StatusOK, ag) +} + func pathParam(c *models.ReqContext, param string) string { return web.Params(c.Req)[param] } diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 58bcf157942..3c28eb9660f 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -422,7 +422,7 @@ func (srv RulerSrv) updateAlertRulesInGroup(c *models.ReqContext, groupKey ngmod for _, rule := range finalChanges.New { inserts = append(inserts, *rule) } - err = srv.store.InsertAlertRules(tranCtx, inserts) + _, err = srv.store.InsertAlertRules(tranCtx, inserts) if err != nil { return fmt.Errorf("failed to add rules: %w", err) } diff --git a/pkg/services/ngalert/api/authorization.go b/pkg/services/ngalert/api/authorization.go index f072035892b..df4fbfcece0 100644 --- a/pkg/services/ngalert/api/authorization.go +++ b/pkg/services/ngalert/api/authorization.go @@ -184,7 +184,8 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodGet + "/api/provisioning/templates", http.MethodGet + "/api/provisioning/templates/{name}", http.MethodGet + "/api/provisioning/mute-timings", - http.MethodGet + "/api/provisioning/mute-timings/{name}": + http.MethodGet + "/api/provisioning/mute-timings/{name}", + http.MethodGet + "/api/provisioning/alert-rules/{UID}": return middleware.ReqSignedIn case http.MethodPut + "/api/provisioning/policies", @@ -195,7 +196,11 @@ func (api *API) authorize(method, path string) web.Handler { http.MethodDelete + "/api/provisioning/templates/{name}", http.MethodPost + "/api/provisioning/mute-timings", http.MethodPut + "/api/provisioning/mute-timings/{name}", - http.MethodDelete + "/api/provisioning/mute-timings/{name}": + http.MethodDelete + "/api/provisioning/mute-timings/{name}", + http.MethodPost + "/api/provisioning/alert-rules", + http.MethodPut + "/api/provisioning/alert-rules/{UID}", + http.MethodDelete + "/api/provisioning/alert-rules/{UID}", + http.MethodPut + "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}": return middleware.ReqEditorRole } diff --git a/pkg/services/ngalert/api/authorization_test.go b/pkg/services/ngalert/api/authorization_test.go index c3969e91a2f..b9b7d9d5c89 100644 --- a/pkg/services/ngalert/api/authorization_test.go +++ b/pkg/services/ngalert/api/authorization_test.go @@ -48,7 +48,7 @@ func TestAuthorize(t *testing.T) { } paths[p] = methods } - require.Len(t, paths, 36) + require.Len(t, paths, 39) ac := acmock.New() api := &API{AccessControl: ac} diff --git a/pkg/services/ngalert/api/forked_provisioning.go b/pkg/services/ngalert/api/forked_provisioning.go index 297147e7e65..a0a94c76e1e 100644 --- a/pkg/services/ngalert/api/forked_provisioning.go +++ b/pkg/services/ngalert/api/forked_provisioning.go @@ -78,3 +78,23 @@ func (f *ForkedProvisioningApi) forkRoutePutMuteTiming(ctx *models.ReqContext, m func (f *ForkedProvisioningApi) forkRouteDeleteMuteTiming(ctx *models.ReqContext) response.Response { return f.svc.RouteDeleteMuteTiming(ctx) } + +func (f *ForkedProvisioningApi) forkRouteGetAlertRule(ctx *models.ReqContext) response.Response { + return f.svc.RouteRouteGetAlertRule(ctx) +} + +func (f *ForkedProvisioningApi) forkRoutePostAlertRule(ctx *models.ReqContext, ar apimodels.AlertRule) response.Response { + return f.svc.RoutePostAlertRule(ctx, ar) +} + +func (f *ForkedProvisioningApi) forkRoutePutAlertRule(ctx *models.ReqContext, ar apimodels.AlertRule) response.Response { + return f.svc.RoutePutAlertRule(ctx, ar) +} + +func (f *ForkedProvisioningApi) forkRouteDeleteAlertRule(ctx *models.ReqContext) response.Response { + return f.svc.RouteDeleteAlertRule(ctx) +} + +func (f *ForkedProvisioningApi) forkRoutePutAlertRuleGroup(ctx *models.ReqContext, ag apimodels.AlertRuleGroup) response.Response { + return f.svc.RoutePutAlertRuleGroup(ctx, ag) +} diff --git a/pkg/services/ngalert/api/generated_base_api_alertmanager.go b/pkg/services/ngalert/api/generated_base_api_alertmanager.go index 5b744e66038..368c0d2d547 100644 --- a/pkg/services/ngalert/api/generated_base_api_alertmanager.go +++ b/pkg/services/ngalert/api/generated_base_api_alertmanager.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -53,7 +52,6 @@ func (f *ForkedAlertmanagerApi) RouteCreateGrafanaSilence(ctx *models.ReqContext } return f.forkRouteCreateGrafanaSilence(ctx, conf) } - func (f *ForkedAlertmanagerApi) RouteCreateSilence(ctx *models.ReqContext) response.Response { conf := apimodels.PostableSilence{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -61,71 +59,54 @@ func (f *ForkedAlertmanagerApi) RouteCreateSilence(ctx *models.ReqContext) respo } return f.forkRouteCreateSilence(ctx, conf) } - func (f *ForkedAlertmanagerApi) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteAlertingConfig(ctx) } - func (f *ForkedAlertmanagerApi) RouteDeleteGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteGrafanaAlertingConfig(ctx) } - func (f *ForkedAlertmanagerApi) RouteDeleteGrafanaSilence(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteGrafanaSilence(ctx) } - func (f *ForkedAlertmanagerApi) RouteDeleteSilence(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteSilence(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { return f.forkRouteGetAMAlertGroups(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { return f.forkRouteGetAMAlerts(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetAMStatus(ctx *models.ReqContext) response.Response { return f.forkRouteGetAMStatus(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetAlertingConfig(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMAlertGroups(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAMAlertGroups(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMAlerts(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAMAlerts(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaAMStatus(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAMStatus(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAlertingConfig(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaSilence(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaSilence(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetGrafanaSilences(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaSilences(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetSilence(ctx *models.ReqContext) response.Response { return f.forkRouteGetSilence(ctx) } - func (f *ForkedAlertmanagerApi) RouteGetSilences(ctx *models.ReqContext) response.Response { return f.forkRouteGetSilences(ctx) } - func (f *ForkedAlertmanagerApi) RoutePostAMAlerts(ctx *models.ReqContext) response.Response { conf := apimodels.PostableAlerts{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -133,7 +114,6 @@ func (f *ForkedAlertmanagerApi) RoutePostAMAlerts(ctx *models.ReqContext) respon } return f.forkRoutePostAMAlerts(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostAlertingConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableUserConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -141,7 +121,6 @@ func (f *ForkedAlertmanagerApi) RoutePostAlertingConfig(ctx *models.ReqContext) } return f.forkRoutePostAlertingConfig(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostGrafanaAMAlerts(ctx *models.ReqContext) response.Response { conf := apimodels.PostableAlerts{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -149,7 +128,6 @@ func (f *ForkedAlertmanagerApi) RoutePostGrafanaAMAlerts(ctx *models.ReqContext) } return f.forkRoutePostGrafanaAMAlerts(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostGrafanaAlertingConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableUserConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -157,7 +135,6 @@ func (f *ForkedAlertmanagerApi) RoutePostGrafanaAlertingConfig(ctx *models.ReqCo } return f.forkRoutePostGrafanaAlertingConfig(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostTestGrafanaReceivers(ctx *models.ReqContext) response.Response { conf := apimodels.TestReceiversConfigBodyParams{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -165,7 +142,6 @@ func (f *ForkedAlertmanagerApi) RoutePostTestGrafanaReceivers(ctx *models.ReqCon } return f.forkRoutePostTestGrafanaReceivers(ctx, conf) } - func (f *ForkedAlertmanagerApi) RoutePostTestReceivers(ctx *models.ReqContext) response.Response { conf := apimodels.TestReceiversConfigBodyParams{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_configuration.go b/pkg/services/ngalert/api/generated_base_api_configuration.go index 4be46dbc058..084c27dec82 100644 --- a/pkg/services/ngalert/api/generated_base_api_configuration.go +++ b/pkg/services/ngalert/api/generated_base_api_configuration.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -29,15 +28,12 @@ type ConfigurationApiForkingService interface { func (f *ForkedConfigurationApi) RouteDeleteNGalertConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteNGalertConfig(ctx) } - func (f *ForkedConfigurationApi) RouteGetAlertmanagers(ctx *models.ReqContext) response.Response { return f.forkRouteGetAlertmanagers(ctx) } - func (f *ForkedConfigurationApi) RouteGetNGalertConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetNGalertConfig(ctx) } - func (f *ForkedConfigurationApi) RoutePostNGalertConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableNGalertConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_prometheus.go b/pkg/services/ngalert/api/generated_base_api_prometheus.go index fd4cadcb18e..32198a858bb 100644 --- a/pkg/services/ngalert/api/generated_base_api_prometheus.go +++ b/pkg/services/ngalert/api/generated_base_api_prometheus.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -27,15 +26,12 @@ type PrometheusApiForkingService interface { func (f *ForkedPrometheusApi) RouteGetAlertStatuses(ctx *models.ReqContext) response.Response { return f.forkRouteGetAlertStatuses(ctx) } - func (f *ForkedPrometheusApi) RouteGetGrafanaAlertStatuses(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaAlertStatuses(ctx) } - func (f *ForkedPrometheusApi) RouteGetGrafanaRuleStatuses(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaRuleStatuses(ctx) } - func (f *ForkedPrometheusApi) RouteGetRuleStatuses(ctx *models.ReqContext) response.Response { return f.forkRouteGetRuleStatuses(ctx) } diff --git a/pkg/services/ngalert/api/generated_base_api_provisioning.go b/pkg/services/ngalert/api/generated_base_api_provisioning.go index edde8b0afd1..b48c5212a9a 100644 --- a/pkg/services/ngalert/api/generated_base_api_provisioning.go +++ b/pkg/services/ngalert/api/generated_base_api_provisioning.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -20,59 +19,68 @@ import ( ) type ProvisioningApiForkingService interface { + RouteDeleteAlertRule(*models.ReqContext) response.Response RouteDeleteContactpoints(*models.ReqContext) response.Response RouteDeleteMuteTiming(*models.ReqContext) response.Response RouteDeleteTemplate(*models.ReqContext) response.Response + RouteGetAlertRule(*models.ReqContext) response.Response RouteGetContactpoints(*models.ReqContext) response.Response RouteGetMuteTiming(*models.ReqContext) response.Response RouteGetMuteTimings(*models.ReqContext) response.Response RouteGetPolicyTree(*models.ReqContext) response.Response RouteGetTemplate(*models.ReqContext) response.Response RouteGetTemplates(*models.ReqContext) response.Response + RoutePostAlertRule(*models.ReqContext) response.Response RoutePostContactpoints(*models.ReqContext) response.Response RoutePostMuteTiming(*models.ReqContext) response.Response + RoutePutAlertRule(*models.ReqContext) response.Response + RoutePutAlertRuleGroup(*models.ReqContext) response.Response RoutePutContactpoint(*models.ReqContext) response.Response RoutePutMuteTiming(*models.ReqContext) response.Response RoutePutPolicyTree(*models.ReqContext) response.Response RoutePutTemplate(*models.ReqContext) response.Response } +func (f *ForkedProvisioningApi) RouteDeleteAlertRule(ctx *models.ReqContext) response.Response { + return f.forkRouteDeleteAlertRule(ctx) +} func (f *ForkedProvisioningApi) RouteDeleteContactpoints(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteContactpoints(ctx) } - func (f *ForkedProvisioningApi) RouteDeleteMuteTiming(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteMuteTiming(ctx) } - func (f *ForkedProvisioningApi) RouteDeleteTemplate(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteTemplate(ctx) } - +func (f *ForkedProvisioningApi) RouteGetAlertRule(ctx *models.ReqContext) response.Response { + return f.forkRouteGetAlertRule(ctx) +} func (f *ForkedProvisioningApi) RouteGetContactpoints(ctx *models.ReqContext) response.Response { return f.forkRouteGetContactpoints(ctx) } - func (f *ForkedProvisioningApi) RouteGetMuteTiming(ctx *models.ReqContext) response.Response { return f.forkRouteGetMuteTiming(ctx) } - func (f *ForkedProvisioningApi) RouteGetMuteTimings(ctx *models.ReqContext) response.Response { return f.forkRouteGetMuteTimings(ctx) } - func (f *ForkedProvisioningApi) RouteGetPolicyTree(ctx *models.ReqContext) response.Response { return f.forkRouteGetPolicyTree(ctx) } - func (f *ForkedProvisioningApi) RouteGetTemplate(ctx *models.ReqContext) response.Response { return f.forkRouteGetTemplate(ctx) } - func (f *ForkedProvisioningApi) RouteGetTemplates(ctx *models.ReqContext) response.Response { return f.forkRouteGetTemplates(ctx) } - +func (f *ForkedProvisioningApi) RoutePostAlertRule(ctx *models.ReqContext) response.Response { + conf := apimodels.AlertRule{} + if err := web.Bind(ctx.Req, &conf); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + return f.forkRoutePostAlertRule(ctx, conf) +} func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) response.Response { conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -80,7 +88,6 @@ func (f *ForkedProvisioningApi) RoutePostContactpoints(ctx *models.ReqContext) r } return f.forkRoutePostContactpoints(ctx, conf) } - func (f *ForkedProvisioningApi) RoutePostMuteTiming(ctx *models.ReqContext) response.Response { conf := apimodels.MuteTimeInterval{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -88,7 +95,20 @@ func (f *ForkedProvisioningApi) RoutePostMuteTiming(ctx *models.ReqContext) resp } return f.forkRoutePostMuteTiming(ctx, conf) } - +func (f *ForkedProvisioningApi) RoutePutAlertRule(ctx *models.ReqContext) response.Response { + conf := apimodels.AlertRule{} + if err := web.Bind(ctx.Req, &conf); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + return f.forkRoutePutAlertRule(ctx, conf) +} +func (f *ForkedProvisioningApi) RoutePutAlertRuleGroup(ctx *models.ReqContext) response.Response { + conf := apimodels.AlertRuleGroup{} + if err := web.Bind(ctx.Req, &conf); err != nil { + return response.Error(http.StatusBadRequest, "bad request data", err) + } + return f.forkRoutePutAlertRuleGroup(ctx, conf) +} func (f *ForkedProvisioningApi) RoutePutContactpoint(ctx *models.ReqContext) response.Response { conf := apimodels.EmbeddedContactPoint{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -96,7 +116,6 @@ func (f *ForkedProvisioningApi) RoutePutContactpoint(ctx *models.ReqContext) res } return f.forkRoutePutContactpoint(ctx, conf) } - func (f *ForkedProvisioningApi) RoutePutMuteTiming(ctx *models.ReqContext) response.Response { conf := apimodels.MuteTimeInterval{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -104,7 +123,6 @@ func (f *ForkedProvisioningApi) RoutePutMuteTiming(ctx *models.ReqContext) respo } return f.forkRoutePutMuteTiming(ctx, conf) } - func (f *ForkedProvisioningApi) RoutePutPolicyTree(ctx *models.ReqContext) response.Response { conf := apimodels.Route{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -112,7 +130,6 @@ func (f *ForkedProvisioningApi) RoutePutPolicyTree(ctx *models.ReqContext) respo } return f.forkRoutePutPolicyTree(ctx, conf) } - func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) response.Response { conf := apimodels.MessageTemplateContent{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -123,6 +140,16 @@ func (f *ForkedProvisioningApi) RoutePutTemplate(ctx *models.ReqContext) respons func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingService, m *metrics.API) { api.RouteRegister.Group("", func(group routing.RouteRegister) { + group.Delete( + toMacaronPath("/api/provisioning/alert-rules/{UID}"), + api.authorize(http.MethodDelete, "/api/provisioning/alert-rules/{UID}"), + metrics.Instrument( + http.MethodDelete, + "/api/provisioning/alert-rules/{UID}", + srv.RouteDeleteAlertRule, + m, + ), + ) group.Delete( toMacaronPath("/api/provisioning/contact-points/{ID}"), api.authorize(http.MethodDelete, "/api/provisioning/contact-points/{ID}"), @@ -153,6 +180,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Get( + toMacaronPath("/api/provisioning/alert-rules/{UID}"), + api.authorize(http.MethodGet, "/api/provisioning/alert-rules/{UID}"), + metrics.Instrument( + http.MethodGet, + "/api/provisioning/alert-rules/{UID}", + srv.RouteGetAlertRule, + m, + ), + ) group.Get( toMacaronPath("/api/provisioning/contact-points"), api.authorize(http.MethodGet, "/api/provisioning/contact-points"), @@ -213,6 +250,16 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Post( + toMacaronPath("/api/provisioning/alert-rules"), + api.authorize(http.MethodPost, "/api/provisioning/alert-rules"), + metrics.Instrument( + http.MethodPost, + "/api/provisioning/alert-rules", + srv.RoutePostAlertRule, + m, + ), + ) group.Post( toMacaronPath("/api/provisioning/contact-points"), api.authorize(http.MethodPost, "/api/provisioning/contact-points"), @@ -233,6 +280,26 @@ func (api *API) RegisterProvisioningApiEndpoints(srv ProvisioningApiForkingServi m, ), ) + group.Put( + toMacaronPath("/api/provisioning/alert-rules/{UID}"), + api.authorize(http.MethodPut, "/api/provisioning/alert-rules/{UID}"), + metrics.Instrument( + http.MethodPut, + "/api/provisioning/alert-rules/{UID}", + srv.RoutePutAlertRule, + m, + ), + ) + group.Put( + toMacaronPath("/api/provisioning/folder/{FolderUID}/rule-groups/{Group}"), + api.authorize(http.MethodPut, "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}"), + metrics.Instrument( + http.MethodPut, + "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}", + srv.RoutePutAlertRuleGroup, + m, + ), + ) group.Put( toMacaronPath("/api/provisioning/contact-points/{ID}"), api.authorize(http.MethodPut, "/api/provisioning/contact-points/{ID}"), diff --git a/pkg/services/ngalert/api/generated_base_api_ruler.go b/pkg/services/ngalert/api/generated_base_api_ruler.go index 3d2979b2ca3..7b2c8967d39 100644 --- a/pkg/services/ngalert/api/generated_base_api_ruler.go +++ b/pkg/services/ngalert/api/generated_base_api_ruler.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -37,43 +36,33 @@ type RulerApiForkingService interface { func (f *ForkedRulerApi) RouteDeleteGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteGrafanaRuleGroupConfig(ctx) } - func (f *ForkedRulerApi) RouteDeleteNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteNamespaceGrafanaRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteNamespaceRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { return f.forkRouteDeleteRuleGroupConfig(ctx) } - func (f *ForkedRulerApi) RouteGetGrafanaRuleGroupConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaRuleGroupConfig(ctx) } - func (f *ForkedRulerApi) RouteGetGrafanaRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetGrafanaRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteGetNamespaceGrafanaRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetNamespaceGrafanaRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetNamespaceRulesConfig(ctx) } - func (f *ForkedRulerApi) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetRulegGroupConfig(ctx) } - func (f *ForkedRulerApi) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { return f.forkRouteGetRulesConfig(ctx) } - func (f *ForkedRulerApi) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableRuleGroupConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -81,7 +70,6 @@ func (f *ForkedRulerApi) RoutePostNameGrafanaRulesConfig(ctx *models.ReqContext) } return f.forkRoutePostNameGrafanaRulesConfig(ctx, conf) } - func (f *ForkedRulerApi) RoutePostNameRulesConfig(ctx *models.ReqContext) response.Response { conf := apimodels.PostableRuleGroupConfig{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/generated_base_api_testing.go b/pkg/services/ngalert/api/generated_base_api_testing.go index 9ba0dc975a4..1dd46c6eb81 100644 --- a/pkg/services/ngalert/api/generated_base_api_testing.go +++ b/pkg/services/ngalert/api/generated_base_api_testing.go @@ -4,7 +4,6 @@ * *Do not manually edit these files, please find ngalert/api/swagger-codegen/ for commands on how to generate them. */ - package api import ( @@ -32,7 +31,6 @@ func (f *ForkedTestingApi) RouteEvalQueries(ctx *models.ReqContext) response.Res } return f.forkRouteEvalQueries(ctx, conf) } - func (f *ForkedTestingApi) RouteTestRuleConfig(ctx *models.ReqContext) response.Response { conf := apimodels.TestRulePayload{} if err := web.Bind(ctx.Req, &conf); err != nil { @@ -40,7 +38,6 @@ func (f *ForkedTestingApi) RouteTestRuleConfig(ctx *models.ReqContext) response. } return f.forkRouteTestRuleConfig(ctx, conf) } - func (f *ForkedTestingApi) RouteTestRuleGrafanaConfig(ctx *models.ReqContext) response.Response { conf := apimodels.TestRulePayload{} if err := web.Bind(ctx.Req, &conf); err != nil { diff --git a/pkg/services/ngalert/api/tooling/Makefile b/pkg/services/ngalert/api/tooling/Makefile index ed4fcfd094c..0e2821c8c67 100644 --- a/pkg/services/ngalert/api/tooling/Makefile +++ b/pkg/services/ngalert/api/tooling/Makefile @@ -35,7 +35,7 @@ api.json: spec-stable.json go run cmd/clean-swagger/main.go -if $(<) -of $@ swagger-codegen-api: - docker run --rm -v $$(pwd):/local --user $$(id -u):$$(id -g) swaggerapi/swagger-codegen-cli generate \ + docker run --rm -v $$(pwd):/local --user $$(id -u):$$(id -g) parsertongue/swagger-codegen-cli:3.0.32 generate \ -i /local/post.json \ -l go-server \ -Dapis \ @@ -49,9 +49,9 @@ copy-files: ls -1 go | xargs -n 1 -I {} mv go/{} ../generated_base_{} fix: - sed -i -e 's/apimodels\.\[\]PostableAlert/apimodels.PostableAlerts/' $(GENERATED_GO_MATCHERS) - sed -i -e 's/apimodels\.\[\]UpdateDashboardAclCommand/apimodels.Permissions/' $(GENERATED_GO_MATCHERS) - sed -i -e 's/apimodels\.\[\]PostableApiReceiver/apimodels.TestReceiversConfigParams/' $(GENERATED_GO_MATCHERS) + sed -i '' -e 's/apimodels\.\[\]PostableAlert/apimodels.PostableAlerts/' $(GENERATED_GO_MATCHERS) + sed -i '' -e 's/apimodels\.\[\]UpdateDashboardAclCommand/apimodels.Permissions/' $(GENERATED_GO_MATCHERS) + sed -i '' -e 's/apimodels\.\[\]PostableApiReceiver/apimodels.TestReceiversConfigParams/' $(GENERATED_GO_MATCHERS) goimports -w -v $(GENERATED_GO_MATCHERS) clean: diff --git a/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go new file mode 100644 index 00000000000..fca432c43db --- /dev/null +++ b/pkg/services/ngalert/api/tooling/definitions/provisioning_alert_rules.go @@ -0,0 +1,144 @@ +package definitions + +import ( + "time" + + "github.com/grafana/grafana/pkg/services/ngalert/models" +) + +// swagger:route GET /api/provisioning/alert-rules/{UID} provisioning RouteGetAlertRule +// +// Get a specific alert rule by UID. +// +// Responses: +// 200: AlertRule +// 400: ValidationError + +// swagger:route POST /api/provisioning/alert-rules provisioning RoutePostAlertRule +// +// Create a new alert rule. +// +// Responses: +// 201: AlertRule +// 400: ValidationError + +// swagger:route PUT /api/provisioning/alert-rules/{UID} provisioning RoutePutAlertRule +// +// Update an existing alert rule. +// +// Consumes: +// - application/json +// +// Responses: +// 200: AlertRule +// 400: ValidationError + +// swagger:route DELETE /api/provisioning/alert-rules/{UID} provisioning RouteDeleteAlertRule +// +// Delete a specific alert rule by UID. +// +// Responses: +// 204: description: The alert rule was deleted successfully. +// 400: ValidationError + +// swagger:parameters RouteGetAlertRule RoutePutAlertRule RouteDeleteAlertRule +type AlertRuleUIDReference struct { + // in:path + UID string +} + +// swagger:parameters RoutePostAlertRule RoutePutAlertRule +type AlertRulePayload struct { + // in:body + Body AlertRule +} + +type AlertRule struct { + ID int64 `json:"id"` + UID string `json:"uid"` + OrgID int64 `json:"orgID"` + FolderUID string `json:"folderUID"` + RuleGroup string `json:"ruleGroup"` + Title string `json:"title"` + Condition string `json:"condition"` + Data []models.AlertQuery `json:"data"` + Updated time.Time `json:"updated,omitempty"` + NoDataState models.NoDataState `json:"noDataState"` + ExecErrState models.ExecutionErrorState `json:"execErrState"` + For time.Duration `json:"for"` + Annotations map[string]string `json:"annotations,omitempty"` + Labels map[string]string `json:"labels,omitempty"` + Provenance models.Provenance `json:"provenance,omitempty"` +} + +func (a *AlertRule) UpstreamModel() models.AlertRule { + return models.AlertRule{ + ID: a.ID, + UID: a.UID, + OrgID: a.OrgID, + NamespaceUID: a.FolderUID, + RuleGroup: a.RuleGroup, + Title: a.Title, + Condition: a.Condition, + Data: a.Data, + Updated: a.Updated, + NoDataState: a.NoDataState, + ExecErrState: a.ExecErrState, + For: a.For, + Annotations: a.Annotations, + Labels: a.Labels, + } +} + +func NewAlertRule(rule models.AlertRule, provenance models.Provenance) AlertRule { + return AlertRule{ + ID: rule.ID, + UID: rule.UID, + OrgID: rule.OrgID, + FolderUID: rule.NamespaceUID, + RuleGroup: rule.RuleGroup, + Title: rule.Title, + For: rule.For, + Condition: rule.Condition, + Data: rule.Data, + Updated: rule.Updated, + NoDataState: rule.NoDataState, + ExecErrState: rule.ExecErrState, + Annotations: rule.Annotations, + Labels: rule.Labels, + Provenance: provenance, + } +} + +// swagger:route PUT /api/provisioning/folder/{FolderUID}/rule-groups/{Group} provisioning RoutePutAlertRuleGroup +// +// Update the interval of a rule group. +// +// Consumes: +// - application/json +// +// Responses: +// 200: AlertRuleGroup +// 400: ValidationError + +// swagger:parameters RoutePutAlertRuleGroup +type FolderUIDPathParam struct { + // in:path + FolderUID string `json:"FolderUID"` +} + +// swagger:parameters RoutePutAlertRuleGroup +type RuleGroupPathParam struct { + // in:path + Group string `json:"Group"` +} + +// swagger:parameters RoutePutAlertRuleGroup +type AlertRuleGroupPayload struct { + // in:body + Body AlertRuleGroup +} + +type AlertRuleGroup struct { + Interval int64 `json:"interval"` +} diff --git a/pkg/services/ngalert/api/tooling/post.json b/pkg/services/ngalert/api/tooling/post.json index a4b6a1cae67..77c9d3f9ffb 100644 --- a/pkg/services/ngalert/api/tooling/post.json +++ b/pkg/services/ngalert/api/tooling/post.json @@ -194,6 +194,91 @@ "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "AlertRule": { + "properties": { + "annotations": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "x-go-name": "Annotations" + }, + "condition": { + "type": "string", + "x-go-name": "Condition" + }, + "data": { + "items": { + "$ref": "#/definitions/AlertQuery" + }, + "type": "array", + "x-go-name": "Data" + }, + "execErrState": { + "$ref": "#/definitions/ExecutionErrorState" + }, + "folderUID": { + "type": "string", + "x-go-name": "FolderUID" + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "id": { + "format": "int64", + "type": "integer", + "x-go-name": "ID" + }, + "labels": { + "additionalProperties": { + "type": "string" + }, + "type": "object", + "x-go-name": "Labels" + }, + "noDataState": { + "$ref": "#/definitions/NoDataState" + }, + "orgID": { + "format": "int64", + "type": "integer", + "x-go-name": "OrgID" + }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, + "ruleGroup": { + "type": "string", + "x-go-name": "RuleGroup" + }, + "title": { + "type": "string", + "x-go-name": "Title" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + }, + "updated": { + "format": "date-time", + "type": "string", + "x-go-name": "Updated" + } + }, + "type": "object", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, + "AlertRuleGroup": { + "properties": { + "interval": { + "format": "int64", + "type": "integer", + "x-go-name": "Interval" + } + }, + "type": "object", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, "AlertingRule": { "description": "adapted from cortex", "properties": { @@ -663,6 +748,10 @@ "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, "EvalQueriesResponse": {}, + "ExecutionErrorState": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models" + }, "ExtendedReceiver": { "properties": { "email_configs": { @@ -1456,6 +1545,10 @@ "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "NoDataState": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models" + }, "NotFound": { "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -2923,6 +3016,7 @@ "x-go-package": "github.com/prometheus/alertmanager/timeinterval" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "properties": { "ForceQuery": { "type": "boolean" @@ -2955,9 +3049,9 @@ "$ref": "#/definitions/Userinfo" } }, - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "type": "object", - "x-go-package": "github.com/prometheus/common/config" + "x-go-package": "net/url" }, "Userinfo": { "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", @@ -3179,12 +3273,11 @@ "type": "object" }, "alertGroups": { + "description": "AlertGroups alert groups", "items": { "$ref": "#/definitions/alertGroup" }, - "type": "array", - "x-go-name": "AlertGroups", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "array" }, "alertStatus": { "description": "AlertStatus alert status", @@ -3304,6 +3397,7 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "properties": { "annotations": { "$ref": "#/definitions/labelSet" @@ -3362,9 +3456,7 @@ "status", "updatedAt" ], - "type": "object", - "x-go-name": "GettableAlert", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "gettableAlerts": { "description": "GettableAlerts gettable alerts", @@ -3374,7 +3466,6 @@ "type": "array" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "properties": { "comment": { "description": "comment", @@ -3426,7 +3517,9 @@ "status", "updatedAt" ], - "type": "object" + "type": "object", + "x-go-name": "GettableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "gettableSilences": { "items": { @@ -3563,6 +3656,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { + "description": "PostableSilence postable silence", "properties": { "comment": { "description": "comment", @@ -3602,11 +3696,10 @@ "matchers", "startsAt" ], - "type": "object", - "x-go-name": "PostableSilence", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "receiver": { + "description": "Receiver receiver", "properties": { "name": { "description": "name", @@ -3617,9 +3710,7 @@ "required": [ "name" ], - "type": "object", - "x-go-name": "Receiver", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" + "type": "object" }, "silence": { "description": "Silence silence", @@ -4838,6 +4929,134 @@ ] } }, + "/api/provisioning/alert-rules": { + "post": { + "operationId": "RoutePostAlertRule", + "parameters": [ + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/AlertRule" + } + } + ], + "responses": { + "201": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Create a new alert rule.", + "tags": [ + "provisioning" + ] + } + }, + "/api/provisioning/alert-rules/{UID}": { + "delete": { + "operationId": "RouteDeleteAlertRule", + "parameters": [ + { + "in": "path", + "name": "UID", + "required": true, + "type": "string" + } + ], + "responses": { + "204": { + "description": " The alert rule was deleted successfully." + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Delete a specific alert rule by UID.", + "tags": [ + "provisioning" + ] + }, + "get": { + "operationId": "RouteGetAlertRule", + "parameters": [ + { + "in": "path", + "name": "UID", + "required": true, + "type": "string" + } + ], + "responses": { + "200": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Get a specific alert rule by UID.", + "tags": [ + "provisioning" + ] + }, + "put": { + "consumes": [ + "application/json" + ], + "operationId": "RoutePutAlertRule", + "parameters": [ + { + "in": "path", + "name": "UID", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/AlertRule" + } + } + ], + "responses": { + "200": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Update an existing alert rule.", + "tags": [ + "provisioning" + ] + } + }, "/api/provisioning/contact-points": { "get": { "operationId": "RouteGetContactpoints", @@ -4969,6 +5188,53 @@ ] } }, + "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "put": { + "consumes": [ + "application/json" + ], + "operationId": "RoutePutAlertRuleGroup", + "parameters": [ + { + "in": "path", + "name": "FolderUID", + "required": true, + "type": "string" + }, + { + "in": "path", + "name": "Group", + "required": true, + "type": "string" + }, + { + "in": "body", + "name": "Body", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } + } + ], + "responses": { + "200": { + "description": "AlertRuleGroup", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + }, + "summary": "Update the interval of an rule group.", + "tags": [ + "provisioning" + ] + } + }, "/api/provisioning/mute-timings": { "get": { "operationId": "RouteGetMuteTimings", diff --git a/pkg/services/ngalert/api/tooling/spec.json b/pkg/services/ngalert/api/tooling/spec.json index eaec39f99c5..f6a58a5786c 100644 --- a/pkg/services/ngalert/api/tooling/spec.json +++ b/pkg/services/ngalert/api/tooling/spec.json @@ -1122,6 +1122,134 @@ } } }, + "/api/provisioning/alert-rules": { + "post": { + "tags": [ + "provisioning" + ], + "summary": "Create a new alert rule.", + "operationId": "RoutePostAlertRule", + "parameters": [ + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/AlertRule" + } + } + ], + "responses": { + "201": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, + "/api/provisioning/alert-rules/{UID}": { + "get": { + "tags": [ + "provisioning" + ], + "summary": "Get a specific alert rule by UID.", + "operationId": "RouteGetAlertRule", + "parameters": [ + { + "type": "string", + "name": "UID", + "in": "path", + "required": true + } + ], + "responses": { + "200": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, + "put": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning" + ], + "summary": "Update an existing alert rule.", + "operationId": "RoutePutAlertRule", + "parameters": [ + { + "type": "string", + "name": "UID", + "in": "path", + "required": true + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/AlertRule" + } + } + ], + "responses": { + "200": { + "description": "AlertRule", + "schema": { + "$ref": "#/definitions/AlertRule" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + }, + "delete": { + "tags": [ + "provisioning" + ], + "summary": "Delete a specific alert rule by UID.", + "operationId": "RouteDeleteAlertRule", + "parameters": [ + { + "type": "string", + "name": "UID", + "in": "path", + "required": true + } + ], + "responses": { + "204": { + "description": " The alert rule was deleted successfully." + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, "/api/provisioning/contact-points": { "get": { "tags": [ @@ -1253,6 +1381,53 @@ } } }, + "/api/provisioning/folder/{FolderUID}/rule-groups/{Group}": { + "put": { + "consumes": [ + "application/json" + ], + "tags": [ + "provisioning" + ], + "summary": "Update the interval of an rule group.", + "operationId": "RoutePutAlertRuleGroup", + "parameters": [ + { + "type": "string", + "name": "FolderUID", + "in": "path", + "required": true + }, + { + "type": "string", + "name": "Group", + "in": "path", + "required": true + }, + { + "name": "Body", + "in": "body", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } + } + ], + "responses": { + "200": { + "description": "AlertRuleGroup", + "schema": { + "$ref": "#/definitions/AlertRuleGroup" + } + }, + "400": { + "description": "ValidationError", + "schema": { + "$ref": "#/definitions/ValidationError" + } + } + } + } + }, "/api/provisioning/mute-timings": { "get": { "tags": [ @@ -2394,6 +2569,91 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "AlertRule": { + "type": "object", + "properties": { + "annotations": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Annotations" + }, + "condition": { + "type": "string", + "x-go-name": "Condition" + }, + "data": { + "type": "array", + "items": { + "$ref": "#/definitions/AlertQuery" + }, + "x-go-name": "Data" + }, + "execErrState": { + "$ref": "#/definitions/ExecutionErrorState" + }, + "folderUID": { + "type": "string", + "x-go-name": "FolderUID" + }, + "for": { + "$ref": "#/definitions/Duration" + }, + "id": { + "type": "integer", + "format": "int64", + "x-go-name": "ID" + }, + "labels": { + "type": "object", + "additionalProperties": { + "type": "string" + }, + "x-go-name": "Labels" + }, + "noDataState": { + "$ref": "#/definitions/NoDataState" + }, + "orgID": { + "type": "integer", + "format": "int64", + "x-go-name": "OrgID" + }, + "provenance": { + "$ref": "#/definitions/Provenance" + }, + "ruleGroup": { + "type": "string", + "x-go-name": "RuleGroup" + }, + "title": { + "type": "string", + "x-go-name": "Title" + }, + "uid": { + "type": "string", + "x-go-name": "UID" + }, + "updated": { + "type": "string", + "format": "date-time", + "x-go-name": "Updated" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, + "AlertRuleGroup": { + "type": "object", + "properties": { + "interval": { + "type": "integer", + "format": "int64", + "x-go-name": "Interval" + } + }, + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" + }, "AlertingRule": { "description": "adapted from cortex", "type": "object", @@ -2866,6 +3126,10 @@ "EvalQueriesResponse": { "$ref": "#/definitions/EvalQueriesResponse" }, + "ExecutionErrorState": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models" + }, "ExtendedReceiver": { "type": "object", "properties": { @@ -3660,6 +3924,10 @@ }, "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" }, + "NoDataState": { + "type": "string", + "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/models" + }, "NotFound": { "type": "object", "x-go-package": "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" @@ -5127,8 +5395,9 @@ "x-go-package": "github.com/prometheus/alertmanager/timeinterval" }, "URL": { + "description": "The general form represented is:\n\n[scheme:][//[userinfo@]host][/]path[?query][#fragment]\n\nURLs that do not start with a slash after the scheme are interpreted as:\n\nscheme:opaque[?query][#fragment]\n\nNote that the Path field is stored in decoded form: /%47%6f%2f becomes /Go/.\nA consequence is that it is impossible to tell which slashes in the Path were\nslashes in the raw URL and which were %2f. This distinction is rarely important,\nbut when it is, the code should use RawPath, an optional field which only gets\nset if the default encoding is different from Path.\n\nURL's String method uses the EscapedPath method to obtain the path. See the\nEscapedPath method for more details.", "type": "object", - "title": "URL is a custom URL type that allows validation at configuration load time.", + "title": "A URL represents a parsed URL (technically, a URI reference).", "properties": { "ForceQuery": { "type": "boolean" @@ -5161,7 +5430,7 @@ "$ref": "#/definitions/Userinfo" } }, - "x-go-package": "github.com/prometheus/common/config" + "x-go-package": "net/url" }, "Userinfo": { "description": "The Userinfo type is an immutable encapsulation of username and\npassword details for a URL. An existing Userinfo value is guaranteed\nto have a username set (potentially empty, as allowed by RFC 2396),\nand optionally a password.", @@ -5384,12 +5653,11 @@ "$ref": "#/definitions/alertGroup" }, "alertGroups": { + "description": "AlertGroups alert groups", "type": "array", "items": { "$ref": "#/definitions/alertGroup" }, - "x-go-name": "AlertGroups", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/alertGroups" }, "alertStatus": { @@ -5510,6 +5778,7 @@ "$ref": "#/definitions/Duration" }, "gettableAlert": { + "description": "GettableAlert gettable alert", "type": "object", "required": [ "labels", @@ -5569,8 +5838,6 @@ "x-go-name": "UpdatedAt" } }, - "x-go-name": "GettableAlert", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableAlert" }, "gettableAlerts": { @@ -5582,7 +5849,6 @@ "$ref": "#/definitions/gettableAlerts" }, "gettableSilence": { - "description": "GettableSilence gettable silence", "type": "object", "required": [ "comment", @@ -5635,6 +5901,8 @@ "x-go-name": "UpdatedAt" } }, + "x-go-name": "GettableSilence", + "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/gettableSilence" }, "gettableSilences": { @@ -5773,6 +6041,7 @@ "x-go-package": "github.com/prometheus/alertmanager/api/v2/models" }, "postableSilence": { + "description": "PostableSilence postable silence", "type": "object", "required": [ "comment", @@ -5813,11 +6082,10 @@ "x-go-name": "StartsAt" } }, - "x-go-name": "PostableSilence", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/postableSilence" }, "receiver": { + "description": "Receiver receiver", "type": "object", "required": [ "name" @@ -5829,8 +6097,6 @@ "x-go-name": "Name" } }, - "x-go-name": "Receiver", - "x-go-package": "github.com/prometheus/alertmanager/api/v2/models", "$ref": "#/definitions/receiver" }, "silence": { diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 6ae853e1326..c8621b22e6e 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -157,6 +157,7 @@ func (ng *AlertNG) init() error { contactPointService := provisioning.NewContactPointService(store, ng.SecretsService, store, store, ng.Log) templateService := provisioning.NewTemplateService(store, store, store, ng.Log) muteTimingService := provisioning.NewMuteTimingService(store, store, store, ng.Log) + alertRuleService := provisioning.NewAlertRuleService(store, store, store, int64(ng.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), ng.Log) api := api.API{ Cfg: ng.Cfg, @@ -180,6 +181,7 @@ func (ng *AlertNG) init() error { ContactPointService: contactPointService, Templates: templateService, MuteTimings: muteTimingService, + AlertRules: alertRuleService, } api.RegisterAPIEndpoints(ng.Metrics.GetAPIMetrics()) diff --git a/pkg/services/ngalert/provisioning/alert_rules.go b/pkg/services/ngalert/provisioning/alert_rules.go new file mode 100644 index 00000000000..483373e5d50 --- /dev/null +++ b/pkg/services/ngalert/provisioning/alert_rules.go @@ -0,0 +1,151 @@ +package provisioning + +import ( + "context" + "errors" + "fmt" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/util" +) + +type AlertRuleService struct { + defaultInterval int64 + ruleStore store.RuleStore + provenanceStore ProvisioningStore + xact TransactionManager + log log.Logger +} + +func NewAlertRuleService(ruleStore store.RuleStore, + provenanceStore ProvisioningStore, + xact TransactionManager, + defaultInterval int64, + log log.Logger) *AlertRuleService { + return &AlertRuleService{ + defaultInterval: defaultInterval, + ruleStore: ruleStore, + provenanceStore: provenanceStore, + xact: xact, + log: log, + } +} + +func (service *AlertRuleService) GetAlertRule(ctx context.Context, orgID int64, ruleUID string) (models.AlertRule, models.Provenance, error) { + query := &models.GetAlertRuleByUIDQuery{ + OrgID: orgID, + UID: ruleUID, + } + err := service.ruleStore.GetAlertRuleByUID(ctx, query) + if err != nil { + return models.AlertRule{}, models.ProvenanceNone, err + } + provenance, err := service.provenanceStore.GetProvenance(ctx, query.Result, orgID) + if err != nil { + return models.AlertRule{}, models.ProvenanceNone, err + } + return *query.Result, provenance, nil +} + +func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) { + if rule.UID == "" { + rule.UID = util.GenerateShortUID() + } + interval, err := service.ruleStore.GetRuleGroupInterval(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup) + // if the alert group does not exists we just use the default interval + if err != nil && errors.Is(err, store.ErrAlertRuleGroupNotFound) { + interval = service.defaultInterval + } else if err != nil { + return models.AlertRule{}, err + } + rule.IntervalSeconds = interval + rule.Updated = time.Now() + err = service.xact.InTransaction(ctx, func(ctx context.Context) error { + ids, err := service.ruleStore.InsertAlertRules(ctx, []models.AlertRule{ + rule, + }) + if err != nil { + return err + } + if id, ok := ids[rule.UID]; ok { + rule.ID = id + } else { + return errors.New("couldn't find newly created id") + } + err = service.ruleStore.UpdateRuleGroup(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup, rule.IntervalSeconds) + if err != nil { + return err + } + return service.provenanceStore.SetProvenance(ctx, &rule, rule.OrgID, provenance) + }) + if err != nil { + return models.AlertRule{}, err + } + return rule, nil +} + +func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) { + storedRule, storedProvenance, err := service.GetAlertRule(ctx, rule.OrgID, rule.UID) + if err != nil { + return models.AlertRule{}, err + } + if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { + return models.AlertRule{}, fmt.Errorf("cannot changed provenance from '%s' to '%s'", storedProvenance, provenance) + } + rule.Updated = time.Now() + rule.ID = storedRule.ID + rule.IntervalSeconds, err = service.ruleStore.GetRuleGroupInterval(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup) + if err != nil { + return models.AlertRule{}, err + } + service.log.Info("update rule", "ID", storedRule.ID, "labels", fmt.Sprintf("%+v", rule.Labels)) + err = service.xact.InTransaction(ctx, func(ctx context.Context) error { + err := service.ruleStore.UpdateAlertRules(ctx, []store.UpdateRule{ + { + Existing: &storedRule, + New: rule, + }, + }) + if err != nil { + return err + } + err = service.ruleStore.UpdateRuleGroup(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup, rule.IntervalSeconds) + if err != nil { + return err + } + return service.provenanceStore.SetProvenance(ctx, &rule, rule.OrgID, provenance) + }) + if err != nil { + return models.AlertRule{}, err + } + return rule, err +} + +func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance models.Provenance) error { + rule := &models.AlertRule{ + OrgID: orgID, + UID: ruleUID, + } + // check that provenance is not changed in a invalid way + storedProvenance, err := service.provenanceStore.GetProvenance(ctx, rule, rule.OrgID) + if err != nil { + return err + } + if storedProvenance != provenance && storedProvenance != models.ProvenanceNone { + return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance) + } + return service.xact.InTransaction(ctx, func(ctx context.Context) error { + err := service.ruleStore.DeleteAlertRulesByUID(ctx, orgID, ruleUID) + if err != nil { + return err + } + return service.provenanceStore.DeleteProvenance(ctx, rule, rule.OrgID) + }) +} + +func (service *AlertRuleService) UpdateAlertGroup(ctx context.Context, orgID int64, folderUID, roulegroup string, interval int64) error { + return service.ruleStore.UpdateRuleGroup(ctx, orgID, folderUID, roulegroup, interval) +} diff --git a/pkg/services/ngalert/provisioning/alert_rules_test.go b/pkg/services/ngalert/provisioning/alert_rules_test.go new file mode 100644 index 00000000000..37bb8f9d6eb --- /dev/null +++ b/pkg/services/ngalert/provisioning/alert_rules_test.go @@ -0,0 +1,166 @@ +package provisioning + +import ( + "context" + "encoding/json" + "testing" + "time" + + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/stretchr/testify/require" +) + +func TestAlertRuleService(t *testing.T) { + ruleService := createAlertRuleService(t) + t.Run("alert rule creation should return the created id", func(t *testing.T) { + var orgID int64 = 1 + rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#1", orgID), models.ProvenanceNone) + require.NoError(t, err) + require.NotEqual(t, 0, rule.ID, "expected to get the created id and not the zero value") + }) + t.Run("alert rule creation should set the right provenance", func(t *testing.T) { + var orgID int64 = 1 + rule, err := ruleService.CreateAlertRule(context.Background(), dummyRule("test#2", orgID), models.ProvenanceAPI) + require.NoError(t, err) + + _, provenance, err := ruleService.GetAlertRule(context.Background(), orgID, rule.UID) + require.NoError(t, err) + require.Equal(t, models.ProvenanceAPI, provenance) + }) + t.Run("alert rule group should be updated correctly", func(t *testing.T) { + var orgID int64 = 1 + rule := dummyRule("test#3", orgID) + rule.RuleGroup = "a" + rule, err := ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone) + require.NoError(t, err) + require.Equal(t, int64(60), rule.IntervalSeconds) + + var interval int64 = 120 + err = ruleService.UpdateAlertGroup(context.Background(), orgID, rule.NamespaceUID, rule.RuleGroup, 120) + require.NoError(t, err) + + rule, _, err = ruleService.GetAlertRule(context.Background(), orgID, rule.UID) + require.NoError(t, err) + require.Equal(t, interval, rule.IntervalSeconds) + }) + t.Run("alert rule should get interval from existing rule group", func(t *testing.T) { + var orgID int64 = 1 + rule := dummyRule("test#4", orgID) + rule.RuleGroup = "b" + rule, err := ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone) + require.NoError(t, err) + + var interval int64 = 120 + err = ruleService.UpdateAlertGroup(context.Background(), orgID, rule.NamespaceUID, rule.RuleGroup, 120) + require.NoError(t, err) + + rule = dummyRule("test#4-1", orgID) + rule.RuleGroup = "b" + rule, err = ruleService.CreateAlertRule(context.Background(), rule, models.ProvenanceNone) + require.NoError(t, err) + require.Equal(t, interval, rule.IntervalSeconds) + }) + t.Run("alert rule provenace should be correctly checked", func(t *testing.T) { + tests := []struct { + name string + from models.Provenance + to models.Provenance + errNil bool + }{ + { + name: "should be able to update from provenance none to api", + from: models.ProvenanceNone, + to: models.ProvenanceAPI, + errNil: true, + }, + { + name: "should be able to update from provenance none to file", + from: models.ProvenanceNone, + to: models.ProvenanceFile, + errNil: true, + }, + { + name: "should not be able to update from provenance api to file", + from: models.ProvenanceAPI, + to: models.ProvenanceFile, + errNil: false, + }, + { + name: "should not be able to update from provenance api to none", + from: models.ProvenanceAPI, + to: models.ProvenanceNone, + errNil: false, + }, + { + name: "should not be able to update from provenance file to api", + from: models.ProvenanceFile, + to: models.ProvenanceAPI, + errNil: false, + }, + { + name: "should not be able to update from provenance file to none", + from: models.ProvenanceFile, + to: models.ProvenanceNone, + errNil: false, + }, + } + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + var orgID int64 = 1 + rule := dummyRule(t.Name(), orgID) + rule, err := ruleService.CreateAlertRule(context.Background(), rule, test.from) + require.NoError(t, err) + + _, err = ruleService.UpdateAlertRule(context.Background(), rule, test.to) + if test.errNil { + require.NoError(t, err) + } else { + require.Error(t, err) + } + }) + } + }) +} + +func createAlertRuleService(t *testing.T) AlertRuleService { + t.Helper() + sqlStore := sqlstore.InitTestDB(t) + store := store.DBstore{ + SQLStore: sqlStore, + BaseInterval: time.Second * 10, + } + return AlertRuleService{ + ruleStore: store, + provenanceStore: store, + xact: sqlStore, + log: log.New("testing"), + defaultInterval: 60, + } +} + +func dummyRule(title string, orgID int64) models.AlertRule { + return models.AlertRule{ + OrgID: orgID, + Title: title, + Condition: "A", + Version: 1, + IntervalSeconds: 60, + Data: []models.AlertQuery{ + { + RefID: "A", + Model: json.RawMessage("{}"), + RelativeTimeRange: models.RelativeTimeRange{ + From: models.Duration(60), + To: models.Duration(0), + }, + }, + }, + RuleGroup: "my-cool-group", + For: time.Second * 60, + NoDataState: models.OK, + ExecErrState: models.OkErrState, + } +} diff --git a/pkg/services/ngalert/store/alert_rule.go b/pkg/services/ngalert/store/alert_rule.go index 4773d9fedb7..2b9df2adcce 100644 --- a/pkg/services/ngalert/store/alert_rule.go +++ b/pkg/services/ngalert/store/alert_rule.go @@ -2,6 +2,7 @@ package store import ( "context" + "errors" "fmt" "strings" "time" @@ -32,6 +33,10 @@ type UpdateRule struct { New ngmodels.AlertRule } +var ( + ErrAlertRuleGroupNotFound = errors.New("rulegroup not found") +) + // RuleStore is the interface for persisting alert rules and instances type RuleStore interface { DeleteAlertRulesByUID(ctx context.Context, orgID int64, ruleUID ...string) error @@ -42,9 +47,14 @@ type RuleStore interface { ListAlertRules(ctx context.Context, query *ngmodels.ListAlertRulesQuery) error // GetRuleGroups returns the unique rule groups across all organizations. GetRuleGroups(ctx context.Context, query *ngmodels.ListRuleGroupsQuery) error + GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) + // UpdateRuleGroup will update the interval for all rules in the group. + UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error GetUserVisibleNamespaces(context.Context, int64, *models.SignedInUser) (map[string]*models.Folder, error) GetNamespaceByTitle(context.Context, string, int64, *models.SignedInUser, bool) (*models.Folder, error) - InsertAlertRules(ctx context.Context, rule []ngmodels.AlertRule) error + // InsertAlertRules will insert all alert rules passed into the function + // and return the map of uuid to id. + InsertAlertRules(ctx context.Context, rule []ngmodels.AlertRule) (map[string]int64, error) UpdateAlertRules(ctx context.Context, rule []UpdateRule) error } @@ -127,17 +137,20 @@ func (st DBstore) GetAlertRulesGroupByRuleUID(ctx context.Context, query *ngmode } // InsertAlertRules is a handler for creating/updating alert rules. -func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRule) error { - return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { +func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRule) (map[string]int64, error) { + ids := make(map[string]int64, len(rules)) + return ids, st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { newRules := make([]ngmodels.AlertRule, 0, len(rules)) ruleVersions := make([]ngmodels.AlertRuleVersion, 0, len(rules)) for i := range rules { r := rules[i] - uid, err := GenerateNewAlertRuleUID(sess, r.OrgID, r.Title) - if err != nil { - return fmt.Errorf("failed to generate UID for alert rule %q: %w", r.Title, err) + if r.UID == "" { + uid, err := GenerateNewAlertRuleUID(sess, r.OrgID, r.Title) + if err != nil { + return fmt.Errorf("failed to generate UID for alert rule %q: %w", r.Title, err) + } + r.UID = uid } - r.UID = uid r.Version = 1 if err := st.validateAlertRule(r); err != nil { return err @@ -147,8 +160,8 @@ func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRu } newRules = append(newRules, r) ruleVersions = append(ruleVersions, ngmodels.AlertRuleVersion{ - RuleOrgID: r.OrgID, RuleUID: r.UID, + RuleOrgID: r.OrgID, RuleNamespaceUID: r.NamespaceUID, RuleGroup: r.RuleGroup, ParentVersion: 0, @@ -166,11 +179,16 @@ func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRu }) } if len(newRules) > 0 { - if _, err := sess.Insert(&newRules); err != nil { - if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { - return ngmodels.ErrAlertRuleUniqueConstraintViolation + // we have to insert the rules one by one as otherwise we are + // not able to fetch the inserted id as it's not supported by xorm + for i := range newRules { + if _, err := sess.Insert(&newRules[i]); err != nil { + if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { + return ngmodels.ErrAlertRuleUniqueConstraintViolation + } + return fmt.Errorf("failed to create new rules: %w", err) } - return fmt.Errorf("failed to create new rules: %w", err) + ids[newRules[i].UID] = newRules[i].ID } } @@ -179,15 +197,13 @@ func (st DBstore) InsertAlertRules(ctx context.Context, rules []ngmodels.AlertRu return fmt.Errorf("failed to create new rule versions: %w", err) } } - return nil }) } -// UpdateAlertRules is a handler for creating/updating alert rules. +// UpdateAlertRules is a handler for updating alert rules. func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) error { return st.SQLStore.WithTransactionalDbSession(ctx, func(sess *sqlstore.DBSession) error { - newRules := make([]ngmodels.AlertRule, 0, len(rules)) ruleVersions := make([]ngmodels.AlertRuleVersion, 0, len(rules)) for _, r := range rules { var parentVersion int64 @@ -226,14 +242,6 @@ func (st DBstore) UpdateAlertRules(ctx context.Context, rules []UpdateRule) erro Labels: r.New.Labels, }) } - if len(newRules) > 0 { - if _, err := sess.Insert(&newRules); err != nil { - if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) { - return ngmodels.ErrAlertRuleUniqueConstraintViolation - } - return fmt.Errorf("failed to create new rules: %w", err) - } - } if len(ruleVersions) > 0 { if _, err := sess.Insert(&ruleVersions); err != nil { return fmt.Errorf("failed to create new rule versions: %w", err) @@ -296,6 +304,32 @@ func (st DBstore) GetRuleGroups(ctx context.Context, query *ngmodels.ListRuleGro }) } +func (st DBstore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) { + var interval int64 = 0 + return interval, st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + ruleGroups := make([]ngmodels.AlertRule, 0) + err := sess.Find( + &ruleGroups, + ngmodels.AlertRule{OrgID: orgID, RuleGroup: ruleGroup, NamespaceUID: namespaceUID}, + ) + if len(ruleGroups) == 0 { + return ErrAlertRuleGroupNotFound + } + interval = ruleGroups[0].IntervalSeconds + return err + }) +} + +func (st DBstore) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error { + return st.SQLStore.WithDbSession(ctx, func(sess *sqlstore.DBSession) error { + _, err := sess.Update( + ngmodels.AlertRule{IntervalSeconds: interval}, + ngmodels.AlertRule{OrgID: orgID, RuleGroup: ruleGroup, NamespaceUID: namespaceUID}, + ) + return err + }) +} + // GetNamespaces returns the folders that are visible to the user and have at least one alert in it func (st DBstore) GetUserVisibleNamespaces(ctx context.Context, orgID int64, user *models.SignedInUser) (map[string]*models.Folder, error) { namespaceMap := make(map[string]*models.Folder) @@ -433,5 +467,13 @@ func (st DBstore) validateAlertRule(alertRule ngmodels.AlertRule) error { return fmt.Errorf("%w: cannot have Panel ID without a Dashboard UID", ngmodels.ErrAlertRuleFailedValidation) } + if _, err := ngmodels.ErrStateFromString(string(alertRule.ExecErrState)); err != nil { + return err + } + + if _, err := ngmodels.NoDataStateFromString(string(alertRule.NoDataState)); err != nil { + return err + } + return nil } diff --git a/pkg/services/ngalert/store/testing.go b/pkg/services/ngalert/store/testing.go index e24ecff1f37..d97304ed83d 100644 --- a/pkg/services/ngalert/store/testing.go +++ b/pkg/services/ngalert/store/testing.go @@ -315,20 +315,43 @@ func (f *FakeRuleStore) UpdateAlertRules(_ context.Context, q []UpdateRule) erro return nil } -func (f *FakeRuleStore) InsertAlertRules(_ context.Context, q []models.AlertRule) error { +func (f *FakeRuleStore) InsertAlertRules(_ context.Context, q []models.AlertRule) (map[string]int64, error) { f.mtx.Lock() defer f.mtx.Unlock() f.RecordedOps = append(f.RecordedOps, q) + ids := make(map[string]int64, len(q)) if err := f.Hook(q); err != nil { - return err + return ids, err } - return nil + return ids, nil } func (f *FakeRuleStore) InTransaction(ctx context.Context, fn func(c context.Context) error) error { return fn(ctx) } +func (f *FakeRuleStore) GetRuleGroupInterval(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string) (int64, error) { + f.mtx.Lock() + defer f.mtx.Unlock() + for _, rule := range f.Rules[orgID] { + if rule.RuleGroup == ruleGroup && rule.NamespaceUID == namespaceUID { + return rule.IntervalSeconds, nil + } + } + return 0, ErrAlertRuleGroupNotFound +} + +func (f *FakeRuleStore) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error { + f.mtx.Lock() + defer f.mtx.Unlock() + for _, rule := range f.Rules[orgID] { + if rule.RuleGroup == ruleGroup && rule.NamespaceUID == namespaceUID { + rule.IntervalSeconds = interval + } + } + return nil +} + type FakeInstanceStore struct { mtx sync.Mutex RecordedOps []interface{} diff --git a/pkg/services/ngalert/tests/util.go b/pkg/services/ngalert/tests/util.go index 373de7d4801..60255895cda 100644 --- a/pkg/services/ngalert/tests/util.go +++ b/pkg/services/ngalert/tests/util.go @@ -84,7 +84,7 @@ func CreateTestAlertRule(t *testing.T, ctx context.Context, dbstore *store.DBsto func CreateTestAlertRuleWithLabels(t *testing.T, ctx context.Context, dbstore *store.DBstore, intervalSeconds int64, orgID int64, labels map[string]string) *models.AlertRule { ruleGroup := fmt.Sprintf("ruleGroup-%s", util.GenerateShortUID()) - err := dbstore.InsertAlertRules(ctx, []models.AlertRule{ + _, err := dbstore.InsertAlertRules(ctx, []models.AlertRule{ { ID: 0,