2021-04-01 11:11:45 +03:00
package store
import (
"context"
"fmt"
2021-07-22 09:53:14 +03:00
"strings"
2021-04-01 11:11:45 +03:00
"time"
"github.com/grafana/grafana/pkg/models"
2022-04-01 19:33:26 -04:00
"github.com/grafana/grafana/pkg/services/guardian"
2021-04-19 14:26:04 -04:00
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
2021-04-01 11:11:45 +03:00
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/sqlstore"
2022-04-01 19:33:26 -04:00
"github.com/grafana/grafana/pkg/services/sqlstore/searchstore"
2021-04-01 11:11:45 +03:00
"github.com/grafana/grafana/pkg/util"
)
// AlertRuleMaxTitleLength is the maximum length of the alert rule title
const AlertRuleMaxTitleLength = 190
// AlertRuleMaxRuleGroupNameLength is the maximum length of the alert rule group name
const AlertRuleMaxRuleGroupNameLength = 190
type UpdateRuleGroupCmd struct {
OrgID int64
NamespaceUID string
RuleGroupConfig apimodels . PostableRuleGroupConfig
}
2022-04-14 14:21:36 +02:00
type UpdateRule struct {
2021-04-01 11:11:45 +03:00
Existing * ngmodels . AlertRule
New ngmodels . AlertRule
}
2022-04-11 10:54:29 -04:00
// RuleStore is the interface for persisting alert rules and instances
2021-04-01 11:11:45 +03:00
type RuleStore interface {
2022-03-23 16:09:53 -04:00
DeleteAlertRulesByUID ( ctx context . Context , orgID int64 , ruleUID ... string ) error
2022-02-08 08:52:03 +00:00
DeleteAlertInstancesByRuleUID ( ctx context . Context , orgID int64 , ruleUID string ) error
GetAlertRuleByUID ( ctx context . Context , query * ngmodels . GetAlertRuleByUIDQuery ) error
GetAlertRulesForScheduling ( ctx context . Context , query * ngmodels . ListAlertRulesQuery ) error
2022-04-25 11:42:42 +01:00
ListAlertRules ( ctx context . Context , query * ngmodels . ListAlertRulesQuery ) error
2022-04-21 17:59:22 +01:00
// GetRuleGroups returns the unique rule groups across all organizations.
GetRuleGroups ( ctx context . Context , query * ngmodels . ListRuleGroupsQuery ) error
2022-04-01 19:33:26 -04:00
GetUserVisibleNamespaces ( context . Context , int64 , * models . SignedInUser ) ( map [ string ] * models . Folder , error )
2021-09-14 16:08:04 +02:00
GetNamespaceByTitle ( context . Context , string , int64 , * models . SignedInUser , bool ) ( * models . Folder , error )
2022-04-14 14:21:36 +02:00
InsertAlertRules ( ctx context . Context , rule [ ] ngmodels . AlertRule ) error
UpdateAlertRules ( ctx context . Context , rule [ ] UpdateRule ) error
2021-04-01 11:11:45 +03:00
}
func getAlertRuleByUID ( sess * sqlstore . DBSession , alertRuleUID string , orgID int64 ) ( * ngmodels . AlertRule , error ) {
// we consider optionally enabling some caching
alertRule := ngmodels . AlertRule { OrgID : orgID , UID : alertRuleUID }
has , err := sess . Get ( & alertRule )
if err != nil {
return nil , err
}
if ! has {
return nil , ngmodels . ErrAlertRuleNotFound
}
return & alertRule , nil
}
2022-03-23 16:09:53 -04:00
// DeleteAlertRulesByUID is a handler for deleting an alert rule.
func ( st DBstore ) DeleteAlertRulesByUID ( ctx context . Context , orgID int64 , ruleUID ... string ) error {
logger := st . Logger . New ( "org_id" , orgID , "rule_uids" , ruleUID )
2022-02-08 08:52:03 +00:00
return st . SQLStore . WithTransactionalDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
2022-03-23 16:09:53 -04:00
rows , err := sess . Table ( "alert_rule" ) . Where ( "org_id = ?" , orgID ) . In ( "uid" , ruleUID ) . Delete ( ngmodels . AlertRule { } )
2021-04-01 11:11:45 +03:00
if err != nil {
return err
}
2022-03-23 16:09:53 -04:00
logger . Debug ( "deleted alert rules" , "count" , rows )
2021-04-01 11:11:45 +03:00
2022-03-23 16:09:53 -04:00
rows , err = sess . Table ( "alert_rule_version" ) . Where ( "rule_org_id = ?" , orgID ) . In ( "rule_uid" , ruleUID ) . Delete ( ngmodels . AlertRule { } )
2021-04-01 11:11:45 +03:00
if err != nil {
return err
}
2022-03-23 16:09:53 -04:00
logger . Debug ( "deleted alert rule versions" , "count" , rows )
2021-04-01 11:11:45 +03:00
2022-03-23 16:09:53 -04:00
rows , err = sess . Table ( "alert_instance" ) . Where ( "rule_org_id = ?" , orgID ) . In ( "rule_uid" , ruleUID ) . Delete ( ngmodels . AlertRule { } )
2021-04-01 11:11:45 +03:00
if err != nil {
return err
}
2022-03-23 16:09:53 -04:00
logger . Debug ( "deleted alert instances" , "count" , rows )
2021-04-01 11:11:45 +03:00
return nil
} )
}
2021-05-06 09:39:34 -07:00
// DeleteAlertInstanceByRuleUID is a handler for deleting alert instances by alert rule UID when a rule has been updated
2022-02-08 08:52:03 +00:00
func ( st DBstore ) DeleteAlertInstancesByRuleUID ( ctx context . Context , orgID int64 , ruleUID string ) error {
return st . SQLStore . WithTransactionalDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
2021-05-12 07:17:43 -04:00
_ , err := sess . Exec ( "DELETE FROM alert_instance WHERE rule_org_id = ? AND rule_uid = ?" , orgID , ruleUID )
2021-05-06 09:39:34 -07:00
if err != nil {
return err
}
return nil
} )
}
2021-04-01 11:11:45 +03:00
// GetAlertRuleByUID is a handler for retrieving an alert rule from that database by its UID and organisation ID.
// It returns ngmodels.ErrAlertRuleNotFound if no alert rule is found for the provided ID.
2022-02-08 08:52:03 +00:00
func ( st DBstore ) GetAlertRuleByUID ( ctx context . Context , query * ngmodels . GetAlertRuleByUIDQuery ) error {
return st . SQLStore . WithDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
2021-04-01 11:11:45 +03:00
alertRule , err := getAlertRuleByUID ( sess , query . UID , query . OrgID )
if err != nil {
return err
}
query . Result = alertRule
return nil
} )
}
2022-04-14 14:21:36 +02:00
// InsertAlertRules is a handler for creating/updating alert rules.
func ( st DBstore ) InsertAlertRules ( ctx context . Context , rules [ ] ngmodels . AlertRule ) error {
2022-02-08 08:52:03 +00:00
return st . SQLStore . WithTransactionalDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
2021-04-01 11:11:45 +03:00
newRules := make ( [ ] ngmodels . AlertRule , 0 , len ( rules ) )
ruleVersions := make ( [ ] ngmodels . AlertRuleVersion , 0 , len ( rules ) )
2022-04-14 14:21:36 +02:00
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 )
}
r . UID = uid
r . Version = 1
if err := st . validateAlertRule ( r ) ; err != nil {
return err
}
if err := ( & r ) . PreSave ( TimeNow ) ; err != nil {
return err
}
newRules = append ( newRules , r )
ruleVersions = append ( ruleVersions , ngmodels . AlertRuleVersion {
RuleOrgID : r . OrgID ,
RuleUID : r . UID ,
RuleNamespaceUID : r . NamespaceUID ,
RuleGroup : r . RuleGroup ,
ParentVersion : 0 ,
Version : r . Version ,
Created : r . Updated ,
Condition : r . Condition ,
Title : r . Title ,
Data : r . Data ,
IntervalSeconds : r . IntervalSeconds ,
NoDataState : r . NoDataState ,
ExecErrState : r . ExecErrState ,
For : r . For ,
Annotations : r . Annotations ,
Labels : r . Labels ,
} )
}
if len ( newRules ) > 0 {
if _ , err := sess . Insert ( & newRules ) ; err != nil {
if st . SQLStore . Dialect . IsUniqueConstraintViolation ( err ) {
return ngmodels . ErrAlertRuleUniqueConstraintViolation
2021-04-01 11:11:45 +03:00
}
2022-04-14 14:21:36 +02:00
return fmt . Errorf ( "failed to create new rules: %w" , err )
}
}
2021-04-01 11:11:45 +03:00
2022-04-14 14:21:36 +02:00
if len ( ruleVersions ) > 0 {
if _ , err := sess . Insert ( & ruleVersions ) ; err != nil {
return fmt . Errorf ( "failed to create new rule versions: %w" , err )
}
}
2021-04-01 11:11:45 +03:00
2022-04-14 14:21:36 +02:00
return nil
} )
}
2021-04-01 11:11:45 +03:00
2022-04-14 14:21:36 +02:00
// UpdateAlertRules is a handler for creating/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
r . New . ID = r . Existing . ID
r . New . Version = r . Existing . Version + 1
if err := st . validateAlertRule ( r . New ) ; err != nil {
return err
}
if err := ( & r . New ) . PreSave ( TimeNow ) ; err != nil {
return err
}
// no way to update multiple rules at once
if _ , err := sess . ID ( r . Existing . ID ) . AllCols ( ) . Update ( r . New ) ; err != nil {
if st . SQLStore . Dialect . IsUniqueConstraintViolation ( err ) {
return ngmodels . ErrAlertRuleUniqueConstraintViolation
2021-04-01 11:11:45 +03:00
}
2022-04-14 14:21:36 +02:00
return fmt . Errorf ( "failed to update rule [%s] %s: %w" , r . New . UID , r . New . Title , err )
2021-04-01 11:11:45 +03:00
}
2022-04-14 14:21:36 +02:00
parentVersion = r . Existing . Version
2021-04-01 11:11:45 +03:00
ruleVersions = append ( ruleVersions , ngmodels . AlertRuleVersion {
RuleOrgID : r . New . OrgID ,
RuleUID : r . New . UID ,
RuleNamespaceUID : r . New . NamespaceUID ,
RuleGroup : r . New . RuleGroup ,
ParentVersion : parentVersion ,
Version : r . New . Version ,
Created : r . New . Updated ,
Condition : r . New . Condition ,
Title : r . New . Title ,
Data : r . New . Data ,
IntervalSeconds : r . New . IntervalSeconds ,
NoDataState : r . New . NoDataState ,
ExecErrState : r . New . ExecErrState ,
2021-04-09 10:50:04 -04:00
For : r . New . For ,
Annotations : r . New . Annotations ,
2021-04-15 15:54:37 +03:00
Labels : r . New . Labels ,
2021-04-01 11:11:45 +03:00
} )
}
if len ( newRules ) > 0 {
if _ , err := sess . Insert ( & newRules ) ; err != nil {
2022-02-23 11:30:04 -05:00
if st . SQLStore . Dialect . IsUniqueConstraintViolation ( err ) {
return ngmodels . ErrAlertRuleUniqueConstraintViolation
}
2021-04-01 11:11:45 +03:00
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 )
}
}
return nil
} )
}
// GetOrgAlertRules is a handler for retrieving alert rules of specific organisation.
2022-04-25 11:42:42 +01:00
func ( st DBstore ) ListAlertRules ( ctx context . Context , query * ngmodels . ListAlertRulesQuery ) error {
2022-02-08 08:52:03 +00:00
return st . SQLStore . WithDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
2022-04-25 11:42:42 +01:00
q := sess . Table ( "alert_rule" )
2021-07-22 09:53:14 +03:00
2022-04-25 11:42:42 +01:00
if query . OrgID >= 0 {
q = q . Where ( "org_id = ?" , query . OrgID )
2021-07-22 09:53:14 +03:00
}
2021-10-04 16:33:55 +01:00
if query . DashboardUID != "" {
2022-04-25 11:42:42 +01:00
q = q . Where ( "dashboard_uid = ?" , query . DashboardUID )
2021-10-04 16:33:55 +01:00
if query . PanelID != 0 {
2022-04-25 11:42:42 +01:00
q = q . Where ( "panel_id = ?" , query . PanelID )
}
}
if len ( query . NamespaceUIDs ) > 0 {
args := make ( [ ] interface { } , 0 , len ( query . NamespaceUIDs ) )
in := make ( [ ] string , 0 , len ( query . NamespaceUIDs ) )
for _ , namespaceUID := range query . NamespaceUIDs {
args = append ( args , namespaceUID )
in = append ( in , "?" )
2021-10-04 16:33:55 +01:00
}
2022-04-25 11:42:42 +01:00
q = q . Where ( fmt . Sprintf ( "namespace_uid IN (%s)" , strings . Join ( in , "," ) ) , args ... )
}
if query . RuleGroup != "" {
q = q . Where ( "rule_group = ?" , query . RuleGroup )
2021-10-04 16:33:55 +01:00
}
2022-04-25 11:42:42 +01:00
q = q . OrderBy ( "id ASC" )
2021-10-04 16:33:55 +01:00
2022-04-25 11:42:42 +01:00
alertRules := make ( [ ] * ngmodels . AlertRule , 0 )
if err := q . Find ( & alertRules ) ; err != nil {
2021-04-01 11:11:45 +03:00
return err
}
query . Result = alertRules
return nil
} )
}
2022-04-21 17:59:22 +01:00
func ( st DBstore ) GetRuleGroups ( ctx context . Context , query * ngmodels . ListRuleGroupsQuery ) error {
return st . SQLStore . WithDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
ruleGroups := make ( [ ] string , 0 )
if err := sess . Table ( "alert_rule" ) . Distinct ( "rule_group" ) . Find ( & ruleGroups ) ; err != nil {
return err
}
query . Result = ruleGroups
return nil
} )
}
2022-04-01 19:33:26 -04:00
// 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 ) {
2021-07-22 09:53:14 +03:00
namespaceMap := make ( map [ string ] * models . Folder )
2022-04-01 19:33:26 -04:00
searchQuery := models . FindPersistedDashboardsQuery {
OrgId : orgID ,
SignedInUser : user ,
Type : searchstore . TypeAlertFolder ,
Limit : - 1 ,
Permission : models . PERMISSION_VIEW ,
Sort : models . SortOption { } ,
Filters : [ ] interface { } {
searchstore . FolderWithAlertsFilter { } ,
} ,
}
2021-07-22 09:53:14 +03:00
var page int64 = 1
for {
2022-04-01 19:33:26 -04:00
query := searchQuery
query . Page = page
proj , err := st . SQLStore . FindDashboards ( ctx , & query )
2021-07-22 09:53:14 +03:00
if err != nil {
return nil , err
}
2022-04-01 19:33:26 -04:00
if len ( proj ) == 0 {
2021-07-22 09:53:14 +03:00
break
}
2022-04-01 19:33:26 -04:00
for _ , hit := range proj {
if ! hit . IsFolder {
continue
}
namespaceMap [ hit . UID ] = & models . Folder {
Id : hit . ID ,
Uid : hit . UID ,
Title : hit . Title ,
}
2021-07-22 09:53:14 +03:00
}
page += 1
}
return namespaceMap , nil
}
2021-04-15 15:54:37 +03:00
// GetNamespaceByTitle is a handler for retrieving a namespace by its title. Alerting rules follow a Grafana folder-like structure which we call namespaces.
2021-09-14 16:08:04 +02:00
func ( st DBstore ) GetNamespaceByTitle ( ctx context . Context , namespace string , orgID int64 , user * models . SignedInUser , withCanSave bool ) ( * models . Folder , error ) {
2022-02-16 14:15:44 +01:00
folder , err := st . FolderService . GetFolderByTitle ( ctx , user , orgID , namespace )
2021-04-01 11:11:45 +03:00
if err != nil {
2021-04-15 15:54:37 +03:00
return nil , err
2021-04-01 11:11:45 +03:00
}
2021-04-15 15:54:37 +03:00
2022-04-01 19:33:26 -04:00
// if access control is disabled, check that the user is allowed to save in the folder.
if withCanSave && st . AccessControl . IsDisabled ( ) {
2021-09-23 17:43:32 +02:00
g := guardian . New ( ctx , folder . Id , orgID , user )
2021-05-20 15:49:33 +03:00
if canSave , err := g . CanSave ( ) ; err != nil || ! canSave {
if err != nil {
st . Logger . Error ( "checking can save permission has failed" , "userId" , user . UserId , "username" , user . Login , "namespace" , namespace , "orgId" , orgID , "error" , err )
}
2021-04-15 15:54:37 +03:00
return nil , ngmodels . ErrCannotEditNamespace
}
}
return folder , nil
2021-04-01 11:11:45 +03:00
}
2021-04-03 20:13:29 +03:00
// GetAlertRulesForScheduling returns alert rule info (identifier, interval, version state)
// that is useful for it's scheduling.
2022-02-08 08:52:03 +00:00
func ( st DBstore ) GetAlertRulesForScheduling ( ctx context . Context , query * ngmodels . ListAlertRulesQuery ) error {
return st . SQLStore . WithDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
2021-04-01 11:11:45 +03:00
alerts := make ( [ ] * ngmodels . AlertRule , 0 )
2021-04-03 20:13:29 +03:00
q := "SELECT uid, org_id, interval_seconds, version FROM alert_rule"
2021-09-29 17:16:40 +03:00
if len ( query . ExcludeOrgs ) > 0 {
q = fmt . Sprintf ( "%s WHERE org_id NOT IN (%s)" , q , strings . Join ( strings . Split ( strings . Trim ( fmt . Sprint ( query . ExcludeOrgs ) , "[]" ) , " " ) , "," ) )
}
2021-04-01 11:11:45 +03:00
if err := sess . SQL ( q ) . Find ( & alerts ) ; err != nil {
return err
}
query . Result = alerts
return nil
} )
}
2021-05-13 22:58:19 +05:30
// GenerateNewAlertRuleUID generates a unique UID for a rule.
// This is set as a variable so that the tests can override it.
// The ruleTitle is only used by the mocked functions.
var GenerateNewAlertRuleUID = func ( sess * sqlstore . DBSession , orgID int64 , ruleTitle string ) ( string , error ) {
2021-04-01 11:11:45 +03:00
for i := 0 ; i < 3 ; i ++ {
uid := util . GenerateShortUID ( )
exists , err := sess . Where ( "org_id=? AND uid=?" , orgID , uid ) . Get ( & ngmodels . AlertRule { } )
if err != nil {
return "" , err
}
if ! exists {
return uid , nil
}
}
return "" , ngmodels . ErrAlertRuleFailedGenerateUniqueUID
}
2021-04-21 17:22:58 +03:00
// validateAlertRule validates the alert rule interval and organisation.
func ( st DBstore ) validateAlertRule ( alertRule ngmodels . AlertRule ) error {
if len ( alertRule . Data ) == 0 {
return fmt . Errorf ( "%w: no queries or expressions are found" , ngmodels . ErrAlertRuleFailedValidation )
2021-04-01 11:11:45 +03:00
}
if alertRule . Title == "" {
2021-04-21 17:22:58 +03:00
return fmt . Errorf ( "%w: title is empty" , ngmodels . ErrAlertRuleFailedValidation )
2021-04-01 11:11:45 +03:00
}
2021-05-19 09:05:32 -07:00
if alertRule . IntervalSeconds % int64 ( st . BaseInterval . Seconds ( ) ) != 0 || alertRule . IntervalSeconds <= 0 {
return fmt . Errorf ( "%w: interval (%v) should be non-zero and divided exactly by scheduler interval: %v" , ngmodels . ErrAlertRuleFailedValidation , time . Duration ( alertRule . IntervalSeconds ) * time . Second , st . BaseInterval )
2021-04-01 11:11:45 +03:00
}
// enfore max name length in SQLite
if len ( alertRule . Title ) > AlertRuleMaxTitleLength {
2021-04-21 17:22:58 +03:00
return fmt . Errorf ( "%w: name length should not be greater than %d" , ngmodels . ErrAlertRuleFailedValidation , AlertRuleMaxTitleLength )
2021-04-01 11:11:45 +03:00
}
2021-04-21 17:22:58 +03:00
// enfore max rule group name length in SQLite
2021-04-01 11:11:45 +03:00
if len ( alertRule . RuleGroup ) > AlertRuleMaxRuleGroupNameLength {
2021-04-21 17:22:58 +03:00
return fmt . Errorf ( "%w: rule group name length should not be greater than %d" , ngmodels . ErrAlertRuleFailedValidation , AlertRuleMaxRuleGroupNameLength )
2021-04-01 11:11:45 +03:00
}
if alertRule . OrgID == 0 {
2021-04-21 17:22:58 +03:00
return fmt . Errorf ( "%w: no organisation is found" , ngmodels . ErrAlertRuleFailedValidation )
2021-04-01 11:11:45 +03:00
}
2021-10-06 11:34:11 +01:00
if alertRule . DashboardUID == nil && alertRule . PanelID != nil {
return fmt . Errorf ( "%w: cannot have Panel ID without a Dashboard UID" , ngmodels . ErrAlertRuleFailedValidation )
}
2021-04-01 11:11:45 +03:00
return nil
}