mirror of
https://github.com/grafana/grafana.git
synced 2025-02-16 10:24:54 -06:00
This commit adds the missing Provenance field to responses for /api/v1/provisioning/alert-rules.
536 lines
18 KiB
Go
536 lines
18 KiB
Go
package provisioning
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
|
"github.com/grafana/grafana/pkg/services/quota"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
type AlertRuleService struct {
|
|
defaultIntervalSeconds int64
|
|
baseIntervalSeconds int64
|
|
ruleStore RuleStore
|
|
provenanceStore ProvisioningStore
|
|
dashboardService dashboards.DashboardService
|
|
quotas QuotaChecker
|
|
xact TransactionManager
|
|
log log.Logger
|
|
}
|
|
|
|
func NewAlertRuleService(ruleStore RuleStore,
|
|
provenanceStore ProvisioningStore,
|
|
dashboardService dashboards.DashboardService,
|
|
quotas QuotaChecker,
|
|
xact TransactionManager,
|
|
defaultIntervalSeconds int64,
|
|
baseIntervalSeconds int64,
|
|
log log.Logger) *AlertRuleService {
|
|
return &AlertRuleService{
|
|
defaultIntervalSeconds: defaultIntervalSeconds,
|
|
baseIntervalSeconds: baseIntervalSeconds,
|
|
ruleStore: ruleStore,
|
|
provenanceStore: provenanceStore,
|
|
dashboardService: dashboardService,
|
|
quotas: quotas,
|
|
xact: xact,
|
|
log: log,
|
|
}
|
|
}
|
|
|
|
func (service *AlertRuleService) GetAlertRules(ctx context.Context, orgID int64) ([]*models.AlertRule, map[string]models.Provenance, error) {
|
|
q := models.ListAlertRulesQuery{
|
|
OrgID: orgID,
|
|
}
|
|
rules, err := service.ruleStore.ListAlertRules(ctx, &q)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
provenances := make(map[string]models.Provenance)
|
|
if len(rules) > 0 {
|
|
resourceType := rules[0].ResourceType()
|
|
provenances, err = service.provenanceStore.GetProvenances(ctx, orgID, resourceType)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
}
|
|
return rules, provenances, nil
|
|
}
|
|
|
|
func (service *AlertRuleService) GetAlertRule(ctx context.Context, orgID int64, ruleUID string) (models.AlertRule, models.Provenance, error) {
|
|
query := &models.GetAlertRuleByUIDQuery{
|
|
OrgID: orgID,
|
|
UID: ruleUID,
|
|
}
|
|
rule, err := service.ruleStore.GetAlertRuleByUID(ctx, query)
|
|
if err != nil {
|
|
return models.AlertRule{}, models.ProvenanceNone, err
|
|
}
|
|
provenance, err := service.provenanceStore.GetProvenance(ctx, rule, orgID)
|
|
if err != nil {
|
|
return models.AlertRule{}, models.ProvenanceNone, err
|
|
}
|
|
return *rule, provenance, nil
|
|
}
|
|
|
|
type AlertRuleWithFolderTitle struct {
|
|
AlertRule models.AlertRule
|
|
FolderTitle string
|
|
}
|
|
|
|
// GetAlertRuleWithFolderTitle returns a single alert rule with its folder title.
|
|
func (service *AlertRuleService) GetAlertRuleWithFolderTitle(ctx context.Context, orgID int64, ruleUID string) (AlertRuleWithFolderTitle, error) {
|
|
query := &models.GetAlertRuleByUIDQuery{
|
|
OrgID: orgID,
|
|
UID: ruleUID,
|
|
}
|
|
rule, err := service.ruleStore.GetAlertRuleByUID(ctx, query)
|
|
if err != nil {
|
|
return AlertRuleWithFolderTitle{}, err
|
|
}
|
|
|
|
dq := dashboards.GetDashboardQuery{
|
|
OrgID: orgID,
|
|
UID: rule.NamespaceUID,
|
|
}
|
|
|
|
dash, err := service.dashboardService.GetDashboard(ctx, &dq)
|
|
if err != nil {
|
|
return AlertRuleWithFolderTitle{}, err
|
|
}
|
|
|
|
return AlertRuleWithFolderTitle{
|
|
AlertRule: *rule,
|
|
FolderTitle: dash.Title,
|
|
}, nil
|
|
}
|
|
|
|
// CreateAlertRule creates a new alert rule. This function will ignore any
|
|
// interval that is set in the rule struct and use the already existing group
|
|
// interval or the default one.
|
|
func (service *AlertRuleService) CreateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance, userID int64) (models.AlertRule, error) {
|
|
if rule.UID == "" {
|
|
rule.UID = util.GenerateShortUID()
|
|
} else if err := util.ValidateUID(rule.UID); err != nil {
|
|
return models.AlertRule{}, errors.Join(models.ErrAlertRuleFailedValidation, fmt.Errorf("cannot create rule with UID '%s': %w", rule.UID, err))
|
|
}
|
|
interval, err := service.ruleStore.GetRuleGroupInterval(ctx, rule.OrgID, rule.NamespaceUID, rule.RuleGroup)
|
|
// if the alert group does not exists we just use the default interval
|
|
if err != nil && errors.Is(err, store.ErrAlertRuleGroupNotFound) {
|
|
interval = service.defaultIntervalSeconds
|
|
} else if err != nil {
|
|
return models.AlertRule{}, err
|
|
}
|
|
rule.IntervalSeconds = interval
|
|
err = rule.SetDashboardAndPanelFromAnnotations()
|
|
if err != nil {
|
|
return models.AlertRule{}, err
|
|
}
|
|
rule.Updated = time.Now()
|
|
err = service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
ids, err := service.ruleStore.InsertAlertRules(ctx, []models.AlertRule{
|
|
rule,
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var fixed bool
|
|
for _, key := range ids {
|
|
if key.UID == rule.UID {
|
|
rule.ID = key.ID
|
|
fixed = true
|
|
break
|
|
}
|
|
}
|
|
if !fixed {
|
|
return errors.New("couldn't find newly created id")
|
|
}
|
|
|
|
if err = service.checkLimitsTransactionCtx(ctx, rule.OrgID, userID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return service.provenanceStore.SetProvenance(ctx, &rule, rule.OrgID, provenance)
|
|
})
|
|
if err != nil {
|
|
return models.AlertRule{}, err
|
|
}
|
|
return rule, nil
|
|
}
|
|
|
|
func (service *AlertRuleService) GetRuleGroup(ctx context.Context, orgID int64, namespaceUID, group string) (models.AlertRuleGroup, error) {
|
|
q := models.ListAlertRulesQuery{
|
|
OrgID: orgID,
|
|
NamespaceUIDs: []string{namespaceUID},
|
|
RuleGroup: group,
|
|
}
|
|
ruleList, err := service.ruleStore.ListAlertRules(ctx, &q)
|
|
if err != nil {
|
|
return models.AlertRuleGroup{}, err
|
|
}
|
|
if len(ruleList) == 0 {
|
|
return models.AlertRuleGroup{}, store.ErrAlertRuleGroupNotFound
|
|
}
|
|
res := models.AlertRuleGroup{
|
|
Title: ruleList[0].RuleGroup,
|
|
FolderUID: ruleList[0].NamespaceUID,
|
|
Interval: ruleList[0].IntervalSeconds,
|
|
Rules: []models.AlertRule{},
|
|
}
|
|
for _, r := range ruleList {
|
|
if r != nil {
|
|
res.Rules = append(res.Rules, *r)
|
|
}
|
|
}
|
|
return res, nil
|
|
}
|
|
|
|
// UpdateRuleGroup will update the interval for all rules in the group.
|
|
func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, intervalSeconds int64) error {
|
|
if err := models.ValidateRuleGroupInterval(intervalSeconds, service.baseIntervalSeconds); err != nil {
|
|
return err
|
|
}
|
|
return service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
query := &models.ListAlertRulesQuery{
|
|
OrgID: orgID,
|
|
NamespaceUIDs: []string{namespaceUID},
|
|
RuleGroup: ruleGroup,
|
|
}
|
|
ruleList, err := service.ruleStore.ListAlertRules(ctx, query)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list alert rules: %w", err)
|
|
}
|
|
updateRules := make([]models.UpdateRule, 0, len(ruleList))
|
|
for _, rule := range ruleList {
|
|
if rule.IntervalSeconds == intervalSeconds {
|
|
continue
|
|
}
|
|
newRule := *rule
|
|
newRule.IntervalSeconds = intervalSeconds
|
|
updateRules = append(updateRules, models.UpdateRule{
|
|
Existing: rule,
|
|
New: newRule,
|
|
})
|
|
}
|
|
return service.ruleStore.UpdateAlertRules(ctx, updateRules)
|
|
})
|
|
}
|
|
|
|
func (service *AlertRuleService) ReplaceRuleGroup(ctx context.Context, orgID int64, group models.AlertRuleGroup, userID int64, provenance models.Provenance) error {
|
|
if err := models.ValidateRuleGroupInterval(group.Interval, service.baseIntervalSeconds); err != nil {
|
|
return err
|
|
}
|
|
|
|
// If the provided request did not provide the rules list at all, treat it as though it does not wish to change rules.
|
|
// This is done for backwards compatibility. Requests which specify only the interval must update only the interval.
|
|
if group.Rules == nil {
|
|
listRulesQuery := models.ListAlertRulesQuery{
|
|
OrgID: orgID,
|
|
NamespaceUIDs: []string{group.FolderUID},
|
|
RuleGroup: group.Title,
|
|
}
|
|
ruleList, err := service.ruleStore.ListAlertRules(ctx, &listRulesQuery)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to list alert rules: %w", err)
|
|
}
|
|
group.Rules = make([]models.AlertRule, 0, len(ruleList))
|
|
for _, r := range ruleList {
|
|
if r != nil {
|
|
group.Rules = append(group.Rules, *r)
|
|
}
|
|
}
|
|
}
|
|
|
|
key := models.AlertRuleGroupKey{
|
|
OrgID: orgID,
|
|
NamespaceUID: group.FolderUID,
|
|
RuleGroup: group.Title,
|
|
}
|
|
rules := make([]*models.AlertRuleWithOptionals, len(group.Rules))
|
|
group = *syncGroupRuleFields(&group, orgID)
|
|
for i := range group.Rules {
|
|
if err := group.Rules[i].SetDashboardAndPanelFromAnnotations(); err != nil {
|
|
return err
|
|
}
|
|
rules = append(rules, &models.AlertRuleWithOptionals{AlertRule: group.Rules[i], HasPause: true})
|
|
}
|
|
delta, err := store.CalculateChanges(ctx, service.ruleStore, key, rules)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to calculate diff for alert rules: %w", err)
|
|
}
|
|
|
|
// Refresh all calculated fields across all rules.
|
|
delta = store.UpdateCalculatedRuleFields(delta)
|
|
|
|
if len(delta.New) == 0 && len(delta.Update) == 0 && len(delta.Delete) == 0 {
|
|
return nil
|
|
}
|
|
|
|
return service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
// Delete first as this could prevent future unique constraint violations.
|
|
if len(delta.Delete) > 0 {
|
|
for _, del := range delta.Delete {
|
|
// check that provenance is not changed in an invalid way
|
|
storedProvenance, err := service.provenanceStore.GetProvenance(ctx, del, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if canUpdate := canUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
|
|
return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance)
|
|
}
|
|
}
|
|
if err := service.deleteRules(ctx, orgID, delta.Delete...); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if len(delta.Update) > 0 {
|
|
updates := make([]models.UpdateRule, 0, len(delta.Update))
|
|
for _, update := range delta.Update {
|
|
// check that provenance is not changed in an invalid way
|
|
storedProvenance, err := service.provenanceStore.GetProvenance(ctx, update.New, orgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if canUpdate := canUpdateProvenanceInRuleGroup(storedProvenance, provenance); !canUpdate {
|
|
return fmt.Errorf("cannot update with provided provenance '%s', needs '%s'", provenance, storedProvenance)
|
|
}
|
|
updates = append(updates, models.UpdateRule{
|
|
Existing: update.Existing,
|
|
New: *update.New,
|
|
})
|
|
}
|
|
if err = service.ruleStore.UpdateAlertRules(ctx, updates); err != nil {
|
|
return fmt.Errorf("failed to update alert rules: %w", err)
|
|
}
|
|
for _, update := range delta.Update {
|
|
if err := service.provenanceStore.SetProvenance(ctx, update.New, orgID, provenance); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if len(delta.New) > 0 {
|
|
uids, err := service.ruleStore.InsertAlertRules(ctx, withoutNilAlertRules(delta.New))
|
|
if err != nil {
|
|
return fmt.Errorf("failed to insert alert rules: %w", err)
|
|
}
|
|
for _, key := range uids {
|
|
if err := service.provenanceStore.SetProvenance(ctx, &models.AlertRule{UID: key.UID}, orgID, provenance); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
if err = service.checkLimitsTransactionCtx(ctx, orgID, userID); err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// UpdateAlertRule updates an alert rule.
|
|
func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, rule models.AlertRule, provenance models.Provenance) (models.AlertRule, error) {
|
|
storedRule, storedProvenance, err := service.GetAlertRule(ctx, rule.OrgID, rule.UID)
|
|
if err != nil {
|
|
return models.AlertRule{}, err
|
|
}
|
|
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
|
return models.AlertRule{}, fmt.Errorf("cannot change provenance from '%s' to '%s'", storedProvenance, provenance)
|
|
}
|
|
rule.Updated = time.Now()
|
|
rule.ID = storedRule.ID
|
|
rule.IntervalSeconds = storedRule.IntervalSeconds
|
|
err = rule.SetDashboardAndPanelFromAnnotations()
|
|
if err != nil {
|
|
return models.AlertRule{}, err
|
|
}
|
|
err = service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
err := service.ruleStore.UpdateAlertRules(ctx, []models.UpdateRule{
|
|
{
|
|
Existing: &storedRule,
|
|
New: rule,
|
|
},
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return service.provenanceStore.SetProvenance(ctx, &rule, rule.OrgID, provenance)
|
|
})
|
|
if err != nil {
|
|
return models.AlertRule{}, err
|
|
}
|
|
return rule, err
|
|
}
|
|
|
|
func (service *AlertRuleService) DeleteAlertRule(ctx context.Context, orgID int64, ruleUID string, provenance models.Provenance) error {
|
|
rule := &models.AlertRule{
|
|
OrgID: orgID,
|
|
UID: ruleUID,
|
|
}
|
|
// check that provenance is not changed in an invalid way
|
|
storedProvenance, err := service.provenanceStore.GetProvenance(ctx, rule, rule.OrgID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if storedProvenance != provenance && storedProvenance != models.ProvenanceNone {
|
|
return fmt.Errorf("cannot delete with provided provenance '%s', needs '%s'", provenance, storedProvenance)
|
|
}
|
|
return service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
|
return service.deleteRules(ctx, orgID, rule)
|
|
})
|
|
}
|
|
|
|
// checkLimitsTransactionCtx checks whether the current transaction (as identified by the ctx) breaches configured alert rule limits.
|
|
func (service *AlertRuleService) checkLimitsTransactionCtx(ctx context.Context, orgID, userID int64) error {
|
|
limitReached, err := service.quotas.CheckQuotaReached(ctx, models.QuotaTargetSrv, "a.ScopeParameters{
|
|
OrgID: orgID,
|
|
UserID: userID,
|
|
})
|
|
if err != nil {
|
|
return fmt.Errorf("failed to check alert rule quota: %w", err)
|
|
}
|
|
if limitReached {
|
|
return models.ErrQuotaReached
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// deleteRules deletes a set of target rules and associated data, while checking for database consistency.
|
|
func (service *AlertRuleService) deleteRules(ctx context.Context, orgID int64, targets ...*models.AlertRule) error {
|
|
uids := make([]string, 0, len(targets))
|
|
for _, tgt := range targets {
|
|
if tgt != nil {
|
|
uids = append(uids, tgt.UID)
|
|
}
|
|
}
|
|
if err := service.ruleStore.DeleteAlertRulesByUID(ctx, orgID, uids...); err != nil {
|
|
return err
|
|
}
|
|
for _, uid := range uids {
|
|
if err := service.provenanceStore.DeleteProvenance(ctx, &models.AlertRule{UID: uid}, orgID); err != nil {
|
|
// We failed to clean up the record, but this doesn't break things. Log it and move on.
|
|
service.log.Warn("Failed to delete provenance record for rule: %w", err)
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// GetAlertRuleGroupWithFolderTitle returns the alert rule group with folder title.
|
|
func (service *AlertRuleService) GetAlertRuleGroupWithFolderTitle(ctx context.Context, orgID int64, namespaceUID, group string) (models.AlertRuleGroupWithFolderTitle, error) {
|
|
q := models.ListAlertRulesQuery{
|
|
OrgID: orgID,
|
|
NamespaceUIDs: []string{namespaceUID},
|
|
RuleGroup: group,
|
|
}
|
|
ruleList, err := service.ruleStore.ListAlertRules(ctx, &q)
|
|
if err != nil {
|
|
return models.AlertRuleGroupWithFolderTitle{}, err
|
|
}
|
|
if len(ruleList) == 0 {
|
|
return models.AlertRuleGroupWithFolderTitle{}, store.ErrAlertRuleGroupNotFound
|
|
}
|
|
|
|
dq := dashboards.GetDashboardQuery{
|
|
OrgID: orgID,
|
|
UID: namespaceUID,
|
|
}
|
|
dash, err := service.dashboardService.GetDashboard(ctx, &dq)
|
|
if err != nil {
|
|
return models.AlertRuleGroupWithFolderTitle{}, err
|
|
}
|
|
|
|
res := models.NewAlertRuleGroupWithFolderTitleFromRulesGroup(ruleList[0].GetGroupKey(), ruleList, dash.Title)
|
|
return res, nil
|
|
}
|
|
|
|
// GetAlertGroupsWithFolderTitle returns all groups with folder title in the folders identified by folderUID that have at least one alert. If argument folderUIDs is nil or empty - returns groups in all folders.
|
|
func (service *AlertRuleService) GetAlertGroupsWithFolderTitle(ctx context.Context, orgID int64, folderUIDs []string) ([]models.AlertRuleGroupWithFolderTitle, error) {
|
|
q := models.ListAlertRulesQuery{
|
|
OrgID: orgID,
|
|
}
|
|
|
|
if len(folderUIDs) > 0 {
|
|
q.NamespaceUIDs = folderUIDs
|
|
}
|
|
|
|
ruleList, err := service.ruleStore.ListAlertRules(ctx, &q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
groups := make(map[models.AlertRuleGroupKey][]models.AlertRule)
|
|
namespaces := make(map[string][]*models.AlertRuleGroupKey)
|
|
for _, r := range ruleList {
|
|
groupKey := r.GetGroupKey()
|
|
group := groups[groupKey]
|
|
group = append(group, *r)
|
|
groups[groupKey] = group
|
|
|
|
namespaces[r.NamespaceUID] = append(namespaces[r.NamespaceUID], &groupKey)
|
|
}
|
|
|
|
if len(namespaces) == 0 {
|
|
return []models.AlertRuleGroupWithFolderTitle{}, nil
|
|
}
|
|
|
|
dq := dashboards.GetDashboardsQuery{
|
|
DashboardUIDs: nil,
|
|
}
|
|
for uid := range namespaces {
|
|
dq.DashboardUIDs = append(dq.DashboardUIDs, uid)
|
|
}
|
|
|
|
// We need folder titles for the provisioning file format. We do it this way instead of using GetUserVisibleNamespaces to avoid folder:read permissions that should not apply to those with alert.provisioning:read.
|
|
dashes, err := service.dashboardService.GetDashboards(ctx, &dq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
folderUidToTitle := make(map[string]string)
|
|
for _, dash := range dashes {
|
|
folderUidToTitle[dash.UID] = dash.Title
|
|
}
|
|
|
|
result := make([]models.AlertRuleGroupWithFolderTitle, 0)
|
|
for groupKey, rules := range groups {
|
|
title, ok := folderUidToTitle[groupKey.NamespaceUID]
|
|
if !ok {
|
|
return nil, fmt.Errorf("cannot find title for folder with uid '%s'", groupKey.NamespaceUID)
|
|
}
|
|
result = append(result, models.NewAlertRuleGroupWithFolderTitle(groupKey, rules, title))
|
|
}
|
|
|
|
// Return results in a stable manner.
|
|
models.SortAlertRuleGroupWithFolderTitle(result)
|
|
return result, nil
|
|
}
|
|
|
|
// syncRuleGroupFields synchronizes calculated fields across multiple rules in a group.
|
|
func syncGroupRuleFields(group *models.AlertRuleGroup, orgID int64) *models.AlertRuleGroup {
|
|
for i := range group.Rules {
|
|
group.Rules[i].IntervalSeconds = group.Interval
|
|
group.Rules[i].RuleGroup = group.Title
|
|
group.Rules[i].NamespaceUID = group.FolderUID
|
|
group.Rules[i].OrgID = orgID
|
|
}
|
|
return group
|
|
}
|
|
|
|
func withoutNilAlertRules(ptrs []*models.AlertRule) []models.AlertRule {
|
|
result := make([]models.AlertRule, 0, len(ptrs))
|
|
for _, ptr := range ptrs {
|
|
if ptr != nil {
|
|
result = append(result, *ptr)
|
|
}
|
|
}
|
|
return result
|
|
}
|