2022-02-24 00:30:04 +08:00
package api
import (
"errors"
"fmt"
2023-06-16 05:37:30 +08:00
"sort"
2023-06-16 01:33:42 +08:00
"strings"
2022-02-24 00:30:04 +08:00
"time"
2024-05-14 20:44:14 +08:00
"github.com/grafana/grafana/pkg/services/featuremgmt"
2022-02-24 00:30:04 +08:00
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-05-22 03:39:28 +08:00
prommodels "github.com/prometheus/common/model"
2022-02-24 00:30:04 +08:00
)
2024-02-29 04:40:13 +08: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
2024-05-14 20:44:14 +08:00
// Whether recording rules are allowed.
RecordingRulesAllowed bool
2024-02-29 04:40:13 +08:00
}
2024-05-14 20:44:14 +08:00
func RuleLimitsFromConfig ( cfg * setting . UnifiedAlertingSettings , toggles featuremgmt . FeatureToggles ) RuleLimits {
2024-02-29 04:40:13 +08:00
return RuleLimits {
DefaultRuleEvaluationInterval : cfg . DefaultRuleEvaluationInterval ,
BaseInterval : cfg . BaseInterval ,
2024-05-14 20:44:14 +08:00
RecordingRulesAllowed : toggles . IsEnabledGlobally ( featuremgmt . FlagGrafanaManagedRecordingRules ) ,
2024-02-29 04:40:13 +08:00
}
}
2022-02-24 00:30:04 +08: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-29 04:40:13 +08:00
namespaceUID string ,
limits RuleLimits ) ( * ngmodels . AlertRule , error ) {
intervalSeconds , err := validateInterval ( interval , limits . BaseInterval )
2022-10-28 22:40:11 +08:00
if err != nil {
return nil , err
2022-02-24 00:30:04 +08:00
}
if ruleNode . GrafanaManagedAlert == nil {
return nil , fmt . Errorf ( "not Grafana managed alert rule" )
}
2024-05-22 03:39:28 +08:00
isRecordingRule := ruleNode . GrafanaManagedAlert . Record != nil
2022-02-24 00:30:04 +08:00
// 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 )
}
2023-03-27 23:55:13 +08:00
queries := AlertQueriesFromApiAlertQueries ( ruleNode . GrafanaManagedAlert . Data )
2022-02-24 00:30:04 +08:00
newAlertRule := ngmodels . AlertRule {
OrgID : orgId ,
Title : ruleNode . GrafanaManagedAlert . Title ,
Condition : ruleNode . GrafanaManagedAlert . Condition ,
2023-03-27 23:55:13 +08:00
Data : queries ,
2022-02-24 00:30:04 +08:00
UID : ruleNode . GrafanaManagedAlert . UID ,
IntervalSeconds : intervalSeconds ,
2024-02-29 04:40:13 +08:00
NamespaceUID : namespaceUID ,
2022-02-24 00:30:04 +08:00
RuleGroup : groupName ,
}
2024-05-22 03:39:28 +08:00
if isRecordingRule {
newAlertRule , err = validateRecordingRuleFields ( ruleNode , newAlertRule , limits , canPatch )
} else {
newAlertRule , err = validateAlertingRuleFields ( ruleNode , newAlertRule , canPatch )
2024-02-15 22:45:10 +08:00
}
2022-06-30 23:46:26 +08:00
if err != nil {
return nil , err
}
2022-02-24 00:30:04 +08:00
if ruleNode . ApiRuleNode != nil {
newAlertRule . Annotations = ruleNode . ApiRuleNode . Annotations
2024-02-15 22:45:10 +08:00
err = validateLabels ( ruleNode . Labels )
if err != nil {
return nil , err
}
2022-02-24 00:30:04 +08:00
newAlertRule . Labels = ruleNode . ApiRuleNode . Labels
2022-12-16 18:47:25 +08:00
err = newAlertRule . SetDashboardAndPanelFromAnnotations ( )
2022-08-03 22:05:32 +08:00
if err != nil {
return nil , err
2022-02-24 00:30:04 +08:00
}
}
return & newAlertRule , nil
}
2024-05-22 03:39:28 +08:00
// validateAlertingRuleFields validates only the fields on a rule that are specific to Alerting rules.
// it will load fields that pass validation onto newRule and return the result.
func validateAlertingRuleFields ( in * apimodels . PostableExtendedRuleNode , newRule ngmodels . AlertRule , canPatch bool ) ( ngmodels . AlertRule , error ) {
var err error
if in . GrafanaManagedAlert . Record != nil {
return ngmodels . AlertRule { } , fmt . Errorf ( "%w: rule cannot be simultaneously an alerting and recording rule" , ngmodels . ErrAlertRuleFailedValidation )
}
noDataState := ngmodels . NoData
if in . GrafanaManagedAlert . NoDataState == "" && canPatch {
noDataState = ""
}
if in . GrafanaManagedAlert . NoDataState != "" {
noDataState , err = ngmodels . NoDataStateFromString ( string ( in . GrafanaManagedAlert . NoDataState ) )
if err != nil {
return ngmodels . AlertRule { } , err
}
}
newRule . NoDataState = noDataState
errorState := ngmodels . AlertingErrState
if in . GrafanaManagedAlert . ExecErrState == "" && canPatch {
errorState = ""
}
if in . GrafanaManagedAlert . ExecErrState != "" {
errorState , err = ngmodels . ErrStateFromString ( string ( in . GrafanaManagedAlert . ExecErrState ) )
if err != nil {
return ngmodels . AlertRule { } , err
}
}
newRule . ExecErrState = errorState
err = validateCondition ( in . GrafanaManagedAlert . Condition , in . GrafanaManagedAlert . Data , canPatch )
if err != nil {
return ngmodels . AlertRule { } , err
}
if in . GrafanaManagedAlert . NotificationSettings != nil {
newRule . NotificationSettings , err = validateNotificationSettings ( in . GrafanaManagedAlert . NotificationSettings )
if err != nil {
return ngmodels . AlertRule { } , err
}
}
newRule . For , err = validateForInterval ( in )
if err != nil {
return ngmodels . AlertRule { } , err
}
return newRule , nil
}
// validateRecordingRuleFields validates only the fields on a rule that are specific to Recording rules.
// it will load fields that pass validation onto newRule and return the result.
func validateRecordingRuleFields ( in * apimodels . PostableExtendedRuleNode , newRule ngmodels . AlertRule , limits RuleLimits , canPatch bool ) ( ngmodels . AlertRule , error ) {
if ! limits . RecordingRulesAllowed {
return ngmodels . AlertRule { } , fmt . Errorf ( "%w: recording rules cannot be created on this instance" , ngmodels . ErrAlertRuleFailedValidation )
}
err := validateCondition ( in . GrafanaManagedAlert . Record . From , in . GrafanaManagedAlert . Data , canPatch )
if err != nil {
return ngmodels . AlertRule { } , fmt . Errorf ( "%w: %s" , ngmodels . ErrAlertRuleFailedValidation , err . Error ( ) )
}
metricName := prommodels . LabelValue ( in . GrafanaManagedAlert . Record . Metric )
if ! metricName . IsValid ( ) {
return ngmodels . AlertRule { } , fmt . Errorf ( "%w: %s" , ngmodels . ErrAlertRuleFailedValidation , "metric name for recording rule must be a valid utf8 string" )
}
if ! prommodels . IsValidMetricName ( metricName ) {
return ngmodels . AlertRule { } , fmt . Errorf ( "%w: %s" , ngmodels . ErrAlertRuleFailedValidation , "metric name for recording rule must be a valid Prometheus metric name" )
}
newRule . Record = ModelRecordFromApiRecord ( in . GrafanaManagedAlert . Record )
newRule . NoDataState = ""
newRule . ExecErrState = ""
newRule . Condition = ""
newRule . For = 0
newRule . NotificationSettings = nil
return newRule , nil
}
2024-02-15 22:45:10 +08: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
}
2024-05-22 03:39:28 +08:00
func validateCondition ( condition string , queries [ ] apimodels . AlertQuery , canPatch bool ) error {
if canPatch {
// Patch requests may leave both query and condition blank. If a request supplies one, it must supply the other.
if len ( queries ) == 0 && condition == "" {
return nil
}
if len ( queries ) == 0 && condition != "" {
return fmt . Errorf ( "%w: query is not specified but condition is. You must specify both query and condition to update existing alert rule" , ngmodels . ErrAlertRuleFailedValidation )
}
if len ( queries ) > 0 && condition == "" {
return fmt . Errorf ( "%w: condition is not specified but query is. You must specify both query and condition to update existing alert rule" , ngmodels . ErrAlertRuleFailedValidation )
}
}
2023-06-16 01:33:42 +08:00
if condition == "" {
2024-05-22 03:39:28 +08:00
return fmt . Errorf ( "%w: condition cannot be empty" , ngmodels . ErrAlertRuleFailedValidation )
2023-06-16 01:33:42 +08:00
}
if len ( queries ) == 0 {
2024-05-22 03:39:28 +08:00
return fmt . Errorf ( "%w: no queries or expressions are found" , ngmodels . ErrAlertRuleFailedValidation )
2023-06-16 01:33:42 +08:00
}
2024-05-22 03:39:28 +08:00
2023-06-16 01:33:42 +08:00
refIDs := make ( map [ string ] int , len ( queries ) )
for idx , query := range queries {
if query . RefID == "" {
2024-05-22 03:39:28 +08:00
return fmt . Errorf ( "%w: refID is not specified for data query/expression at index %d" , ngmodels . ErrAlertRuleFailedValidation , idx )
2023-06-16 01:33:42 +08:00
}
if usedIdx , ok := refIDs [ query . RefID ] ; ok {
2024-05-22 03:39:28 +08:00
return fmt . Errorf ( "%w: refID '%s' is already used by query/expression at index %d" , ngmodels . ErrAlertRuleFailedValidation , query . RefID , usedIdx )
2023-06-16 01:33:42 +08:00
}
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-16 05:37:30 +08:00
sort . Strings ( ids )
2024-05-22 03:39:28 +08:00
return fmt . Errorf ( "%w: condition %s does not exist, must be one of [%s]" , ngmodels . ErrAlertRuleFailedValidation , condition , strings . Join ( ids , "," ) )
2023-06-16 01:33:42 +08:00
}
return nil
}
2024-02-29 04:40:13 +08:00
func validateInterval ( interval , baseInterval time . Duration ) ( int64 , error ) {
2022-10-28 22:40:11 +08:00
intervalSeconds := int64 ( interval . Seconds ( ) )
2024-02-29 04:40:13 +08:00
baseIntervalSeconds := int64 ( baseInterval . Seconds ( ) )
2022-10-28 22:40:11 +08: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 23:46:26 +08: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-29 04:40:13 +08:00
// ValidateRuleGroup validates API model (definitions.PostableRuleGroupConfig) and converts it to a collection of models.AlertRule.
2022-02-24 00:30:04 +08: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 20:15:03 +08: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-29 04:40:13 +08:00
func ValidateRuleGroup (
2022-02-24 00:30:04 +08:00
ruleGroupConfig * apimodels . PostableRuleGroupConfig ,
orgId int64 ,
2024-02-29 04:40:13 +08:00
namespaceUID string ,
limits RuleLimits ) ( [ ] * ngmodels . AlertRuleWithOptionals , error ) {
2022-02-24 00:30:04 +08: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-29 04:40:13 +08:00
interval = limits . DefaultRuleEvaluationInterval
2022-02-24 00:30:04 +08:00
}
2024-02-29 04:40:13 +08: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-24 00:30:04 +08: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 20:15:03 +08:00
result := make ( [ ] * ngmodels . AlertRuleWithOptionals , 0 , len ( ruleGroupConfig . Rules ) )
2022-02-24 00:30:04 +08:00
uids := make ( map [ string ] int , cap ( result ) )
for idx := range ruleGroupConfig . Rules {
2024-02-29 04:40:13 +08:00
rule , err := validateRuleNode ( & ruleGroupConfig . Rules [ idx ] , ruleGroupConfig . Name , interval , orgId , namespaceUID , limits )
2022-02-24 00:30:04 +08: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 20:15:03 +08: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 22:52:46 +08:00
rule . RuleGroupIndex = idx + 1
2023-02-01 20:15:03 +08:00
ruleWithOptionals . AlertRule = * rule
ruleWithOptionals . HasPause = hasPause
result = append ( result , & ruleWithOptionals )
2022-02-24 00:30:04 +08:00
}
return result , nil
}
2024-02-15 22:45:10 +08: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
}