2022-02-23 10:30:04 -06:00
package api
import (
"errors"
"fmt"
2023-06-15 16:37:30 -05:00
"sort"
2023-06-15 12:33:42 -05:00
"strings"
2022-02-23 10:30:04 -06:00
"time"
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/store"
"github.com/grafana/grafana/pkg/setting"
)
2024-02-28 14:40:13 -06:00
type RuleLimits struct {
// The default interval if not specified.
DefaultRuleEvaluationInterval time . Duration
// All intervals must be an integer multiple of this duration.
BaseInterval time . Duration
}
func RuleLimitsFromConfig ( cfg * setting . UnifiedAlertingSettings ) RuleLimits {
return RuleLimits {
DefaultRuleEvaluationInterval : cfg . DefaultRuleEvaluationInterval ,
BaseInterval : cfg . BaseInterval ,
}
}
2022-02-23 10:30:04 -06:00
// validateRuleNode validates API model (definitions.PostableExtendedRuleNode) and converts it to models.AlertRule
func validateRuleNode (
ruleNode * apimodels . PostableExtendedRuleNode ,
groupName string ,
interval time . Duration ,
orgId int64 ,
2024-02-28 14:40:13 -06:00
namespaceUID string ,
limits RuleLimits ) ( * ngmodels . AlertRule , error ) {
intervalSeconds , err := validateInterval ( interval , limits . BaseInterval )
2022-10-28 09:40:11 -05:00
if err != nil {
return nil , err
2022-02-23 10:30:04 -06:00
}
if ruleNode . GrafanaManagedAlert == nil {
return nil , fmt . Errorf ( "not Grafana managed alert rule" )
}
// if UID is specified then we can accept partial model. Therefore, some validation can be skipped as it will be patched later
canPatch := ruleNode . GrafanaManagedAlert . UID != ""
if ruleNode . GrafanaManagedAlert . Title == "" && ! canPatch {
return nil , errors . New ( "alert rule title cannot be empty" )
}
if len ( ruleNode . GrafanaManagedAlert . Title ) > store . AlertRuleMaxTitleLength {
return nil , fmt . Errorf ( "alert rule title is too long. Max length is %d" , store . AlertRuleMaxTitleLength )
}
noDataState := ngmodels . NoData
if ruleNode . GrafanaManagedAlert . NoDataState == "" && canPatch {
noDataState = ""
}
if ruleNode . GrafanaManagedAlert . NoDataState != "" {
noDataState , err = ngmodels . NoDataStateFromString ( string ( ruleNode . GrafanaManagedAlert . NoDataState ) )
if err != nil {
return nil , err
}
}
errorState := ngmodels . AlertingErrState
if ruleNode . GrafanaManagedAlert . ExecErrState == "" && canPatch {
errorState = ""
}
if ruleNode . GrafanaManagedAlert . ExecErrState != "" {
errorState , err = ngmodels . ErrStateFromString ( string ( ruleNode . GrafanaManagedAlert . ExecErrState ) )
if err != nil {
return nil , err
}
}
if len ( ruleNode . GrafanaManagedAlert . Data ) == 0 {
if canPatch {
if ruleNode . GrafanaManagedAlert . Condition != "" {
return nil , fmt . Errorf ( "%w: query is not specified by condition is. You must specify both query and condition to update existing alert rule" , ngmodels . ErrAlertRuleFailedValidation )
}
} else {
return nil , fmt . Errorf ( "%w: no queries or expressions are found" , ngmodels . ErrAlertRuleFailedValidation )
}
2023-06-15 12:33:42 -05:00
} else {
err = validateCondition ( ruleNode . GrafanaManagedAlert . Condition , ruleNode . GrafanaManagedAlert . Data )
if err != nil {
return nil , fmt . Errorf ( "%w: %s" , ngmodels . ErrAlertRuleFailedValidation , err . Error ( ) )
}
2022-02-23 10:30:04 -06:00
}
2023-03-27 10:55:13 -05:00
queries := AlertQueriesFromApiAlertQueries ( ruleNode . GrafanaManagedAlert . Data )
2022-02-23 10:30:04 -06:00
newAlertRule := ngmodels . AlertRule {
OrgID : orgId ,
Title : ruleNode . GrafanaManagedAlert . Title ,
Condition : ruleNode . GrafanaManagedAlert . Condition ,
2023-03-27 10:55:13 -05:00
Data : queries ,
2022-02-23 10:30:04 -06:00
UID : ruleNode . GrafanaManagedAlert . UID ,
IntervalSeconds : intervalSeconds ,
2024-02-28 14:40:13 -06:00
NamespaceUID : namespaceUID ,
2022-02-23 10:30:04 -06:00
RuleGroup : groupName ,
NoDataState : noDataState ,
ExecErrState : errorState ,
}
2024-02-15 08:45:10 -06:00
if ruleNode . GrafanaManagedAlert . NotificationSettings != nil {
newAlertRule . NotificationSettings , err = validateNotificationSettings ( ruleNode . GrafanaManagedAlert . NotificationSettings )
if err != nil {
return nil , err
}
}
2022-06-30 10:46:26 -05:00
newAlertRule . For , err = validateForInterval ( ruleNode )
if err != nil {
return nil , err
}
2022-02-23 10:30:04 -06:00
if ruleNode . ApiRuleNode != nil {
newAlertRule . Annotations = ruleNode . ApiRuleNode . Annotations
2024-02-15 08:45:10 -06:00
err = validateLabels ( ruleNode . Labels )
if err != nil {
return nil , err
}
2022-02-23 10:30:04 -06:00
newAlertRule . Labels = ruleNode . ApiRuleNode . Labels
2022-12-16 04:47:25 -06:00
err = newAlertRule . SetDashboardAndPanelFromAnnotations ( )
2022-08-03 09:05:32 -05:00
if err != nil {
return nil , err
2022-02-23 10:30:04 -06:00
}
}
return & newAlertRule , nil
}
2024-02-15 08:45:10 -06:00
func validateLabels ( l map [ string ] string ) error {
for key := range l {
if _ , ok := ngmodels . LabelsUserCannotSpecify [ key ] ; ok {
return fmt . Errorf ( "system reserved labels cannot be defined in the rule. Label %s is the reserved" , key )
}
}
return nil
}
2023-06-15 12:33:42 -05:00
func validateCondition ( condition string , queries [ ] apimodels . AlertQuery ) error {
if condition == "" {
return errors . New ( "condition cannot be empty" )
}
if len ( queries ) == 0 {
return errors . New ( "no query/expressions specified" )
}
refIDs := make ( map [ string ] int , len ( queries ) )
for idx , query := range queries {
if query . RefID == "" {
return fmt . Errorf ( "refID is not specified for data query/expression at index %d" , idx )
}
if usedIdx , ok := refIDs [ query . RefID ] ; ok {
return fmt . Errorf ( "refID '%s' is already used by query/expression at index %d" , query . RefID , usedIdx )
}
refIDs [ query . RefID ] = idx
}
if _ , ok := refIDs [ condition ] ; ! ok {
ids := make ( [ ] string , 0 , len ( refIDs ) )
for id := range refIDs {
ids = append ( ids , id )
}
2023-06-15 16:37:30 -05:00
sort . Strings ( ids )
2023-06-15 12:33:42 -05:00
return fmt . Errorf ( "condition %s does not exist, must be one of [%s]" , condition , strings . Join ( ids , "," ) )
}
return nil
}
2024-02-28 14:40:13 -06:00
func validateInterval ( interval , baseInterval time . Duration ) ( int64 , error ) {
2022-10-28 09:40:11 -05:00
intervalSeconds := int64 ( interval . Seconds ( ) )
2024-02-28 14:40:13 -06:00
baseIntervalSeconds := int64 ( baseInterval . Seconds ( ) )
2022-10-28 09:40:11 -05:00
if interval <= 0 {
return 0 , fmt . Errorf ( "rule evaluation interval must be positive duration that is multiple of the base interval %d seconds" , baseIntervalSeconds )
}
if intervalSeconds % baseIntervalSeconds != 0 {
return 0 , fmt . Errorf ( "rule evaluation interval %d should be multiple of the base interval of %d seconds" , int64 ( interval . Seconds ( ) ) , baseIntervalSeconds )
}
return intervalSeconds , nil
}
2022-06-30 10:46:26 -05:00
// validateForInterval validates ApiRuleNode.For and converts it to time.Duration. If the field is not specified returns 0 if GrafanaManagedAlert.UID is empty and -1 if it is not.
func validateForInterval ( ruleNode * apimodels . PostableExtendedRuleNode ) ( time . Duration , error ) {
if ruleNode . ApiRuleNode == nil || ruleNode . ApiRuleNode . For == nil {
if ruleNode . GrafanaManagedAlert . UID != "" {
return - 1 , nil // will be patched later with the real value of the current version of the rule
}
return 0 , nil // if it's a new rule, use the 0 as the default
}
duration := time . Duration ( * ruleNode . ApiRuleNode . For )
if duration < 0 {
return 0 , fmt . Errorf ( "field `for` cannot be negative [%v]. 0 or any positive duration are allowed" , * ruleNode . ApiRuleNode . For )
}
return duration , nil
}
2024-02-28 14:40:13 -06:00
// ValidateRuleGroup validates API model (definitions.PostableRuleGroupConfig) and converts it to a collection of models.AlertRule.
2022-02-23 10:30:04 -06:00
// Returns a slice that contains all rules described by API model or error if either group specification or an alert definition is not valid.
2023-02-01 06:15:03 -06:00
// It also returns a map containing current existing alerts that don't contain the is_paused field in the body of the call.
2024-02-28 14:40:13 -06:00
func ValidateRuleGroup (
2022-02-23 10:30:04 -06:00
ruleGroupConfig * apimodels . PostableRuleGroupConfig ,
orgId int64 ,
2024-02-28 14:40:13 -06:00
namespaceUID string ,
limits RuleLimits ) ( [ ] * ngmodels . AlertRuleWithOptionals , error ) {
2022-02-23 10:30:04 -06:00
if ruleGroupConfig . Name == "" {
return nil , errors . New ( "rule group name cannot be empty" )
}
if len ( ruleGroupConfig . Name ) > store . AlertRuleMaxRuleGroupNameLength {
return nil , fmt . Errorf ( "rule group name is too long. Max length is %d" , store . AlertRuleMaxRuleGroupNameLength )
}
interval := time . Duration ( ruleGroupConfig . Interval )
if interval == 0 {
// if group interval is 0 (undefined) then we automatically fall back to the default interval
2024-02-28 14:40:13 -06:00
interval = limits . DefaultRuleEvaluationInterval
2022-02-23 10:30:04 -06:00
}
2024-02-28 14:40:13 -06:00
if interval < 0 || int64 ( interval . Seconds ( ) ) % int64 ( limits . BaseInterval . Seconds ( ) ) != 0 {
return nil , fmt . Errorf ( "rule evaluation interval (%d second) should be positive number that is multiple of the base interval of %d seconds" , int64 ( interval . Seconds ( ) ) , int64 ( limits . BaseInterval . Seconds ( ) ) )
2022-02-23 10:30:04 -06:00
}
// TODO should we validate that interval is >= cfg.MinInterval? Currently, we allow to save but fix the specified interval if it is < cfg.MinInterval
2023-02-01 06:15:03 -06:00
result := make ( [ ] * ngmodels . AlertRuleWithOptionals , 0 , len ( ruleGroupConfig . Rules ) )
2022-02-23 10:30:04 -06:00
uids := make ( map [ string ] int , cap ( result ) )
for idx := range ruleGroupConfig . Rules {
2024-02-28 14:40:13 -06:00
rule , err := validateRuleNode ( & ruleGroupConfig . Rules [ idx ] , ruleGroupConfig . Name , interval , orgId , namespaceUID , limits )
2022-02-23 10:30:04 -06:00
// TODO do not stop on the first failure but return all failures
if err != nil {
return nil , fmt . Errorf ( "invalid rule specification at index [%d]: %w" , idx , err )
}
if rule . UID != "" {
if existingIdx , ok := uids [ rule . UID ] ; ok {
return nil , fmt . Errorf ( "rule [%d] has UID %s that is already assigned to another rule at index %d" , idx , rule . UID , existingIdx )
}
uids [ rule . UID ] = idx
}
2023-02-01 06:15:03 -06:00
var hasPause , isPaused bool
original := ruleGroupConfig . Rules [ idx ]
if alert := original . GrafanaManagedAlert ; alert != nil {
if alert . IsPaused != nil {
isPaused = * alert . IsPaused
hasPause = true
}
}
ruleWithOptionals := ngmodels . AlertRuleWithOptionals { }
rule . IsPaused = isPaused
2022-06-22 09:52:46 -05:00
rule . RuleGroupIndex = idx + 1
2023-02-01 06:15:03 -06:00
ruleWithOptionals . AlertRule = * rule
ruleWithOptionals . HasPause = hasPause
result = append ( result , & ruleWithOptionals )
2022-02-23 10:30:04 -06:00
}
return result , nil
}
2024-02-15 08:45:10 -06:00
func validateNotificationSettings ( n * apimodels . AlertRuleNotificationSettings ) ( [ ] ngmodels . NotificationSettings , error ) {
s := ngmodels . NotificationSettings {
Receiver : n . Receiver ,
GroupBy : n . GroupBy ,
GroupWait : n . GroupWait ,
GroupInterval : n . GroupInterval ,
RepeatInterval : n . RepeatInterval ,
MuteTimeIntervals : n . MuteTimeIntervals ,
}
if err := s . Validate ( ) ; err != nil {
return nil , fmt . Errorf ( "invalid notification settings: %w" , err )
}
return [ ] ngmodels . NotificationSettings {
s ,
} , nil
}