2021-04-01 11:11:45 +03:00
package store
import (
"context"
2024-02-15 09:45:10 -05:00
"encoding/json"
2022-06-02 14:48:53 +02:00
"errors"
2021-04-01 11:11:45 +03:00
"fmt"
2021-07-22 09:53:14 +03:00
"strings"
2021-04-01 11:11:45 +03:00
2023-06-08 18:51:50 -04:00
"github.com/google/uuid"
2024-02-06 17:12:13 -05:00
"golang.org/x/exp/maps"
2024-02-15 09:45:10 -05:00
"golang.org/x/exp/slices"
2023-06-08 18:51:50 -04:00
2024-05-03 15:32:30 -04:00
"xorm.io/xorm"
2024-06-13 07:11:35 +03:00
"github.com/grafana/grafana/pkg/apimachinery/identity"
2022-10-19 09:02:15 -04:00
"github.com/grafana/grafana/pkg/infra/db"
2024-11-27 01:43:31 +05:30
"github.com/grafana/grafana/pkg/infra/log"
2023-12-04 10:34:38 +01:00
"github.com/grafana/grafana/pkg/services/accesscontrol"
2023-01-26 08:46:30 -05:00
"github.com/grafana/grafana/pkg/services/dashboards"
2024-01-10 15:52:58 -05:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2022-11-11 14:28:24 +01:00
"github.com/grafana/grafana/pkg/services/folder"
2021-04-01 11:11:45 +03:00
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2024-02-06 17:12:13 -05:00
"github.com/grafana/grafana/pkg/services/org"
2022-11-14 21:08:10 +02:00
"github.com/grafana/grafana/pkg/services/sqlstore"
2024-02-15 09:45:10 -05:00
"github.com/grafana/grafana/pkg/services/sqlstore/migrator"
2023-06-02 16:38:02 +02:00
"github.com/grafana/grafana/pkg/services/store/entity"
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
2022-06-02 14:48:53 +02:00
var (
2024-03-12 15:38:21 -04:00
ErrOptimisticLock = errors . New ( "version conflict while updating a record in the database with optimistic locking" )
2022-06-02 14:48:53 +02:00
)
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-10-19 09:02:15 -04:00
return st . SQLStore . WithTransactionalDbSession ( ctx , func ( sess * db . Session ) error {
2024-09-12 13:20:33 -04:00
rows , err := sess . Table ( alertRule { } ) . Where ( "org_id = ?" , orgID ) . In ( "uid" , ruleUID ) . Delete ( alertRule { } )
2021-04-01 11:11:45 +03:00
if err != nil {
return err
}
2023-09-04 18:46:34 +02:00
logger . Debug ( "Deleted alert rules" , "count" , rows )
2024-10-11 12:19:52 -04:00
if rows > 0 {
keys := make ( [ ] ngmodels . AlertRuleKey , 0 , len ( ruleUID ) )
for _ , uid := range ruleUID {
keys = append ( keys , ngmodels . AlertRuleKey { OrgID : orgID , UID : uid } )
}
_ = st . Bus . Publish ( ctx , & RuleChangeEvent {
RuleKeys : keys ,
} )
}
2021-04-01 11:11:45 +03:00
2024-09-12 13:20:33 -04:00
rows , err = sess . Table ( alertRuleVersion { } ) . Where ( "rule_org_id = ?" , orgID ) . In ( "rule_uid" , ruleUID ) . Delete ( alertRule { } )
2021-04-01 11:11:45 +03:00
if err != nil {
return err
}
2023-09-04 18:46:34 +02:00
logger . Debug ( "Deleted alert rule versions" , "count" , rows )
2021-04-01 11:11:45 +03:00
2024-09-12 13:20:33 -04:00
rows , err = sess . Table ( "alert_instance" ) . Where ( "rule_org_id = ?" , orgID ) . In ( "rule_uid" , ruleUID ) . Delete ( alertRule { } )
2021-04-01 11:11:45 +03:00
if err != nil {
return err
}
2023-09-04 18:46:34 +02:00
logger . Debug ( "Deleted alert instances" , "count" , rows )
2021-04-01 11:11:45 +03:00
return nil
} )
}
2024-08-13 12:26:26 +02:00
// IncreaseVersionForAllRulesInNamespaces Increases version for all rules that have specified namespace. Returns all rules that belong to the namespaces
func ( st DBstore ) IncreaseVersionForAllRulesInNamespaces ( ctx context . Context , orgID int64 , namespaceUIDs [ ] string ) ( [ ] ngmodels . AlertRuleKeyWithVersion , error ) {
2024-02-13 10:56:24 -06:00
var keys [ ] ngmodels . AlertRuleKeyWithVersion
2022-10-19 09:02:15 -04:00
err := st . SQLStore . WithTransactionalDbSession ( ctx , func ( sess * db . Session ) error {
2022-08-01 19:28:38 -04:00
now := TimeNow ( )
2024-08-13 12:26:26 +02:00
namespaceUIDsArgs , in := getINSubQueryArgs ( namespaceUIDs )
sql := fmt . Sprintf (
"UPDATE alert_rule SET version = version + 1, updated = ? WHERE org_id = ? AND namespace_uid IN (%s)" ,
strings . Join ( in , "," ) ,
)
args := make ( [ ] interface { } , 0 , 3 + len ( namespaceUIDsArgs ) )
args = append ( args , sql , now , orgID )
args = append ( args , namespaceUIDsArgs ... )
_ , err := sess . Exec ( args ... )
2022-08-01 19:28:38 -04:00
if err != nil {
return err
}
2024-08-13 12:26:26 +02:00
2024-09-12 13:20:33 -04:00
return sess . Table ( alertRule { } ) . Where ( "org_id = ?" , orgID ) . In ( "namespace_uid" , namespaceUIDs ) . Find ( & keys )
2022-08-01 19:28:38 -04:00
} )
return keys , err
}
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.
2023-03-28 10:34:35 +02:00
func ( st DBstore ) GetAlertRuleByUID ( ctx context . Context , query * ngmodels . GetAlertRuleByUIDQuery ) ( result * ngmodels . AlertRule , err error ) {
err = st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
2024-09-12 13:20:33 -04:00
alertRule := alertRule { OrgID : query . OrgID , UID : query . UID }
has , err := sess . Get ( & alertRule )
2021-04-01 11:11:45 +03:00
if err != nil {
return err
}
2024-09-12 13:20:33 -04:00
if ! has {
return ngmodels . ErrAlertRuleNotFound
}
r , err := alertRuleToModelsAlertRule ( alertRule , st . Logger )
if err != nil {
return fmt . Errorf ( "failed to convert alert rule: %w" , err )
}
result = & r
return nil
} )
return result , err
}
2025-02-03 13:26:18 -05:00
func ( st DBstore ) GetAlertRuleVersions ( ctx context . Context , key ngmodels . AlertRuleKey ) ( [ ] * ngmodels . AlertRule , error ) {
alertRules := make ( [ ] * ngmodels . AlertRule , 0 )
err := st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
2025-02-10 09:20:35 -05:00
rows , err := sess . Table ( new ( alertRuleVersion ) ) . Where ( "rule_org_id = ? AND rule_uid = ?" , key . OrgID , key . UID ) . Asc ( "id" ) . Rows ( new ( alertRuleVersion ) )
2025-02-03 13:26:18 -05:00
if err != nil {
return err
}
// Deserialize each rule separately in case any of them contain invalid JSON.
2025-02-10 09:20:35 -05:00
var previousVersion * alertRuleVersion
2025-02-03 13:26:18 -05:00
for rows . Next ( ) {
rule := new ( alertRuleVersion )
err = rows . Scan ( rule )
if err != nil {
st . Logger . Error ( "Invalid rule version found in DB store, ignoring it" , "func" , "GetAlertRuleVersions" , "error" , err )
continue
}
2025-02-10 09:20:35 -05:00
// skip version that has no diff with previous version
// this is pretty basic comparison, it may have false negatives
if previousVersion != nil && previousVersion . EqualSpec ( * rule ) {
continue
}
2025-02-03 13:26:18 -05:00
converted , err := alertRuleToModelsAlertRule ( alertRuleVersionToAlertRule ( * rule ) , st . Logger )
if err != nil {
st . Logger . Error ( "Invalid rule found in DB store, cannot convert, ignoring it" , "func" , "GetAlertRuleVersions" , "error" , err , "version_id" , rule . ID )
continue
}
2025-02-10 09:20:35 -05:00
previousVersion = rule
2025-02-03 13:26:18 -05:00
alertRules = append ( alertRules , & converted )
}
return nil
} )
if err != nil {
return nil , err
}
2025-02-10 09:20:35 -05:00
slices . SortFunc ( alertRules , func ( a , b * ngmodels . AlertRule ) int {
if a . ID > b . ID {
return - 1
}
if a . ID < b . ID {
return 1
}
return 0
} )
2025-02-03 13:26:18 -05:00
return alertRules , nil
}
2024-09-12 13:20:33 -04:00
// GetRuleByID retrieves models.AlertRule by ID.
// It returns models.ErrAlertRuleNotFound if no alert rule is found for the provided ID.
func ( st DBstore ) GetRuleByID ( ctx context . Context , query ngmodels . GetAlertRuleByIDQuery ) ( result * ngmodels . AlertRule , err error ) {
err = st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
alertRule := alertRule { OrgID : query . OrgID , ID : query . ID }
has , err := sess . Get ( & alertRule )
if err != nil {
return err
}
if ! has {
return ngmodels . ErrAlertRuleNotFound
}
r , err := alertRuleToModelsAlertRule ( alertRule , st . Logger )
if err != nil {
return fmt . Errorf ( "failed to convert alert rule: %w" , err )
}
result = & r
2021-04-01 11:11:45 +03:00
return nil
} )
2023-03-28 10:34:35 +02:00
return result , err
2021-04-01 11:11:45 +03:00
}
2022-06-01 10:23:54 -04:00
// GetAlertRulesGroupByRuleUID is a handler for retrieving a group of alert rules from that database by UID and organisation ID of one of rules that belong to that group.
2023-03-28 10:34:35 +02:00
func ( st DBstore ) GetAlertRulesGroupByRuleUID ( ctx context . Context , query * ngmodels . GetAlertRulesGroupByRuleUIDQuery ) ( result [ ] * ngmodels . AlertRule , err error ) {
err = st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
2024-09-12 13:20:33 -04:00
var rules [ ] alertRule
2022-10-07 10:18:49 +01:00
err := sess . Table ( "alert_rule" ) . Alias ( "a" ) . Join (
2022-06-01 10:23:54 -04:00
"INNER" ,
2022-10-07 10:18:49 +01:00
"alert_rule AS b" , "a.org_id = b.org_id AND a.namespace_uid = b.namespace_uid AND a.rule_group = b.rule_group AND b.uid = ?" , query . UID ,
2023-03-28 10:34:35 +02:00
) . Where ( "a.org_id = ?" , query . OrgID ) . Select ( "a.*" ) . Find ( & rules )
2022-06-01 10:23:54 -04:00
if err != nil {
return err
}
2024-06-10 19:05:47 -04:00
// MySQL by default compares strings without case-sensitivity, make sure we keep the case-sensitive comparison.
2024-09-12 13:20:33 -04:00
var groupName , namespaceUID string
2024-06-10 19:05:47 -04:00
// find the rule, which group we fetch
for _ , rule := range rules {
if rule . UID == query . UID {
2024-09-12 13:20:33 -04:00
groupName = rule . RuleGroup
namespaceUID = rule . NamespaceUID
2024-06-10 19:05:47 -04:00
break
}
}
result = make ( [ ] * ngmodels . AlertRule , 0 , len ( rules ) )
// MySQL (and potentially other databases) can use case-insensitive comparison.
// This code makes sure we return groups that only exactly match the filter.
for _ , rule := range rules {
2024-09-12 13:20:33 -04:00
if rule . RuleGroup != groupName || rule . NamespaceUID != namespaceUID {
continue
2024-06-10 19:05:47 -04:00
}
2024-09-12 13:20:33 -04:00
convert , err := alertRuleToModelsAlertRule ( rule , st . Logger )
if err != nil {
return fmt . Errorf ( "failed to convert alert rule %q: %w" , rule . UID , err )
}
result = append ( result , & convert )
2024-06-10 19:05:47 -04:00
}
2022-06-01 10:23:54 -04:00
return nil
} )
2023-03-28 10:34:35 +02:00
return result , err
2022-06-01 10:23:54 -04:00
}
2022-04-14 14:21:36 +02:00
// InsertAlertRules is a handler for creating/updating alert rules.
2023-10-06 18:11:24 -04:00
// Returns the UID and ID of rules that were created in the same order as the input rules.
2025-01-24 12:09:17 -05:00
func ( st DBstore ) InsertAlertRules ( ctx context . Context , user * ngmodels . UserUID , rules [ ] ngmodels . AlertRule ) ( [ ] ngmodels . AlertRuleKeyWithId , error ) {
2023-10-06 18:11:24 -04:00
ids := make ( [ ] ngmodels . AlertRuleKeyWithId , 0 , len ( rules ) )
2024-10-11 12:19:52 -04:00
keys := make ( [ ] ngmodels . AlertRuleKey , 0 , len ( rules ) )
2022-10-19 09:02:15 -04:00
return ids , st . SQLStore . WithTransactionalDbSession ( ctx , func ( sess * db . Session ) error {
2024-09-12 13:20:33 -04:00
newRules := make ( [ ] alertRule , 0 , len ( rules ) )
ruleVersions := make ( [ ] alertRuleVersion , 0 , len ( rules ) )
2022-04-14 14:21:36 +02:00
for i := range rules {
r := rules [ i ]
2022-06-02 14:48:53 +02:00
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
2022-04-14 14:21:36 +02:00
}
r . Version = 1
if err := st . validateAlertRule ( r ) ; err != nil {
return err
}
2025-01-24 12:09:17 -05:00
if err := ( & r ) . PreSave ( TimeNow , user ) ; err != nil {
2022-04-14 14:21:36 +02:00
return err
}
2024-09-12 13:20:33 -04:00
converted , err := alertRuleFromModelsAlertRule ( r )
if err != nil {
return fmt . Errorf ( "failed to convert alert rule %q to storage model: %w" , r . Title , err )
}
newRules = append ( newRules , converted )
ruleVersions = append ( ruleVersions , alertRuleToAlertRuleVersion ( converted ) )
2022-04-14 14:21:36 +02:00
}
if len ( newRules ) > 0 {
2022-06-02 14:48:53 +02:00
// 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 {
2022-10-14 15:33:06 -04:00
if st . SQLStore . GetDialect ( ) . IsUniqueConstraintViolation ( err ) {
2024-11-27 01:43:31 +05:30
return ruleConstraintViolationToErr ( sess , rules [ i ] , err , st . Logger )
2022-06-02 14:48:53 +02:00
}
return fmt . Errorf ( "failed to create new rules: %w" , err )
2021-04-01 11:11:45 +03:00
}
2024-09-12 13:20:33 -04:00
r := newRules [ i ]
2024-10-11 12:19:52 -04:00
key := ngmodels . AlertRuleKey { OrgID : r . OrgID , UID : r . UID }
ids = append ( ids , ngmodels . AlertRuleKeyWithId { AlertRuleKey : key , ID : r . ID } )
keys = append ( keys , key )
2022-04-14 14:21:36 +02:00
}
}
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 )
}
}
2024-10-11 12:19:52 -04:00
if len ( keys ) > 0 {
_ = st . Bus . Publish ( ctx , & RuleChangeEvent {
RuleKeys : keys ,
} )
}
2022-04-14 14:21:36 +02:00
return nil
} )
}
2021-04-01 11:11:45 +03:00
2022-06-02 14:48:53 +02:00
// UpdateAlertRules is a handler for updating alert rules.
2025-01-24 12:09:17 -05:00
func ( st DBstore ) UpdateAlertRules ( ctx context . Context , user * ngmodels . UserUID , rules [ ] ngmodels . UpdateRule ) error {
2022-10-19 09:02:15 -04:00
return st . SQLStore . WithTransactionalDbSession ( ctx , func ( sess * db . Session ) error {
2023-06-08 18:51:50 -04:00
err := st . preventIntermediateUniqueConstraintViolations ( sess , rules )
if err != nil {
return fmt . Errorf ( "failed when preventing intermediate unique constraint violation: %w" , err )
}
2024-09-12 13:20:33 -04:00
ruleVersions := make ( [ ] alertRuleVersion , 0 , len ( rules ) )
2024-10-11 12:19:52 -04:00
keys := make ( [ ] ngmodels . AlertRuleKey , 0 , len ( rules ) )
2024-03-25 15:28:24 +01:00
for i := range rules {
// We do indexed access way to avoid "G601: Implicit memory aliasing in for loop."
// Doing this will be unnecessary with go 1.22 https://stackoverflow.com/a/68247837/767660
r := rules [ i ]
2022-04-14 14:21:36 +02:00
r . New . ID = r . Existing . ID
2022-06-13 12:15:28 -04:00
r . New . Version = r . Existing . Version // xorm will take care of increasing it (see https://xorm.io/docs/chapter-06/1.lock/)
2022-04-14 14:21:36 +02:00
if err := st . validateAlertRule ( r . New ) ; err != nil {
return err
}
2025-01-24 12:09:17 -05:00
if err := ( & r . New ) . PreSave ( TimeNow , user ) ; err != nil {
2022-04-14 14:21:36 +02:00
return err
}
2024-09-12 13:20:33 -04:00
converted , err := alertRuleFromModelsAlertRule ( r . New )
if err != nil {
return fmt . Errorf ( "failed to convert alert rule %s to storage model: %w" , r . New . UID , err )
}
2022-04-14 14:21:36 +02:00
// no way to update multiple rules at once
2024-09-12 13:20:33 -04:00
if updated , err := sess . ID ( r . Existing . ID ) . AllCols ( ) . Update ( converted ) ; err != nil || updated == 0 {
2022-06-13 12:15:28 -04:00
if err != nil {
2022-10-14 15:33:06 -04:00
if st . SQLStore . GetDialect ( ) . IsUniqueConstraintViolation ( err ) {
2024-11-27 01:43:31 +05:30
return ruleConstraintViolationToErr ( sess , r . New , err , st . Logger )
2022-06-13 12:15:28 -04: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-06-13 12:15:28 -04:00
return fmt . Errorf ( "%w: alert rule UID %s version %d" , ErrOptimisticLock , r . New . UID , r . New . Version )
2021-04-01 11:11:45 +03:00
}
2024-09-12 13:20:33 -04:00
v := alertRuleToAlertRuleVersion ( converted )
v . Version ++
v . ParentVersion = r . Existing . Version
ruleVersions = append ( ruleVersions , v )
2024-10-11 12:19:52 -04:00
keys = append ( keys , ngmodels . AlertRuleKey { OrgID : r . New . OrgID , UID : r . New . UID } )
2021-04-01 11:11:45 +03:00
}
if len ( ruleVersions ) > 0 {
if _ , err := sess . Insert ( & ruleVersions ) ; err != nil {
return fmt . Errorf ( "failed to create new rule versions: %w" , err )
}
2024-10-04 14:31:21 -07:00
for _ , rule := range ruleVersions {
// delete old versions of alert rule
_ , err = st . deleteOldAlertRuleVersions ( ctx , rule . RuleUID , rule . RuleOrgID , st . Cfg . RuleVersionRecordLimit )
if err != nil {
st . Logger . Warn ( "Failed to delete old alert rule versions" , "org" , rule . RuleOrgID , "rule" , rule . RuleUID , "error" , err )
}
}
}
2024-10-11 12:19:52 -04:00
if len ( keys ) > 0 {
_ = st . Bus . Publish ( ctx , & RuleChangeEvent {
RuleKeys : keys ,
} )
}
2024-10-04 14:31:21 -07:00
return nil
} )
}
func ( st DBstore ) deleteOldAlertRuleVersions ( ctx context . Context , ruleUID string , orgID int64 , limit int ) ( int64 , error ) {
if limit < 0 {
return 0 , fmt . Errorf ( "failed to delete old alert rule versions: limit is set to '%d' but needs to be > 0" , limit )
}
if limit < 1 {
return 0 , nil
}
var affectedRows int64
err := st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
highest := & alertRuleVersion { }
ok , err := sess . Table ( "alert_rule_version" ) . Desc ( "id" ) . Where ( "rule_org_id = ?" , orgID ) . Where ( "rule_uid = ?" , ruleUID ) . Limit ( 1 , limit ) . Get ( highest )
if err != nil {
return err
}
if ! ok {
// No alert rule versions past the limit exist. Nothing to clean up.
affectedRows = 0
return nil
}
res , err := sess . Exec ( `
DELETE FROM
alert_rule_version
WHERE
rule_org_id = ? AND rule_uid = ?
AND
id <= ?
` , orgID , ruleUID , highest . ID )
if err != nil {
return err
}
rows , err := res . RowsAffected ( )
if err != nil {
return err
}
affectedRows = rows
if affectedRows > 0 {
st . Logger . Info ( "Deleted old alert_rule_version(s)" , "org" , orgID , "limit" , limit , "delete_count" , affectedRows )
2021-04-01 11:11:45 +03:00
}
return nil
} )
2024-10-04 14:31:21 -07:00
return affectedRows , err
2021-04-01 11:11:45 +03:00
}
2023-06-08 18:51:50 -04:00
// preventIntermediateUniqueConstraintViolations prevents unique constraint violations caused by an intermediate update.
// The uniqueness constraint for titles within an org+folder is enforced on every update within a transaction
// instead of on commit (deferred constraint). This means that there could be a set of updates that will throw
// a unique constraint violation in an intermediate step even though the final state is valid.
// For example, a chain of updates RuleA -> RuleB -> RuleC could fail if not executed in the correct order, or
// a swap of titles RuleA <-> RuleB cannot be executed in any order without violating the constraint.
func ( st DBstore ) preventIntermediateUniqueConstraintViolations ( sess * db . Session , updates [ ] ngmodels . UpdateRule ) error {
// The exact solution to this is complex and requires determining directed paths and cycles in the update graph,
// adding in temporary updates to break cycles, and then executing the updates in reverse topological order.
// This is not implemented here. Instead, we choose a simpler solution that works in all cases but might perform
// more updates than necessary. This simpler solution makes a determination of whether an intermediate collision
// could occur and if so, adds a temporary title on all updated rules to break any cycles and remove the need for
// specific ordering.
titleUpdates := make ( [ ] ngmodels . UpdateRule , 0 )
for _ , update := range updates {
if update . Existing . Title != update . New . Title {
titleUpdates = append ( titleUpdates , update )
}
}
// If there is no overlap then an intermediate unique constraint violation is not possible. If there is an overlap,
// then there is the possibility of intermediate unique constraint violation.
if ! newTitlesOverlapExisting ( titleUpdates ) {
return nil
}
2023-09-04 18:46:34 +02:00
st . Logger . Debug ( "Detected possible intermediate unique constraint violation, creating temporary title updates" , "updates" , len ( titleUpdates ) )
2023-06-08 18:51:50 -04:00
for _ , update := range titleUpdates {
r := update . Existing
u := uuid . New ( ) . String ( )
// Some defensive programming in case the temporary title is somehow persisted it will still be recognizable.
uniqueTempTitle := r . Title + u
if len ( uniqueTempTitle ) > AlertRuleMaxTitleLength {
uniqueTempTitle = r . Title [ : AlertRuleMaxTitleLength - len ( u ) ] + uuid . New ( ) . String ( )
}
2024-09-12 13:20:33 -04:00
if updated , err := sess . ID ( r . ID ) . Cols ( "title" ) . Update ( & alertRule { Title : uniqueTempTitle , Version : r . Version } ) ; err != nil || updated == 0 {
2023-06-08 18:51:50 -04:00
if err != nil {
return fmt . Errorf ( "failed to set temporary rule title [%s] %s: %w" , r . UID , r . Title , err )
}
return fmt . Errorf ( "%w: alert rule UID %s version %d" , ErrOptimisticLock , r . UID , r . Version )
}
// Otherwise optimistic locking will conflict on the 2nd update.
r . Version ++
// For consistency.
r . Title = uniqueTempTitle
}
return nil
}
// newTitlesOverlapExisting returns true if any new titles overlap with existing titles.
// It does so in a case-insensitive manner as some supported databases perform case-insensitive comparisons.
func newTitlesOverlapExisting ( rules [ ] ngmodels . UpdateRule ) bool {
existingTitles := make ( map [ string ] struct { } , len ( rules ) )
for _ , r := range rules {
existingTitles [ strings . ToLower ( r . Existing . Title ) ] = struct { } { }
}
// Check if there is any overlap between lower case existing and new titles.
for _ , r := range rules {
if _ , ok := existingTitles [ strings . ToLower ( r . New . Title ) ] ; ok {
return true
}
}
return false
}
2023-06-02 16:38:02 +02:00
// CountInFolder is a handler for retrieving the number of alert rules of
2022-11-08 05:51:00 -05:00
// specific organisation associated with a given namespace (parent folder).
2024-10-04 14:31:21 -07:00
func ( st DBstore ) CountInFolders ( ctx context . Context , orgID int64 , folderUIDs [ ] string , _ identity . Requester ) ( int64 , error ) {
2024-01-30 18:26:34 +02:00
if len ( folderUIDs ) == 0 {
return 0 , nil
}
2022-11-08 05:51:00 -05:00
var count int64
var err error
err = st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
2024-01-30 18:26:34 +02:00
args := make ( [ ] any , 0 , len ( folderUIDs ) )
for _ , folderUID := range folderUIDs {
args = append ( args , folderUID )
}
q := sess . Table ( "alert_rule" ) . Where ( "org_id = ?" , orgID ) . Where ( fmt . Sprintf ( "namespace_uid IN (%s)" , strings . Repeat ( "?," , len ( folderUIDs ) - 1 ) + "?" ) , args ... )
2022-11-08 05:51:00 -05:00
count , err = q . Count ( )
return err
} )
return count , err
}
2022-06-13 12:15:28 -04:00
// ListAlertRules is a handler for retrieving alert rules of specific organisation.
2023-03-28 10:34:35 +02:00
func ( st DBstore ) ListAlertRules ( ctx context . Context , query * ngmodels . ListAlertRulesQuery ) ( result ngmodels . RulesGroup , err error ) {
err = st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) 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 {
2024-05-29 11:50:33 +01:00
args , in := getINSubQueryArgs ( query . NamespaceUIDs )
2022-04-25 11:42:42 +01:00
q = q . Where ( fmt . Sprintf ( "namespace_uid IN (%s)" , strings . Join ( in , "," ) ) , args ... )
2024-05-30 12:04:47 -04:00
}
if len ( query . RuleUIDs ) > 0 {
args , in := getINSubQueryArgs ( query . RuleUIDs )
q = q . Where ( fmt . Sprintf ( "uid IN (%s)" , strings . Join ( in , "," ) ) , args ... )
2022-04-25 11:42:42 +01:00
}
2024-06-10 19:05:47 -04:00
var groupsMap map [ string ] struct { }
2024-05-29 11:50:33 +01:00
if len ( query . RuleGroups ) > 0 {
2024-06-10 19:05:47 -04:00
groupsMap = make ( map [ string ] struct { } )
2024-05-29 11:50:33 +01:00
args , in := getINSubQueryArgs ( query . RuleGroups )
q = q . Where ( fmt . Sprintf ( "rule_group IN (%s)" , strings . Join ( in , "," ) ) , args ... )
2024-06-10 19:05:47 -04:00
for _ , group := range query . RuleGroups {
groupsMap [ group ] = struct { } { }
}
2021-10-04 16:33:55 +01:00
}
2024-02-15 09:45:10 -05:00
if query . ReceiverName != "" {
2024-07-17 10:53:54 -04:00
q , err = st . filterByContentInNotificationSettings ( query . ReceiverName , q )
if err != nil {
return err
}
}
if query . TimeIntervalName != "" {
q , err = st . filterByContentInNotificationSettings ( query . TimeIntervalName , q )
2024-02-15 09:45:10 -05:00
if err != nil {
return err
}
}
2022-06-22 10:52:46 -04:00
q = q . Asc ( "namespace_uid" , "rule_group" , "rule_group_idx" , "id" )
2021-10-04 16:33:55 +01:00
2022-04-25 11:42:42 +01:00
alertRules := make ( [ ] * ngmodels . AlertRule , 0 )
2024-09-12 13:20:33 -04:00
rule := new ( alertRule )
2023-02-21 15:54:20 +01:00
rows , err := q . Rows ( rule )
if err != nil {
2021-04-01 11:11:45 +03:00
return err
}
2023-02-21 15:54:20 +01:00
defer func ( ) {
_ = rows . Close ( )
} ( )
// Deserialize each rule separately in case any of them contain invalid JSON.
for rows . Next ( ) {
2024-09-12 13:20:33 -04:00
rule := new ( alertRule )
2023-02-21 15:54:20 +01:00
err = rows . Scan ( rule )
if err != nil {
st . Logger . Error ( "Invalid rule found in DB store, ignoring it" , "func" , "ListAlertRules" , "error" , err )
continue
}
2024-09-12 13:20:33 -04:00
converted , err := alertRuleToModelsAlertRule ( * rule , st . Logger )
if err != nil {
st . Logger . Error ( "Invalid rule found in DB store, cannot convert, ignoring it" , "func" , "ListAlertRules" , "error" , err )
continue
}
2024-02-15 09:45:10 -05:00
if query . ReceiverName != "" { // remove false-positive hits from the result
2024-09-12 13:20:33 -04:00
if ! slices . ContainsFunc ( converted . NotificationSettings , func ( settings ngmodels . NotificationSettings ) bool {
2024-02-15 09:45:10 -05:00
return settings . Receiver == query . ReceiverName
} ) {
continue
}
}
2024-07-17 10:53:54 -04:00
if query . TimeIntervalName != "" {
2024-09-12 13:20:33 -04:00
if ! slices . ContainsFunc ( converted . NotificationSettings , func ( settings ngmodels . NotificationSettings ) bool {
2024-07-17 10:53:54 -04:00
return slices . Contains ( settings . MuteTimeIntervals , query . TimeIntervalName )
} ) {
continue
}
}
2024-06-10 19:05:47 -04:00
// MySQL (and potentially other databases) can use case-insensitive comparison.
// This code makes sure we return groups that only exactly match the filter.
if groupsMap != nil {
2024-09-12 13:20:33 -04:00
if _ , ok := groupsMap [ converted . RuleGroup ] ; ! ok {
2024-06-10 19:05:47 -04:00
continue
}
}
2024-09-12 13:20:33 -04:00
alertRules = append ( alertRules , & converted )
2023-02-21 15:54:20 +01:00
}
2021-04-01 11:11:45 +03:00
2023-03-28 10:34:35 +02:00
result = alertRules
2021-04-01 11:11:45 +03:00
return nil
} )
2023-03-28 10:34:35 +02:00
return result , err
2021-04-01 11:11:45 +03:00
}
2022-11-14 21:08:10 +02:00
// Count returns either the number of the alert rules under a specific org (if orgID is not zero)
// or the number of all the alert rules
func ( st DBstore ) Count ( ctx context . Context , orgID int64 ) ( int64 , error ) {
type result struct {
Count int64
}
r := result { }
err := st . SQLStore . WithDbSession ( ctx , func ( sess * sqlstore . DBSession ) error {
rawSQL := "SELECT COUNT(*) as count from alert_rule"
2023-08-30 08:46:47 -07:00
args := make ( [ ] any , 0 )
2022-11-14 21:08:10 +02:00
if orgID != 0 {
rawSQL += " WHERE org_id=?"
args = append ( args , orgID )
}
if _ , err := sess . SQL ( rawSQL , args ... ) . Get ( & r ) ; err != nil {
return err
}
return nil
} )
return r . Count , err
}
2022-06-02 14:48:53 +02:00
func ( st DBstore ) GetRuleGroupInterval ( ctx context . Context , orgID int64 , namespaceUID string , ruleGroup string ) ( int64 , error ) {
var interval int64 = 0
2022-10-19 09:02:15 -04:00
return interval , st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
2024-09-12 13:20:33 -04:00
ruleGroups := make ( [ ] alertRule , 0 )
2022-06-02 14:48:53 +02:00
err := sess . Find (
& ruleGroups ,
2024-09-12 13:20:33 -04:00
alertRule { OrgID : orgID , RuleGroup : ruleGroup , NamespaceUID : namespaceUID } ,
2022-06-02 14:48:53 +02:00
)
if len ( ruleGroups ) == 0 {
2024-03-12 15:38:21 -04:00
return ngmodels . ErrAlertRuleGroupNotFound . Errorf ( "" )
2022-06-02 14:48:53 +02:00
}
interval = ruleGroups [ 0 ] . IntervalSeconds
return err
} )
}
2024-02-06 17:12:13 -05:00
// GetUserVisibleNamespaces returns the folders that are visible to the user
2023-11-14 15:47:34 +01:00
func ( st DBstore ) GetUserVisibleNamespaces ( ctx context . Context , orgID int64 , user identity . Requester ) ( map [ string ] * folder . Folder , error ) {
2024-02-06 17:12:13 -05:00
folders , err := st . FolderService . GetFolders ( ctx , folder . GetFoldersQuery {
OrgID : orgID ,
WithFullpath : true ,
2022-04-01 19:33:26 -04:00
SignedInUser : user ,
2024-02-06 17:12:13 -05:00
} )
if err != nil {
return nil , err
2022-04-01 19:33:26 -04:00
}
2024-02-06 17:12:13 -05:00
namespaceMap := make ( map [ string ] * folder . Folder )
for _ , f := range folders {
namespaceMap [ f . UID ] = f
2021-07-22 09:53:14 +03:00
}
return namespaceMap , nil
}
2022-06-17 13:10:49 -04:00
// GetNamespaceByUID is a handler for retrieving a namespace by its UID. Alerting rules follow a Grafana folder-like structure which we call namespaces.
2023-11-14 15:47:34 +01:00
func ( st DBstore ) GetNamespaceByUID ( ctx context . Context , uid string , orgID int64 , user identity . Requester ) ( * folder . Folder , error ) {
2024-02-06 17:12:13 -05:00
f , err := st . FolderService . GetFolders ( ctx , folder . GetFoldersQuery { OrgID : orgID , UIDs : [ ] string { uid } , WithFullpath : true , SignedInUser : user } )
2022-06-17 13:10:49 -04:00
if err != nil {
return nil , err
}
2024-02-06 17:12:13 -05:00
if len ( f ) == 0 {
return nil , dashboards . ErrFolderAccessDenied
}
return f [ 0 ] , nil
2022-06-17 13:10:49 -04:00
}
2022-08-31 11:08:19 -04:00
func ( st DBstore ) GetAlertRulesKeysForScheduling ( ctx context . Context ) ( [ ] ngmodels . AlertRuleKeyWithVersion , error ) {
var result [ ] ngmodels . AlertRuleKeyWithVersion
2022-10-19 09:02:15 -04:00
err := st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
2023-04-13 12:55:42 +01:00
alertRulesSql := sess . Table ( "alert_rule" ) . Select ( "org_id, uid, version" )
var disabledOrgs [ ] int64
for orgID := range st . Cfg . DisabledOrgs {
disabledOrgs = append ( disabledOrgs , orgID )
2022-08-31 11:08:19 -04:00
}
2023-04-13 12:55:42 +01:00
if len ( disabledOrgs ) > 0 {
alertRulesSql = alertRulesSql . NotIn ( "org_id" , disabledOrgs )
}
if err := alertRulesSql . Find ( & result ) ; err != nil {
2022-08-31 11:08:19 -04:00
return err
}
2023-04-13 12:55:42 +01:00
2022-08-31 11:08:19 -04:00
return nil
} )
return result , err
}
2022-05-12 09:55:05 -04:00
// GetAlertRulesForScheduling returns a short version of all alert rules except those that belong to an excluded list of organizations
func ( st DBstore ) GetAlertRulesForScheduling ( ctx context . Context , query * ngmodels . GetAlertRulesForSchedulingQuery ) error {
2022-08-31 11:08:19 -04:00
var rules [ ] * ngmodels . AlertRule
2022-10-19 09:02:15 -04:00
return st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
2023-04-13 12:55:42 +01:00
var disabledOrgs [ ] int64
for orgID := range st . Cfg . DisabledOrgs {
disabledOrgs = append ( disabledOrgs , orgID )
}
alertRulesSql := sess . Table ( "alert_rule" )
if len ( disabledOrgs ) > 0 {
alertRulesSql . NotIn ( "org_id" , disabledOrgs )
}
2024-06-10 19:05:47 -04:00
var groupsMap map [ string ] struct { }
2023-04-13 12:55:42 +01:00
if len ( query . RuleGroups ) > 0 {
alertRulesSql . In ( "rule_group" , query . RuleGroups )
2024-06-10 19:05:47 -04:00
groupsMap = make ( map [ string ] struct { } , len ( query . RuleGroups ) )
for _ , group := range query . RuleGroups {
groupsMap [ group ] = struct { } { }
}
2021-09-29 17:16:40 +03:00
}
2022-08-31 11:08:19 -04:00
2024-09-12 13:20:33 -04:00
rule := new ( alertRule )
2023-04-13 12:55:42 +01:00
rows , err := alertRulesSql . Rows ( rule )
2023-02-21 15:54:20 +01:00
if err != nil {
2022-08-31 11:08:19 -04:00
return fmt . Errorf ( "failed to fetch alert rules: %w" , err )
}
2023-02-21 15:54:20 +01:00
defer func ( ) {
2023-04-13 12:55:42 +01:00
if err := rows . Close ( ) ; err != nil {
2023-09-04 18:46:34 +02:00
st . Logger . Error ( "Unable to close rows session" , "error" , err )
2023-04-13 12:55:42 +01:00
}
2023-02-21 15:54:20 +01:00
} ( )
// Deserialize each rule separately in case any of them contain invalid JSON.
for rows . Next ( ) {
2024-09-12 13:20:33 -04:00
rule := new ( alertRule )
2023-02-21 15:54:20 +01:00
err = rows . Scan ( rule )
if err != nil {
st . Logger . Error ( "Invalid rule found in DB store, ignoring it" , "func" , "GetAlertRulesForScheduling" , "error" , err )
continue
}
2024-09-12 13:20:33 -04:00
converted , err := alertRuleToModelsAlertRule ( * rule , st . Logger )
if err != nil {
st . Logger . Error ( "Invalid rule found in DB store, cannot convert it" , "func" , "GetAlertRulesForScheduling" , "error" , err )
continue
}
2024-06-10 19:05:47 -04:00
// MySQL (and potentially other databases) uses case-insensitive comparison.
// This code makes sure we return groups that only exactly match the filter
if groupsMap != nil {
2024-09-12 13:20:33 -04:00
if _ , ok := groupsMap [ converted . RuleGroup ] ; ! ok { // compare groups using case-sensitive logic.
2024-06-10 19:05:47 -04:00
continue
}
}
2024-01-10 15:52:58 -05:00
if st . FeatureToggles . IsEnabled ( ctx , featuremgmt . FlagAlertingQueryOptimization ) {
2024-09-12 13:20:33 -04:00
if optimizations , err := OptimizeAlertQueries ( converted . Data ) ; err != nil {
2024-01-10 15:52:58 -05:00
st . Logger . Error ( "Could not migrate rule from range to instant query" , "rule" , rule . UID , "err" , err )
} else if len ( optimizations ) > 0 {
st . Logger . Info ( "Migrated rule from range to instant query" , "rule" , rule . UID , "migrated_queries" , len ( optimizations ) )
}
2023-06-16 19:55:49 +02:00
}
2024-09-12 13:20:33 -04:00
rules = append ( rules , & converted )
2023-02-21 15:54:20 +01:00
}
2022-08-31 11:08:19 -04:00
query . ResultRules = rules
2023-04-13 12:55:42 +01:00
2022-08-31 11:08:19 -04:00
if query . PopulateFolders {
2024-02-06 17:12:13 -05:00
query . ResultFoldersTitles = map [ ngmodels . FolderKey ] string { }
uids := map [ int64 ] map [ string ] struct { } { }
for _ , r := range rules {
om , ok := uids [ r . OrgID ]
if ! ok {
om = make ( map [ string ] struct { } )
uids [ r . OrgID ] = om
}
om [ r . NamespaceUID ] = struct { } { }
2022-08-31 11:08:19 -04:00
}
2024-02-06 17:12:13 -05:00
for orgID , uids := range uids {
schedulerUser := accesscontrol . BackgroundUser ( "grafana_scheduler" , orgID , org . RoleAdmin ,
[ ] accesscontrol . Permission {
{
Action : dashboards . ActionFoldersRead , Scope : dashboards . ScopeFoldersAll ,
} ,
} )
folders , err := st . FolderService . GetFolders ( ctx , folder . GetFoldersQuery {
OrgID : orgID ,
UIDs : maps . Keys ( uids ) ,
WithFullpath : true ,
SignedInUser : schedulerUser ,
} )
if err != nil {
return fmt . Errorf ( "failed to fetch a list of folders that contain alert rules: %w" , err )
}
for _ , f := range folders {
query . ResultFoldersTitles [ ngmodels . FolderKey { OrgID : f . OrgID , UID : f . UID } ] = f . Fullpath
2024-01-30 17:14:11 -05:00
}
2022-08-31 11:08:19 -04:00
}
2021-04-01 11:11:45 +03:00
}
return nil
} )
}
2023-06-02 16:38:02 +02:00
// DeleteInFolder deletes the rules contained in a given folder along with their associated data.
2024-01-30 18:26:34 +02:00
func ( st DBstore ) DeleteInFolders ( ctx context . Context , orgID int64 , folderUIDs [ ] string , user identity . Requester ) error {
for _ , folderUID := range folderUIDs {
evaluator := accesscontrol . EvalPermission ( accesscontrol . ActionAlertingRuleDelete , dashboards . ScopeFoldersProvider . GetResourceScopeUID ( folderUID ) )
canSave , err := st . AccessControl . Evaluate ( ctx , user , evaluator )
if err != nil {
st . Logger . Error ( "Failed to evaluate access control" , "error" , err )
return err
}
if ! canSave {
st . Logger . Error ( "user is not allowed to delete alert rules in folder" , "folder" , folderUID , "user" )
return dashboards . ErrFolderAccessDenied
}
2023-12-04 10:34:38 +01:00
2024-01-30 18:26:34 +02:00
rules , err := st . ListAlertRules ( ctx , & ngmodels . ListAlertRulesQuery {
OrgID : orgID ,
NamespaceUIDs : [ ] string { folderUID } ,
} )
if err != nil {
return err
}
2023-06-02 16:38:02 +02:00
2024-01-30 18:26:34 +02:00
uids := make ( [ ] string , 0 , len ( rules ) )
for _ , tgt := range rules {
if tgt != nil {
uids = append ( uids , tgt . UID )
}
2023-06-02 16:38:02 +02:00
}
2024-01-30 18:26:34 +02:00
if err := st . DeleteAlertRulesByUID ( ctx , orgID , uids ... ) ; err != nil {
return err
}
2023-06-02 16:38:02 +02:00
}
return nil
}
// Kind returns the name of the alert rule type of entity.
func ( st DBstore ) Kind ( ) string { return entity . StandardKindAlertRule }
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.
2022-10-19 09:02:15 -04:00
var GenerateNewAlertRuleUID = func ( sess * db . Session , orgID int64 , ruleTitle string ) ( string , error ) {
2021-04-01 11:11:45 +03:00
for i := 0 ; i < 3 ; i ++ {
uid := util . GenerateShortUID ( )
2024-09-12 13:20:33 -04:00
exists , err := sess . Where ( "org_id=? AND uid=?" , orgID , uid ) . Get ( & alertRule { } )
2021-04-01 11:11:45 +03:00
if err != nil {
return "" , err
}
if ! exists {
return uid , nil
}
}
return "" , ngmodels . ErrAlertRuleFailedGenerateUniqueUID
}
2023-11-17 11:20:50 -05:00
// validateAlertRule validates the alert rule including db-level restrictions on field lengths.
2021-04-21 17:22:58 +03:00
func ( st DBstore ) validateAlertRule ( alertRule ngmodels . AlertRule ) error {
2023-11-17 11:20:50 -05:00
if err := alertRule . ValidateAlertRule ( st . Cfg ) ; err != nil {
2022-06-09 09:28:32 +02:00
return err
2021-04-01 11:11:45 +03:00
}
2023-11-17 11:20:50 -05:00
// enforce max name length.
2021-04-01 11:11:45 +03:00
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
}
2023-11-17 11:20:50 -05:00
// enforce max rule group name length.
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
}
return nil
}
2024-02-15 09:45:10 -05:00
// ListNotificationSettings fetches all notification settings for given organization
func ( st DBstore ) ListNotificationSettings ( ctx context . Context , q ngmodels . ListNotificationSettingsQuery ) ( map [ ngmodels . AlertRuleKey ] [ ] ngmodels . NotificationSettings , error ) {
2024-09-12 13:20:33 -04:00
var rules [ ] alertRule
2024-02-15 09:45:10 -05:00
err := st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
2024-09-12 13:20:33 -04:00
query := sess . Table ( alertRule { } ) . Select ( "uid, notification_settings" ) . Where ( "org_id = ?" , q . OrgID )
2024-07-17 10:53:54 -04:00
hasFilter := false
2024-02-15 09:45:10 -05:00
if q . ReceiverName != "" {
var err error
2024-07-17 10:53:54 -04:00
query , err = st . filterByContentInNotificationSettings ( q . ReceiverName , query )
2024-02-15 09:45:10 -05:00
if err != nil {
return err
}
2024-07-17 10:53:54 -04:00
hasFilter = true
}
if q . TimeIntervalName != "" {
var err error
query , err = st . filterByContentInNotificationSettings ( q . TimeIntervalName , query )
if err != nil {
return err
}
hasFilter = true
}
if ! hasFilter {
2024-09-12 13:20:33 -04:00
query = query . And ( "notification_settings IS NOT NULL AND notification_settings <> 'null' AND notification_settings <> ''" )
2024-02-15 09:45:10 -05:00
}
return query . Find ( & rules )
} )
if err != nil {
return nil , err
}
result := make ( map [ ngmodels . AlertRuleKey ] [ ] ngmodels . NotificationSettings , len ( rules ) )
for _ , rule := range rules {
2024-09-12 13:20:33 -04:00
if rule . NotificationSettings == "" {
continue
}
converted , err := parseNotificationSettings ( rule . NotificationSettings )
if err != nil {
return nil , fmt . Errorf ( "failed to convert notification settings %s to models: %w" , rule . UID , err )
}
2024-07-17 10:53:54 -04:00
ns := make ( [ ] ngmodels . NotificationSettings , 0 , len ( rule . NotificationSettings ) )
2024-09-12 13:20:33 -04:00
for _ , setting := range converted {
2024-07-17 10:53:54 -04:00
if q . ReceiverName != "" && q . ReceiverName != setting . Receiver { // currently, there can be only one setting. If in future there are more, we will return all settings of a rule that has a setting with receiver
continue
}
if q . TimeIntervalName != "" && ! slices . Contains ( setting . MuteTimeIntervals , q . TimeIntervalName ) {
continue
2024-02-15 09:45:10 -05:00
}
2024-07-17 10:53:54 -04:00
ns = append ( ns , setting )
2024-02-15 09:45:10 -05:00
}
if len ( ns ) > 0 {
key := ngmodels . AlertRuleKey {
OrgID : q . OrgID ,
UID : rule . UID ,
}
2024-09-12 13:20:33 -04:00
result [ key ] = ns
2024-02-15 09:45:10 -05:00
}
}
return result , nil
}
2024-07-17 10:53:54 -04:00
func ( st DBstore ) filterByContentInNotificationSettings ( value string , sess * xorm . Session ) ( * xorm . Session , error ) {
if value == "" {
2024-02-15 09:45:10 -05:00
return sess , nil
}
// marshall string according to JSON rules so we follow escaping rules.
2024-07-17 10:53:54 -04:00
b , err := json . Marshal ( value )
2024-02-15 09:45:10 -05:00
if err != nil {
2024-07-17 10:53:54 -04:00
return nil , fmt . Errorf ( "failed to marshall string for notification settings content filter: %w" , err )
2024-02-15 09:45:10 -05:00
}
var search = string ( b )
if st . SQLStore . GetDialect ( ) . DriverName ( ) != migrator . SQLite {
// this escapes escaped double quote (\") to \\\"
search = strings . ReplaceAll ( strings . ReplaceAll ( search , ` \ ` , ` \\ ` ) , ` " ` , ` \" ` )
}
return sess . And ( fmt . Sprintf ( "notification_settings %s ?" , st . SQLStore . GetDialect ( ) . LikeStr ( ) ) , "%" + search + "%" ) , nil
}
2024-09-17 12:07:31 -04:00
func ( st DBstore ) RenameReceiverInNotificationSettings ( ctx context . Context , orgID int64 , oldReceiver , newReceiver string , validateProvenance func ( ngmodels . Provenance ) bool , dryRun bool ) ( [ ] ngmodels . AlertRuleKey , [ ] ngmodels . AlertRuleKey , error ) {
2024-02-15 09:45:10 -05:00
// fetch entire rules because Update method requires it because it copies rules to version table
rules , err := st . ListAlertRules ( ctx , & ngmodels . ListAlertRulesQuery {
OrgID : orgID ,
ReceiverName : oldReceiver ,
} )
if err != nil {
2024-09-17 12:07:31 -04:00
return nil , nil , err
2024-02-15 09:45:10 -05:00
}
if len ( rules ) == 0 {
2024-09-17 12:07:31 -04:00
return nil , nil , nil
}
provenances , err := st . GetProvenances ( ctx , orgID , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
if err != nil {
return nil , nil , err
2024-02-15 09:45:10 -05:00
}
2024-06-14 14:16:36 -04:00
2024-09-17 12:07:31 -04:00
var invalidProvenance [ ] ngmodels . AlertRuleKey
result := make ( [ ] ngmodels . AlertRuleKey , 0 , len ( rules ) )
2024-06-14 14:16:36 -04:00
updates := make ( [ ] ngmodels . UpdateRule , 0 , len ( rules ) )
2024-02-15 09:45:10 -05:00
for _ , rule := range rules {
2024-09-17 12:07:31 -04:00
provenance , ok := provenances [ rule . UID ]
if ! ok {
provenance = ngmodels . ProvenanceNone
}
if ! validateProvenance ( provenance ) {
invalidProvenance = append ( invalidProvenance , rule . GetKey ( ) )
}
if len ( invalidProvenance ) > 0 { // do not do any fixes if there is at least one rule with not allowed provenance
continue
}
result = append ( result , rule . GetKey ( ) )
if dryRun {
continue
}
2025-02-11 09:46:02 -05:00
r := rule . Copy ( )
2024-02-15 09:45:10 -05:00
for idx := range r . NotificationSettings {
if r . NotificationSettings [ idx ] . Receiver == oldReceiver {
r . NotificationSettings [ idx ] . Receiver = newReceiver
}
}
2024-06-14 14:16:36 -04:00
2024-02-15 09:45:10 -05:00
updates = append ( updates , ngmodels . UpdateRule {
Existing : rule ,
New : * r ,
} )
}
2024-09-17 12:07:31 -04:00
if len ( invalidProvenance ) > 0 {
return nil , invalidProvenance , nil
}
if dryRun {
return result , nil , nil
}
2025-01-24 12:09:17 -05:00
// Provide empty user identifier to ensure it's clear that the rule update was made by the system
// and not by the user who changed the receiver's title.
2025-02-04 14:23:15 -05:00
return result , nil , st . UpdateAlertRules ( ctx , & ngmodels . AlertingUserUID , updates )
2024-02-15 09:45:10 -05:00
}
2024-04-22 12:28:46 -04:00
2024-08-16 13:55:03 -04:00
// RenameTimeIntervalInNotificationSettings renames all rules that use old time interval name to the new name.
// Before renaming, it checks that all rules that need to be updated have allowed provenance status, and skips updating
// if at least one rule does not have allowed provenance.
// It returns a tuple:
// - a collection of models.AlertRuleKey of rules that were updated,
// - a collection of rules that have invalid provenance status,
// - database error
func ( st DBstore ) RenameTimeIntervalInNotificationSettings (
ctx context . Context ,
orgID int64 ,
oldTimeInterval , newTimeInterval string ,
validateProvenance func ( ngmodels . Provenance ) bool ,
dryRun bool ,
) ( [ ] ngmodels . AlertRuleKey , [ ] ngmodels . AlertRuleKey , error ) {
// fetch entire rules because Update method requires it because it copies rules to version table
rules , err := st . ListAlertRules ( ctx , & ngmodels . ListAlertRulesQuery {
OrgID : orgID ,
TimeIntervalName : oldTimeInterval ,
} )
if err != nil {
return nil , nil , err
}
if len ( rules ) == 0 {
return nil , nil , nil
}
provenances , err := st . GetProvenances ( ctx , orgID , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
if err != nil {
return nil , nil , err
}
var invalidProvenance [ ] ngmodels . AlertRuleKey
result := make ( [ ] ngmodels . AlertRuleKey , 0 , len ( rules ) )
updates := make ( [ ] ngmodels . UpdateRule , 0 , len ( rules ) )
for _ , rule := range rules {
provenance , ok := provenances [ rule . UID ]
if ! ok {
provenance = ngmodels . ProvenanceNone
}
if ! validateProvenance ( provenance ) {
invalidProvenance = append ( invalidProvenance , rule . GetKey ( ) )
}
if len ( invalidProvenance ) > 0 { // do not do any fixes if there is at least one rule with not allowed provenance
continue
}
result = append ( result , rule . GetKey ( ) )
if dryRun {
continue
}
2025-02-11 09:46:02 -05:00
r := rule . Copy ( )
2024-08-16 13:55:03 -04:00
for idx := range r . NotificationSettings {
for mtIdx := range r . NotificationSettings [ idx ] . MuteTimeIntervals {
if r . NotificationSettings [ idx ] . MuteTimeIntervals [ mtIdx ] == oldTimeInterval {
r . NotificationSettings [ idx ] . MuteTimeIntervals [ mtIdx ] = newTimeInterval
}
}
}
updates = append ( updates , ngmodels . UpdateRule {
Existing : rule ,
New : * r ,
} )
}
if len ( invalidProvenance ) > 0 {
return nil , invalidProvenance , nil
}
if dryRun {
return result , nil , nil
}
2025-01-24 12:09:17 -05:00
// Provide empty user identifier to ensure it's clear that the rule update was made by the system
// and not by the user who changed the receiver's title.
2025-02-04 14:23:15 -05:00
return result , nil , st . UpdateAlertRules ( ctx , & ngmodels . AlertingUserUID , updates )
2024-08-16 13:55:03 -04:00
}
2024-11-27 01:43:31 +05:30
func ruleConstraintViolationToErr ( sess * db . Session , rule ngmodels . AlertRule , err error , logger log . Logger ) error {
2024-04-22 12:28:46 -04:00
msg := err . Error ( )
if strings . Contains ( msg , "UQE_alert_rule_org_id_namespace_uid_title" ) || strings . Contains ( msg , "alert_rule.org_id, alert_rule.namespace_uid, alert_rule.title" ) {
2024-11-27 01:43:31 +05:30
// return verbose conflicting alert rule error response
// see: https://github.com/grafana/grafana/issues/89755
var fetched_uid string
var existingPartialAlertRule ngmodels . AlertRule
ok , uid_fetch_err := sess . Table ( "alert_rule" ) . Cols ( "uid" ) . Where ( "org_id = ? AND title = ? AND namespace_uid = ?" , rule . OrgID , rule . Title , rule . NamespaceUID ) . Get ( & fetched_uid )
if uid_fetch_err != nil {
logger . Error ( "Error fetching uid from alert_rule table" , "reason" , uid_fetch_err . Error ( ) )
}
if ok {
existingPartialAlertRule = ngmodels . AlertRule { UID : fetched_uid , Title : rule . Title , NamespaceUID : rule . NamespaceUID }
}
return ngmodels . ErrAlertRuleConflictVerbose ( existingPartialAlertRule , rule , ngmodels . ErrAlertRuleUniqueConstraintViolation )
2024-04-22 12:28:46 -04:00
} else if strings . Contains ( msg , "UQE_alert_rule_org_id_uid" ) || strings . Contains ( msg , "alert_rule.org_id, alert_rule.uid" ) {
2024-11-27 01:43:31 +05:30
// return verbose conflicting alert rule error response
// see: https://github.com/grafana/grafana/issues/89755
existingPartialAlertRule := ngmodels . AlertRule { UID : rule . UID }
return ngmodels . ErrAlertRuleConflictVerbose ( existingPartialAlertRule , rule , errors . New ( "rule UID under the same organisation should be unique" ) )
2024-04-22 12:28:46 -04:00
} else {
return ngmodels . ErrAlertRuleConflict ( rule , err )
}
}
2024-05-03 15:32:30 -04:00
// GetNamespacesByRuleUID returns a map of rule UIDs to their namespace UID.
func ( st DBstore ) GetNamespacesByRuleUID ( ctx context . Context , orgID int64 , uids ... string ) ( map [ string ] string , error ) {
result := make ( map [ string ] string )
err := st . SQLStore . WithDbSession ( ctx , func ( sess * db . Session ) error {
2024-09-12 13:20:33 -04:00
var rules [ ] alertRule
err := sess . Table ( alertRule { } ) . Select ( "uid, namespace_uid" ) . Where ( "org_id = ?" , orgID ) . In ( "uid" , uids ) . Find ( & rules )
2024-05-03 15:32:30 -04:00
if err != nil {
return err
}
for _ , rule := range rules {
result [ rule . UID ] = rule . NamespaceUID
}
return nil
} )
return result , err
}
2024-05-29 11:50:33 +01:00
func getINSubQueryArgs [ T any ] ( inputSlice [ ] T ) ( [ ] any , [ ] string ) {
args := make ( [ ] any , 0 , len ( inputSlice ) )
in := make ( [ ] string , 0 , len ( inputSlice ) )
for _ , t := range inputSlice {
args = append ( args , t )
in = append ( in , "?" )
}
return args , in
}