mirror of
https://github.com/grafana/grafana.git
synced 2024-11-25 18:30:41 -06:00
b9abb8cabb
* allow users with regular actions access provisioning API paths * update methods that read rules skip new authorization logic if user CanReadAllRules to avoid performance impact on file-provisioning update all methods to accept identity.Requester that contains all permissions and is required by access control. * create deltas for single rul e * update modify methods skip new authorization logic if user CanWriteAllRules to avoid performance impact on file-provisioning update all methods to accept identity.Requester that contains all permissions and is required by access control. * implement RuleAccessControlService in provisioning * update file provisioning user to have all permissions to bypass authz * update provisioning API to return errutil errors correctly --------- Co-authored-by: Alexander Weaver <weaver.alex.d@gmail.com>
311 lines
10 KiB
Go
311 lines
10 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/util/cmputil"
|
|
)
|
|
|
|
// AlertRuleFieldsToIgnoreInDiff contains fields that are ignored when calculating the RuleDelta.Diff.
|
|
var AlertRuleFieldsToIgnoreInDiff = [...]string{"ID", "Version", "Updated"}
|
|
|
|
type RuleDelta struct {
|
|
Existing *models.AlertRule
|
|
New *models.AlertRule
|
|
Diff cmputil.DiffReport
|
|
}
|
|
|
|
type GroupDelta struct {
|
|
GroupKey models.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
|
|
AffectedGroups map[models.AlertRuleGroupKey]models.RulesGroup
|
|
New []*models.AlertRule
|
|
Update []RuleDelta
|
|
Delete []*models.AlertRule
|
|
}
|
|
|
|
func (c *GroupDelta) IsEmpty() bool {
|
|
return len(c.Update)+len(c.New)+len(c.Delete) == 0
|
|
}
|
|
|
|
// NewOrUpdatedNotificationSettings returns a list of notification settings that are either new or updated in the group.
|
|
func (c *GroupDelta) NewOrUpdatedNotificationSettings() []models.NotificationSettings {
|
|
var settings []models.NotificationSettings
|
|
for _, rule := range c.New {
|
|
if len(rule.NotificationSettings) > 0 {
|
|
settings = append(settings, rule.NotificationSettings...)
|
|
}
|
|
}
|
|
for _, delta := range c.Update {
|
|
if len(delta.New.NotificationSettings) == 0 {
|
|
continue
|
|
}
|
|
d := delta.Diff.GetDiffsForField("NotificationSettings")
|
|
if len(d) == 0 {
|
|
continue
|
|
}
|
|
settings = append(settings, delta.New.NotificationSettings...)
|
|
}
|
|
return settings
|
|
}
|
|
|
|
type RuleReader interface {
|
|
ListAlertRules(ctx context.Context, query *models.ListAlertRulesQuery) (models.RulesGroup, error)
|
|
GetAlertRulesGroupByRuleUID(ctx context.Context, query *models.GetAlertRulesGroupByRuleUIDQuery) ([]*models.AlertRule, error)
|
|
}
|
|
|
|
// 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.
|
|
func CalculateChanges(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey, submittedRules []*models.AlertRuleWithOptionals) (*GroupDelta, error) {
|
|
q := &models.ListAlertRulesQuery{
|
|
OrgID: groupKey.OrgID,
|
|
NamespaceUIDs: []string{groupKey.NamespaceUID},
|
|
RuleGroup: groupKey.RuleGroup,
|
|
}
|
|
existingGroupRules, err := ruleReader.ListAlertRules(ctx, q)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query database for rules in the group %s: %w", groupKey, err)
|
|
}
|
|
|
|
return calculateChanges(ctx, ruleReader, groupKey, existingGroupRules, submittedRules)
|
|
}
|
|
|
|
func calculateChanges(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey, existingGroupRules []*models.AlertRule, submittedRules []*models.AlertRuleWithOptionals) (*GroupDelta, error) {
|
|
affectedGroups := make(map[models.AlertRuleGroupKey]models.RulesGroup)
|
|
|
|
if len(existingGroupRules) > 0 {
|
|
affectedGroups[groupKey] = existingGroupRules
|
|
}
|
|
|
|
existingGroupRulesUIDs := make(map[string]*models.AlertRule, len(existingGroupRules))
|
|
for _, r := range existingGroupRules {
|
|
existingGroupRulesUIDs[r.UID] = r
|
|
}
|
|
|
|
//nolint:prealloc // difficult logic
|
|
var toAdd []*models.AlertRule
|
|
//nolint:prealloc // difficult logic
|
|
var toUpdate []RuleDelta
|
|
loadedRulesByUID := map[string]*models.AlertRule{} // auxiliary cache to avoid unnecessary queries if there are multiple moves from the same group
|
|
for _, r := range submittedRules {
|
|
if r == nil {
|
|
continue
|
|
}
|
|
var existing *models.AlertRule = nil
|
|
if r.UID != "" {
|
|
if existingGroupRule, ok := existingGroupRulesUIDs[r.UID]; ok {
|
|
existing = existingGroupRule
|
|
// remove the rule from existingGroupRulesUIDs
|
|
delete(existingGroupRulesUIDs, r.UID)
|
|
} else if existing, ok = loadedRulesByUID[r.UID]; !ok { // check the "cache" and if there is no hit, query the database
|
|
// Rule can be from other group or namespace
|
|
q := &models.GetAlertRulesGroupByRuleUIDQuery{OrgID: groupKey.OrgID, UID: r.UID}
|
|
ruleList, err := ruleReader.GetAlertRulesGroupByRuleUID(ctx, q)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to query database for a group of alert rules: %w", err)
|
|
}
|
|
for _, rule := range ruleList {
|
|
if rule.UID == r.UID {
|
|
existing = rule
|
|
}
|
|
loadedRulesByUID[rule.UID] = rule
|
|
}
|
|
if existing == nil {
|
|
return nil, fmt.Errorf("failed to update rule with UID %s because %w", r.UID, models.ErrAlertRuleNotFound)
|
|
}
|
|
affectedGroups[existing.GetGroupKey()] = ruleList
|
|
}
|
|
}
|
|
|
|
if existing == nil {
|
|
toAdd = append(toAdd, &r.AlertRule)
|
|
continue
|
|
}
|
|
|
|
models.PatchPartialAlertRule(existing, r)
|
|
|
|
diff := existing.Diff(&r.AlertRule, AlertRuleFieldsToIgnoreInDiff[:]...)
|
|
if len(diff) == 0 {
|
|
continue
|
|
}
|
|
|
|
toUpdate = append(toUpdate, RuleDelta{
|
|
Existing: existing,
|
|
New: &r.AlertRule,
|
|
Diff: diff,
|
|
})
|
|
continue
|
|
}
|
|
|
|
toDelete := make([]*models.AlertRule, 0, len(existingGroupRulesUIDs))
|
|
for _, rule := range existingGroupRulesUIDs {
|
|
toDelete = append(toDelete, rule)
|
|
}
|
|
|
|
return &GroupDelta{
|
|
GroupKey: groupKey,
|
|
AffectedGroups: affectedGroups,
|
|
New: toAdd,
|
|
Delete: toDelete,
|
|
Update: toUpdate,
|
|
}, nil
|
|
}
|
|
|
|
// UpdateCalculatedRuleFields refreshes the calculated fields in a set of alert rule changes.
|
|
// This may generate new changes to keep a group consistent, such as versions or rule indexes.
|
|
func UpdateCalculatedRuleFields(ch *GroupDelta) *GroupDelta {
|
|
updatingRules := make(map[models.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 []RuleDelta
|
|
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 := RuleDelta{
|
|
Existing: rule,
|
|
New: rule,
|
|
}
|
|
if groupKey != ch.GroupKey {
|
|
if rule.RuleGroupIndex != idx {
|
|
upd.New = models.CopyRule(rule)
|
|
upd.New.RuleGroupIndex = idx
|
|
upd.Diff = rule.Diff(upd.New, AlertRuleFieldsToIgnoreInDiff[:]...)
|
|
}
|
|
idx++
|
|
}
|
|
toUpdate = append(toUpdate, upd)
|
|
}
|
|
}
|
|
return &GroupDelta{
|
|
GroupKey: ch.GroupKey,
|
|
AffectedGroups: ch.AffectedGroups,
|
|
New: ch.New,
|
|
Update: append(ch.Update, toUpdate...),
|
|
Delete: ch.Delete,
|
|
}
|
|
}
|
|
|
|
// CalculateRuleUpdate calculates GroupDelta for rule update operation
|
|
func CalculateRuleUpdate(ctx context.Context, ruleReader RuleReader, rule *models.AlertRuleWithOptionals) (*GroupDelta, error) {
|
|
q := &models.ListAlertRulesQuery{
|
|
OrgID: rule.OrgID,
|
|
NamespaceUIDs: []string{rule.NamespaceUID},
|
|
RuleGroup: rule.RuleGroup,
|
|
}
|
|
existingGroupRules, err := ruleReader.ListAlertRules(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
newGroup := make([]*models.AlertRuleWithOptionals, 0, len(existingGroupRules)+1)
|
|
added := false
|
|
for _, alertRule := range existingGroupRules {
|
|
if alertRule.GetKey() == rule.GetKey() {
|
|
newGroup = append(newGroup, rule)
|
|
added = true
|
|
}
|
|
newGroup = append(newGroup, &models.AlertRuleWithOptionals{AlertRule: *alertRule})
|
|
}
|
|
if !added {
|
|
newGroup = append(newGroup, rule)
|
|
}
|
|
|
|
return calculateChanges(ctx, ruleReader, rule.GetGroupKey(), existingGroupRules, newGroup)
|
|
}
|
|
|
|
// CalculateRuleGroupDelete calculates GroupDelta that reflects an operation of removing entire group
|
|
func CalculateRuleGroupDelete(ctx context.Context, ruleReader RuleReader, groupKey models.AlertRuleGroupKey) (*GroupDelta, error) {
|
|
// List all rules in the group.
|
|
q := models.ListAlertRulesQuery{
|
|
OrgID: groupKey.OrgID,
|
|
NamespaceUIDs: []string{groupKey.NamespaceUID},
|
|
RuleGroup: groupKey.RuleGroup,
|
|
}
|
|
ruleList, err := ruleReader.ListAlertRules(ctx, &q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(ruleList) == 0 {
|
|
return nil, models.ErrAlertRuleGroupNotFound.Errorf("")
|
|
}
|
|
|
|
delta := &GroupDelta{
|
|
GroupKey: groupKey,
|
|
Delete: ruleList,
|
|
AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{
|
|
groupKey: ruleList,
|
|
},
|
|
}
|
|
return delta, nil
|
|
}
|
|
|
|
// CalculateRuleDelete calculates GroupDelta that reflects an operation of removing a rule from the group.
|
|
func CalculateRuleDelete(ctx context.Context, ruleReader RuleReader, ruleKey models.AlertRuleKey) (*GroupDelta, error) {
|
|
q := &models.GetAlertRulesGroupByRuleUIDQuery{
|
|
UID: ruleKey.UID,
|
|
OrgID: ruleKey.OrgID,
|
|
}
|
|
group, err := ruleReader.GetAlertRulesGroupByRuleUID(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var toDelete *models.AlertRule
|
|
for _, rule := range group {
|
|
if rule.GetKey() == ruleKey {
|
|
toDelete = rule
|
|
break
|
|
}
|
|
}
|
|
if toDelete == nil { // should not happen if rule exists.
|
|
return nil, models.ErrAlertRuleNotFound
|
|
}
|
|
groupKey := group[0].GetGroupKey()
|
|
delta := &GroupDelta{
|
|
GroupKey: groupKey,
|
|
Delete: []*models.AlertRule{toDelete},
|
|
AffectedGroups: map[models.AlertRuleGroupKey]models.RulesGroup{
|
|
groupKey: group,
|
|
},
|
|
}
|
|
return delta, nil
|
|
}
|
|
|
|
// CalculateRuleCreate calculates GroupDelta that reflects an operation of adding a new rule to the group.
|
|
func CalculateRuleCreate(ctx context.Context, ruleReader RuleReader, rule *models.AlertRule) (*GroupDelta, error) {
|
|
q := &models.ListAlertRulesQuery{
|
|
OrgID: rule.OrgID,
|
|
NamespaceUIDs: []string{rule.NamespaceUID},
|
|
RuleGroup: rule.RuleGroup,
|
|
}
|
|
group, err := ruleReader.ListAlertRules(ctx, q)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
delta := &GroupDelta{
|
|
GroupKey: rule.GetGroupKey(),
|
|
AffectedGroups: make(map[models.AlertRuleGroupKey]models.RulesGroup),
|
|
New: []*models.AlertRule{rule},
|
|
Update: nil,
|
|
Delete: nil,
|
|
}
|
|
|
|
if len(group) > 0 {
|
|
delta.AffectedGroups[rule.GetGroupKey()] = group
|
|
}
|
|
return delta, nil
|
|
}
|