mirror of
https://github.com/grafana/grafana.git
synced 2025-01-27 00:37:04 -06:00
171 lines
5.4 KiB
Go
171 lines
5.4 KiB
Go
package alerting
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/identity"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
|
"github.com/grafana/grafana/pkg/services/folder"
|
|
"github.com/grafana/grafana/pkg/services/folder/folderimpl"
|
|
alert_models "github.com/grafana/grafana/pkg/services/ngalert/models"
|
|
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/util"
|
|
)
|
|
|
|
type AlertRuleProvisioner interface {
|
|
Provision(ctx context.Context, files []*AlertingFile) error
|
|
}
|
|
|
|
func NewAlertRuleProvisioner(
|
|
logger log.Logger,
|
|
folderService folder.Service,
|
|
dashboardProvService dashboards.DashboardProvisioningService,
|
|
ruleService provisioning.AlertRuleService) AlertRuleProvisioner {
|
|
return &defaultAlertRuleProvisioner{
|
|
logger: logger,
|
|
folderService: folderService,
|
|
dashboardProvService: dashboardProvService,
|
|
ruleService: ruleService,
|
|
}
|
|
}
|
|
|
|
type defaultAlertRuleProvisioner struct {
|
|
logger log.Logger
|
|
folderService folder.Service
|
|
dashboardProvService dashboards.DashboardProvisioningService
|
|
ruleService provisioning.AlertRuleService
|
|
}
|
|
|
|
func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
|
|
files []*AlertingFile) error {
|
|
for _, file := range files {
|
|
for _, group := range file.Groups {
|
|
u := provisionerUser(group.OrgID)
|
|
folderUID, err := prov.getOrCreateFolderFullpath(ctx, group.FolderFullpath, group.OrgID)
|
|
if err != nil {
|
|
prov.logger.Error("failed to get or create folder", "folder", group.FolderFullpath, "org", group.OrgID, "err", err)
|
|
return err
|
|
}
|
|
prov.logger.Debug("provisioning alert rule group",
|
|
"org", group.OrgID,
|
|
"folder", group.FolderFullpath,
|
|
"folderUID", folderUID,
|
|
"name", group.Title)
|
|
for _, rule := range group.Rules {
|
|
rule.NamespaceUID = folderUID
|
|
rule.RuleGroup = group.Title
|
|
err = prov.provisionRule(ctx, u, rule)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
err = prov.ruleService.UpdateRuleGroup(ctx, u, folderUID, group.Title, group.Interval)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
for _, deleteRule := range file.DeleteRules {
|
|
err := prov.ruleService.DeleteAlertRule(ctx, provisionerUser(deleteRule.OrgID), deleteRule.UID, alert_models.ProvenanceFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (prov *defaultAlertRuleProvisioner) provisionRule(
|
|
ctx context.Context,
|
|
user identity.Requester,
|
|
rule alert_models.AlertRule) error {
|
|
prov.logger.Debug("provisioning alert rule", "uid", rule.UID, "org", rule.OrgID)
|
|
_, _, err := prov.ruleService.GetAlertRule(ctx, user, rule.UID)
|
|
if err != nil && !errors.Is(err, alert_models.ErrAlertRuleNotFound) {
|
|
return err
|
|
} else if err != nil {
|
|
prov.logger.Debug("creating rule", "uid", rule.UID, "org", rule.OrgID)
|
|
// a nil user is passed in as then the quota logic will only check for
|
|
// the organization quota since we don't have any user scope here.
|
|
_, err = prov.ruleService.CreateAlertRule(ctx, user, rule, alert_models.ProvenanceFile)
|
|
} else {
|
|
prov.logger.Debug("updating rule", "uid", rule.UID, "org", rule.OrgID)
|
|
_, err = prov.ruleService.UpdateAlertRule(ctx, user, rule, alert_models.ProvenanceFile)
|
|
}
|
|
return err
|
|
}
|
|
|
|
func (prov *defaultAlertRuleProvisioner) getOrCreateFolderFullpath(
|
|
ctx context.Context, folderFullpath string, orgID int64) (string, error) {
|
|
folderTitles := folderimpl.SplitFullpath(folderFullpath)
|
|
if len(folderTitles) == 0 {
|
|
return "", fmt.Errorf("invalid folder fullpath: %s", folderFullpath)
|
|
}
|
|
|
|
var folderUID *string
|
|
for i := range folderTitles {
|
|
uid, err := prov.getOrCreateFolderByTitle(ctx, folderTitles[i], orgID, folderUID)
|
|
if err != nil {
|
|
prov.logger.Error("failed to get or create folder", "folder", folderTitles[i], "org", orgID, "err", err)
|
|
return "", err
|
|
}
|
|
folderUID = &uid
|
|
}
|
|
return *folderUID, nil
|
|
}
|
|
|
|
func (prov *defaultAlertRuleProvisioner) getOrCreateFolderByTitle(
|
|
ctx context.Context, folderName string, orgID int64, parentUID *string) (string, error) {
|
|
cmd := &folder.GetFolderQuery{
|
|
Title: &folderName,
|
|
ParentUID: parentUID,
|
|
OrgID: orgID,
|
|
SignedInUser: provisionerUser(orgID),
|
|
}
|
|
|
|
cmdResult, err := prov.folderService.Get(ctx, cmd)
|
|
if err != nil && !errors.Is(err, dashboards.ErrFolderNotFound) {
|
|
return "", err
|
|
}
|
|
|
|
// dashboard folder not found. create one.
|
|
if errors.Is(err, dashboards.ErrFolderNotFound) {
|
|
createCmd := &folder.CreateFolderCommand{
|
|
OrgID: orgID,
|
|
UID: util.GenerateShortUID(),
|
|
Title: folderName,
|
|
}
|
|
|
|
if parentUID != nil {
|
|
createCmd.ParentUID = *parentUID
|
|
}
|
|
|
|
f, err := prov.dashboardProvService.SaveFolderForProvisionedDashboards(ctx, createCmd)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return f.UID, nil
|
|
}
|
|
|
|
return cmdResult.UID, nil
|
|
}
|
|
|
|
var provisionerUser = func(orgID int64) identity.Requester {
|
|
// this user has 0 ID and therefore, organization wide quota will be applied
|
|
return accesscontrol.BackgroundUser(
|
|
"alert_provisioner",
|
|
orgID,
|
|
org.RoleAdmin,
|
|
[]accesscontrol.Permission{
|
|
{Action: dashboards.ActionFoldersRead, Scope: dashboards.ScopeFoldersAll},
|
|
{Action: accesscontrol.ActionAlertingProvisioningReadSecrets, Scope: dashboards.ScopeFoldersAll},
|
|
{Action: accesscontrol.ActionAlertingProvisioningWrite, Scope: dashboards.ScopeFoldersAll},
|
|
},
|
|
)
|
|
}
|