package ngalert import ( "context" "errors" "fmt" "strings" "time" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/util" ) type store interface { deleteAlertDefinitionByUID(*deleteAlertDefinitionByUIDCommand) error getAlertDefinitionByUID(*getAlertDefinitionByUIDQuery) error getAlertDefinitions(query *listAlertDefinitionsQuery) error getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error saveAlertDefinition(*saveAlertDefinitionCommand) error updateAlertDefinition(*updateAlertDefinitionCommand) error getAlertInstance(*getAlertInstanceQuery) error listAlertInstances(cmd *listAlertInstancesQuery) error saveAlertInstance(cmd *saveAlertInstanceCommand) error validateAlertDefinition(*AlertDefinition, bool) error updateAlertDefinitionPaused(*updateAlertDefinitionPausedCommand) error } type storeImpl struct { // the base scheduler tick rate; it's used for validating definition interval baseInterval time.Duration SQLStore *sqlstore.SQLStore `inject:""` } func getAlertDefinitionByUID(sess *sqlstore.DBSession, alertDefinitionUID string, orgID int64) (*AlertDefinition, error) { // we consider optionally enabling some caching alertDefinition := AlertDefinition{OrgID: orgID, UID: alertDefinitionUID} has, err := sess.Get(&alertDefinition) if !has { return nil, errAlertDefinitionNotFound } if err != nil { return nil, err } return &alertDefinition, nil } // deleteAlertDefinitionByID is a handler for deleting an alert definition. // It returns models.ErrAlertDefinitionNotFound if no alert definition is found for the provided ID. func (st storeImpl) deleteAlertDefinitionByUID(cmd *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 storeImpl) getAlertDefinitionByUID(query *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 storeImpl) saveAlertDefinition(cmd *saveAlertDefinitionCommand) error { return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { intervalSeconds := 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 := &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(); 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 := 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 storeImpl) updateAlertDefinition(cmd *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, 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 := &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(); 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 := 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 storeImpl) getOrgAlertDefinitions(query *listAlertDefinitionsQuery) error { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { alertDefinitions := make([]*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 }) } func (st storeImpl) getAlertDefinitions(query *listAlertDefinitionsQuery) error { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { alerts := make([]*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 }) } func (st storeImpl) updateAlertDefinitionPaused(cmd *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(&AlertDefinition{}) if err != nil { return "", err } if !exists { return uid, nil } } return "", errAlertDefinitionFailedGenerateUniqueUID }