2021-04-01 03:11:45 -05:00
package api
import (
2022-02-23 10:30:04 -06:00
"context"
2021-04-15 07:54:37 -05:00
"errors"
2021-09-02 11:38:42 -05:00
"fmt"
2021-04-01 03:11:45 -05:00
"net/http"
2022-06-15 15:01:14 -05:00
"strings"
2021-04-01 03:11:45 -05:00
"time"
2022-03-21 18:20:35 -05:00
"github.com/grafana/grafana/pkg/services/accesscontrol"
2022-06-30 08:31:54 -05:00
"github.com/grafana/grafana/pkg/services/dashboards"
2022-01-11 10:39:34 -06:00
"github.com/grafana/grafana/pkg/services/datasources"
2022-04-05 16:48:51 -05:00
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
2022-01-11 10:39:34 -06:00
"github.com/grafana/grafana/pkg/services/ngalert/store"
"github.com/grafana/grafana/pkg/services/quota"
2022-02-23 10:30:04 -06:00
"github.com/grafana/grafana/pkg/setting"
2022-03-04 15:16:33 -06:00
"github.com/grafana/grafana/pkg/util/cmputil"
2022-01-11 10:39:34 -06:00
"github.com/prometheus/common/model"
2021-08-25 08:11:22 -05:00
"github.com/grafana/grafana/pkg/api/apierrors"
2021-04-01 03:11:45 -05:00
"github.com/grafana/grafana/pkg/api/response"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/models"
2021-04-19 13:26:04 -05:00
apimodels "github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
2021-04-01 03:11:45 -05:00
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
2022-01-11 10:39:34 -06:00
"github.com/grafana/grafana/pkg/services/ngalert/schedule"
2021-04-01 03:11:45 -05:00
"github.com/grafana/grafana/pkg/util"
)
type RulerSrv struct {
2022-04-05 16:48:51 -05:00
xactManager provisioning . TransactionManager
2022-04-28 14:27:34 -05:00
provenanceStore provisioning . ProvisioningStore
2021-04-28 03:31:51 -05:00
store store . RuleStore
DatasourceCache datasources . CacheService
2021-05-04 11:16:28 -05:00
QuotaService * quota . QuotaService
2022-01-11 10:39:34 -06:00
scheduleService schedule . ScheduleService
2021-04-28 03:31:51 -05:00
log log . Logger
2022-02-23 10:30:04 -06:00
cfg * setting . UnifiedAlertingSettings
2022-03-21 18:20:35 -05:00
ac accesscontrol . AccessControl
2021-04-01 03:11:45 -05:00
}
2022-02-23 10:30:04 -06:00
var (
2022-06-15 15:01:14 -05:00
errQuotaReached = errors . New ( "quota has been exceeded" )
errProvisionedResource = errors . New ( "request affects resources created via provisioning API" )
2022-02-23 10:30:04 -06:00
)
2022-06-23 15:13:39 -05:00
// RouteDeleteAlertRules deletes all alert rules user is authorized to access in the given namespace
// or, if non-empty, a specific group of rules in the namespace
func ( srv RulerSrv ) RouteDeleteAlertRules ( c * models . ReqContext , namespaceTitle string , group string ) response . Response {
2021-09-14 09:08:04 -05:00
namespace , err := srv . store . GetNamespaceByTitle ( c . Req . Context ( ) , namespaceTitle , c . SignedInUser . OrgId , c . SignedInUser , true )
2021-04-01 03:11:45 -05:00
if err != nil {
2021-04-15 07:54:37 -05:00
return toNamespaceErrorResponse ( err )
2021-04-01 03:11:45 -05:00
}
2022-03-25 11:39:24 -05:00
var loggerCtx = [ ] interface { } {
"namespace" ,
namespace . Title ,
}
2022-04-25 05:42:42 -05:00
var ruleGroup string
2022-06-23 15:13:39 -05:00
if group != "" {
2022-04-25 05:42:42 -05:00
ruleGroup = group
2022-03-25 11:39:24 -05:00
loggerCtx = append ( loggerCtx , "group" , group )
2021-04-01 03:11:45 -05:00
}
2022-03-25 11:39:24 -05:00
logger := srv . log . New ( loggerCtx ... )
2021-05-03 13:01:33 -05:00
2022-03-25 11:39:24 -05:00
hasAccess := func ( evaluator accesscontrol . Evaluator ) bool {
return accesscontrol . HasAccess ( srv . ac , c ) ( accesscontrol . ReqOrgAdminOrEditor , evaluator )
2021-05-03 13:01:33 -05:00
}
2022-05-06 13:55:27 -05:00
provenances , err := srv . provenanceStore . GetProvenances ( c . Req . Context ( ) , c . SignedInUser . OrgId , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to fetch provenances of alert rules" )
}
var deletableRules [ ] string
2022-03-25 11:39:24 -05:00
err = srv . xactManager . InTransaction ( c . Req . Context ( ) , func ( ctx context . Context ) error {
2022-04-25 05:42:42 -05:00
q := ngmodels . ListAlertRulesQuery {
OrgID : c . SignedInUser . OrgId ,
NamespaceUIDs : [ ] string { namespace . Uid } ,
RuleGroup : ruleGroup ,
2022-03-25 11:39:24 -05:00
}
2022-04-25 05:42:42 -05:00
if err = srv . store . ListAlertRules ( ctx , & q ) ; err != nil {
2022-03-25 11:39:24 -05:00
return err
}
2021-04-01 03:11:45 -05:00
2022-03-25 11:39:24 -05:00
if len ( q . Result ) == 0 {
logger . Debug ( "no alert rules to delete from namespace/group" )
return nil
}
2022-05-06 13:55:27 -05:00
var canDelete [ ] * ngmodels . AlertRule
var cannotDelete [ ] string
// partition will partation the given rules in two, one partition
// being the rules that fulfill the predicate the other partation being
// the ruleIDs not fulfilling it.
partition := func ( alerts [ ] * ngmodels . AlertRule , predicate func ( rule * ngmodels . AlertRule ) bool ) ( [ ] * ngmodels . AlertRule , [ ] string ) {
positive , negative := make ( [ ] * ngmodels . AlertRule , 0 , len ( alerts ) ) , make ( [ ] string , 0 , len ( alerts ) )
for _ , rule := range alerts {
if predicate ( rule ) {
positive = append ( positive , rule )
continue
}
negative = append ( negative , rule . UID )
2022-03-25 11:39:24 -05:00
}
2022-05-06 13:55:27 -05:00
return positive , negative
2022-03-25 11:39:24 -05:00
}
2022-05-06 13:55:27 -05:00
canDelete , cannotDelete = partition ( q . Result , func ( rule * ngmodels . AlertRule ) bool {
return authorizeDatasourceAccessForRule ( rule , hasAccess )
} )
2022-03-25 11:39:24 -05:00
if len ( canDelete ) == 0 {
return fmt . Errorf ( "%w to delete rules because user is not authorized to access data sources used by the rules" , ErrAuthorization )
}
if len ( cannotDelete ) > 0 {
logger . Info ( "user cannot delete one or many alert rules because it does not have access to data sources. Those rules will be skipped" , "expected" , len ( q . Result ) , "authorized" , len ( canDelete ) , "unauthorized" , cannotDelete )
}
2022-05-06 13:55:27 -05:00
canDelete , cannotDelete = partition ( canDelete , func ( rule * ngmodels . AlertRule ) bool {
provenance , exists := provenances [ rule . UID ]
return ( exists && provenance == ngmodels . ProvenanceNone ) || ! exists
} )
if len ( canDelete ) == 0 {
return fmt . Errorf ( "all rules have been provisioned and cannot be deleted through this api" )
}
if len ( cannotDelete ) > 0 {
logger . Info ( "user cannot delete one or many alert rules because it does have a provenance set. Those rules will be skipped" , "expected" , len ( q . Result ) , "provenance_none" , len ( canDelete ) , "provenance_set" , cannotDelete )
}
for _ , rule := range canDelete {
deletableRules = append ( deletableRules , rule . UID )
}
return srv . store . DeleteAlertRulesByUID ( ctx , c . SignedInUser . OrgId , deletableRules ... )
2022-03-25 11:39:24 -05:00
} )
2021-05-03 13:01:33 -05:00
if err != nil {
2022-03-25 11:39:24 -05:00
if errors . Is ( err , ErrAuthorization ) {
return ErrResp ( http . StatusUnauthorized , err , "" )
2021-04-16 07:00:07 -05:00
}
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusInternalServerError , err , "failed to delete rule group" )
2021-04-01 03:11:45 -05:00
}
2021-04-16 07:00:07 -05:00
2022-03-25 11:39:24 -05:00
logger . Debug ( "rules have been deleted from the store. updating scheduler" )
2022-05-06 13:55:27 -05:00
for _ , uid := range deletableRules {
2022-01-11 10:39:34 -06:00
srv . scheduleService . DeleteAlertRule ( ngmodels . AlertRuleKey {
OrgID : c . SignedInUser . OrgId ,
UID : uid ,
} )
2021-05-03 13:01:33 -05:00
}
2022-03-25 11:39:24 -05:00
return response . JSON ( http . StatusAccepted , util . DynMap { "message" : "rules deleted" } )
2021-04-01 03:11:45 -05:00
}
2022-06-17 12:55:31 -05:00
// RouteGetNamespaceRulesConfig returns all rules in a specific folder that user has access to
2022-06-23 15:13:39 -05:00
func ( srv RulerSrv ) RouteGetNamespaceRulesConfig ( c * models . ReqContext , namespaceTitle string ) response . Response {
2021-09-14 09:08:04 -05:00
namespace , err := srv . store . GetNamespaceByTitle ( c . Req . Context ( ) , namespaceTitle , c . SignedInUser . OrgId , c . SignedInUser , false )
2021-04-01 03:11:45 -05:00
if err != nil {
2021-04-15 07:54:37 -05:00
return toNamespaceErrorResponse ( err )
2021-04-01 03:11:45 -05:00
}
2022-04-25 05:42:42 -05:00
q := ngmodels . ListAlertRulesQuery {
OrgID : c . SignedInUser . OrgId ,
NamespaceUIDs : [ ] string { namespace . Uid } ,
2021-04-01 03:11:45 -05:00
}
2022-04-25 05:42:42 -05:00
if err := srv . store . ListAlertRules ( c . Req . Context ( ) , & q ) ; err != nil {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusInternalServerError , err , "failed to update rule group" )
2021-04-01 03:11:45 -05:00
}
result := apimodels . NamespaceConfigResponse { }
2022-04-11 16:37:44 -05:00
hasAccess := func ( evaluator accesscontrol . Evaluator ) bool {
2022-05-19 08:22:26 -05:00
return accesscontrol . HasAccess ( srv . ac , c ) ( accesscontrol . ReqViewer , evaluator )
2022-04-11 16:37:44 -05:00
}
2022-04-28 14:27:34 -05:00
provenanceRecords , err := srv . provenanceStore . GetProvenances ( c . Req . Context ( ) , c . SignedInUser . OrgId , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get provenance for rule group" )
}
2022-06-22 09:52:46 -05:00
ruleGroups := make ( map [ string ] ngmodels . RulesGroup )
2021-04-01 03:11:45 -05:00
for _ , r := range q . Result {
2022-06-17 12:55:31 -05:00
ruleGroups [ r . RuleGroup ] = append ( ruleGroups [ r . RuleGroup ] , r )
2021-04-01 03:11:45 -05:00
}
2022-06-17 12:55:31 -05:00
for groupName , rules := range ruleGroups {
if ! authorizeAccessToRuleGroup ( rules , hasAccess ) {
continue
}
result [ namespaceTitle ] = append ( result [ namespaceTitle ] , toGettableRuleGroupConfig ( groupName , rules , namespace . Id , provenanceRecords ) )
2021-04-01 03:11:45 -05:00
}
return response . JSON ( http . StatusAccepted , result )
}
2022-06-17 12:55:31 -05:00
// RouteGetRulesGroupConfig returns rules that belong to a specific group in a specific namespace (folder).
// If user does not have access to at least one of the rule in the group, returns status 401 Unauthorized
2022-06-23 15:13:39 -05:00
func ( srv RulerSrv ) RouteGetRulesGroupConfig ( c * models . ReqContext , namespaceTitle string , ruleGroup string ) response . Response {
2021-09-14 09:08:04 -05:00
namespace , err := srv . store . GetNamespaceByTitle ( c . Req . Context ( ) , namespaceTitle , c . SignedInUser . OrgId , c . SignedInUser , false )
2021-04-01 03:11:45 -05:00
if err != nil {
2021-04-15 07:54:37 -05:00
return toNamespaceErrorResponse ( err )
2021-04-01 03:11:45 -05:00
}
2022-04-25 05:42:42 -05:00
q := ngmodels . ListAlertRulesQuery {
OrgID : c . SignedInUser . OrgId ,
NamespaceUIDs : [ ] string { namespace . Uid } ,
RuleGroup : ruleGroup ,
2021-04-01 03:11:45 -05:00
}
2022-04-25 05:42:42 -05:00
if err := srv . store . ListAlertRules ( c . Req . Context ( ) , & q ) ; err != nil {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusInternalServerError , err , "failed to get group alert rules" )
2021-04-01 03:11:45 -05:00
}
2022-04-11 16:37:44 -05:00
hasAccess := func ( evaluator accesscontrol . Evaluator ) bool {
2022-05-19 08:22:26 -05:00
return accesscontrol . HasAccess ( srv . ac , c ) ( accesscontrol . ReqViewer , evaluator )
2022-04-11 16:37:44 -05:00
}
2022-04-28 14:27:34 -05:00
provenanceRecords , err := srv . provenanceStore . GetProvenances ( c . Req . Context ( ) , c . SignedInUser . OrgId , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get group alert rules" )
}
2022-06-17 12:55:31 -05:00
if ! authorizeAccessToRuleGroup ( q . Result , hasAccess ) {
return ErrResp ( http . StatusUnauthorized , fmt . Errorf ( "%w to access the group because it does not have access to one or many data sources one or many rules in the group use" , ErrAuthorization ) , "" )
2021-04-01 03:11:45 -05:00
}
result := apimodels . RuleGroupConfigResponse {
2022-06-17 12:55:31 -05:00
GettableRuleGroupConfig : toGettableRuleGroupConfig ( ruleGroup , q . Result , namespace . Id , provenanceRecords ) ,
2021-04-01 03:11:45 -05:00
}
return response . JSON ( http . StatusAccepted , result )
}
2022-06-17 12:55:31 -05:00
// RouteGetRulesConfig returns all alert rules that are available to the current user
2021-04-01 03:11:45 -05:00
func ( srv RulerSrv ) RouteGetRulesConfig ( c * models . ReqContext ) response . Response {
2022-04-01 18:33:26 -05:00
namespaceMap , err := srv . store . GetUserVisibleNamespaces ( c . Req . Context ( ) , c . OrgId , c . SignedInUser )
2021-07-22 01:53:14 -05:00
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get namespaces visible to the user" )
}
2021-11-08 07:26:08 -06:00
result := apimodels . NamespaceConfigResponse { }
if len ( namespaceMap ) == 0 {
2022-06-07 12:54:23 -05:00
srv . log . Debug ( "user has no access to any namespaces" )
2021-11-08 07:26:08 -06:00
return response . JSON ( http . StatusOK , result )
}
2021-07-22 01:53:14 -05:00
namespaceUIDs := make ( [ ] string , len ( namespaceMap ) )
for k := range namespaceMap {
namespaceUIDs = append ( namespaceUIDs , k )
}
2021-10-04 10:33:55 -05:00
dashboardUID := c . Query ( "dashboard_uid" )
panelID , err := getPanelIDFromRequest ( c . Req )
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "invalid panel_id" )
}
if dashboardUID == "" && panelID != 0 {
return ErrResp ( http . StatusBadRequest , errors . New ( "panel_id must be set with dashboard_uid" ) , "" )
}
2021-04-01 03:11:45 -05:00
q := ngmodels . ListAlertRulesQuery {
2021-07-22 01:53:14 -05:00
OrgID : c . SignedInUser . OrgId ,
NamespaceUIDs : namespaceUIDs ,
2021-10-04 10:33:55 -05:00
DashboardUID : dashboardUID ,
PanelID : panelID ,
2021-04-01 03:11:45 -05:00
}
2021-07-22 01:53:14 -05:00
2022-04-25 05:42:42 -05:00
if err := srv . store . ListAlertRules ( c . Req . Context ( ) , & q ) ; err != nil {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusInternalServerError , err , "failed to get alert rules" )
2021-04-01 03:11:45 -05:00
}
2022-04-11 16:37:44 -05:00
hasAccess := func ( evaluator accesscontrol . Evaluator ) bool {
2022-05-19 08:22:26 -05:00
return accesscontrol . HasAccess ( srv . ac , c ) ( accesscontrol . ReqViewer , evaluator )
2022-04-11 16:37:44 -05:00
}
2022-04-28 14:27:34 -05:00
provenanceRecords , err := srv . provenanceStore . GetProvenances ( c . Req . Context ( ) , c . SignedInUser . OrgId , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
if err != nil {
return ErrResp ( http . StatusInternalServerError , err , "failed to get alert rules" )
}
2022-06-22 09:52:46 -05:00
configs := make ( map [ ngmodels . AlertRuleGroupKey ] ngmodels . RulesGroup )
2021-04-01 03:11:45 -05:00
for _ , r := range q . Result {
2022-05-16 14:45:45 -05:00
groupKey := r . GetGroupKey ( )
group := configs [ groupKey ]
2022-05-12 09:42:31 -05:00
group = append ( group , r )
2022-05-16 14:45:45 -05:00
configs [ groupKey ] = group
2021-04-01 03:11:45 -05:00
}
2022-05-16 14:45:45 -05:00
for groupKey , rules := range configs {
folder , ok := namespaceMap [ groupKey . NamespaceUID ]
2022-05-12 09:42:31 -05:00
if ! ok {
2022-05-16 14:45:45 -05:00
srv . log . Error ( "namespace not visible to the user" , "user" , c . SignedInUser . UserId , "namespace" , groupKey . NamespaceUID )
2022-05-12 09:42:31 -05:00
continue
}
2022-06-01 09:23:54 -05:00
if ! authorizeAccessToRuleGroup ( rules , hasAccess ) {
continue
}
2022-05-12 09:42:31 -05:00
namespace := folder . Title
2022-05-16 14:45:45 -05:00
result [ namespace ] = append ( result [ namespace ] , toGettableRuleGroupConfig ( groupKey . RuleGroup , rules , folder . Id , provenanceRecords ) )
2021-04-01 03:11:45 -05:00
}
2021-10-04 10:33:55 -05:00
return response . JSON ( http . StatusOK , result )
2021-04-01 03:11:45 -05:00
}
2022-06-23 15:13:39 -05:00
func ( srv RulerSrv ) RoutePostNameRulesConfig ( c * models . ReqContext , ruleGroupConfig apimodels . PostableRuleGroupConfig , namespaceTitle string ) response . Response {
2021-09-14 09:08:04 -05:00
namespace , err := srv . store . GetNamespaceByTitle ( c . Req . Context ( ) , namespaceTitle , c . SignedInUser . OrgId , c . SignedInUser , true )
2021-04-01 03:11:45 -05:00
if err != nil {
2021-04-15 07:54:37 -05:00
return toNamespaceErrorResponse ( err )
2021-04-01 03:11:45 -05:00
}
2022-02-23 10:30:04 -06:00
rules , err := validateRuleGroup ( & ruleGroupConfig , c . SignedInUser . OrgId , namespace , conditionValidator ( c , srv . DatasourceCache ) , srv . cfg )
if err != nil {
return ErrResp ( http . StatusBadRequest , err , "" )
2021-04-16 07:00:07 -05:00
}
2022-05-16 14:45:45 -05:00
groupKey := ngmodels . AlertRuleGroupKey {
OrgID : c . SignedInUser . OrgId ,
NamespaceUID : namespace . Uid ,
RuleGroup : ruleGroupConfig . Name ,
}
return srv . updateAlertRulesInGroup ( c , groupKey , rules )
2022-02-23 10:30:04 -06:00
}
2022-03-24 15:53:00 -05:00
// updateAlertRulesInGroup calculates changes (rules to add,update,delete), verifies that the user is authorized to do the calculated changes and updates database.
// All operations are performed in a single transaction
2022-05-16 14:45:45 -05:00
func ( srv RulerSrv ) updateAlertRulesInGroup ( c * models . ReqContext , groupKey ngmodels . AlertRuleGroupKey , rules [ ] * ngmodels . AlertRule ) response . Response {
2022-05-06 13:55:27 -05:00
var finalChanges * changes
2022-03-21 18:20:35 -05:00
hasAccess := accesscontrol . HasAccess ( srv . ac , c )
2022-03-15 11:48:42 -05:00
err := srv . xactManager . InTransaction ( c . Req . Context ( ) , func ( tranCtx context . Context ) error {
2022-05-16 14:45:45 -05:00
logger := srv . log . New ( "namespace_uid" , groupKey . NamespaceUID , "group" , groupKey . RuleGroup , "org_id" , groupKey . OrgID , "user_id" , c . UserId )
groupChanges , err := calculateChanges ( tranCtx , srv . store , groupKey , rules )
2022-02-23 10:30:04 -06:00
if err != nil {
return err
2021-04-28 03:31:51 -05:00
}
2022-02-23 10:30:04 -06:00
2022-03-04 15:16:33 -06:00
if groupChanges . isEmpty ( ) {
2022-05-06 13:55:27 -05:00
finalChanges = groupChanges
2022-03-24 15:53:00 -05:00
logger . Info ( "no changes detected in the request. Do nothing" )
2022-03-04 15:16:33 -06:00
return nil
2021-09-02 11:38:42 -05:00
}
2022-02-23 10:30:04 -06:00
2022-06-01 09:23:54 -05:00
// if RBAC is disabled the permission are limited to folder access that is done upstream
2022-05-23 08:58:20 -05:00
if ! srv . ac . IsDisabled ( ) {
2022-06-01 09:23:54 -05:00
err = authorizeRuleChanges ( groupChanges , func ( evaluator accesscontrol . Evaluator ) bool {
2022-05-23 08:58:20 -05:00
return hasAccess ( accesscontrol . ReqOrgAdminOrEditor , evaluator )
} )
if err != nil {
return err
}
2022-03-24 15:53:00 -05:00
}
2022-06-15 15:01:14 -05:00
if err := verifyProvisionedRulesNotAffected ( c . Req . Context ( ) , srv . provenanceStore , c . OrgId , groupChanges ) ; err != nil {
2022-05-06 13:55:27 -05:00
return err
}
2022-06-22 09:52:46 -05:00
finalChanges = calculateAutomaticChanges ( groupChanges )
2022-05-06 13:55:27 -05:00
logger . Debug ( "updating database with the authorized changes" , "add" , len ( finalChanges . New ) , "update" , len ( finalChanges . New ) , "delete" , len ( finalChanges . Delete ) )
2022-03-21 18:20:35 -05:00
2022-05-06 13:55:27 -05:00
if len ( finalChanges . Update ) > 0 || len ( finalChanges . New ) > 0 {
updates := make ( [ ] store . UpdateRule , 0 , len ( finalChanges . Update ) )
inserts := make ( [ ] ngmodels . AlertRule , 0 , len ( finalChanges . New ) )
for _ , update := range finalChanges . Update {
2022-03-24 15:53:00 -05:00
logger . Debug ( "updating rule" , "rule_uid" , update . New . UID , "diff" , update . Diff . String ( ) )
2022-04-14 07:21:36 -05:00
updates = append ( updates , store . UpdateRule {
2022-03-04 15:16:33 -06:00
Existing : update . Existing ,
New : * update . New ,
} )
}
2022-05-06 13:55:27 -05:00
for _ , rule := range finalChanges . New {
2022-04-14 07:21:36 -05:00
inserts = append ( inserts , * rule )
}
2022-06-02 07:48:53 -05:00
_ , err = srv . store . InsertAlertRules ( tranCtx , inserts )
2022-04-14 07:21:36 -05:00
if err != nil {
return fmt . Errorf ( "failed to add rules: %w" , err )
2022-03-04 15:16:33 -06:00
}
2022-04-14 07:21:36 -05:00
err = srv . store . UpdateAlertRules ( tranCtx , updates )
2022-03-04 15:16:33 -06:00
if err != nil {
2022-04-14 07:21:36 -05:00
return fmt . Errorf ( "failed to update rules: %w" , err )
2022-03-04 15:16:33 -06:00
}
}
2022-05-06 13:55:27 -05:00
if len ( finalChanges . Delete ) > 0 {
UIDs := make ( [ ] string , 0 , len ( finalChanges . Delete ) )
for _ , rule := range finalChanges . Delete {
2022-03-23 15:09:53 -05:00
UIDs = append ( UIDs , rule . UID )
}
if err = srv . store . DeleteAlertRulesByUID ( tranCtx , c . SignedInUser . OrgId , UIDs ... ) ; err != nil {
return fmt . Errorf ( "failed to delete rules: %w" , err )
2021-09-02 11:38:42 -05:00
}
}
2022-05-06 13:55:27 -05:00
if len ( finalChanges . New ) > 0 {
2022-02-23 10:30:04 -06:00
limitReached , err := srv . QuotaService . CheckQuotaReached ( tranCtx , "alert_rule" , & quota . ScopeParameters {
OrgId : c . OrgId ,
UserId : c . UserId ,
} ) // alert rule is table name
if err != nil {
return fmt . Errorf ( "failed to get alert rules quota: %w" , err )
}
if limitReached {
return errQuotaReached
}
2021-04-28 03:31:51 -05:00
}
2022-02-23 10:30:04 -06:00
return nil
} )
2021-04-28 03:31:51 -05:00
2022-02-23 10:30:04 -06:00
if err != nil {
2021-04-15 07:54:37 -05:00
if errors . Is ( err , ngmodels . ErrAlertRuleNotFound ) {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusNotFound , err , "failed to update rule group" )
2022-06-15 15:01:14 -05:00
} else if errors . Is ( err , ngmodels . ErrAlertRuleFailedValidation ) || errors . Is ( err , errProvisionedResource ) {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusBadRequest , err , "failed to update rule group" )
2022-02-23 10:30:04 -06:00
} else if errors . Is ( err , errQuotaReached ) {
return ErrResp ( http . StatusForbidden , err , "" )
2022-03-21 18:20:35 -05:00
} else if errors . Is ( err , ErrAuthorization ) {
return ErrResp ( http . StatusUnauthorized , err , "" )
2022-06-13 11:15:28 -05:00
} else if errors . Is ( err , store . ErrOptimisticLock ) {
return ErrResp ( http . StatusConflict , err , "" )
2021-04-15 07:54:37 -05:00
}
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusInternalServerError , err , "failed to update rule group" )
2021-04-01 03:11:45 -05:00
}
2022-05-06 13:55:27 -05:00
for _ , rule := range finalChanges . Update {
2022-03-04 15:16:33 -06:00
srv . scheduleService . UpdateAlertRule ( ngmodels . AlertRuleKey {
OrgID : c . SignedInUser . OrgId ,
UID : rule . Existing . UID ,
} )
}
2022-02-23 10:30:04 -06:00
2022-05-06 13:55:27 -05:00
for _ , rule := range finalChanges . Delete {
2022-02-23 10:30:04 -06:00
srv . scheduleService . DeleteAlertRule ( ngmodels . AlertRuleKey {
2022-01-11 10:39:34 -06:00
OrgID : c . SignedInUser . OrgId ,
2022-02-23 10:30:04 -06:00
UID : rule . UID ,
2022-01-11 10:39:34 -06:00
} )
2021-05-06 11:39:34 -05:00
}
2022-05-06 13:55:27 -05:00
if finalChanges . isEmpty ( ) {
2022-03-04 15:16:33 -06:00
return response . JSON ( http . StatusAccepted , util . DynMap { "message" : "no changes detected in the rule group" } )
}
2021-04-01 03:11:45 -05:00
return response . JSON ( http . StatusAccepted , util . DynMap { "message" : "rule group updated successfully" } )
}
2022-06-22 09:52:46 -05:00
func toGettableRuleGroupConfig ( groupName string , rules ngmodels . RulesGroup , namespaceID int64 , provenanceRecords map [ string ] ngmodels . Provenance ) apimodels . GettableRuleGroupConfig {
rules . SortByGroupIndex ( )
2022-05-12 09:42:31 -05:00
ruleNodes := make ( [ ] apimodels . GettableExtendedRuleNode , 0 , len ( rules ) )
var interval time . Duration
if len ( rules ) > 0 {
interval = time . Duration ( rules [ 0 ] . IntervalSeconds ) * time . Second
}
for _ , r := range rules {
ruleNodes = append ( ruleNodes , toGettableExtendedRuleNode ( * r , namespaceID , provenanceRecords ) )
}
return apimodels . GettableRuleGroupConfig {
Name : groupName ,
Interval : model . Duration ( interval ) ,
Rules : ruleNodes ,
}
}
2022-04-28 14:27:34 -05:00
func toGettableExtendedRuleNode ( r ngmodels . AlertRule , namespaceID int64 , provenanceRecords map [ string ] ngmodels . Provenance ) apimodels . GettableExtendedRuleNode {
provenance := ngmodels . ProvenanceNone
if prov , exists := provenanceRecords [ r . ResourceID ( ) ] ; exists {
provenance = prov
}
2021-04-15 07:54:37 -05:00
gettableExtendedRuleNode := apimodels . GettableExtendedRuleNode {
2021-04-01 03:11:45 -05:00
GrafanaManagedAlert : & apimodels . GettableGrafanaRule {
ID : r . ID ,
OrgID : r . OrgID ,
Title : r . Title ,
Condition : r . Condition ,
Data : r . Data ,
Updated : r . Updated ,
IntervalSeconds : r . IntervalSeconds ,
Version : r . Version ,
UID : r . UID ,
NamespaceUID : r . NamespaceUID ,
2021-04-15 07:54:37 -05:00
NamespaceID : namespaceID ,
2021-04-01 03:11:45 -05:00
RuleGroup : r . RuleGroup ,
NoDataState : apimodels . NoDataState ( r . NoDataState ) ,
ExecErrState : apimodels . ExecutionErrorState ( r . ExecErrState ) ,
2022-04-28 14:27:34 -05:00
Provenance : provenance ,
2021-04-01 03:11:45 -05:00
} ,
}
2022-06-30 10:46:26 -05:00
forDuration := model . Duration ( r . For )
2021-04-15 07:54:37 -05:00
gettableExtendedRuleNode . ApiRuleNode = & apimodels . ApiRuleNode {
2022-06-30 10:46:26 -05:00
For : & forDuration ,
2021-04-15 07:54:37 -05:00
Annotations : r . Annotations ,
Labels : r . Labels ,
}
return gettableExtendedRuleNode
2021-04-01 03:11:45 -05:00
}
2021-04-07 07:28:06 -05:00
2021-04-15 07:54:37 -05:00
func toNamespaceErrorResponse ( err error ) response . Response {
if errors . Is ( err , ngmodels . ErrCannotEditNamespace ) {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusForbidden , err , err . Error ( ) )
2021-04-15 07:54:37 -05:00
}
2022-06-30 08:31:54 -05:00
if errors . Is ( err , dashboards . ErrDashboardIdentifierNotSet ) {
2021-05-28 10:55:03 -05:00
return ErrResp ( http . StatusBadRequest , err , err . Error ( ) )
2021-04-15 07:54:37 -05:00
}
2021-08-25 08:11:22 -05:00
return apierrors . ToFolderErrorResponse ( err )
2021-04-07 07:28:06 -05:00
}
2022-02-23 10:30:04 -06:00
2022-03-04 15:16:33 -06:00
type ruleUpdate struct {
Existing * ngmodels . AlertRule
New * ngmodels . AlertRule
Diff cmputil . DiffReport
}
type changes struct {
2022-06-15 15:01:14 -05:00
GroupKey ngmodels . AlertRuleGroupKey
// AffectedGroups contains all rules of all groups that are affected by these changes.
// For example, during moving a rule from one group to another this map will contain all rules from two groups
2022-06-22 09:52:46 -05:00
AffectedGroups map [ ngmodels . AlertRuleGroupKey ] ngmodels . RulesGroup
2022-06-01 09:23:54 -05:00
New [ ] * ngmodels . AlertRule
Update [ ] ruleUpdate
Delete [ ] * ngmodels . AlertRule
2022-03-04 15:16:33 -06:00
}
func ( c * changes ) isEmpty ( ) bool {
return len ( c . Update ) + len ( c . New ) + len ( c . Delete ) == 0
2022-02-23 10:30:04 -06:00
}
2022-06-15 15:01:14 -05:00
// verifyProvisionedRulesNotAffected check that neither of provisioned alerts are affected by changes.
// Returns errProvisionedResource if there is at least one rule in groups affected by changes that was provisioned.
func verifyProvisionedRulesNotAffected ( ctx context . Context , provenanceStore provisioning . ProvisioningStore , orgID int64 , ch * changes ) error {
provenances , err := provenanceStore . GetProvenances ( ctx , orgID , ( & ngmodels . AlertRule { } ) . ResourceType ( ) )
if err != nil {
return err
}
errorMsg := strings . Builder { }
for group , alertRules := range ch . AffectedGroups {
for _ , rule := range alertRules {
if provenance , exists := provenances [ rule . UID ] ; ( exists && provenance == ngmodels . ProvenanceNone ) || ! exists {
continue
}
if errorMsg . Len ( ) > 0 {
errorMsg . WriteRune ( ',' )
}
errorMsg . WriteString ( group . String ( ) )
break
}
}
if errorMsg . Len ( ) == 0 {
return nil
}
return fmt . Errorf ( "%w: alert rule group [%s]" , errProvisionedResource , errorMsg . String ( ) )
}
2022-02-23 10:30:04 -06:00
// calculateChanges calculates the difference between rules in the group in the database and the submitted rules. If a submitted rule has UID it tries to find it in the database (in other groups).
// returns a list of rules that need to be added, updated and deleted. Deleted considered rules in the database that belong to the group but do not exist in the list of submitted rules.
2022-05-16 14:45:45 -05:00
func calculateChanges ( ctx context . Context , ruleStore store . RuleStore , groupKey ngmodels . AlertRuleGroupKey , submittedRules [ ] * ngmodels . AlertRule ) ( * changes , error ) {
2022-06-22 09:52:46 -05:00
affectedGroups := make ( map [ ngmodels . AlertRuleGroupKey ] ngmodels . RulesGroup )
2022-04-25 05:42:42 -05:00
q := & ngmodels . ListAlertRulesQuery {
2022-05-16 14:45:45 -05:00
OrgID : groupKey . OrgID ,
NamespaceUIDs : [ ] string { groupKey . NamespaceUID } ,
RuleGroup : groupKey . RuleGroup ,
2022-02-23 10:30:04 -06:00
}
2022-04-25 05:42:42 -05:00
if err := ruleStore . ListAlertRules ( ctx , q ) ; err != nil {
2022-05-16 14:45:45 -05:00
return nil , fmt . Errorf ( "failed to query database for rules in the group %s: %w" , groupKey , err )
2022-02-23 10:30:04 -06:00
}
existingGroupRules := q . Result
2022-06-01 09:23:54 -05:00
if len ( existingGroupRules ) > 0 {
affectedGroups [ groupKey ] = existingGroupRules
}
2022-02-23 10:30:04 -06:00
existingGroupRulesUIDs := make ( map [ string ] * ngmodels . AlertRule , len ( existingGroupRules ) )
for _ , r := range existingGroupRules {
existingGroupRulesUIDs [ r . UID ] = r
}
2022-03-04 15:16:33 -06:00
var toAdd , toDelete [ ] * ngmodels . AlertRule
var toUpdate [ ] ruleUpdate
2022-06-01 09:23:54 -05:00
loadedRulesByUID := map [ string ] * ngmodels . AlertRule { } // auxiliary cache to avoid unnecessary queries if there are multiple moves from the same group
2022-02-23 10:30:04 -06:00
for _ , r := range submittedRules {
var existing * ngmodels . AlertRule = nil
if r . UID != "" {
if existingGroupRule , ok := existingGroupRulesUIDs [ r . UID ] ; ok {
existing = existingGroupRule
// remove the rule from existingGroupRulesUIDs
delete ( existingGroupRulesUIDs , r . UID )
2022-06-01 09:23:54 -05:00
} else if existing , ok = loadedRulesByUID [ r . UID ] ; ! ok { // check the "cache" and if there is no hit, query the database
2022-02-23 10:30:04 -06:00
// Rule can be from other group or namespace
2022-06-01 09:23:54 -05:00
q := & ngmodels . GetAlertRulesGroupByRuleUIDQuery { OrgID : groupKey . OrgID , UID : r . UID }
if err := ruleStore . GetAlertRulesGroupByRuleUID ( ctx , q ) ; err != nil {
return nil , fmt . Errorf ( "failed to query database for a group of alert rules: %w" , err )
}
for _ , rule := range q . Result {
if rule . UID == r . UID {
existing = rule
2022-02-23 10:30:04 -06:00
}
2022-06-01 09:23:54 -05:00
loadedRulesByUID [ rule . UID ] = rule
}
if existing == nil {
return nil , fmt . Errorf ( "failed to update rule with UID %s because %w" , r . UID , ngmodels . ErrAlertRuleNotFound )
2022-02-23 10:30:04 -06:00
}
2022-06-01 09:23:54 -05:00
affectedGroups [ existing . GetGroupKey ( ) ] = q . Result
2022-02-23 10:30:04 -06:00
}
}
if existing == nil {
2022-03-04 15:16:33 -06:00
toAdd = append ( toAdd , r )
2022-02-23 10:30:04 -06:00
continue
}
ngmodels . PatchPartialAlertRule ( existing , r )
2022-03-04 15:16:33 -06:00
diff := existing . Diff ( r , alertRuleFieldsToIgnoreInDiff ... )
if len ( diff ) == 0 {
continue
}
toUpdate = append ( toUpdate , ruleUpdate {
2022-02-23 10:30:04 -06:00
Existing : existing ,
2022-03-04 15:16:33 -06:00
New : r ,
Diff : diff ,
2022-02-23 10:30:04 -06:00
} )
continue
}
for _ , rule := range existingGroupRulesUIDs {
toDelete = append ( toDelete , rule )
}
2022-03-04 15:16:33 -06:00
return & changes {
2022-06-01 09:23:54 -05:00
GroupKey : groupKey ,
AffectedGroups : affectedGroups ,
New : toAdd ,
Delete : toDelete ,
Update : toUpdate ,
2022-02-23 10:30:04 -06:00
} , nil
}
2022-03-04 15:16:33 -06:00
2022-06-22 09:52:46 -05:00
// calculateAutomaticChanges scans all affected groups and creates either a noop update that will increment the version of each rule as well as re-index other groups.
// this is needed to make sure that there are no any concurrent changes made to all affected groups.
// Returns a copy of changes enriched with either noop or group index changes for all rules in
func calculateAutomaticChanges ( ch * changes ) * changes {
updatingRules := make ( map [ ngmodels . AlertRuleKey ] struct { } , len ( ch . Delete ) + len ( ch . Update ) )
for _ , update := range ch . Update {
updatingRules [ update . Existing . GetKey ( ) ] = struct { } { }
}
for _ , del := range ch . Delete {
updatingRules [ del . GetKey ( ) ] = struct { } { }
}
var toUpdate [ ] ruleUpdate
for groupKey , rules := range ch . AffectedGroups {
if groupKey != ch . GroupKey {
rules . SortByGroupIndex ( )
}
idx := 1
for _ , rule := range rules {
if _ , ok := updatingRules [ rule . GetKey ( ) ] ; ok { // exclude rules that are going to be either updated or deleted
continue
}
upd := ruleUpdate {
Existing : rule ,
New : rule ,
}
if groupKey != ch . GroupKey {
if rule . RuleGroupIndex != idx {
upd . New = ngmodels . CopyRule ( rule )
upd . New . RuleGroupIndex = idx
upd . Diff = rule . Diff ( upd . New , alertRuleFieldsToIgnoreInDiff ... )
}
idx ++
}
toUpdate = append ( toUpdate , upd )
}
}
return & changes {
GroupKey : ch . GroupKey ,
AffectedGroups : ch . AffectedGroups ,
New : ch . New ,
Update : append ( ch . Update , toUpdate ... ) ,
Delete : ch . Delete ,
}
}
2022-03-04 15:16:33 -06:00
// alertRuleFieldsToIgnoreInDiff contains fields that the AlertRule.Diff should ignore
var alertRuleFieldsToIgnoreInDiff = [ ] string { "ID" , "Version" , "Updated" }