From b8f01fe0345949d45327211faed538118e3964f7 Mon Sep 17 00:00:00 2001 From: Kyle Brandt Date: Fri, 30 Apr 2021 13:21:57 -0400 Subject: [PATCH] Alerting: backend "ng" code cleanup (#33578) --- pkg/services/ngalert/api/api.go | 41 -- pkg/services/ngalert/api/api_ruler.go | 19 - pkg/services/ngalert/api/legacy_api_routes.go | 215 --------- pkg/services/ngalert/api/legacy_trans_dev.go | 294 ------------ pkg/services/ngalert/models/instance.go | 1 - pkg/services/ngalert/models/legacy.go | 133 ------ pkg/services/ngalert/store/database.go | 307 ------------ pkg/services/ngalert/tests/database_test.go | 450 ------------------ .../ngalert/tests/instance_database_test.go | 59 ++- 9 files changed, 35 insertions(+), 1484 deletions(-) delete mode 100644 pkg/services/ngalert/api/legacy_api_routes.go delete mode 100644 pkg/services/ngalert/api/legacy_trans_dev.go delete mode 100644 pkg/services/ngalert/models/legacy.go delete mode 100644 pkg/services/ngalert/tests/database_test.go diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index fe982da2cf3..107cfcd6771 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -6,15 +6,11 @@ import ( "github.com/grafana/grafana/pkg/services/ngalert/metrics" "github.com/grafana/grafana/pkg/services/ngalert/state" - "github.com/go-macaron/binding" - "github.com/grafana/grafana/pkg/api/routing" "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/middleware" "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/datasources" apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/schedule" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/setting" @@ -86,41 +82,4 @@ func (api *API) RegisterAPIEndpoints(m *metrics.Metrics) { DatasourceCache: api.DatasourceCache, log: logger, }, m) - - // Legacy routes; they will be removed in v8 - api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) { - alertDefinitions.Get("", middleware.ReqSignedIn, routing.Wrap(api.listAlertDefinitions)) - alertDefinitions.Get("/eval/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.alertDefinitionEvalEndpoint)) - alertDefinitions.Post("/eval", middleware.ReqSignedIn, binding.Bind(ngmodels.EvalAlertConditionCommand{}), routing.Wrap(api.conditionEvalEndpoint)) - alertDefinitions.Get("/:alertDefinitionUID", middleware.ReqSignedIn, api.validateOrgAlertDefinition, routing.Wrap(api.getAlertDefinitionEndpoint)) - alertDefinitions.Delete("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, routing.Wrap(api.deleteAlertDefinitionEndpoint)) - alertDefinitions.Post("/", middleware.ReqEditorRole, binding.Bind(ngmodels.SaveAlertDefinitionCommand{}), routing.Wrap(api.createAlertDefinitionEndpoint)) - alertDefinitions.Put("/:alertDefinitionUID", middleware.ReqEditorRole, api.validateOrgAlertDefinition, binding.Bind(ngmodels.UpdateAlertDefinitionCommand{}), routing.Wrap(api.updateAlertDefinitionEndpoint)) - alertDefinitions.Post("/pause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionPauseEndpoint)) - alertDefinitions.Post("/unpause", middleware.ReqEditorRole, binding.Bind(ngmodels.UpdateAlertDefinitionPausedCommand{}), routing.Wrap(api.alertDefinitionUnpauseEndpoint)) - }) - - if api.Cfg.Env == setting.Dev { - api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) { - alertDefinitions.Post("/evalOld", middleware.ReqSignedIn, routing.Wrap(api.conditionEvalOldEndpoint)) - }) - api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) { - alertDefinitions.Get("/evalOldByID/:id", middleware.ReqSignedIn, routing.Wrap(api.conditionEvalOldEndpointByID)) - }) - api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) { - alertDefinitions.Get("/oldByID/:id", middleware.ReqSignedIn, routing.Wrap(api.conditionOldEndpointByID)) - }) - api.RouteRegister.Group("/api/alert-definitions", func(alertDefinitions routing.RouteRegister) { - alertDefinitions.Get("/ruleGroupByOldID/:id", middleware.ReqSignedIn, routing.Wrap(api.ruleGroupByOldID)) - }) - } - - api.RouteRegister.Group("/api/ngalert/", func(schedulerRouter routing.RouteRegister) { - schedulerRouter.Post("/pause", routing.Wrap(api.pauseScheduler)) - schedulerRouter.Post("/unpause", routing.Wrap(api.unpauseScheduler)) - }, middleware.ReqOrgAdmin) - - api.RouteRegister.Group("/api/alert-instances", func(alertInstances routing.RouteRegister) { - alertInstances.Get("", middleware.ReqSignedIn, routing.Wrap(api.listAlertInstancesEndpoint)) - }) } diff --git a/pkg/services/ngalert/api/api_ruler.go b/pkg/services/ngalert/api/api_ruler.go index 960f1ad3780..855a76bf762 100644 --- a/pkg/services/ngalert/api/api_ruler.go +++ b/pkg/services/ngalert/api/api_ruler.go @@ -258,25 +258,6 @@ func toGettableExtendedRuleNode(r ngmodels.AlertRule, namespaceID int64) apimode return gettableExtendedRuleNode } -func toPostableExtendedRuleNode(r ngmodels.AlertRule) apimodels.PostableExtendedRuleNode { - postableExtendedRuleNode := apimodels.PostableExtendedRuleNode{ - GrafanaManagedAlert: &apimodels.PostableGrafanaRule{ - Title: r.Title, - Condition: r.Condition, - Data: r.Data, - UID: r.UID, - NoDataState: apimodels.NoDataState(r.NoDataState), - ExecErrState: apimodels.ExecutionErrorState(r.ExecErrState), - }, - } - 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) diff --git a/pkg/services/ngalert/api/legacy_api_routes.go b/pkg/services/ngalert/api/legacy_api_routes.go deleted file mode 100644 index e00bf165eda..00000000000 --- a/pkg/services/ngalert/api/legacy_api_routes.go +++ /dev/null @@ -1,215 +0,0 @@ -package api - -import ( - "fmt" - - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/ngalert/eval" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/util" -) - -// listAlertInstancesEndpoint handles GET /api/alert-instances. -func (api *API) listAlertInstancesEndpoint(c *models.ReqContext) response.Response { - cmd := ngmodels.ListAlertInstancesQuery{DefinitionOrgID: c.SignedInUser.OrgId} - - if err := api.Store.ListAlertInstances(&cmd); err != nil { - return response.Error(500, "Failed to list alert instances", err) - } - - return response.JSON(200, cmd.Result) -} - -// conditionEvalEndpoint handles POST /api/alert-definitions/eval. -func (api *API) conditionEvalEndpoint(c *models.ReqContext, cmd ngmodels.EvalAlertConditionCommand) response.Response { - return conditionEval(c, cmd, api.DatasourceCache, api.DataService, api.Cfg) -} - -// alertDefinitionEvalEndpoint handles GET /api/alert-definitions/eval/:alertDefinitionUID. -func (api *API) alertDefinitionEvalEndpoint(c *models.ReqContext) response.Response { - alertDefinitionUID := c.Params(":alertDefinitionUID") - - condition, err := api.LoadAlertCondition(alertDefinitionUID, c.SignedInUser.OrgId) - if err != nil { - return response.Error(400, "Failed to load alert definition conditions", err) - } - - if err := validateCondition(*condition, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil { - return response.Error(400, "invalid condition", err) - } - - evaluator := eval.Evaluator{Cfg: api.Cfg} - evalResults, err := evaluator.ConditionEval(condition, timeNow(), api.DataService) - if err != nil { - return response.Error(400, "Failed to evaluate alert", err) - } - frame := evalResults.AsDataFrame() - - return response.JSONStreaming(200, util.DynMap{ - "instances": []*data.Frame{&frame}, - }) -} - -// getAlertDefinitionEndpoint handles GET /api/alert-definitions/:alertDefinitionUID. -func (api *API) getAlertDefinitionEndpoint(c *models.ReqContext) response.Response { - alertDefinitionUID := c.Params(":alertDefinitionUID") - - query := ngmodels.GetAlertDefinitionByUIDQuery{ - UID: alertDefinitionUID, - OrgID: c.SignedInUser.OrgId, - } - - if err := api.Store.GetAlertDefinitionByUID(&query); err != nil { - return response.Error(500, "Failed to get alert definition", err) - } - - return response.JSON(200, &query.Result) -} - -// deleteAlertDefinitionEndpoint handles DELETE /api/alert-definitions/:alertDefinitionUID. -func (api *API) deleteAlertDefinitionEndpoint(c *models.ReqContext) response.Response { - alertDefinitionUID := c.Params(":alertDefinitionUID") - - cmd := ngmodels.DeleteAlertDefinitionByUIDCommand{ - UID: alertDefinitionUID, - OrgID: c.SignedInUser.OrgId, - } - - if err := api.Store.DeleteAlertDefinitionByUID(&cmd); err != nil { - return response.Error(500, "Failed to delete alert definition", err) - } - - return response.Success("Alert definition deleted") -} - -// updateAlertDefinitionEndpoint handles PUT /api/alert-definitions/:alertDefinitionUID. -func (api *API) updateAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionCommand) response.Response { - cmd.UID = c.Params(":alertDefinitionUID") - cmd.OrgID = c.SignedInUser.OrgId - - evalCond := ngmodels.Condition{ - Condition: cmd.Condition, - OrgID: c.SignedInUser.OrgId, - Data: cmd.Data, - } - - if err := validateCondition(evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil { - return response.Error(400, "invalid condition", err) - } - - if err := api.Store.UpdateAlertDefinition(&cmd); err != nil { - return response.Error(500, "Failed to update alert definition", err) - } - - return response.JSON(200, cmd.Result) -} - -// createAlertDefinitionEndpoint handles POST /api/alert-definitions. -func (api *API) createAlertDefinitionEndpoint(c *models.ReqContext, cmd ngmodels.SaveAlertDefinitionCommand) response.Response { - cmd.OrgID = c.SignedInUser.OrgId - - evalCond := ngmodels.Condition{ - Condition: cmd.Condition, - OrgID: c.SignedInUser.OrgId, - Data: cmd.Data, - } - - if err := validateCondition(evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil { - return response.Error(400, "invalid condition", err) - } - - if err := api.Store.SaveAlertDefinition(&cmd); err != nil { - return response.Error(500, "Failed to create alert definition", err) - } - - return response.JSON(200, cmd.Result) -} - -// listAlertDefinitions handles GET /api/alert-definitions. -func (api *API) listAlertDefinitions(c *models.ReqContext) response.Response { - query := ngmodels.ListAlertDefinitionsQuery{OrgID: c.SignedInUser.OrgId} - - if err := api.Store.GetOrgAlertDefinitions(&query); err != nil { - return response.Error(500, "Failed to list alert definitions", err) - } - - return response.JSON(200, util.DynMap{"results": query.Result}) -} - -func (api *API) pauseScheduler() response.Response { - err := api.Schedule.Pause() - if err != nil { - return response.Error(500, "Failed to pause scheduler", err) - } - return response.JSON(200, util.DynMap{"message": "alert definition scheduler paused"}) -} - -func (api *API) unpauseScheduler() response.Response { - err := api.Schedule.Unpause() - if err != nil { - return response.Error(500, "Failed to unpause scheduler", err) - } - return response.JSON(200, util.DynMap{"message": "alert definition scheduler unpaused"}) -} - -// alertDefinitionPauseEndpoint handles POST /api/alert-definitions/pause. -func (api *API) alertDefinitionPauseEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionPausedCommand) response.Response { - cmd.OrgID = c.SignedInUser.OrgId - cmd.Paused = true - - err := api.Store.UpdateAlertDefinitionPaused(&cmd) - if err != nil { - return response.Error(500, "Failed to pause alert definition", err) - } - return response.JSON(200, util.DynMap{"message": fmt.Sprintf("%d alert definitions paused", cmd.ResultCount)}) -} - -// alertDefinitionUnpauseEndpoint handles POST /api/alert-definitions/unpause. -func (api *API) alertDefinitionUnpauseEndpoint(c *models.ReqContext, cmd ngmodels.UpdateAlertDefinitionPausedCommand) response.Response { - cmd.OrgID = c.SignedInUser.OrgId - cmd.Paused = false - - err := api.Store.UpdateAlertDefinitionPaused(&cmd) - if err != nil { - return response.Error(500, "Failed to unpause alert definition", err) - } - return response.JSON(200, util.DynMap{"message": fmt.Sprintf("%d alert definitions unpaused", cmd.ResultCount)}) -} - -// LoadAlertCondition returns a Condition object for the given alertDefinitionID. -func (api *API) LoadAlertCondition(alertDefinitionUID string, orgID int64) (*ngmodels.Condition, error) { - q := ngmodels.GetAlertDefinitionByUIDQuery{UID: alertDefinitionUID, OrgID: orgID} - if err := api.Store.GetAlertDefinitionByUID(&q); err != nil { - return nil, err - } - alertDefinition := q.Result - - err := api.Store.ValidateAlertDefinition(alertDefinition, true) - if err != nil { - return nil, err - } - - return &ngmodels.Condition{ - Condition: alertDefinition.Condition, - OrgID: alertDefinition.OrgID, - Data: alertDefinition.Data, - }, nil -} - -func (api *API) validateOrgAlertDefinition(c *models.ReqContext) { - uid := c.ParamsEscape(":alertDefinitionUID") - - if uid == "" { - c.JsonApiErr(403, "Permission denied", nil) - return - } - - query := ngmodels.GetAlertDefinitionByUIDQuery{UID: uid, OrgID: c.SignedInUser.OrgId} - - if err := api.Store.GetAlertDefinitionByUID(&query); err != nil { - c.JsonApiErr(404, "Alert definition not found", nil) - return - } -} diff --git a/pkg/services/ngalert/api/legacy_trans_dev.go b/pkg/services/ngalert/api/legacy_trans_dev.go deleted file mode 100644 index 32492e24318..00000000000 --- a/pkg/services/ngalert/api/legacy_trans_dev.go +++ /dev/null @@ -1,294 +0,0 @@ -package api - -import ( - "fmt" - "time" - - "github.com/prometheus/common/model" - - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/expr/translate" - "github.com/grafana/grafana/pkg/models" - apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions" - "github.com/grafana/grafana/pkg/services/ngalert/eval" - ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/ngalert/store" - "github.com/grafana/grafana/pkg/util" -) - -// conditionEvalEndpoint handles POST /api/alert-definitions/evalOld. -func (api *API) conditionEvalOldEndpoint(c *models.ReqContext) response.Response { - b, err := c.Req.Body().Bytes() - if err != nil { - response.Error(400, "failed to read body", err) - } - evalCond, err := translate.DashboardAlertConditions(b, c.OrgId) - if err != nil { - return response.Error(400, "Failed to translate alert conditions", err) - } - - if err := validateCondition(*evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil { - return response.Error(400, "invalid condition", err) - } - //now := cmd.Now - //if now.IsZero() { - //now := timeNow() - //} - evaluator := eval.Evaluator{Cfg: api.Cfg} - evalResults, err := evaluator.ConditionEval(evalCond, timeNow(), api.DataService) - if err != nil { - return response.Error(400, "Failed to evaluate conditions", err) - } - frame := evalResults.AsDataFrame() - return response.JSONStreaming(200, util.DynMap{ - "instances": []*data.Frame{&frame}, - }) -} - -// conditionEvalEndpoint handles POST /api/alert-definitions/evalOld. -func (api *API) conditionEvalOldEndpointByID(c *models.ReqContext) response.Response { - id := c.ParamsInt64("id") - if id == 0 { - return response.Error(400, "missing id", nil) - } - getAlert := &models.GetAlertByIdQuery{ - Id: id, - } - if err := bus.Dispatch(getAlert); err != nil { - return response.Error(400, fmt.Sprintf("could find alert with id %v", id), err) - } - if getAlert.Result.OrgId != c.SignedInUser.OrgId { - return response.Error(403, "alert does not match organization of user", nil) - } - settings := getAlert.Result.Settings - sb, err := settings.ToDB() - if err != nil { - return response.Error(400, "failed to marshal alert settings", err) - } - evalCond, err := translate.DashboardAlertConditions(sb, c.OrgId) - if err != nil { - return response.Error(400, "Failed to translate alert conditions", err) - } - - if err := validateCondition(*evalCond, c.SignedInUser, c.SkipCache, api.DatasourceCache); err != nil { - return response.Error(400, "invalid condition", err) - } - //now := cmd.Now - //if now.IsZero() { - //now := timeNow() - //} - evaluator := eval.Evaluator{Cfg: api.Cfg} - evalResults, err := evaluator.ConditionEval(evalCond, timeNow(), api.DataService) - if err != nil { - return response.Error(400, "Failed to evaluate conditions", err) - } - frame := evalResults.AsDataFrame() - return response.JSONStreaming(200, util.DynMap{ - "instances": []*data.Frame{&frame}, - }) -} - -// conditionEvalEndpoint handles POST /api/alert-definitions/oldByID. -func (api *API) conditionOldEndpointByID(c *models.ReqContext) response.Response { - id := c.ParamsInt64("id") - if id == 0 { - return response.Error(400, "missing id", nil) - } - getAlert := &models.GetAlertByIdQuery{ - Id: id, - } - if err := bus.Dispatch(getAlert); err != nil { - return response.Error(400, fmt.Sprintf("could find alert with id %v", id), err) - } - if getAlert.Result.OrgId != c.SignedInUser.OrgId { - return response.Error(403, "alert does not match organization of user", nil) - } - settings := getAlert.Result.Settings - sb, err := settings.ToDB() - if err != nil { - return response.Error(400, "failed to marshal alert settings", err) - } - evalCond, err := translate.DashboardAlertConditions(sb, c.OrgId) - if err != nil { - return response.Error(400, "Failed to translate alert conditions", err) - } - return response.JSON(200, evalCond) -} - -// ruleGroupByOldID handles POST /api/alert-definitions/ruleGroupByOldID. -func (api *API) ruleGroupByOldID(c *models.ReqContext) response.Response { - id := c.ParamsInt64("id") - if id == 0 { - return response.Error(400, "missing id", nil) - } - save := c.Query("save") == "true" - // Get dashboard alert definition from database. - oldAlert, status, err := transGetAlertById(id, *c.SignedInUser) - if err != nil { - return response.Error(status, "failed to get alert", fmt.Errorf("failed to get alert for alert id %v: %w", id, err)) - } - // Translate the dashboard's alerts conditions into SSE queries and conditions. - sseCond, err := transToSSECondition(oldAlert, *c.SignedInUser) - if err != nil { - return response.Error(400, "failed to translate alert conditions", - fmt.Errorf("failed to translate alert conditions for alert id %v: %w", id, err)) - } - // Get the dashboard that contains the dashboard Alert. - oldAlertsDash, status, err := transGetAlertsDashById(oldAlert.DashboardId, *c.SignedInUser) - if err != nil { - return response.Error(status, "failed to get alert's dashboard", fmt.Errorf("failed to get dashboard for alert id %v, %w", id, err)) - } - isGeneralFolder := oldAlertsDash.FolderId == 0 && !oldAlertsDash.IsFolder - var namespaceUID string - if isGeneralFolder { - namespaceUID = "General" - } else { - // Get the folder that contains the dashboard that contains the dashboard alert. - getFolder := &models.GetDashboardQuery{ - Id: oldAlertsDash.FolderId, - OrgId: oldAlertsDash.OrgId, - } - if err := bus.Dispatch(getFolder); err != nil { - return response.Error(400, fmt.Sprintf("could find folder %v for alert with id %v", getFolder.Id, id), err) - } - namespaceUID = getFolder.Result.Uid - } - noDataSetting, execErrSetting, err := transNoDataExecSettings(oldAlert, *c.SignedInUser) - if err != nil { - return response.Error(400, "unable to translate nodata/exec error settings", - fmt.Errorf("unable to translate nodata/exec error settings for alert id %v: %w", id, err)) - } - - ruleTags := map[string]string{} - - for k, v := range oldAlert.Settings.Get("alertRuleTags").MustMap() { - sV, ok := v.(string) - if !ok { - return response.Error(400, "unable to unmarshal rule tags", - fmt.Errorf("unexpected type %T for tag %v", v, k)) - } - ruleTags[k] = sV - } - - rule := ngmodels.AlertRule{ - Title: oldAlert.Name, - Data: sseCond.Data, - Condition: sseCond.Condition, - NoDataState: *noDataSetting, - ExecErrState: *execErrSetting, - For: oldAlert.For, - Annotations: ruleTags, - } - rgc := apimodels.PostableRuleGroupConfig{ - // TODO? Generate new name on conflict? - Name: oldAlert.Name, - Interval: transAdjustInterval(oldAlert.Frequency), - Rules: []apimodels.PostableExtendedRuleNode{ - toPostableExtendedRuleNode(rule), - }, - } - cmd := store.UpdateRuleGroupCmd{ - OrgID: oldAlert.OrgId, - NamespaceUID: namespaceUID, - RuleGroupConfig: rgc, - } - if !save { - return response.JSON(200, cmd) - } - // note: Update rule group will set the Interval within the grafana_alert from - // the interval of the group. - err = api.RuleStore.UpdateRuleGroup(cmd) - if err != nil { - return response.JSON(400, util.DynMap{ - "message:": "failed to save alert rule", - "error": err.Error(), - "cmd": cmd, - }) - } - return response.JSON(200, cmd) -} - -func transAdjustInterval(freq int64) model.Duration { - // 10 corresponds to the SchedulerCfg, but TODO not worrying about fetching for now. - var baseFreq int64 = 10 - if freq <= baseFreq { - return model.Duration(time.Second * 10) - } - return model.Duration(time.Duration((freq - (freq % baseFreq))) * time.Second) -} - -func transGetAlertById(id int64, user models.SignedInUser) (*models.Alert, int, error) { - getAlert := &models.GetAlertByIdQuery{ - Id: id, - } - if err := bus.Dispatch(getAlert); err != nil { - return nil, 400, fmt.Errorf("could find alert with id %v: %w", id, err) - } - if getAlert.Result.OrgId != user.OrgId { - return nil, 403, fmt.Errorf("alert does not match organization of user") - } - return getAlert.Result, 0, nil -} - -func transGetAlertsDashById(dashboardId int64, user models.SignedInUser) (*models.Dashboard, int, error) { - getDash := &models.GetDashboardQuery{ - Id: dashboardId, - OrgId: user.OrgId, - } - if err := bus.Dispatch(getDash); err != nil { - return nil, 400, fmt.Errorf("could find dashboard with id %v: %w", dashboardId, err) - } - return getDash.Result, 0, nil -} - -func transToSSECondition(m *models.Alert, user models.SignedInUser) (*ngmodels.Condition, error) { - sb, err := m.Settings.ToDB() - if err != nil { - return nil, fmt.Errorf("failed to marshal alert settings: %w", err) - } - evalCond, err := translate.DashboardAlertConditions(sb, user.OrgId) - if err != nil { - return nil, fmt.Errorf("failed to translate dashboard alert to SSE conditions: %w", err) - } - return evalCond, nil -} - -func transNoDataExecSettings(m *models.Alert, user models.SignedInUser) (*ngmodels.NoDataState, *ngmodels.ExecutionErrorState, error) { - oldNoData := m.Settings.Get("noDataState").MustString() - noDataSetting, err := transNoData(oldNoData) - if err != nil { - return nil, nil, err - } - oldExecErr := m.Settings.Get("executionErrorState").MustString() - execErrSetting, err := transExecErr(oldExecErr) - if err != nil { - return nil, nil, err - } - return &noDataSetting, &execErrSetting, nil -} - -func transNoData(s string) (ngmodels.NoDataState, error) { - switch s { - case "ok": - return ngmodels.OK, nil - case "no_data": - return ngmodels.NoData, nil - case "alerting": - return ngmodels.Alerting, nil - case "keep_state": - return ngmodels.KeepLastState, nil - } - return ngmodels.NoData, fmt.Errorf("unrecognized No Data setting %v", s) -} - -func transExecErr(s string) (ngmodels.ExecutionErrorState, error) { - switch s { - case "alerting": - return ngmodels.AlertingErrState, nil - case "KeepLastState": - return ngmodels.KeepLastStateErrState, nil - } - return ngmodels.AlertingErrState, fmt.Errorf("unrecognized Execution Error setting %v", s) -} diff --git a/pkg/services/ngalert/models/instance.go b/pkg/services/ngalert/models/instance.go index 1a9eb69164c..39708b40309 100644 --- a/pkg/services/ngalert/models/instance.go +++ b/pkg/services/ngalert/models/instance.go @@ -80,7 +80,6 @@ type FetchUniqueOrgIdsQuery struct { type ListAlertInstancesQueryResult struct { DefinitionOrgID int64 `xorm:"def_org_id" json:"definitionOrgId"` DefinitionUID string `xorm:"def_uid" json:"definitionUid"` - DefinitionTitle string `xorm:"def_title" json:"definitionTitle"` Labels InstanceLabels `json:"labels"` LabelsHash string `json:"labeHash"` CurrentState InstanceStateType `json:"currentState"` diff --git a/pkg/services/ngalert/models/legacy.go b/pkg/services/ngalert/models/legacy.go deleted file mode 100644 index 7f809af393b..00000000000 --- a/pkg/services/ngalert/models/legacy.go +++ /dev/null @@ -1,133 +0,0 @@ -package models - -import ( - "errors" - "fmt" - "time" -) - -var ( - // ErrAlertDefinitionNotFound is an error for an unknown alert definition. - ErrAlertDefinitionNotFound = fmt.Errorf("could not find alert definition") - // ErrAlertDefinitionFailedGenerateUniqueUID is an error for failure to generate alert definition UID - ErrAlertDefinitionFailedGenerateUniqueUID = errors.New("failed to generate alert definition UID") -) - -// AlertDefinition is the model for alert definitions in Alerting NG. -// Legacy model; It will be removed in v8 -type AlertDefinition struct { - ID int64 `xorm:"pk autoincr 'id'" json:"id"` - OrgID int64 `xorm:"org_id" json:"orgId"` - Title string `json:"title"` - Condition string `json:"condition"` - Data []AlertQuery `json:"data"` - Updated time.Time `json:"updated"` - IntervalSeconds int64 `json:"intervalSeconds"` - Version int64 `json:"version"` - UID string `xorm:"uid" json:"uid"` - Paused bool `json:"paused"` -} - -// AlertDefinitionKey is the alert definition identifier -type AlertDefinitionKey struct { - OrgID int64 - DefinitionUID string -} - -func (k AlertDefinitionKey) String() string { - return fmt.Sprintf("{orgID: %d, definitionUID: %s}", k.OrgID, k.DefinitionUID) -} - -// GetKey returns the alert definitions identifier -func (alertDefinition *AlertDefinition) GetKey() AlertDefinitionKey { - return AlertDefinitionKey{OrgID: alertDefinition.OrgID, DefinitionUID: alertDefinition.UID} -} - -// PreSave sets datasource and loads the updated model for each alert query. -func (alertDefinition *AlertDefinition) PreSave(timeNow func() time.Time) error { - for i, q := range alertDefinition.Data { - err := q.PreSave() - if err != nil { - return fmt.Errorf("invalid alert query %s: %w", q.RefID, err) - } - alertDefinition.Data[i] = q - } - alertDefinition.Updated = timeNow() - return nil -} - -// AlertDefinitionVersion is the model for alert definition versions in Alerting NG. -// Legacy model; It will be removed in v8 -type AlertDefinitionVersion struct { - ID int64 `xorm:"pk autoincr 'id'"` - AlertDefinitionID int64 `xorm:"alert_definition_id"` - AlertDefinitionUID string `xorm:"alert_definition_uid"` - ParentVersion int64 - RestoredFrom int64 - Version int64 - - Created time.Time - Title string - Condition string - Data []AlertQuery - IntervalSeconds int64 -} - -// GetAlertDefinitionByUIDQuery is the query for retrieving/deleting an alert definition by UID and organisation ID. -// Legacy model; It will be removed in v8 -type GetAlertDefinitionByUIDQuery struct { - UID string - OrgID int64 - - Result *AlertDefinition -} - -// DeleteAlertDefinitionByUIDCommand is the command for deleting an alert definition -// Legacy model; It will be removed in v8 -type DeleteAlertDefinitionByUIDCommand struct { - UID string - OrgID int64 -} - -// SaveAlertDefinitionCommand is the query for saving a new alert definition. -// Legacy model; It will be removed in v8 -type SaveAlertDefinitionCommand struct { - Title string `json:"title"` - OrgID int64 `json:"-"` - Condition string `json:"condition"` - Data []AlertQuery `json:"data"` - IntervalSeconds *int64 `json:"intervalSeconds"` - - Result *AlertDefinition -} - -// UpdateAlertDefinitionCommand is the query for updating an existing alert definition. -// Legacy model; It will be removed in v8 -type UpdateAlertDefinitionCommand struct { - Title string `json:"title"` - OrgID int64 `json:"-"` - Condition string `json:"condition"` - Data []AlertQuery `json:"data"` - IntervalSeconds *int64 `json:"intervalSeconds"` - UID string `json:"-"` - - Result *AlertDefinition -} - -// ListAlertDefinitionsQuery is the query for listing alert definitions -// Legacy model; It will be removed in v8 -type ListAlertDefinitionsQuery struct { - OrgID int64 `json:"-"` - - Result []*AlertDefinition -} - -// UpdateAlertDefinitionPausedCommand is the command for updating an alert definitions -// Legacy model; It will be removed in v8 -type UpdateAlertDefinitionPausedCommand struct { - OrgID int64 `json:"-"` - UIDs []string `json:"uids"` - Paused bool `json:"-"` - - ResultCount int64 -} diff --git a/pkg/services/ngalert/store/database.go b/pkg/services/ngalert/store/database.go index 9030e5d07e1..f5fcf59a4fc 100644 --- a/pkg/services/ngalert/store/database.go +++ b/pkg/services/ngalert/store/database.go @@ -1,16 +1,11 @@ package store import ( - "context" - "errors" - "fmt" - "strings" "time" "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/util" ) // TimeNow makes it possible to test usage of time @@ -21,17 +16,9 @@ const AlertDefinitionMaxTitleLength = 190 // Store is the interface for persisting alert definitions and instances type Store interface { - DeleteAlertDefinitionByUID(*models.DeleteAlertDefinitionByUIDCommand) error - GetAlertDefinitionByUID(*models.GetAlertDefinitionByUIDQuery) error - GetAlertDefinitions(*models.ListAlertDefinitionsQuery) error - GetOrgAlertDefinitions(*models.ListAlertDefinitionsQuery) error - SaveAlertDefinition(*models.SaveAlertDefinitionCommand) error - UpdateAlertDefinition(*models.UpdateAlertDefinitionCommand) error GetAlertInstance(*models.GetAlertInstanceQuery) error ListAlertInstances(*models.ListAlertInstancesQuery) error SaveAlertInstance(*models.SaveAlertInstanceCommand) error - ValidateAlertDefinition(*models.AlertDefinition, bool) error - UpdateAlertDefinitionPaused(*models.UpdateAlertDefinitionPausedCommand) error FetchOrgIds(cmd *models.FetchUniqueOrgIdsQuery) error } @@ -50,297 +37,3 @@ type DBstore struct { DefaultIntervalSeconds int64 SQLStore *sqlstore.SQLStore `inject:""` } - -func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string, orgID int64) (*models.AlertDefinition, error) { - // we consider optionally enabling some caching - alertDefinition := models.AlertDefinition{OrgID: orgID, UID: alertDefinitionUID} - has, err := sess.Get(&alertDefinition) - if !has { - return nil, models.ErrAlertDefinitionNotFound - } - if err != nil { - return nil, err - } - return &alertDefinition, nil -} - -// DeleteAlertDefinitionByUID is a handler for deleting an alert definition. -// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID. -func (st DBstore) DeleteAlertDefinitionByUID(cmd *models.DeleteAlertDefinitionByUIDCommand) error { - return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - _, err := sess.Exec("DELETE FROM alert_definition WHERE uid = ? AND org_id = ?", cmd.UID, cmd.OrgID) - if err != nil { - return err - } - - _, err = sess.Exec("DELETE FROM alert_definition_version WHERE alert_definition_uid = ?", cmd.UID) - if err != nil { - return err - } - - _, err = sess.Exec("DELETE FROM alert_instance WHERE def_org_id = ? AND def_uid = ?", cmd.OrgID, cmd.UID) - if err != nil { - return err - } - return nil - }) -} - -// GetAlertDefinitionByUID is a handler for retrieving an alert definition from that database by its UID and organisation ID. -// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID. -func (st DBstore) GetAlertDefinitionByUID(query *models.GetAlertDefinitionByUIDQuery) error { - return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - alertDefinition, err := getAlertDefinitionByUID(sess, query.UID, query.OrgID) - if err != nil { - return err - } - query.Result = alertDefinition - return nil - }) -} - -// SaveAlertDefinition is a handler for saving a new alert definition. -func (st DBstore) SaveAlertDefinition(cmd *models.SaveAlertDefinitionCommand) error { - return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - intervalSeconds := st.DefaultIntervalSeconds - if cmd.IntervalSeconds != nil { - intervalSeconds = *cmd.IntervalSeconds - } - - var initialVersion int64 = 1 - - uid, err := generateNewAlertDefinitionUID(sess, cmd.OrgID) - if err != nil { - return fmt.Errorf("failed to generate UID for alert definition %q: %w", cmd.Title, err) - } - - alertDefinition := &models.AlertDefinition{ - OrgID: cmd.OrgID, - Title: cmd.Title, - Condition: cmd.Condition, - Data: cmd.Data, - IntervalSeconds: intervalSeconds, - Version: initialVersion, - UID: uid, - } - - if err := st.ValidateAlertDefinition(alertDefinition, false); err != nil { - return err - } - - if err := alertDefinition.PreSave(TimeNow); err != nil { - return err - } - - if _, err := sess.Insert(alertDefinition); err != nil { - if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) && strings.Contains(err.Error(), "title") { - return fmt.Errorf("an alert definition with the title '%s' already exists: %w", cmd.Title, err) - } - return err - } - - alertDefVersion := models.AlertDefinitionVersion{ - AlertDefinitionID: alertDefinition.ID, - AlertDefinitionUID: alertDefinition.UID, - Version: alertDefinition.Version, - Created: alertDefinition.Updated, - Condition: alertDefinition.Condition, - Title: alertDefinition.Title, - Data: alertDefinition.Data, - IntervalSeconds: alertDefinition.IntervalSeconds, - } - if _, err := sess.Insert(alertDefVersion); err != nil { - return err - } - - cmd.Result = alertDefinition - return nil - }) -} - -// UpdateAlertDefinition is a handler for updating an existing alert definition. -// It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID. -func (st DBstore) UpdateAlertDefinition(cmd *models.UpdateAlertDefinitionCommand) error { - return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - existingAlertDefinition, err := getAlertDefinitionByUID(sess, cmd.UID, cmd.OrgID) - if err != nil { - if errors.Is(err, models.ErrAlertDefinitionNotFound) { - return nil - } - return err - } - - title := cmd.Title - if title == "" { - title = existingAlertDefinition.Title - } - condition := cmd.Condition - if condition == "" { - condition = existingAlertDefinition.Condition - } - data := cmd.Data - if data == nil { - data = existingAlertDefinition.Data - } - intervalSeconds := cmd.IntervalSeconds - if intervalSeconds == nil { - intervalSeconds = &existingAlertDefinition.IntervalSeconds - } - - // explicitly set all fields regardless of being provided or not - alertDefinition := &models.AlertDefinition{ - ID: existingAlertDefinition.ID, - Title: title, - Condition: condition, - Data: data, - OrgID: existingAlertDefinition.OrgID, - IntervalSeconds: *intervalSeconds, - UID: existingAlertDefinition.UID, - } - - if err := st.ValidateAlertDefinition(alertDefinition, true); err != nil { - return err - } - - if err := alertDefinition.PreSave(TimeNow); err != nil { - return err - } - - alertDefinition.Version = existingAlertDefinition.Version + 1 - - _, err = sess.ID(existingAlertDefinition.ID).Update(alertDefinition) - if err != nil { - if st.SQLStore.Dialect.IsUniqueConstraintViolation(err) && strings.Contains(err.Error(), "title") { - return fmt.Errorf("an alert definition with the title '%s' already exists: %w", cmd.Title, err) - } - return err - } - - alertDefVersion := models.AlertDefinitionVersion{ - AlertDefinitionID: alertDefinition.ID, - AlertDefinitionUID: alertDefinition.UID, - ParentVersion: alertDefinition.Version, - Version: alertDefinition.Version, - Condition: alertDefinition.Condition, - Created: alertDefinition.Updated, - Title: alertDefinition.Title, - Data: alertDefinition.Data, - IntervalSeconds: alertDefinition.IntervalSeconds, - } - if _, err := sess.Insert(alertDefVersion); err != nil { - return err - } - - cmd.Result = alertDefinition - return nil - }) -} - -// GetOrgAlertDefinitions is a handler for retrieving alert definitions of specific organisation. -func (st DBstore) GetOrgAlertDefinitions(query *models.ListAlertDefinitionsQuery) error { - return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - alertDefinitions := make([]*models.AlertDefinition, 0) - q := "SELECT * FROM alert_definition WHERE org_id = ?" - if err := sess.SQL(q, query.OrgID).Find(&alertDefinitions); err != nil { - return err - } - - query.Result = alertDefinitions - return nil - }) -} - -// GetAlertDefinitions returns alert definition identifier, interval, version and pause state -// that are useful for it's scheduling. -func (st DBstore) GetAlertDefinitions(query *models.ListAlertDefinitionsQuery) error { - return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - alerts := make([]*models.AlertDefinition, 0) - q := "SELECT uid, org_id, interval_seconds, version, paused FROM alert_definition" - if err := sess.SQL(q).Find(&alerts); err != nil { - return err - } - - query.Result = alerts - return nil - }) -} - -// UpdateAlertDefinitionPaused update the pause state of an alert definition. -func (st DBstore) UpdateAlertDefinitionPaused(cmd *models.UpdateAlertDefinitionPausedCommand) error { - return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - if len(cmd.UIDs) == 0 { - return nil - } - placeHolders := strings.Builder{} - const separator = ", " - separatorVar := separator - params := []interface{}{cmd.Paused, cmd.OrgID} - for i, UID := range cmd.UIDs { - if i == len(cmd.UIDs)-1 { - separatorVar = "" - } - placeHolders.WriteString(fmt.Sprintf("?%s", separatorVar)) - params = append(params, UID) - } - sql := fmt.Sprintf("UPDATE alert_definition SET paused = ? WHERE org_id = ? AND uid IN (%s)", placeHolders.String()) - - // prepend sql statement to params - var i interface{} - params = append(params, i) - copy(params[1:], params[0:]) - params[0] = sql - - res, err := sess.Exec(params...) - if err != nil { - return err - } - if resultCount, err := res.RowsAffected(); err == nil { - cmd.ResultCount = resultCount - } - return nil - }) -} - -func generateNewAlertDefinitionUID(sess *sqlstore.DBSession, orgID int64) (string, error) { - for i := 0; i < 3; i++ { - uid := util.GenerateShortUID() - - exists, err := sess.Where("org_id=? AND uid=?", orgID, uid).Get(&models.AlertDefinition{}) - if err != nil { - return "", err - } - - if !exists { - return uid, nil - } - } - - return "", models.ErrAlertDefinitionFailedGenerateUniqueUID -} - -// ValidateAlertDefinition validates the alert definition interval and organisation. -// If requireData is true checks that it contains at least one alert query -func (st DBstore) ValidateAlertDefinition(alertDefinition *models.AlertDefinition, requireData bool) error { - if !requireData && len(alertDefinition.Data) == 0 { - return fmt.Errorf("no queries or expressions are found") - } - - if alertDefinition.Title == "" { - return fmt.Errorf("title is empty") - } - - if alertDefinition.IntervalSeconds%int64(st.BaseInterval.Seconds()) != 0 { - return fmt.Errorf("invalid interval: %v: interval should be divided exactly by scheduler interval: %v", time.Duration(alertDefinition.IntervalSeconds)*time.Second, st.BaseInterval) - } - - // enfore max name length in SQLite - if len(alertDefinition.Title) > AlertDefinitionMaxTitleLength { - return fmt.Errorf("name length should not be greater than %d", AlertDefinitionMaxTitleLength) - } - - if alertDefinition.OrgID == 0 { - return fmt.Errorf("no organisation is found") - } - - return nil -} diff --git a/pkg/services/ngalert/tests/database_test.go b/pkg/services/ngalert/tests/database_test.go deleted file mode 100644 index 5d36d4c1c63..00000000000 --- a/pkg/services/ngalert/tests/database_test.go +++ /dev/null @@ -1,450 +0,0 @@ -// +build integration - -package tests - -import ( - "encoding/json" - "errors" - "fmt" - "math/rand" - "testing" - "time" - - "github.com/grafana/grafana/pkg/services/ngalert/models" - "github.com/grafana/grafana/pkg/services/ngalert/store" - - "github.com/grafana/grafana/pkg/registry" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const baseIntervalSeconds = 10 - -func mockTimeNow() { - var timeSeed int64 - store.TimeNow = func() time.Time { - fakeNow := time.Unix(timeSeed, 0).UTC() - timeSeed++ - return fakeNow - } -} - -func resetTimeNow() { - store.TimeNow = time.Now -} - -func TestCreatingAlertDefinition(t *testing.T) { - mockTimeNow() - defer resetTimeNow() - - dbstore := setupTestEnv(t, baseIntervalSeconds) - t.Cleanup(registry.ClearOverrides) - - var customIntervalSeconds int64 = 120 - testCases := []struct { - desc string - inputIntervalSeconds *int64 - inputTitle string - expectedError error - expectedInterval int64 - - expectedUpdated time.Time - }{ - { - desc: "should create successfully an alert definition with default interval", - inputIntervalSeconds: nil, - inputTitle: "a name", - expectedInterval: dbstore.DefaultIntervalSeconds, - expectedUpdated: time.Unix(0, 0).UTC(), - }, - { - desc: "should create successfully an alert definition with custom interval", - inputIntervalSeconds: &customIntervalSeconds, - inputTitle: "another name", - expectedInterval: customIntervalSeconds, - expectedUpdated: time.Unix(1, 0).UTC(), - }, - { - desc: "should fail to create an alert definition with too big name", - inputIntervalSeconds: &customIntervalSeconds, - inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1), - expectedError: errors.New(""), - }, - { - desc: "should fail to create an alert definition with empty title", - inputIntervalSeconds: &customIntervalSeconds, - inputTitle: "", - expectedError: fmt.Errorf("title is empty"), - }, - } - - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - - q := models.SaveAlertDefinitionCommand{ - OrgID: 1, - Title: tc.inputTitle, - Condition: "B", - Data: []models.AlertQuery{ - { - Model: json.RawMessage(`{ - "datasourceUid": "-100", - "type":"math", - "expression":"2 + 3 > 1" - }`), - RefID: "B", - RelativeTimeRange: models.RelativeTimeRange{ - From: models.Duration(time.Duration(5) * time.Hour), - To: models.Duration(time.Duration(3) * time.Hour), - }, - }, - }, - } - if tc.inputIntervalSeconds != nil { - q.IntervalSeconds = tc.inputIntervalSeconds - } - err := dbstore.SaveAlertDefinition(&q) - switch { - case tc.expectedError != nil: - require.Error(t, err) - default: - require.NoError(t, err) - assert.Equal(t, tc.expectedUpdated, q.Result.Updated) - assert.Equal(t, tc.expectedInterval, q.Result.IntervalSeconds) - assert.Equal(t, int64(1), q.Result.Version) - - } - }) - } -} -func TestCreatingConflictionAlertDefinition(t *testing.T) { - t.Run("Should fail to create alert definition with conflicting org_id, title", func(t *testing.T) { - dbstore := setupTestEnv(t, baseIntervalSeconds) - t.Cleanup(registry.ClearOverrides) - - q := models.SaveAlertDefinitionCommand{ - OrgID: 1, - Title: "title", - Condition: "B", - Data: []models.AlertQuery{ - { - Model: json.RawMessage(`{ - "datasourceUid": "-100", - "type":"math", - "expression":"2 + 3 > 1" - }`), - RefID: "B", - RelativeTimeRange: models.RelativeTimeRange{ - From: models.Duration(time.Duration(5) * time.Hour), - To: models.Duration(time.Duration(3) * time.Hour), - }, - }, - }, - } - - err := dbstore.SaveAlertDefinition(&q) - require.NoError(t, err) - - err = dbstore.SaveAlertDefinition(&q) - require.Error(t, err) - assert.True(t, dbstore.SQLStore.Dialect.IsUniqueConstraintViolation(err)) - }) -} - -func TestUpdatingAlertDefinition(t *testing.T) { - t.Run("zero rows affected when updating unknown alert", func(t *testing.T) { - mockTimeNow() - defer resetTimeNow() - - dbstore := setupTestEnv(t, baseIntervalSeconds) - t.Cleanup(registry.ClearOverrides) - - q := models.UpdateAlertDefinitionCommand{ - UID: "unknown", - OrgID: 1, - Title: "something completely different", - Condition: "A", - Data: []models.AlertQuery{ - { - Model: json.RawMessage(`{ - "datasourceUid": "-100", - "type":"math", - "expression":"2 + 2 > 1" - }`), - RefID: "A", - RelativeTimeRange: models.RelativeTimeRange{ - From: models.Duration(time.Duration(5) * time.Hour), - To: models.Duration(time.Duration(3) * time.Hour), - }, - }, - }, - } - - err := dbstore.UpdateAlertDefinition(&q) - require.NoError(t, err) - }) - - t.Run("updating existing alert", func(t *testing.T) { - mockTimeNow() - defer resetTimeNow() - - dbstore := setupTestEnv(t, baseIntervalSeconds) - t.Cleanup(registry.ClearOverrides) - - var initialInterval int64 = 120 - alertDefinition := createTestAlertDefinition(t, dbstore, initialInterval) - created := alertDefinition.Updated - - var customInterval int64 = 30 - testCases := []struct { - desc string - inputOrgID int64 - inputTitle string - inputInterval *int64 - expectedError error - expectedIntervalSeconds int64 - expectedUpdated time.Time - expectedTitle string - }{ - { - desc: "should not update previous interval if it's not provided", - inputInterval: nil, - inputOrgID: alertDefinition.OrgID, - inputTitle: "something completely different", - expectedIntervalSeconds: initialInterval, - expectedUpdated: time.Unix(1, 0).UTC(), - expectedTitle: "something completely different", - }, - { - desc: "should update interval if it's provided", - inputInterval: &customInterval, - inputOrgID: alertDefinition.OrgID, - inputTitle: "something completely different", - expectedIntervalSeconds: customInterval, - expectedUpdated: time.Unix(2, 0).UTC(), - expectedTitle: "something completely different", - }, - { - desc: "should not update organisation if it's provided", - inputInterval: &customInterval, - inputOrgID: 0, - inputTitle: "something completely different", - expectedIntervalSeconds: customInterval, - expectedUpdated: time.Unix(3, 0).UTC(), - expectedTitle: "something completely different", - }, - { - desc: "should not update alert definition if the title it's too big", - inputInterval: &customInterval, - inputOrgID: 0, - inputTitle: getLongString(store.AlertDefinitionMaxTitleLength + 1), - expectedError: errors.New(""), - }, - { - desc: "should not update alert definition title if the title is empty", - inputInterval: &customInterval, - inputOrgID: 0, - inputTitle: "", - expectedIntervalSeconds: customInterval, - expectedUpdated: time.Unix(4, 0).UTC(), - expectedTitle: "something completely different", - }, - } - - q := models.UpdateAlertDefinitionCommand{ - UID: (*alertDefinition).UID, - Condition: "B", - Data: []models.AlertQuery{ - { - Model: json.RawMessage(`{ - "datasourceUid": "-100", - "type":"math", - "expression":"2 + 3 > 1" - }`), - RefID: "B", - RelativeTimeRange: models.RelativeTimeRange{ - From: models.Duration(5 * time.Hour), - To: models.Duration(3 * time.Hour), - }, - }, - }, - } - - lastUpdated := created - previousAlertDefinition := alertDefinition - for _, tc := range testCases { - t.Run(tc.desc, func(t *testing.T) { - if tc.inputInterval != nil { - q.IntervalSeconds = tc.inputInterval - } - if tc.inputOrgID != 0 { - q.OrgID = tc.inputOrgID - } - q.Title = tc.inputTitle - err := dbstore.UpdateAlertDefinition(&q) - switch { - case tc.expectedError != nil: - require.Error(t, err) - - assert.Equal(t, previousAlertDefinition.Title, q.Result.Title) - assert.Equal(t, previousAlertDefinition.Condition, q.Result.Condition) - assert.Equal(t, len(previousAlertDefinition.Data), len(q.Result.Data)) - assert.Equal(t, previousAlertDefinition.IntervalSeconds, q.Result.IntervalSeconds) - assert.Equal(t, previousAlertDefinition.Updated, q.Result.Updated) - assert.Equal(t, previousAlertDefinition.Version, q.Result.Version) - assert.Equal(t, previousAlertDefinition.OrgID, q.Result.OrgID) - assert.Equal(t, previousAlertDefinition.UID, q.Result.UID) - default: - require.NoError(t, err) - assert.Equal(t, previousAlertDefinition.ID, q.Result.ID) - assert.Equal(t, previousAlertDefinition.UID, q.Result.UID) - assert.True(t, q.Result.Updated.After(lastUpdated)) - assert.Equal(t, tc.expectedUpdated, q.Result.Updated) - assert.Equal(t, previousAlertDefinition.Version+1, q.Result.Version) - - assert.Equal(t, alertDefinition.OrgID, q.Result.OrgID) - - assert.Equal(t, "something completely different", q.Result.Title) - assert.Equal(t, "B", q.Result.Condition) - assert.Equal(t, 1, len(q.Result.Data)) - assert.Equal(t, tc.expectedUpdated, q.Result.Updated) - assert.Equal(t, tc.expectedIntervalSeconds, q.Result.IntervalSeconds) - assert.Equal(t, previousAlertDefinition.Version+1, q.Result.Version) - assert.Equal(t, alertDefinition.OrgID, q.Result.OrgID) - assert.Equal(t, alertDefinition.UID, q.Result.UID) - - previousAlertDefinition = q.Result - } - }) - - } - - }) -} - -func TestUpdatingConflictingAlertDefinition(t *testing.T) { - t.Run("should fail to update alert definition with reserved title", func(t *testing.T) { - mockTimeNow() - defer resetTimeNow() - - dbstore := setupTestEnv(t, baseIntervalSeconds) - t.Cleanup(registry.ClearOverrides) - - var initialInterval int64 = 120 - alertDef1 := createTestAlertDefinition(t, dbstore, initialInterval) - alertDef2 := createTestAlertDefinition(t, dbstore, initialInterval) - - q := models.UpdateAlertDefinitionCommand{ - UID: (*alertDef2).UID, - Title: alertDef1.Title, - Condition: "B", - Data: []models.AlertQuery{ - { - Model: json.RawMessage(`{ - "datasourceUid": "-100", - "type":"math", - "expression":"2 + 3 > 1" - }`), - RefID: "B", - RelativeTimeRange: models.RelativeTimeRange{ - From: models.Duration(5 * time.Hour), - To: models.Duration(3 * time.Hour), - }, - }, - }, - } - - err := dbstore.UpdateAlertDefinition(&q) - require.Error(t, err) - assert.True(t, dbstore.SQLStore.Dialect.IsUniqueConstraintViolation(err)) - }) -} - -func TestDeletingAlertDefinition(t *testing.T) { - t.Run("zero rows affected when deleting unknown alert", func(t *testing.T) { - dbstore := setupTestEnv(t, baseIntervalSeconds) - t.Cleanup(registry.ClearOverrides) - - q := models.DeleteAlertDefinitionByUIDCommand{ - UID: "unknown", - OrgID: 1, - } - - err := dbstore.DeleteAlertDefinitionByUID(&q) - require.NoError(t, err) - }) - - t.Run("deleting successfully existing alert", func(t *testing.T) { - dbstore := setupTestEnv(t, baseIntervalSeconds) - t.Cleanup(registry.ClearOverrides) - - alertDefinition := createTestAlertDefinition(t, dbstore, 60) - - q := models.DeleteAlertDefinitionByUIDCommand{ - UID: (*alertDefinition).UID, - OrgID: 1, - } - - // save an instance for the definition - saveCmd := &models.SaveAlertInstanceCommand{ - DefinitionOrgID: alertDefinition.OrgID, - DefinitionUID: alertDefinition.UID, - State: models.InstanceStateFiring, - Labels: models.InstanceLabels{"test": "testValue"}, - } - err := dbstore.SaveAlertInstance(saveCmd) - require.NoError(t, err) - listQuery := &models.ListAlertInstancesQuery{ - DefinitionOrgID: alertDefinition.OrgID, - DefinitionUID: alertDefinition.UID, - } - err = dbstore.ListAlertInstances(listQuery) - require.NoError(t, err) - require.Len(t, listQuery.Result, 1) - - err = dbstore.DeleteAlertDefinitionByUID(&q) - require.NoError(t, err) - - // assert that alert instance is deleted - err = dbstore.ListAlertInstances(listQuery) - require.NoError(t, err) - - require.Len(t, listQuery.Result, 0) - }) -} - -func getLongString(n int) string { - b := make([]rune, n) - for i := range b { - b[i] = 'a' - } - return string(b) -} - -// createTestAlertDefinition creates a dummy alert definition to be used by the tests. -func createTestAlertDefinition(t *testing.T, dbstore *store.DBstore, intervalSeconds int64) *models.AlertDefinition { - cmd := models.SaveAlertDefinitionCommand{ - OrgID: 1, - Title: fmt.Sprintf("an alert definition %d", rand.Intn(1000)), - Condition: "A", - Data: []models.AlertQuery{ - { - Model: json.RawMessage(`{ - "datasourceUid": "-100", - "type":"math", - "expression":"2 + 2 > 1" - }`), - RelativeTimeRange: models.RelativeTimeRange{ - From: models.Duration(5 * time.Hour), - To: models.Duration(3 * time.Hour), - }, - RefID: "A", - }, - }, - IntervalSeconds: &intervalSeconds, - } - err := dbstore.SaveAlertDefinition(&cmd) - require.NoError(t, err) - t.Logf("alert definition: %v with interval: %d created", cmd.Result.GetKey(), intervalSeconds) - return cmd.Result -} diff --git a/pkg/services/ngalert/tests/instance_database_test.go b/pkg/services/ngalert/tests/instance_database_test.go index e630c5de149..03371b9bda6 100644 --- a/pkg/services/ngalert/tests/instance_database_test.go +++ b/pkg/services/ngalert/tests/instance_database_test.go @@ -4,31 +4,44 @@ package tests import ( "testing" + "time" "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/stretchr/testify/require" ) +const baseIntervalSeconds = 10 + +func mockTimeNow() { + var timeSeed int64 + store.TimeNow = func() time.Time { + fakeNow := time.Unix(timeSeed, 0).UTC() + timeSeed++ + return fakeNow + } +} + func TestAlertInstanceOperations(t *testing.T) { dbstore := setupTestEnv(t, baseIntervalSeconds) - alertDefinition1 := createTestAlertDefinition(t, dbstore, 60) - orgID := alertDefinition1.OrgID + alertRule1 := createTestAlertRule(t, dbstore, 60) + orgID := alertRule1.OrgID - alertDefinition2 := createTestAlertDefinition(t, dbstore, 60) - require.Equal(t, orgID, alertDefinition2.OrgID) + alertRule2 := createTestAlertRule(t, dbstore, 60) + require.Equal(t, orgID, alertRule2.OrgID) - alertDefinition3 := createTestAlertDefinition(t, dbstore, 60) - require.Equal(t, orgID, alertDefinition3.OrgID) + alertRule3 := createTestAlertRule(t, dbstore, 60) + require.Equal(t, orgID, alertRule3.OrgID) - alertDefinition4 := createTestAlertDefinition(t, dbstore, 60) - require.Equal(t, orgID, alertDefinition4.OrgID) + alertRule4 := createTestAlertRule(t, dbstore, 60) + require.Equal(t, orgID, alertRule4.OrgID) t.Run("can save and read new alert instance", func(t *testing.T) { saveCmd := &models.SaveAlertInstanceCommand{ - DefinitionOrgID: alertDefinition1.OrgID, - DefinitionUID: alertDefinition1.UID, + DefinitionOrgID: alertRule1.OrgID, + DefinitionUID: alertRule1.UID, State: models.InstanceStateFiring, Labels: models.InstanceLabels{"test": "testValue"}, } @@ -45,14 +58,14 @@ func TestAlertInstanceOperations(t *testing.T) { require.NoError(t, err) require.Equal(t, saveCmd.Labels, getCmd.Result.Labels) - require.Equal(t, alertDefinition1.OrgID, getCmd.Result.DefinitionOrgID) - require.Equal(t, alertDefinition1.UID, getCmd.Result.DefinitionUID) + require.Equal(t, alertRule1.OrgID, getCmd.Result.DefinitionOrgID) + require.Equal(t, alertRule1.UID, getCmd.Result.DefinitionUID) }) t.Run("can save and read new alert instance with no labels", func(t *testing.T) { saveCmd := &models.SaveAlertInstanceCommand{ - DefinitionOrgID: alertDefinition2.OrgID, - DefinitionUID: alertDefinition2.UID, + DefinitionOrgID: alertRule2.OrgID, + DefinitionUID: alertRule2.UID, State: models.InstanceStateNormal, Labels: models.InstanceLabels{}, } @@ -67,15 +80,15 @@ func TestAlertInstanceOperations(t *testing.T) { err = dbstore.GetAlertInstance(getCmd) require.NoError(t, err) - require.Equal(t, alertDefinition2.OrgID, getCmd.Result.DefinitionOrgID) - require.Equal(t, alertDefinition2.UID, getCmd.Result.DefinitionUID) + require.Equal(t, alertRule2.OrgID, getCmd.Result.DefinitionOrgID) + require.Equal(t, alertRule2.UID, getCmd.Result.DefinitionUID) require.Equal(t, saveCmd.Labels, getCmd.Result.Labels) }) t.Run("can save two instances with same org_id, uid and different labels", func(t *testing.T) { saveCmdOne := &models.SaveAlertInstanceCommand{ - DefinitionOrgID: alertDefinition3.OrgID, - DefinitionUID: alertDefinition3.UID, + DefinitionOrgID: alertRule3.OrgID, + DefinitionUID: alertRule3.UID, State: models.InstanceStateFiring, Labels: models.InstanceLabels{"test": "testValue"}, } @@ -128,8 +141,8 @@ func TestAlertInstanceOperations(t *testing.T) { t.Run("update instance with same org_id, uid and different labels", func(t *testing.T) { saveCmdOne := &models.SaveAlertInstanceCommand{ - DefinitionOrgID: alertDefinition4.OrgID, - DefinitionUID: alertDefinition4.UID, + DefinitionOrgID: alertRule4.OrgID, + DefinitionUID: alertRule4.UID, State: models.InstanceStateFiring, Labels: models.InstanceLabels{"test": "testValue"}, } @@ -147,8 +160,8 @@ func TestAlertInstanceOperations(t *testing.T) { require.NoError(t, err) listQuery := &models.ListAlertInstancesQuery{ - DefinitionOrgID: alertDefinition4.OrgID, - DefinitionUID: alertDefinition4.UID, + DefinitionOrgID: alertRule4.OrgID, + DefinitionUID: alertRule4.UID, } err = dbstore.ListAlertInstances(listQuery) @@ -160,7 +173,5 @@ func TestAlertInstanceOperations(t *testing.T) { require.Equal(t, saveCmdTwo.DefinitionUID, listQuery.Result[0].DefinitionUID) require.Equal(t, saveCmdTwo.Labels, listQuery.Result[0].Labels) require.Equal(t, saveCmdTwo.State, listQuery.Result[0].CurrentState) - require.NotEmpty(t, listQuery.Result[0].DefinitionTitle) - require.Equal(t, alertDefinition4.Title, listQuery.Result[0].DefinitionTitle) }) }