mirror of
https://github.com/grafana/grafana.git
synced 2025-07-30 11:02:21 +08:00
Alerting: Add file provisioning for alert rules (#51635)
This commit is contained in:

committed by
GitHub

parent
e5e8747ee9
commit
41790083d2
@ -31,6 +31,7 @@ var (
|
|||||||
ScopeProvisionersPlugins = ac.Scope("provisioners", "plugins")
|
ScopeProvisionersPlugins = ac.Scope("provisioners", "plugins")
|
||||||
ScopeProvisionersDatasources = ac.Scope("provisioners", "datasources")
|
ScopeProvisionersDatasources = ac.Scope("provisioners", "datasources")
|
||||||
ScopeProvisionersNotifications = ac.Scope("provisioners", "notifications")
|
ScopeProvisionersNotifications = ac.Scope("provisioners", "notifications")
|
||||||
|
ScopeProvisionersAlertRules = ac.Scope("provisioners", "alerting")
|
||||||
)
|
)
|
||||||
|
|
||||||
// declareFixedRoles declares to the AccessControl service fixed roles and their
|
// declareFixedRoles declares to the AccessControl service fixed roles and their
|
||||||
|
@ -39,3 +39,11 @@ func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *models.ReqContext)
|
|||||||
}
|
}
|
||||||
return response.Success("Notifications config reloaded")
|
return response.Success("Notifications config reloaded")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (hs *HTTPServer) AdminProvisioningReloadAlerting(c *models.ReqContext) response.Response {
|
||||||
|
err := hs.ProvisioningService.ProvisionAlertRules(c.Req.Context())
|
||||||
|
if err != nil {
|
||||||
|
return response.Error(500, "", err)
|
||||||
|
}
|
||||||
|
return response.Success("Alerting config reloaded")
|
||||||
|
}
|
||||||
|
@ -135,6 +135,33 @@ func TestAPI_AdminProvisioningReload_AccessControl(t *testing.T) {
|
|||||||
url: "/api/admin/provisioning/plugins/reload",
|
url: "/api/admin/provisioning/plugins/reload",
|
||||||
exit: true,
|
exit: true,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
desc: "should fail for alerting with no permission",
|
||||||
|
expectedCode: http.StatusForbidden,
|
||||||
|
url: "/api/admin/provisioning/alerting/reload",
|
||||||
|
exit: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should work for alert rules with specific scope",
|
||||||
|
expectedCode: http.StatusOK,
|
||||||
|
expectedBody: `{"message":"Alerting config reloaded"}`,
|
||||||
|
permissions: []accesscontrol.Permission{
|
||||||
|
{
|
||||||
|
Action: ActionProvisioningReload,
|
||||||
|
Scope: ScopeProvisionersAlertRules,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
url: "/api/admin/provisioning/alerting/reload",
|
||||||
|
checkCall: func(mock provisioning.ProvisioningServiceMock) {
|
||||||
|
assert.Len(t, mock.Calls.ProvisionAlertRules, 1)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
desc: "should fail for alerting with no permission",
|
||||||
|
expectedCode: http.StatusForbidden,
|
||||||
|
url: "/api/admin/provisioning/alerting/reload",
|
||||||
|
exit: true,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
cfg := setting.NewCfg()
|
cfg := setting.NewCfg()
|
||||||
|
@ -576,6 +576,7 @@ func (hs *HTTPServer) registerRoutes() {
|
|||||||
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
|
adminRoute.Post("/provisioning/plugins/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersPlugins)), routing.Wrap(hs.AdminProvisioningReloadPlugins))
|
||||||
adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDatasources)), routing.Wrap(hs.AdminProvisioningReloadDatasources))
|
adminRoute.Post("/provisioning/datasources/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersDatasources)), routing.Wrap(hs.AdminProvisioningReloadDatasources))
|
||||||
adminRoute.Post("/provisioning/notifications/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersNotifications)), routing.Wrap(hs.AdminProvisioningReloadNotifications))
|
adminRoute.Post("/provisioning/notifications/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersNotifications)), routing.Wrap(hs.AdminProvisioningReloadNotifications))
|
||||||
|
adminRoute.Post("/provisioning/alerting/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ActionProvisioningReload, ScopeProvisionersAlertRules)), routing.Wrap(hs.AdminProvisioningReloadAlerting))
|
||||||
|
|
||||||
adminRoute.Post("/ldap/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPConfigReload)), routing.Wrap(hs.ReloadLDAPCfg))
|
adminRoute.Post("/ldap/reload", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPConfigReload)), routing.Wrap(hs.ReloadLDAPCfg))
|
||||||
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersSync)), routing.Wrap(hs.PostSyncUserWithLDAP))
|
adminRoute.Post("/ldap/sync/:id", authorize(reqGrafanaAdmin, ac.EvalPermission(ac.ActionLDAPUsersSync)), routing.Wrap(hs.PostSyncUserWithLDAP))
|
||||||
|
@ -133,8 +133,8 @@ func (service *AlertRuleService) GetRuleGroup(ctx context.Context, orgID int64,
|
|||||||
}
|
}
|
||||||
|
|
||||||
// UpdateRuleGroup will update the interval for all rules in the group.
|
// UpdateRuleGroup will update the interval for all rules in the group.
|
||||||
func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, interval int64) error {
|
func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, orgID int64, namespaceUID string, ruleGroup string, intervalSeconds int64) error {
|
||||||
if err := models.ValidateRuleGroupInterval(interval, service.baseIntervalSeconds); err != nil {
|
if err := models.ValidateRuleGroupInterval(intervalSeconds, service.baseIntervalSeconds); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
return service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||||
@ -149,11 +149,11 @@ func (service *AlertRuleService) UpdateRuleGroup(ctx context.Context, orgID int6
|
|||||||
}
|
}
|
||||||
updateRules := make([]store.UpdateRule, 0, len(query.Result))
|
updateRules := make([]store.UpdateRule, 0, len(query.Result))
|
||||||
for _, rule := range query.Result {
|
for _, rule := range query.Result {
|
||||||
if rule.IntervalSeconds == interval {
|
if rule.IntervalSeconds == intervalSeconds {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
newRule := *rule
|
newRule := *rule
|
||||||
newRule.IntervalSeconds = interval
|
newRule.IntervalSeconds = intervalSeconds
|
||||||
updateRules = append(updateRules, store.UpdateRule{
|
updateRules = append(updateRules, store.UpdateRule{
|
||||||
Existing: rule,
|
Existing: rule,
|
||||||
New: newRule,
|
New: newRule,
|
||||||
@ -180,7 +180,6 @@ func (service *AlertRuleService) UpdateAlertRule(ctx context.Context, rule model
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return models.AlertRule{}, err
|
return models.AlertRule{}, err
|
||||||
}
|
}
|
||||||
service.log.Info("update rule", "ID", storedRule.ID, "labels", fmt.Sprintf("%+v", rule.Labels))
|
|
||||||
err = service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
err = service.xact.InTransaction(ctx, func(ctx context.Context) error {
|
||||||
err := service.ruleStore.UpdateAlertRules(ctx, []store.UpdateRule{
|
err := service.ruleStore.UpdateAlertRules(ctx, []store.UpdateRule{
|
||||||
{
|
{
|
||||||
|
77
pkg/services/provisioning/alerting/rules/config_reader.go
Normal file
77
pkg/services/provisioning/alerting/rules/config_reader.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
"io/ioutil"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type rulesConfigReader struct {
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRulesConfigReader(logger log.Logger) rulesConfigReader {
|
||||||
|
return rulesConfigReader{
|
||||||
|
log: logger,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *rulesConfigReader) readConfig(ctx context.Context, path string) ([]*RuleFile, error) {
|
||||||
|
var alertRulesFiles []*RuleFile
|
||||||
|
cr.log.Debug("looking for alert rules provisioning files", "path", path)
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
cr.log.Error("can't read alert rules provisioning files from directory", "path", path, "error", err)
|
||||||
|
return alertRulesFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
cr.log.Debug("parsing alert rules provisioning file", "path", path, "file.Name", file.Name())
|
||||||
|
if !cr.isYAML(file.Name()) && !cr.isJSON(file.Name()) {
|
||||||
|
return nil, fmt.Errorf("file has invalid suffix '%s' (.yaml,.yml,.json accepted)", file.Name())
|
||||||
|
}
|
||||||
|
ruleFileV1, err := cr.parseConfig(path, file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if ruleFileV1 != nil {
|
||||||
|
ruleFile, err := ruleFileV1.MapToModel()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
alertRulesFiles = append(alertRulesFiles, &ruleFile)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return alertRulesFiles, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *rulesConfigReader) isYAML(file string) bool {
|
||||||
|
return strings.HasSuffix(file, ".yaml") || strings.HasSuffix(file, ".yml")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *rulesConfigReader) isJSON(file string) bool {
|
||||||
|
return strings.HasSuffix(file, ".json")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *rulesConfigReader) parseConfig(path string, file fs.FileInfo) (*RuleFileV1, error) {
|
||||||
|
filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
|
||||||
|
// nolint:gosec
|
||||||
|
// We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath
|
||||||
|
yamlFile, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var cfg *RuleFileV1
|
||||||
|
err = yaml.Unmarshal(yamlFile, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return cfg, nil
|
||||||
|
}
|
@ -0,0 +1,66 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
testFileBrokenYAML = "./testdata/broken-yaml"
|
||||||
|
testFileCorrectProperties = "./testdata/correct-properties"
|
||||||
|
testFileCorrectPropertiesWithOrg = "./testdata/correct-properties-with-org"
|
||||||
|
testFileEmptyFile = "./testdata/empty-file"
|
||||||
|
testFileEmptyFolder = "./testdata/empty-folder"
|
||||||
|
testFileMultipleRules = "./testdata/multiple-rules"
|
||||||
|
testFileMultipleFiles = "./testdata/multiple-files"
|
||||||
|
testFileSupportedFiletypes = "./testdata/supported-filetypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigReader(t *testing.T) {
|
||||||
|
configReader := newRulesConfigReader(log.NewNopLogger())
|
||||||
|
ctx := context.Background()
|
||||||
|
t.Run("a broken YAML file should error", func(t *testing.T) {
|
||||||
|
_, err := configReader.readConfig(ctx, testFileBrokenYAML)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule file with correct properties should not error", func(t *testing.T) {
|
||||||
|
ruleFiles, err := configReader.readConfig(ctx, testFileCorrectProperties)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Run("when no organization is present it should be set to 1", func(t *testing.T) {
|
||||||
|
require.Equal(t, int64(1), ruleFiles[0].Groups[0].Rules[0].OrgID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("a rule file with correct properties and specific org should not error", func(t *testing.T) {
|
||||||
|
ruleFiles, err := configReader.readConfig(ctx, testFileCorrectPropertiesWithOrg)
|
||||||
|
require.NoError(t, err)
|
||||||
|
t.Run("when an organization is set it should not overwrite if with the default of 1", func(t *testing.T) {
|
||||||
|
require.Equal(t, int64(1337), ruleFiles[0].Groups[0].Rules[0].OrgID)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
t.Run("an empty rule file should not make the config reader error", func(t *testing.T) {
|
||||||
|
_, err := configReader.readConfig(ctx, testFileEmptyFile)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
t.Run("an empty folder should not make the config reader error", func(t *testing.T) {
|
||||||
|
_, err := configReader.readConfig(ctx, testFileEmptyFolder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
t.Run("the config reader should be able to read multiple files in the folder", func(t *testing.T) {
|
||||||
|
ruleFiles, err := configReader.readConfig(ctx, testFileMultipleFiles)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, ruleFiles, 2)
|
||||||
|
})
|
||||||
|
t.Run("the config reader should be able to read multiple rule groups", func(t *testing.T) {
|
||||||
|
ruleFiles, err := configReader.readConfig(ctx, testFileMultipleRules)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, ruleFiles[0].Groups, 2)
|
||||||
|
})
|
||||||
|
t.Run("the config reader should support .yaml,.yml and .json files", func(t *testing.T) {
|
||||||
|
ruleFiles, err := configReader.readConfig(ctx, testFileSupportedFiletypes)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Len(t, ruleFiles, 3)
|
||||||
|
})
|
||||||
|
}
|
165
pkg/services/provisioning/alerting/rules/provisioner.go
Normal file
165
pkg/services/provisioning/alerting/rules/provisioner.go
Normal file
@ -0,0 +1,165 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
|
alert_models "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AlertRuleProvisioner interface {
|
||||||
|
Provision(ctx context.Context, path string) error
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAlertRuleProvisioner(
|
||||||
|
logger log.Logger,
|
||||||
|
dashboardService dashboards.DashboardService,
|
||||||
|
dashboardProvService dashboards.DashboardProvisioningService,
|
||||||
|
ruleService provisioning.AlertRuleService) AlertRuleProvisioner {
|
||||||
|
return &defaultAlertRuleProvisioner{
|
||||||
|
logger: logger,
|
||||||
|
cfgReader: newRulesConfigReader(logger),
|
||||||
|
dashboardService: dashboardService,
|
||||||
|
dashboardProvService: dashboardProvService,
|
||||||
|
ruleService: ruleService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type defaultAlertRuleProvisioner struct {
|
||||||
|
logger log.Logger
|
||||||
|
cfgReader rulesConfigReader
|
||||||
|
dashboardService dashboards.DashboardService
|
||||||
|
dashboardProvService dashboards.DashboardProvisioningService
|
||||||
|
ruleService provisioning.AlertRuleService
|
||||||
|
}
|
||||||
|
|
||||||
|
func Provision(
|
||||||
|
ctx context.Context,
|
||||||
|
path string,
|
||||||
|
dashboardService dashboards.DashboardService,
|
||||||
|
dashboardProvisioningService dashboards.DashboardProvisioningService,
|
||||||
|
ruleService provisioning.AlertRuleService,
|
||||||
|
) error {
|
||||||
|
ruleProvisioner := NewAlertRuleProvisioner(
|
||||||
|
log.New("provisioning.alerting"),
|
||||||
|
dashboardService,
|
||||||
|
dashboardProvisioningService,
|
||||||
|
ruleService,
|
||||||
|
)
|
||||||
|
return ruleProvisioner.Provision(ctx, path)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
|
||||||
|
path string) error {
|
||||||
|
prov.logger.Info("starting to provision the alert rules")
|
||||||
|
ruleFiles, err := prov.cfgReader.readConfig(ctx, path)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read alert rules files: %w", err)
|
||||||
|
}
|
||||||
|
prov.logger.Debug("read all alert rules files", "file_count", len(ruleFiles))
|
||||||
|
err = prov.provsionRuleFiles(ctx, ruleFiles)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to provision alert rules: %w", err)
|
||||||
|
}
|
||||||
|
prov.logger.Info("finished to provision the alert rules")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prov *defaultAlertRuleProvisioner) provsionRuleFiles(ctx context.Context,
|
||||||
|
ruleFiles []*RuleFile) error {
|
||||||
|
for _, file := range ruleFiles {
|
||||||
|
for _, group := range file.Groups {
|
||||||
|
folderUID, err := prov.getOrCreateFolderUID(ctx, group.Folder, group.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
prov.logger.Debug("provisioning alert rule group",
|
||||||
|
"org", group.OrgID,
|
||||||
|
"folder", group.Folder,
|
||||||
|
"folderUID", folderUID,
|
||||||
|
"name", group.Name)
|
||||||
|
for _, rule := range group.Rules {
|
||||||
|
rule.NamespaceUID = folderUID
|
||||||
|
rule.RuleGroup = group.Name
|
||||||
|
err = prov.provisionRule(ctx, group.OrgID, rule, group.Folder, folderUID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = prov.ruleService.UpdateRuleGroup(ctx, group.OrgID, folderUID, group.Name, int64(group.Interval.Seconds()))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, deleteRule := range file.DeleteRules {
|
||||||
|
err := prov.ruleService.DeleteAlertRule(ctx, deleteRule.OrgID,
|
||||||
|
deleteRule.UID, alert_models.ProvenanceFile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prov *defaultAlertRuleProvisioner) provisionRule(
|
||||||
|
ctx context.Context,
|
||||||
|
orgID int64,
|
||||||
|
rule alert_models.AlertRule,
|
||||||
|
folder,
|
||||||
|
folderUID string) error {
|
||||||
|
prov.logger.Debug("provisioning alert rule", "uid", rule.UID, "org", rule.OrgID)
|
||||||
|
_, _, err := prov.ruleService.GetAlertRule(ctx, orgID, 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)
|
||||||
|
// 0 is passed as userID as then the quota logic will only check for
|
||||||
|
// the organization quota, as we don't have any user scope here.
|
||||||
|
_, err = prov.ruleService.CreateAlertRule(ctx, rule, alert_models.ProvenanceFile, 0)
|
||||||
|
} else {
|
||||||
|
prov.logger.Debug("updating rule", "uid", rule.UID, "org", rule.OrgID)
|
||||||
|
_, err = prov.ruleService.UpdateAlertRule(ctx, rule, alert_models.ProvenanceFile)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (prov *defaultAlertRuleProvisioner) getOrCreateFolderUID(
|
||||||
|
ctx context.Context, folderName string, orgID int64) (string, error) {
|
||||||
|
cmd := &models.GetDashboardQuery{
|
||||||
|
Slug: models.SlugifyTitle(folderName),
|
||||||
|
OrgId: orgID,
|
||||||
|
}
|
||||||
|
err := prov.dashboardService.GetDashboard(ctx, cmd)
|
||||||
|
if err != nil && !errors.Is(err, dashboards.ErrDashboardNotFound) {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
// dashboard folder not found. create one.
|
||||||
|
if errors.Is(err, dashboards.ErrDashboardNotFound) {
|
||||||
|
dash := &dashboards.SaveDashboardDTO{}
|
||||||
|
dash.Dashboard = models.NewDashboardFolder(folderName)
|
||||||
|
dash.Dashboard.IsFolder = true
|
||||||
|
dash.Overwrite = true
|
||||||
|
dash.OrgId = orgID
|
||||||
|
dash.Dashboard.SetUid(util.GenerateShortUID())
|
||||||
|
dbDash, err := prov.dashboardProvService.SaveFolderForProvisionedDashboards(ctx, dash)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return dbDash.Uid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !cmd.Result.IsFolder {
|
||||||
|
return "", fmt.Errorf("got invalid response. expected folder, found dashboard")
|
||||||
|
}
|
||||||
|
|
||||||
|
return cmd.Result.Uid, nil
|
||||||
|
}
|
56
pkg/services/provisioning/alerting/rules/testdata/broken-yaml/rules.yml
vendored
Normal file
56
pkg/services/provisioning/alerting/rules/testdata/broken-yaml/rules.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
groups:
|
||||||
|
- name: my_group
|
||||||
|
folder: my_folder
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- title: my_first_rule
|
||||||
|
uid: my_first_rule
|
||||||
|
condition: A
|
||||||
|
for: 1m
|
||||||
|
annotations:
|
||||||
|
runbook: https://grafana.com
|
||||||
|
labels:
|
||||||
|
team: infra
|
||||||
|
severity: warning
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUID: PD8C576611E62080A
|
||||||
|
model:
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 0
|
||||||
|
to: 0
|
||||||
|
datasourceUID: "-100"
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 3
|
||||||
|
type: gt
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- A
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: "-100"
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: B
|
||||||
|
type: classic_conditions
|
57
pkg/services/provisioning/alerting/rules/testdata/correct-properties-with-org/rules.yml
vendored
Normal file
57
pkg/services/provisioning/alerting/rules/testdata/correct-properties-with-org/rules.yml
vendored
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
groups:
|
||||||
|
- name: my_group
|
||||||
|
folder: my_folder
|
||||||
|
interval: 10s
|
||||||
|
orgId: 1337
|
||||||
|
rules:
|
||||||
|
- title: my_first_rule
|
||||||
|
uid: my_first_rule
|
||||||
|
condition: A
|
||||||
|
for: 1m
|
||||||
|
annotations:
|
||||||
|
runbook: https://grafana.com
|
||||||
|
labels:
|
||||||
|
team: infra
|
||||||
|
severity: warning
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUID: PD8C576611E62080A
|
||||||
|
model:
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 0
|
||||||
|
to: 0
|
||||||
|
datasourceUID: "-100"
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 3
|
||||||
|
type: gt
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- A
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: "-100"
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: B
|
||||||
|
type: classic_conditions
|
56
pkg/services/provisioning/alerting/rules/testdata/correct-properties/rules.yml
vendored
Normal file
56
pkg/services/provisioning/alerting/rules/testdata/correct-properties/rules.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
groups:
|
||||||
|
- name: my_group
|
||||||
|
folder: my_folder
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- title: my_first_rule
|
||||||
|
uid: my_first_rule
|
||||||
|
condition: A
|
||||||
|
for: 1m
|
||||||
|
annotations:
|
||||||
|
runbook: https://grafana.com
|
||||||
|
labels:
|
||||||
|
team: infra
|
||||||
|
severity: warning
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUID: PD8C576611E62080A
|
||||||
|
model:
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 0
|
||||||
|
to: 0
|
||||||
|
datasourceUID: "-100"
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 3
|
||||||
|
type: gt
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- A
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: "-100"
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: B
|
||||||
|
type: classic_conditions
|
0
pkg/services/provisioning/alerting/rules/testdata/empty-file/rules.yml
vendored
Normal file
0
pkg/services/provisioning/alerting/rules/testdata/empty-file/rules.yml
vendored
Normal file
56
pkg/services/provisioning/alerting/rules/testdata/multiple-files/rules.yml
vendored
Normal file
56
pkg/services/provisioning/alerting/rules/testdata/multiple-files/rules.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
groups:
|
||||||
|
- name: my_group
|
||||||
|
folder: my_folder
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- title: my_first_rule
|
||||||
|
uid: my_first_rule
|
||||||
|
condition: A
|
||||||
|
for: 1m
|
||||||
|
annotations:
|
||||||
|
runbook: https://grafana.com
|
||||||
|
labels:
|
||||||
|
team: infra
|
||||||
|
severity: warning
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUID: PD8C576611E62080A
|
||||||
|
model:
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 0
|
||||||
|
to: 0
|
||||||
|
datasourceUID: "-100"
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 3
|
||||||
|
type: gt
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- A
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: "-100"
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: B
|
||||||
|
type: classic_conditions
|
56
pkg/services/provisioning/alerting/rules/testdata/multiple-files/rules2.yml
vendored
Normal file
56
pkg/services/provisioning/alerting/rules/testdata/multiple-files/rules2.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
groups:
|
||||||
|
- name: my_other_group
|
||||||
|
folder: my_other_folder
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- title: my_other_rule
|
||||||
|
uid: my_other_rule
|
||||||
|
condition: A
|
||||||
|
for: 1m
|
||||||
|
annotations:
|
||||||
|
runbook: https://grafana.com
|
||||||
|
labels:
|
||||||
|
team: infra
|
||||||
|
severity: warning
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUID: PD8C576611E62080A
|
||||||
|
model:
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 0
|
||||||
|
to: 0
|
||||||
|
datasourceUID: "-100"
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 3
|
||||||
|
type: gt
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- A
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: "-100"
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: B
|
||||||
|
type: classic_conditions
|
110
pkg/services/provisioning/alerting/rules/testdata/multiple-rules/rules.yml
vendored
Normal file
110
pkg/services/provisioning/alerting/rules/testdata/multiple-rules/rules.yml
vendored
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
groups:
|
||||||
|
- name: my_group
|
||||||
|
folder: my_folder
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- title: my_first_rule
|
||||||
|
uid: my_first_rule
|
||||||
|
condition: A
|
||||||
|
for: 1m
|
||||||
|
annotations:
|
||||||
|
runbook: https://grafana.com
|
||||||
|
labels:
|
||||||
|
team: infra
|
||||||
|
severity: warning
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUID: PD8C576611E62080A
|
||||||
|
model:
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 0
|
||||||
|
to: 0
|
||||||
|
datasourceUID: "-100"
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 3
|
||||||
|
type: gt
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- A
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: "-100"
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: B
|
||||||
|
type: classic_conditions
|
||||||
|
- name: my_other_group
|
||||||
|
folder: my_other_folder
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- title: my_other_rule
|
||||||
|
uid: my_other_rule
|
||||||
|
condition: A
|
||||||
|
for: 1m
|
||||||
|
annotations:
|
||||||
|
runbook: https://grafana.com
|
||||||
|
labels:
|
||||||
|
team: infra
|
||||||
|
severity: warning
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUID: PD8C576611E62080A
|
||||||
|
model:
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 0
|
||||||
|
to: 0
|
||||||
|
datasourceUID: "-100"
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 3
|
||||||
|
type: gt
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- A
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: "-100"
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: B
|
||||||
|
type: classic_conditions
|
87
pkg/services/provisioning/alerting/rules/testdata/supported-filetypes/rules.json
vendored
Normal file
87
pkg/services/provisioning/alerting/rules/testdata/supported-filetypes/rules.json
vendored
Normal file
@ -0,0 +1,87 @@
|
|||||||
|
{
|
||||||
|
"apiVersion": 1,
|
||||||
|
"groups": [
|
||||||
|
{
|
||||||
|
"name": "my_json_group",
|
||||||
|
"folder": "my_json_folder",
|
||||||
|
"interval": "10s",
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"title": "my_json_rule",
|
||||||
|
"uid": "my_json_rule",
|
||||||
|
"condition": "A",
|
||||||
|
"for": "1m",
|
||||||
|
"annotations": {
|
||||||
|
"runbook": "http://google.com/"
|
||||||
|
},
|
||||||
|
"labels": {
|
||||||
|
"team": "infra",
|
||||||
|
"severity": "warning"
|
||||||
|
},
|
||||||
|
"data": [
|
||||||
|
{
|
||||||
|
"refId": "A",
|
||||||
|
"queryType": "",
|
||||||
|
"relativeTimeRange": {
|
||||||
|
"from": 600,
|
||||||
|
"to": 0
|
||||||
|
},
|
||||||
|
"datasourceUID": "PD8C576611E62080A",
|
||||||
|
"model": {
|
||||||
|
"hide": false,
|
||||||
|
"intervalMs": 1000,
|
||||||
|
"maxDataPoints": 43200,
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"refId": "B",
|
||||||
|
"queryType": "",
|
||||||
|
"relativeTimeRange": {
|
||||||
|
"from": 0,
|
||||||
|
"to": 0
|
||||||
|
},
|
||||||
|
"datasourceUID": "-100",
|
||||||
|
"model": {
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"evaluator": {
|
||||||
|
"params": [
|
||||||
|
3
|
||||||
|
],
|
||||||
|
"type": "gt"
|
||||||
|
},
|
||||||
|
"operator": {
|
||||||
|
"type": "and"
|
||||||
|
},
|
||||||
|
"query": {
|
||||||
|
"params": [
|
||||||
|
"A"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"reducer": {
|
||||||
|
"params": [
|
||||||
|
|
||||||
|
],
|
||||||
|
"type": "last"
|
||||||
|
},
|
||||||
|
"type": "query"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"datasource": {
|
||||||
|
"type": "__expr__",
|
||||||
|
"uid": "-100"
|
||||||
|
},
|
||||||
|
"hide": false,
|
||||||
|
"intervalMs": 1000,
|
||||||
|
"maxDataPoints": 43200,
|
||||||
|
"refId": "B",
|
||||||
|
"type": "classic_conditions"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
56
pkg/services/provisioning/alerting/rules/testdata/supported-filetypes/rules.yaml
vendored
Normal file
56
pkg/services/provisioning/alerting/rules/testdata/supported-filetypes/rules.yaml
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
groups:
|
||||||
|
- name: my_group
|
||||||
|
folder: my_folder
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- title: my_first_rule
|
||||||
|
uid: my_first_rule
|
||||||
|
condition: A
|
||||||
|
for: 1m
|
||||||
|
annotations:
|
||||||
|
runbook: https://grafana.com
|
||||||
|
labels:
|
||||||
|
team: infra
|
||||||
|
severity: warning
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUID: PD8C576611E62080A
|
||||||
|
model:
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 0
|
||||||
|
to: 0
|
||||||
|
datasourceUID: "-100"
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 3
|
||||||
|
type: gt
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- A
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: "-100"
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: B
|
||||||
|
type: classic_conditions
|
56
pkg/services/provisioning/alerting/rules/testdata/supported-filetypes/rules.yml
vendored
Normal file
56
pkg/services/provisioning/alerting/rules/testdata/supported-filetypes/rules.yml
vendored
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
apiVersion: 1
|
||||||
|
groups:
|
||||||
|
- name: my_other_group
|
||||||
|
folder: my_other_folder
|
||||||
|
interval: 10s
|
||||||
|
rules:
|
||||||
|
- title: my_other_rule
|
||||||
|
uid: my_other_rule
|
||||||
|
condition: A
|
||||||
|
for: 1m
|
||||||
|
annotations:
|
||||||
|
runbook: https://grafana.com
|
||||||
|
labels:
|
||||||
|
team: infra
|
||||||
|
severity: warning
|
||||||
|
data:
|
||||||
|
- refId: A
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 600
|
||||||
|
to: 0
|
||||||
|
datasourceUID: PD8C576611E62080A
|
||||||
|
model:
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: A
|
||||||
|
- refId: B
|
||||||
|
queryType: ''
|
||||||
|
relativeTimeRange:
|
||||||
|
from: 0
|
||||||
|
to: 0
|
||||||
|
datasourceUID: "-100"
|
||||||
|
model:
|
||||||
|
conditions:
|
||||||
|
- evaluator:
|
||||||
|
params:
|
||||||
|
- 3
|
||||||
|
type: gt
|
||||||
|
operator:
|
||||||
|
type: and
|
||||||
|
query:
|
||||||
|
params:
|
||||||
|
- A
|
||||||
|
reducer:
|
||||||
|
params: []
|
||||||
|
type: last
|
||||||
|
type: query
|
||||||
|
datasource:
|
||||||
|
type: __expr__
|
||||||
|
uid: "-100"
|
||||||
|
hide: false
|
||||||
|
intervalMs: 1000
|
||||||
|
maxDataPoints: 43200
|
||||||
|
refId: B
|
||||||
|
type: classic_conditions
|
210
pkg/services/provisioning/alerting/rules/types.go
Normal file
210
pkg/services/provisioning/alerting/rules/types.go
Normal file
@ -0,0 +1,210 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/provisioning/values"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configVersion struct {
|
||||||
|
APIVersion values.Int64Value `json:"apiVersion" yaml:"apiVersion"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleFile struct {
|
||||||
|
configVersion
|
||||||
|
Groups []AlertRuleGroup
|
||||||
|
DeleteRules []RuleDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleFileV1 struct {
|
||||||
|
configVersion
|
||||||
|
Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"`
|
||||||
|
DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ruleFileV1 *RuleFileV1) MapToModel() (RuleFile, error) {
|
||||||
|
ruleFile := RuleFile{}
|
||||||
|
ruleFile.configVersion = ruleFileV1.configVersion
|
||||||
|
for _, groupV1 := range ruleFileV1.Groups {
|
||||||
|
group, err := groupV1.mapToModel()
|
||||||
|
if err != nil {
|
||||||
|
return RuleFile{}, err
|
||||||
|
}
|
||||||
|
ruleFile.Groups = append(ruleFile.Groups, group)
|
||||||
|
}
|
||||||
|
for _, ruleDeleteV1 := range ruleFileV1.DeleteRules {
|
||||||
|
orgID := ruleDeleteV1.OrgID.Value()
|
||||||
|
if orgID < 1 {
|
||||||
|
orgID = 1
|
||||||
|
}
|
||||||
|
ruleDelete := RuleDelete{
|
||||||
|
UID: ruleDeleteV1.UID.Value(),
|
||||||
|
OrgID: orgID,
|
||||||
|
}
|
||||||
|
ruleFile.DeleteRules = append(ruleFile.DeleteRules, ruleDelete)
|
||||||
|
}
|
||||||
|
return ruleFile, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleDelete struct {
|
||||||
|
UID string
|
||||||
|
OrgID int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type RuleDeleteV1 struct {
|
||||||
|
UID values.StringValue `json:"uid" yaml:"uid"`
|
||||||
|
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertRuleGroupV1 struct {
|
||||||
|
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
|
||||||
|
Name values.StringValue `json:"name" yaml:"name"`
|
||||||
|
Folder values.StringValue `json:"folder" yaml:"folder"`
|
||||||
|
Interval values.StringValue `json:"interval" yaml:"interval"`
|
||||||
|
Rules []AlertRuleV1 `json:"rules" yaml:"rules"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ruleGroupV1 *AlertRuleGroupV1) mapToModel() (AlertRuleGroup, error) {
|
||||||
|
ruleGroup := AlertRuleGroup{}
|
||||||
|
ruleGroup.Name = ruleGroupV1.Name.Value()
|
||||||
|
if strings.TrimSpace(ruleGroup.Name) == "" {
|
||||||
|
return AlertRuleGroup{}, errors.New("rule group has no name set")
|
||||||
|
}
|
||||||
|
ruleGroup.OrgID = ruleGroupV1.OrgID.Value()
|
||||||
|
if ruleGroup.OrgID < 1 {
|
||||||
|
ruleGroup.OrgID = 1
|
||||||
|
}
|
||||||
|
interval, err := time.ParseDuration(ruleGroupV1.Interval.Value())
|
||||||
|
if err != nil {
|
||||||
|
return AlertRuleGroup{}, err
|
||||||
|
}
|
||||||
|
ruleGroup.Interval = interval
|
||||||
|
ruleGroup.Folder = ruleGroupV1.Folder.Value()
|
||||||
|
if strings.TrimSpace(ruleGroup.Folder) == "" {
|
||||||
|
return AlertRuleGroup{}, errors.New("rule group has no folder set")
|
||||||
|
}
|
||||||
|
for _, ruleV1 := range ruleGroupV1.Rules {
|
||||||
|
rule, err := ruleV1.mapToModel(ruleGroup.OrgID)
|
||||||
|
if err != nil {
|
||||||
|
return AlertRuleGroup{}, err
|
||||||
|
}
|
||||||
|
ruleGroup.Rules = append(ruleGroup.Rules, rule)
|
||||||
|
}
|
||||||
|
return ruleGroup, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertRuleGroup struct {
|
||||||
|
OrgID int64
|
||||||
|
Name string
|
||||||
|
Folder string
|
||||||
|
Interval time.Duration
|
||||||
|
Rules []models.AlertRule
|
||||||
|
}
|
||||||
|
|
||||||
|
type AlertRuleV1 struct {
|
||||||
|
UID values.StringValue `json:"uid" yaml:"uid"`
|
||||||
|
Title values.StringValue `json:"title" yaml:"title"`
|
||||||
|
Condition values.StringValue `json:"condition" yaml:"condition"`
|
||||||
|
Data []QueryV1 `json:"data" yaml:"data"`
|
||||||
|
DashboardUID values.StringValue `json:"dasboardUid" yaml:"dashboardUid"`
|
||||||
|
PanelID values.Int64Value `json:"panelId" yaml:"panelId"`
|
||||||
|
NoDataState values.StringValue `json:"noDataState" yaml:"noDataState"`
|
||||||
|
ExecErrState values.StringValue `json:"execErrState" yaml:"execErrState"`
|
||||||
|
For values.StringValue `json:"for" yaml:"for"`
|
||||||
|
Annotations values.StringMapValue `json:"annotations" yaml:"annotations"`
|
||||||
|
Labels values.StringMapValue `json:"labels" yaml:"labels"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rule *AlertRuleV1) mapToModel(orgID int64) (models.AlertRule, error) {
|
||||||
|
alertRule := models.AlertRule{}
|
||||||
|
alertRule.Title = rule.Title.Value()
|
||||||
|
if alertRule.Title == "" {
|
||||||
|
return models.AlertRule{}, fmt.Errorf("rule has no title set")
|
||||||
|
}
|
||||||
|
alertRule.UID = rule.UID.Value()
|
||||||
|
if alertRule.UID == "" {
|
||||||
|
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no UID set", alertRule.Title)
|
||||||
|
}
|
||||||
|
alertRule.OrgID = orgID
|
||||||
|
duration, err := time.ParseDuration(rule.For.Value())
|
||||||
|
if err != nil {
|
||||||
|
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err)
|
||||||
|
}
|
||||||
|
alertRule.For = duration
|
||||||
|
dashboardUID := rule.DashboardUID.Value()
|
||||||
|
alertRule.DashboardUID = &dashboardUID
|
||||||
|
panelID := rule.PanelID.Value()
|
||||||
|
alertRule.PanelID = &panelID
|
||||||
|
execErrStateValue := strings.TrimSpace(rule.ExecErrState.Value())
|
||||||
|
execErrState, err := models.ErrStateFromString(execErrStateValue)
|
||||||
|
if err != nil && execErrStateValue != "" {
|
||||||
|
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err)
|
||||||
|
}
|
||||||
|
if execErrStateValue == "" {
|
||||||
|
execErrState = models.AlertingErrState
|
||||||
|
}
|
||||||
|
alertRule.ExecErrState = execErrState
|
||||||
|
noDataStateValue := strings.TrimSpace(rule.NoDataState.Value())
|
||||||
|
noDataState, err := models.NoDataStateFromString(noDataStateValue)
|
||||||
|
if err != nil && noDataStateValue != "" {
|
||||||
|
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err)
|
||||||
|
}
|
||||||
|
if noDataStateValue == "" {
|
||||||
|
noDataState = models.NoData
|
||||||
|
}
|
||||||
|
alertRule.NoDataState = noDataState
|
||||||
|
alertRule.Condition = rule.Condition.Value()
|
||||||
|
if alertRule.Condition == "" {
|
||||||
|
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no condition set", alertRule.Title)
|
||||||
|
}
|
||||||
|
alertRule.Annotations = rule.Annotations.Value()
|
||||||
|
alertRule.Labels = rule.Labels.Value()
|
||||||
|
for _, queryV1 := range rule.Data {
|
||||||
|
query, err := queryV1.mapToModel()
|
||||||
|
if err != nil {
|
||||||
|
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: %w", alertRule.Title, err)
|
||||||
|
}
|
||||||
|
alertRule.Data = append(alertRule.Data, query)
|
||||||
|
}
|
||||||
|
if len(alertRule.Data) == 0 {
|
||||||
|
return models.AlertRule{}, fmt.Errorf("rule '%s' failed to parse: no data set", alertRule.Title)
|
||||||
|
}
|
||||||
|
return alertRule, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type QueryV1 struct {
|
||||||
|
RefID values.StringValue `json:"refId" yaml:"refId"`
|
||||||
|
QueryType values.StringValue `json:"queryType" yaml:"queryType"`
|
||||||
|
RelativeTimeRange models.RelativeTimeRange `json:"relativeTimeRange" yaml:"relativeTimeRange"`
|
||||||
|
DatasourceUID values.StringValue `json:"datasourceUid" yaml:"datasourceUid"`
|
||||||
|
Model values.JSONValue `json:"model" yaml:"model"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (queryV1 *QueryV1) mapToModel() (models.AlertQuery, error) {
|
||||||
|
// In order to get the model into the format we need,
|
||||||
|
// we marshal it back to json and unmarshal it again
|
||||||
|
// in json.RawMessage. We do this as we cannot use
|
||||||
|
// json.RawMessage with a yaml files and have to use
|
||||||
|
// JSONValue that supports both, json and yaml.
|
||||||
|
encoded, err := json.Marshal(queryV1.Model.Value())
|
||||||
|
if err != nil {
|
||||||
|
return models.AlertQuery{}, err
|
||||||
|
}
|
||||||
|
var rawMessage json.RawMessage
|
||||||
|
err = json.Unmarshal(encoded, &rawMessage)
|
||||||
|
if err != nil {
|
||||||
|
return models.AlertQuery{}, err
|
||||||
|
}
|
||||||
|
return models.AlertQuery{
|
||||||
|
RefID: queryV1.RefID.Value(),
|
||||||
|
QueryType: queryV1.QueryType.Value(),
|
||||||
|
DatasourceUID: queryV1.DatasourceUID.Value(),
|
||||||
|
RelativeTimeRange: queryV1.RelativeTimeRange,
|
||||||
|
Model: rawMessage,
|
||||||
|
}, nil
|
||||||
|
}
|
218
pkg/services/provisioning/alerting/rules/types_test.go
Normal file
218
pkg/services/provisioning/alerting/rules/types_test.go
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
package rules
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/provisioning/values"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestRuleGroup(t *testing.T) {
|
||||||
|
t.Run("a valid rule group should not error", func(t *testing.T) {
|
||||||
|
rg := validRuleGroupV1(t)
|
||||||
|
_, err := rg.mapToModel()
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule group with out a name should error", func(t *testing.T) {
|
||||||
|
rg := validRuleGroupV1(t)
|
||||||
|
var name values.StringValue
|
||||||
|
err := yaml.Unmarshal([]byte(""), &name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rg.Name = name
|
||||||
|
_, err = rg.mapToModel()
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule group with out a folder should error", func(t *testing.T) {
|
||||||
|
rg := validRuleGroupV1(t)
|
||||||
|
var folder values.StringValue
|
||||||
|
err := yaml.Unmarshal([]byte(""), &folder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rg.Folder = folder
|
||||||
|
_, err = rg.mapToModel()
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule group with out an interval should error", func(t *testing.T) {
|
||||||
|
rg := validRuleGroupV1(t)
|
||||||
|
var interval values.StringValue
|
||||||
|
err := yaml.Unmarshal([]byte(""), &interval)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rg.Interval = interval
|
||||||
|
_, err = rg.mapToModel()
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule group with an invalid interval should error", func(t *testing.T) {
|
||||||
|
rg := validRuleGroupV1(t)
|
||||||
|
var interval values.StringValue
|
||||||
|
err := yaml.Unmarshal([]byte("10x"), &interval)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rg.Interval = interval
|
||||||
|
_, err = rg.mapToModel()
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule group with an empty org id should default to 1", func(t *testing.T) {
|
||||||
|
rg := validRuleGroupV1(t)
|
||||||
|
rg.OrgID = values.Int64Value{}
|
||||||
|
rgMapped, err := rg.mapToModel()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), rgMapped.OrgID)
|
||||||
|
})
|
||||||
|
t.Run("a rule group with a negative org id should default to 1", func(t *testing.T) {
|
||||||
|
rg := validRuleGroupV1(t)
|
||||||
|
orgID := values.Int64Value{}
|
||||||
|
err := yaml.Unmarshal([]byte("-1"), &orgID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rg.OrgID = orgID
|
||||||
|
rgMapped, err := rg.mapToModel()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, int64(1), rgMapped.OrgID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRules(t *testing.T) {
|
||||||
|
t.Run("a valid rule should not error", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
_, err := rule.mapToModel(1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule with out a uid should error", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
rule.UID = values.StringValue{}
|
||||||
|
_, err := rule.mapToModel(1)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule with out a title should error", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
rule.Title = values.StringValue{}
|
||||||
|
_, err := rule.mapToModel(1)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule with out a for duration should error", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
rule.For = values.StringValue{}
|
||||||
|
_, err := rule.mapToModel(1)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule with an invalid for duration should error", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
forDuration := values.StringValue{}
|
||||||
|
err := yaml.Unmarshal([]byte("10x"), &forDuration)
|
||||||
|
rule.For = forDuration
|
||||||
|
require.NoError(t, err)
|
||||||
|
_, err = rule.mapToModel(1)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule with out a condition should error", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
rule.Condition = values.StringValue{}
|
||||||
|
_, err := rule.mapToModel(1)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule with out data should error", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
rule.Data = []QueryV1{}
|
||||||
|
_, err := rule.mapToModel(1)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule with out execErrState should have sane defaults", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
ruleMapped, err := rule.mapToModel(1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ruleMapped.ExecErrState, models.AlertingErrState)
|
||||||
|
})
|
||||||
|
t.Run("a rule with invalid execErrState should error", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
execErrState := values.StringValue{}
|
||||||
|
err := yaml.Unmarshal([]byte("abc"), &execErrState)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rule.ExecErrState = execErrState
|
||||||
|
_, err = rule.mapToModel(1)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule with a valid execErrState should map it correctly", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
execErrState := values.StringValue{}
|
||||||
|
err := yaml.Unmarshal([]byte(models.OkErrState), &execErrState)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rule.ExecErrState = execErrState
|
||||||
|
ruleMapped, err := rule.mapToModel(1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ruleMapped.ExecErrState, models.OkErrState)
|
||||||
|
})
|
||||||
|
t.Run("a rule with out noDataState should have sane defaults", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
ruleMapped, err := rule.mapToModel(1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ruleMapped.NoDataState, models.NoData)
|
||||||
|
})
|
||||||
|
t.Run("a rule with an invalid noDataState should error", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
noDataState := values.StringValue{}
|
||||||
|
err := yaml.Unmarshal([]byte("abc"), &noDataState)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rule.NoDataState = noDataState
|
||||||
|
_, err = rule.mapToModel(1)
|
||||||
|
require.Error(t, err)
|
||||||
|
})
|
||||||
|
t.Run("a rule with a valid noDataState should map it correctly", func(t *testing.T) {
|
||||||
|
rule := validRuleV1(t)
|
||||||
|
noDataState := values.StringValue{}
|
||||||
|
err := yaml.Unmarshal([]byte(models.NoData), &noDataState)
|
||||||
|
require.NoError(t, err)
|
||||||
|
rule.NoDataState = noDataState
|
||||||
|
ruleMapped, err := rule.mapToModel(1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, ruleMapped.NoDataState, models.NoData)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func validRuleGroupV1(t *testing.T) AlertRuleGroupV1 {
|
||||||
|
t.Helper()
|
||||||
|
var (
|
||||||
|
orgID values.Int64Value
|
||||||
|
name values.StringValue
|
||||||
|
folder values.StringValue
|
||||||
|
interval values.StringValue
|
||||||
|
)
|
||||||
|
err := yaml.Unmarshal([]byte("1"), &orgID)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = yaml.Unmarshal([]byte("Test"), &name)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = yaml.Unmarshal([]byte("Test"), &folder)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = yaml.Unmarshal([]byte("10s"), &interval)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return AlertRuleGroupV1{
|
||||||
|
OrgID: orgID,
|
||||||
|
Name: name,
|
||||||
|
Folder: folder,
|
||||||
|
Interval: interval,
|
||||||
|
Rules: []AlertRuleV1{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validRuleV1(t *testing.T) AlertRuleV1 {
|
||||||
|
t.Helper()
|
||||||
|
var (
|
||||||
|
title values.StringValue
|
||||||
|
uid values.StringValue
|
||||||
|
forDuration values.StringValue
|
||||||
|
condition values.StringValue
|
||||||
|
)
|
||||||
|
err := yaml.Unmarshal([]byte("test"), &title)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = yaml.Unmarshal([]byte("test_uid"), &uid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = yaml.Unmarshal([]byte("10s"), &forDuration)
|
||||||
|
require.NoError(t, err)
|
||||||
|
err = yaml.Unmarshal([]byte("A"), &condition)
|
||||||
|
require.NoError(t, err)
|
||||||
|
return AlertRuleV1{
|
||||||
|
Title: title,
|
||||||
|
UID: uid,
|
||||||
|
For: forDuration,
|
||||||
|
Condition: condition,
|
||||||
|
Data: []QueryV1{{}},
|
||||||
|
}
|
||||||
|
}
|
@ -9,37 +9,50 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
plugifaces "github.com/grafana/grafana/pkg/plugins"
|
plugifaces "github.com/grafana/grafana/pkg/plugins"
|
||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
|
"github.com/grafana/grafana/pkg/services/accesscontrol"
|
||||||
"github.com/grafana/grafana/pkg/services/alerting"
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards"
|
dashboardservice "github.com/grafana/grafana/pkg/services/dashboards"
|
||||||
datasourceservice "github.com/grafana/grafana/pkg/services/datasources"
|
datasourceservice "github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/encryption"
|
"github.com/grafana/grafana/pkg/services/encryption"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
|
||||||
|
"github.com/grafana/grafana/pkg/services/ngalert/store"
|
||||||
"github.com/grafana/grafana/pkg/services/notifications"
|
"github.com/grafana/grafana/pkg/services/notifications"
|
||||||
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
"github.com/grafana/grafana/pkg/services/pluginsettings"
|
||||||
|
"github.com/grafana/grafana/pkg/services/provisioning/alerting/rules"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
|
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning/notifiers"
|
"github.com/grafana/grafana/pkg/services/provisioning/notifiers"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning/plugins"
|
"github.com/grafana/grafana/pkg/services/provisioning/plugins"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning/utils"
|
"github.com/grafana/grafana/pkg/services/provisioning/utils"
|
||||||
|
"github.com/grafana/grafana/pkg/services/quota"
|
||||||
"github.com/grafana/grafana/pkg/services/searchV2"
|
"github.com/grafana/grafana/pkg/services/searchV2"
|
||||||
"github.com/grafana/grafana/pkg/services/sqlstore"
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore plugifaces.Store,
|
func ProvideService(
|
||||||
encryptionService encryption.Internal, notificatonService *notifications.NotificationService,
|
ac accesscontrol.AccessControl,
|
||||||
|
cfg *setting.Cfg,
|
||||||
|
sqlStore *sqlstore.SQLStore,
|
||||||
|
pluginStore plugifaces.Store,
|
||||||
|
encryptionService encryption.Internal,
|
||||||
|
notificatonService *notifications.NotificationService,
|
||||||
dashboardProvisioningService dashboardservice.DashboardProvisioningService,
|
dashboardProvisioningService dashboardservice.DashboardProvisioningService,
|
||||||
datasourceService datasourceservice.DataSourceService,
|
datasourceService datasourceservice.DataSourceService,
|
||||||
dashboardService dashboardservice.DashboardService,
|
dashboardService dashboardservice.DashboardService,
|
||||||
alertingService *alerting.AlertNotificationService, pluginSettings pluginsettings.Service,
|
folderService dashboardservice.FolderService,
|
||||||
|
alertingService *alerting.AlertNotificationService,
|
||||||
|
pluginSettings pluginsettings.Service,
|
||||||
searchService searchV2.SearchService,
|
searchService searchV2.SearchService,
|
||||||
|
quotaService *quota.QuotaService,
|
||||||
) (*ProvisioningServiceImpl, error) {
|
) (*ProvisioningServiceImpl, error) {
|
||||||
s := &ProvisioningServiceImpl{
|
s := &ProvisioningServiceImpl{
|
||||||
Cfg: cfg,
|
Cfg: cfg,
|
||||||
SQLStore: sqlStore,
|
SQLStore: sqlStore,
|
||||||
|
ac: ac,
|
||||||
pluginStore: pluginStore,
|
pluginStore: pluginStore,
|
||||||
EncryptionService: encryptionService,
|
EncryptionService: encryptionService,
|
||||||
NotificationService: notificatonService,
|
NotificationService: notificatonService,
|
||||||
log: log.New("provisioning"),
|
|
||||||
newDashboardProvisioner: dashboards.New,
|
newDashboardProvisioner: dashboards.New,
|
||||||
provisionNotifiers: notifiers.Provision,
|
provisionNotifiers: notifiers.Provision,
|
||||||
provisionDatasources: datasources.Provision,
|
provisionDatasources: datasources.Provision,
|
||||||
@ -50,6 +63,8 @@ func ProvideService(cfg *setting.Cfg, sqlStore *sqlstore.SQLStore, pluginStore p
|
|||||||
alertingService: alertingService,
|
alertingService: alertingService,
|
||||||
pluginsSettings: pluginSettings,
|
pluginsSettings: pluginSettings,
|
||||||
searchService: searchService,
|
searchService: searchService,
|
||||||
|
quotaService: quotaService,
|
||||||
|
log: log.New("provisioning"),
|
||||||
}
|
}
|
||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
@ -61,18 +76,21 @@ type ProvisioningService interface {
|
|||||||
ProvisionPlugins(ctx context.Context) error
|
ProvisionPlugins(ctx context.Context) error
|
||||||
ProvisionNotifications(ctx context.Context) error
|
ProvisionNotifications(ctx context.Context) error
|
||||||
ProvisionDashboards(ctx context.Context) error
|
ProvisionDashboards(ctx context.Context) error
|
||||||
|
ProvisionAlertRules(ctx context.Context) error
|
||||||
GetDashboardProvisionerResolvedPath(name string) string
|
GetDashboardProvisionerResolvedPath(name string) string
|
||||||
GetAllowUIUpdatesFromConfig(name string) bool
|
GetAllowUIUpdatesFromConfig(name string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a public constructor for overriding service to be able to instantiate OSS as fallback
|
// Add a public constructor for overriding service to be able to instantiate OSS as fallback
|
||||||
func NewProvisioningServiceImpl() *ProvisioningServiceImpl {
|
func NewProvisioningServiceImpl() *ProvisioningServiceImpl {
|
||||||
|
logger := log.New("provisioning")
|
||||||
return &ProvisioningServiceImpl{
|
return &ProvisioningServiceImpl{
|
||||||
log: log.New("provisioning"),
|
log: logger,
|
||||||
newDashboardProvisioner: dashboards.New,
|
newDashboardProvisioner: dashboards.New,
|
||||||
provisionNotifiers: notifiers.Provision,
|
provisionNotifiers: notifiers.Provision,
|
||||||
provisionDatasources: datasources.Provision,
|
provisionDatasources: datasources.Provision,
|
||||||
provisionPlugins: plugins.Provision,
|
provisionPlugins: plugins.Provision,
|
||||||
|
provisionRules: rules.Provision,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +113,7 @@ func newProvisioningServiceImpl(
|
|||||||
type ProvisioningServiceImpl struct {
|
type ProvisioningServiceImpl struct {
|
||||||
Cfg *setting.Cfg
|
Cfg *setting.Cfg
|
||||||
SQLStore *sqlstore.SQLStore
|
SQLStore *sqlstore.SQLStore
|
||||||
|
ac accesscontrol.AccessControl
|
||||||
pluginStore plugifaces.Store
|
pluginStore plugifaces.Store
|
||||||
EncryptionService encryption.Internal
|
EncryptionService encryption.Internal
|
||||||
NotificationService *notifications.NotificationService
|
NotificationService *notifications.NotificationService
|
||||||
@ -105,6 +124,7 @@ type ProvisioningServiceImpl struct {
|
|||||||
provisionNotifiers func(context.Context, string, notifiers.Manager, notifiers.SQLStore, encryption.Internal, *notifications.NotificationService) error
|
provisionNotifiers func(context.Context, string, notifiers.Manager, notifiers.SQLStore, encryption.Internal, *notifications.NotificationService) error
|
||||||
provisionDatasources func(context.Context, string, datasources.Store, utils.OrgStore) error
|
provisionDatasources func(context.Context, string, datasources.Store, utils.OrgStore) error
|
||||||
provisionPlugins func(context.Context, string, plugins.Store, plugifaces.Store, pluginsettings.Service) error
|
provisionPlugins func(context.Context, string, plugins.Store, plugifaces.Store, pluginsettings.Service) error
|
||||||
|
provisionRules func(context.Context, string, dashboardservice.DashboardService, dashboardservice.DashboardProvisioningService, provisioning.AlertRuleService) error
|
||||||
mutex sync.Mutex
|
mutex sync.Mutex
|
||||||
dashboardProvisioningService dashboardservice.DashboardProvisioningService
|
dashboardProvisioningService dashboardservice.DashboardProvisioningService
|
||||||
dashboardService dashboardservice.DashboardService
|
dashboardService dashboardservice.DashboardService
|
||||||
@ -112,6 +132,7 @@ type ProvisioningServiceImpl struct {
|
|||||||
alertingService *alerting.AlertNotificationService
|
alertingService *alerting.AlertNotificationService
|
||||||
pluginsSettings pluginsettings.Service
|
pluginsSettings pluginsettings.Service
|
||||||
searchService searchV2.SearchService
|
searchService searchV2.SearchService
|
||||||
|
quotaService quota.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error {
|
func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error {
|
||||||
@ -130,6 +151,11 @@ func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) erro
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
err = ps.ProvisionAlertRules(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -218,6 +244,29 @@ func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) erro
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (ps *ProvisioningServiceImpl) ProvisionAlertRules(ctx context.Context) error {
|
||||||
|
alertRulesPath := filepath.Join(ps.Cfg.ProvisioningPath, "alerting")
|
||||||
|
st := store.DBstore{
|
||||||
|
BaseInterval: ps.Cfg.UnifiedAlerting.BaseInterval,
|
||||||
|
DefaultInterval: ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval,
|
||||||
|
SQLStore: ps.SQLStore,
|
||||||
|
Logger: ps.log,
|
||||||
|
FolderService: nil, // we don't use it yet
|
||||||
|
AccessControl: ps.ac,
|
||||||
|
DashboardService: ps.dashboardService,
|
||||||
|
}
|
||||||
|
ruleService := provisioning.NewAlertRuleService(
|
||||||
|
st,
|
||||||
|
st,
|
||||||
|
ps.quotaService,
|
||||||
|
ps.SQLStore,
|
||||||
|
int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
|
||||||
|
int64(ps.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
|
||||||
|
ps.log)
|
||||||
|
return rules.Provision(ctx, alertRulesPath, ps.dashboardService,
|
||||||
|
ps.dashboardProvisioningService, *ruleService)
|
||||||
|
}
|
||||||
|
|
||||||
func (ps *ProvisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string {
|
func (ps *ProvisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string {
|
||||||
return ps.dashboardProvisioner.GetProvisionerResolvedPath(name)
|
return ps.dashboardProvisioner.GetProvisionerResolvedPath(name)
|
||||||
}
|
}
|
||||||
|
@ -8,6 +8,7 @@ type Calls struct {
|
|||||||
ProvisionPlugins []interface{}
|
ProvisionPlugins []interface{}
|
||||||
ProvisionNotifications []interface{}
|
ProvisionNotifications []interface{}
|
||||||
ProvisionDashboards []interface{}
|
ProvisionDashboards []interface{}
|
||||||
|
ProvisionAlertRules []interface{}
|
||||||
GetDashboardProvisionerResolvedPath []interface{}
|
GetDashboardProvisionerResolvedPath []interface{}
|
||||||
GetAllowUIUpdatesFromConfig []interface{}
|
GetAllowUIUpdatesFromConfig []interface{}
|
||||||
Run []interface{}
|
Run []interface{}
|
||||||
@ -71,6 +72,11 @@ func (mock *ProvisioningServiceMock) ProvisionDashboards(ctx context.Context) er
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (mock *ProvisioningServiceMock) ProvisionAlertRules(ctx context.Context) error {
|
||||||
|
mock.Calls.ProvisionAlertRules = append(mock.Calls.ProvisionAlertRules, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (mock *ProvisioningServiceMock) GetDashboardProvisionerResolvedPath(name string) string {
|
func (mock *ProvisioningServiceMock) GetDashboardProvisionerResolvedPath(name string) string {
|
||||||
mock.Calls.GetDashboardProvisionerResolvedPath = append(mock.Calls.GetDashboardProvisionerResolvedPath, name)
|
mock.Calls.GetDashboardProvisionerResolvedPath = append(mock.Calls.GetDashboardProvisionerResolvedPath, name)
|
||||||
if mock.GetDashboardProvisionerResolvedPathFunc != nil {
|
if mock.GetDashboardProvisionerResolvedPathFunc != nil {
|
||||||
|
Reference in New Issue
Block a user