Alerting: Add file provisioning for contact points (#51924)

This commit is contained in:
Jean-Philippe Quéméner
2022-08-01 18:17:42 +02:00
committed by GitHub
parent e791a4e576
commit d9cace4dca
31 changed files with 611 additions and 184 deletions

View File

@ -101,7 +101,7 @@ func (hs *HTTPServer) AdminProvisioningReloadNotifications(c *models.ReqContext)
} }
func (hs *HTTPServer) AdminProvisioningReloadAlerting(c *models.ReqContext) response.Response { func (hs *HTTPServer) AdminProvisioningReloadAlerting(c *models.ReqContext) response.Response {
err := hs.ProvisioningService.ProvisionAlertRules(c.Req.Context()) err := hs.ProvisioningService.ProvisionAlerting(c.Req.Context())
if err != nil { if err != nil {
return response.Error(500, "", err) return response.Error(500, "", err)
} }

View File

@ -153,7 +153,7 @@ func TestAPI_AdminProvisioningReload_AccessControl(t *testing.T) {
}, },
url: "/api/admin/provisioning/alerting/reload", url: "/api/admin/provisioning/alerting/reload",
checkCall: func(mock provisioning.ProvisioningServiceMock) { checkCall: func(mock provisioning.ProvisioningServiceMock) {
assert.Len(t, mock.Calls.ProvisionAlertRules, 1) assert.Len(t, mock.Calls.ProvisionAlerting, 1)
}, },
}, },
{ {

View File

@ -1,4 +1,4 @@
package rules package alerting
import ( import (
"context" "context"
@ -22,34 +22,35 @@ func newRulesConfigReader(logger log.Logger) rulesConfigReader {
} }
} }
func (cr *rulesConfigReader) readConfig(ctx context.Context, path string) ([]*RuleFile, error) { func (cr *rulesConfigReader) readConfig(ctx context.Context, path string) ([]*AlertingFile, error) {
var alertRulesFiles []*RuleFile var alertFiles []*AlertingFile
cr.log.Debug("looking for alert rules provisioning files", "path", path) cr.log.Debug("looking for alerting provisioning files", "path", path)
files, err := ioutil.ReadDir(path) files, err := ioutil.ReadDir(path)
if err != nil { if err != nil {
cr.log.Error("can't read alert rules provisioning files from directory", "path", path, "error", err) cr.log.Error("can't read alerting provisioning files from directory", "path", path, "error", err)
return alertRulesFiles, nil return alertFiles, nil
} }
for _, file := range files { for _, file := range files {
cr.log.Debug("parsing alert rules provisioning file", "path", path, "file.Name", file.Name()) cr.log.Debug("parsing alerting provisioning file", "path", path, "file.Name", file.Name())
if !cr.isYAML(file.Name()) && !cr.isJSON(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()) return nil, fmt.Errorf("file has invalid suffix '%s' (.yaml,.yml,.json accepted)", file.Name())
} }
ruleFileV1, err := cr.parseConfig(path, file) alertFileV1, err := cr.parseConfig(path, file)
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failure to parse file %s: %w", file.Name(), err)
} }
if ruleFileV1 != nil { if alertFileV1 != nil {
ruleFile, err := ruleFileV1.MapToModel() alertFileV1.Filename = file.Name()
alertFile, err := alertFileV1.MapToModel()
if err != nil { if err != nil {
return nil, err return nil, fmt.Errorf("failure to map file %s: %w", alertFileV1.Filename, err)
} }
alertRulesFiles = append(alertRulesFiles, &ruleFile) alertFiles = append(alertFiles, &alertFile)
} }
} }
return alertRulesFiles, nil return alertFiles, nil
} }
func (cr *rulesConfigReader) isYAML(file string) bool { func (cr *rulesConfigReader) isYAML(file string) bool {
@ -60,7 +61,7 @@ func (cr *rulesConfigReader) isJSON(file string) bool {
return strings.HasSuffix(file, ".json") return strings.HasSuffix(file, ".json")
} }
func (cr *rulesConfigReader) parseConfig(path string, file fs.FileInfo) (*RuleFileV1, error) { func (cr *rulesConfigReader) parseConfig(path string, file fs.FileInfo) (*AlertingFileV1, error) {
filename, _ := filepath.Abs(filepath.Join(path, file.Name())) filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
// nolint:gosec // nolint:gosec
// We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath // We can ignore the gosec G304 warning on this one because `filename` comes from ps.Cfg.ProvisioningPath
@ -68,7 +69,7 @@ func (cr *rulesConfigReader) parseConfig(path string, file fs.FileInfo) (*RuleFi
if err != nil { if err != nil {
return nil, err return nil, err
} }
var cfg *RuleFileV1 var cfg *AlertingFileV1
err = yaml.Unmarshal(yamlFile, &cfg) err = yaml.Unmarshal(yamlFile, &cfg)
if err != nil { if err != nil {
return nil, err return nil, err

View File

@ -0,0 +1,103 @@
package alerting
import (
"context"
"testing"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/stretchr/testify/require"
)
const (
testFileBrokenYAML = "./testdata/common/broken-yaml"
testFileEmptyFile = "./testdata/common/empty-file"
testFileEmptyFolder = "./testdata/common/empty-folder"
testFileSupportedFiletypes = "./testdata/common/supported-filetypes"
testFileCorrectProperties = "./testdata/alert_rules/correct-properties"
testFileCorrectPropertiesWithOrg = "./testdata/alert_rules/correct-properties-with-org"
testFileMultipleRules = "./testdata/alert_rules/multiple-rules"
testFileMultipleFiles = "./testdata/alert_rules/multiple-files"
testFileCorrectProperties_cp = "./testdata/contact_points/correct-properties"
testFileCorrectPropertiesWithOrg_cp = "./testdata/contact_points/correct-properties-with-org"
testFileEmptyUID = "./testdata/contact_points/empty-uid"
testFileMissingUID = "./testdata/contact_points/missing-uid"
testFileWhitespaceUID = "./testdata/contact_points/whitespace-uid"
testFileMultipleCps = "./testdata/contact_points/multiple-contact-points"
)
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)
})
t.Run("a contact point file with correct properties should not error", func(t *testing.T) {
file, err := configReader.readConfig(ctx, testFileCorrectProperties_cp)
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), file[0].ContactPoints[0].OrgID)
})
})
t.Run("a contact point file with correct properties and specific org should not error", func(t *testing.T) {
file, err := configReader.readConfig(ctx, testFileCorrectPropertiesWithOrg_cp)
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), file[0].ContactPoints[0].OrgID)
})
})
t.Run("a contact point file with empty UID should fail", func(t *testing.T) {
_, err := configReader.readConfig(ctx, testFileEmptyUID)
require.Error(t, err)
})
t.Run("a contact point file with missing UID should fail", func(t *testing.T) {
_, err := configReader.readConfig(ctx, testFileMissingUID)
require.Error(t, err)
})
t.Run("a contact point file with whitespace UID should fail", func(t *testing.T) {
_, err := configReader.readConfig(ctx, testFileWhitespaceUID)
require.Error(t, err)
})
t.Run("the config reader should be able to read a file with multiple contact points", func(t *testing.T) {
file, err := configReader.readConfig(ctx, testFileMultipleCps)
require.NoError(t, err)
require.Len(t, file[0].ContactPoints, 2)
})
}

View File

@ -0,0 +1,80 @@
package alerting
import (
"context"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
)
type ContactPointProvisioner interface {
Provision(ctx context.Context, files []*AlertingFile) error
Unprovision(ctx context.Context, files []*AlertingFile) error
}
type defaultContactPointProvisioner struct {
logger log.Logger
contactPointService provisioning.ContactPointService
}
func NewContactPointProvisoner(logger log.Logger,
contactPointService provisioning.ContactPointService) ContactPointProvisioner {
return &defaultContactPointProvisioner{
logger: logger,
contactPointService: contactPointService,
}
}
func (c *defaultContactPointProvisioner) Provision(ctx context.Context,
files []*AlertingFile) error {
cpsCache := map[int64][]definitions.EmbeddedContactPoint{}
for _, file := range files {
for _, contactPointsConfig := range file.ContactPoints {
// check if we already fetched the contact points for this org.
// if not we fetch them and populate the cache.
if _, exists := cpsCache[contactPointsConfig.OrgID]; !exists {
cps, err := c.contactPointService.GetContactPoints(ctx, provisioning.ContactPointQuery{
OrgID: contactPointsConfig.OrgID,
})
if err != nil {
return err
}
cpsCache[contactPointsConfig.OrgID] = cps
}
outer:
for _, contactPoint := range contactPointsConfig.ContactPoints {
for _, fetchedCP := range cpsCache[contactPointsConfig.OrgID] {
if fetchedCP.UID == contactPoint.UID {
err := c.contactPointService.UpdateContactPoint(ctx,
contactPointsConfig.OrgID, contactPoint, models.ProvenanceFile)
if err != nil {
return err
}
continue outer
}
}
_, err := c.contactPointService.CreateContactPoint(ctx, contactPointsConfig.OrgID,
contactPoint, models.ProvenanceFile)
if err != nil {
return err
}
}
}
}
return nil
}
func (c *defaultContactPointProvisioner) Unprovision(ctx context.Context,
files []*AlertingFile) error {
for _, file := range files {
for _, cp := range file.DeleteContactPoints {
err := c.contactPointService.DeleteContactPoint(ctx, cp.OrgID, cp.UID)
if err != nil {
return err
}
}
}
return nil
}

View File

@ -0,0 +1,104 @@
package alerting
import (
"context"
"fmt"
"strings"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/services/ngalert/api/tooling/definitions"
"github.com/grafana/grafana/pkg/services/ngalert/models"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
type DeleteContactPointV1 struct {
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
UID values.StringValue `json:"uid" yaml:"uid"`
}
func (v1 *DeleteContactPointV1) MapToModel() DeleteContactPoint {
orgID := v1.OrgID.Value()
if orgID < 1 {
orgID = 1
}
return DeleteContactPoint{
OrgID: orgID,
UID: v1.UID.Value(),
}
}
type DeleteContactPoint struct {
OrgID int64 `json:"orgId" yaml:"orgId"`
UID string `json:"uid" yaml:"uid"`
}
type ContactPointV1 struct {
OrgID values.Int64Value `json:"orgId" yaml:"orgId"`
Name values.StringValue `json:"name" yaml:"name"`
Receivers []ReceiverV1 `json:"receivers" yaml:"receivers"`
}
func (cpV1 *ContactPointV1) MapToModel() (ContactPoint, error) {
contactPoint := ContactPoint{}
orgID := cpV1.OrgID.Value()
if orgID < 1 {
orgID = 1
}
contactPoint.OrgID = orgID
name := strings.TrimSpace(cpV1.Name.Value())
if name == "" {
return ContactPoint{}, fmt.Errorf("no name is set")
}
for _, receiverV1 := range cpV1.Receivers {
embeddedCP, err := receiverV1.mapToModel(name)
if err != nil {
return ContactPoint{}, fmt.Errorf("%s: %w", name, err)
}
contactPoint.ContactPoints = append(contactPoint.ContactPoints, embeddedCP)
}
return contactPoint, nil
}
type ContactPoint struct {
OrgID int64 `json:"orgId" yaml:"orgId"`
ContactPoints []definitions.EmbeddedContactPoint `json:"configs" yaml:"configs"`
}
type ReceiverV1 struct {
UID values.StringValue `json:"uid" yaml:"uid"`
Type values.StringValue `json:"type" yaml:"type"`
Settings values.JSONValue `json:"settings" yaml:"settings"`
DisableResolveMessage values.BoolValue `json:"disableResolveMessage"`
}
func (config *ReceiverV1) mapToModel(name string) (definitions.EmbeddedContactPoint, error) {
uid := strings.TrimSpace(config.UID.Value())
if uid == "" {
return definitions.EmbeddedContactPoint{}, fmt.Errorf("no uid is set")
}
cpType := strings.TrimSpace(config.Type.Value())
if cpType == "" {
return definitions.EmbeddedContactPoint{}, fmt.Errorf("no type is set")
}
if len(config.Settings.Value()) == 0 {
return definitions.EmbeddedContactPoint{}, fmt.Errorf("no settings are set")
}
settings := simplejson.NewFromAny(config.Settings.Raw)
cp := definitions.EmbeddedContactPoint{
UID: uid,
Name: name,
Type: cpType,
DisableResolveMessage: config.DisableResolveMessage.Value(),
Provenance: string(models.ProvenanceFile),
Settings: settings,
}
// As the values are not encrypted when coming from disk files,
// we can simply return the fallback for validation.
err := cp.Valid(func(_ context.Context, _ map[string][]byte, _, fallback string) string {
return fallback
})
if err != nil {
return definitions.EmbeddedContactPoint{}, err
}
return cp, nil
}

View File

@ -0,0 +1,86 @@
package alerting
import (
"testing"
"github.com/grafana/grafana/pkg/services/provisioning/values"
"github.com/stretchr/testify/require"
"gopkg.in/yaml.v2"
)
func TestReceivers(t *testing.T) {
t.Run("Valid config should not error on mapping", func(t *testing.T) {
cp := validReceiverV1(t)
_, err := cp.mapToModel("test")
require.NoError(t, err)
})
t.Run("Invalid config should error on mapping", func(t *testing.T) {
cp := validReceiverV1(t)
var settings values.JSONValue
err := yaml.Unmarshal([]byte(`{"not-valid": "http://test-url"}`), &settings)
require.NoError(t, err)
cp.Settings = settings
_, err = cp.mapToModel("test")
require.Error(t, err)
})
t.Run("Empty config should error on mapping", func(t *testing.T) {
cp := validReceiverV1(t)
var settings values.JSONValue
err := yaml.Unmarshal([]byte(`{}`), &settings)
require.NoError(t, err)
cp.Settings = settings
_, err = cp.mapToModel("test")
require.Error(t, err)
})
t.Run("Missing UID should error on mapping", func(t *testing.T) {
cp := validReceiverV1(t)
var uid values.StringValue
err := yaml.Unmarshal([]byte(""), &uid)
require.NoError(t, err)
cp.UID = uid
_, err = cp.mapToModel("test")
require.Error(t, err)
})
t.Run("Missing type should error on mapping", func(t *testing.T) {
cp := validReceiverV1(t)
var _type values.StringValue
err := yaml.Unmarshal([]byte(""), &_type)
require.NoError(t, err)
cp.Type = _type
_, err = cp.mapToModel("test")
require.Error(t, err)
})
t.Run("Ivalid type should error on mapping", func(t *testing.T) {
cp := validReceiverV1(t)
var _type values.StringValue
err := yaml.Unmarshal([]byte("some-type-that-does-not-exist"), &_type)
require.NoError(t, err)
cp.Type = _type
_, err = cp.mapToModel("test")
require.Error(t, err)
})
}
func validReceiverV1(t *testing.T) ReceiverV1 {
t.Helper()
var (
uid values.StringValue
_type values.StringValue
settings values.JSONValue
disableResolveMessage values.BoolValue
)
err := yaml.Unmarshal([]byte("my_uid"), &uid)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("prometheus-alertmanager"), &_type)
require.NoError(t, err)
err = yaml.Unmarshal([]byte(`{"url": "http://test-url"}`), &settings)
require.NoError(t, err)
err = yaml.Unmarshal([]byte("false"), &disableResolveMessage)
require.NoError(t, err)
return ReceiverV1{
UID: uid,
Type: _type,
Settings: settings,
DisableResolveMessage: disableResolveMessage,
}
}

View File

@ -0,0 +1,50 @@
package alerting
import (
"context"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/dashboards"
"github.com/grafana/grafana/pkg/services/ngalert/provisioning"
)
type ProvisionerConfig struct {
Path string
DashboardService dashboards.DashboardService
DashboardProvService dashboards.DashboardProvisioningService
RuleService provisioning.AlertRuleService
ContactPointService provisioning.ContactPointService
}
func Provision(ctx context.Context, cfg ProvisionerConfig) error {
logger := log.New("provisioning.alerting")
cfgReader := newRulesConfigReader(logger)
files, err := cfgReader.readConfig(ctx, cfg.Path)
if err != nil {
return err
}
logger.Info("starting to provision alerting")
logger.Debug("read all alerting files", "file_count", len(files))
ruleProvisioner := NewAlertRuleProvisioner(
logger,
cfg.DashboardService,
cfg.DashboardProvService,
cfg.RuleService)
err = ruleProvisioner.Provision(ctx, files)
if err != nil {
return err
}
cpProvisioner := NewContactPointProvisoner(logger, cfg.ContactPointService)
err = cpProvisioner.Provision(ctx, files)
if err != nil {
return err
}
// TODO: provision notificiation policy in between so that when applying it
// new objects already exists and old ones are still there
err = cpProvisioner.Unprovision(ctx, files)
if err != nil {
return err
}
logger.Info("finished to provision alerting")
return nil
}

View File

@ -1,66 +0,0 @@
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)
})
}

View File

@ -1,4 +1,4 @@
package rules package alerting
import ( import (
"context" "context"
@ -14,7 +14,7 @@ import (
) )
type AlertRuleProvisioner interface { type AlertRuleProvisioner interface {
Provision(ctx context.Context, path string) error Provision(ctx context.Context, files []*AlertingFile) error
} }
func NewAlertRuleProvisioner( func NewAlertRuleProvisioner(
@ -24,7 +24,6 @@ func NewAlertRuleProvisioner(
ruleService provisioning.AlertRuleService) AlertRuleProvisioner { ruleService provisioning.AlertRuleService) AlertRuleProvisioner {
return &defaultAlertRuleProvisioner{ return &defaultAlertRuleProvisioner{
logger: logger, logger: logger,
cfgReader: newRulesConfigReader(logger),
dashboardService: dashboardService, dashboardService: dashboardService,
dashboardProvService: dashboardProvService, dashboardProvService: dashboardProvService,
ruleService: ruleService, ruleService: ruleService,
@ -33,47 +32,14 @@ func NewAlertRuleProvisioner(
type defaultAlertRuleProvisioner struct { type defaultAlertRuleProvisioner struct {
logger log.Logger logger log.Logger
cfgReader rulesConfigReader
dashboardService dashboards.DashboardService dashboardService dashboards.DashboardService
dashboardProvService dashboards.DashboardProvisioningService dashboardProvService dashboards.DashboardProvisioningService
ruleService provisioning.AlertRuleService 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, func (prov *defaultAlertRuleProvisioner) Provision(ctx context.Context,
path string) error { files []*AlertingFile) error {
prov.logger.Info("starting to provision the alert rules") for _, file := range files {
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 { for _, group := range file.Groups {
folderUID, err := prov.getOrCreateFolderUID(ctx, group.Folder, group.OrgID) folderUID, err := prov.getOrCreateFolderUID(ctx, group.Folder, group.OrgID)
if err != nil { if err != nil {

View File

@ -1,4 +1,4 @@
package rules package alerting
import ( import (
"encoding/json" "encoding/json"
@ -11,46 +11,6 @@ import (
"github.com/grafana/grafana/pkg/services/provisioning/values" "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 { type RuleDelete struct {
UID string UID string
OrgID int64 OrgID int64
@ -69,7 +29,7 @@ type AlertRuleGroupV1 struct {
Rules []AlertRuleV1 `json:"rules" yaml:"rules"` Rules []AlertRuleV1 `json:"rules" yaml:"rules"`
} }
func (ruleGroupV1 *AlertRuleGroupV1) mapToModel() (AlertRuleGroup, error) { func (ruleGroupV1 *AlertRuleGroupV1) MapToModel() (AlertRuleGroup, error) {
ruleGroup := AlertRuleGroup{} ruleGroup := AlertRuleGroup{}
ruleGroup.Name = ruleGroupV1.Name.Value() ruleGroup.Name = ruleGroupV1.Name.Value()
if strings.TrimSpace(ruleGroup.Name) == "" { if strings.TrimSpace(ruleGroup.Name) == "" {

View File

@ -1,4 +1,4 @@
package rules package alerting
import ( import (
"testing" "testing"
@ -12,7 +12,7 @@ import (
func TestRuleGroup(t *testing.T) { func TestRuleGroup(t *testing.T) {
t.Run("a valid rule group should not error", func(t *testing.T) { t.Run("a valid rule group should not error", func(t *testing.T) {
rg := validRuleGroupV1(t) rg := validRuleGroupV1(t)
_, err := rg.mapToModel() _, err := rg.MapToModel()
require.NoError(t, err) require.NoError(t, err)
}) })
t.Run("a rule group with out a name should error", func(t *testing.T) { t.Run("a rule group with out a name should error", func(t *testing.T) {
@ -21,7 +21,7 @@ func TestRuleGroup(t *testing.T) {
err := yaml.Unmarshal([]byte(""), &name) err := yaml.Unmarshal([]byte(""), &name)
require.NoError(t, err) require.NoError(t, err)
rg.Name = name rg.Name = name
_, err = rg.mapToModel() _, err = rg.MapToModel()
require.Error(t, err) require.Error(t, err)
}) })
t.Run("a rule group with out a folder should error", func(t *testing.T) { t.Run("a rule group with out a folder should error", func(t *testing.T) {
@ -30,7 +30,7 @@ func TestRuleGroup(t *testing.T) {
err := yaml.Unmarshal([]byte(""), &folder) err := yaml.Unmarshal([]byte(""), &folder)
require.NoError(t, err) require.NoError(t, err)
rg.Folder = folder rg.Folder = folder
_, err = rg.mapToModel() _, err = rg.MapToModel()
require.Error(t, err) require.Error(t, err)
}) })
t.Run("a rule group with out an interval should error", func(t *testing.T) { t.Run("a rule group with out an interval should error", func(t *testing.T) {
@ -39,7 +39,7 @@ func TestRuleGroup(t *testing.T) {
err := yaml.Unmarshal([]byte(""), &interval) err := yaml.Unmarshal([]byte(""), &interval)
require.NoError(t, err) require.NoError(t, err)
rg.Interval = interval rg.Interval = interval
_, err = rg.mapToModel() _, err = rg.MapToModel()
require.Error(t, err) require.Error(t, err)
}) })
t.Run("a rule group with an invalid interval should error", func(t *testing.T) { t.Run("a rule group with an invalid interval should error", func(t *testing.T) {
@ -48,13 +48,13 @@ func TestRuleGroup(t *testing.T) {
err := yaml.Unmarshal([]byte("10x"), &interval) err := yaml.Unmarshal([]byte("10x"), &interval)
require.NoError(t, err) require.NoError(t, err)
rg.Interval = interval rg.Interval = interval
_, err = rg.mapToModel() _, err = rg.MapToModel()
require.Error(t, err) require.Error(t, err)
}) })
t.Run("a rule group with an empty org id should default to 1", func(t *testing.T) { t.Run("a rule group with an empty org id should default to 1", func(t *testing.T) {
rg := validRuleGroupV1(t) rg := validRuleGroupV1(t)
rg.OrgID = values.Int64Value{} rg.OrgID = values.Int64Value{}
rgMapped, err := rg.mapToModel() rgMapped, err := rg.MapToModel()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(1), rgMapped.OrgID) require.Equal(t, int64(1), rgMapped.OrgID)
}) })
@ -64,7 +64,7 @@ func TestRuleGroup(t *testing.T) {
err := yaml.Unmarshal([]byte("-1"), &orgID) err := yaml.Unmarshal([]byte("-1"), &orgID)
require.NoError(t, err) require.NoError(t, err)
rg.OrgID = orgID rg.OrgID = orgID
rgMapped, err := rg.mapToModel() rgMapped, err := rg.MapToModel()
require.NoError(t, err) require.NoError(t, err)
require.Equal(t, int64(1), rgMapped.OrgID) require.Equal(t, int64(1), rgMapped.OrgID)
}) })

View File

@ -0,0 +1,9 @@
apiVersion: 1
contactPoints:
- name: cp_1
orgId: 1337
receivers:
- uid: first_uid
type: prometheus-alertmanager
settings:
url: http://test:9000

View File

@ -0,0 +1,8 @@
apiVersion: 1
contactPoints:
- name: cp_1
receivers:
- uid: first_uid
type: prometheus-alertmanager
settings:
url: http://test:9000

View File

@ -0,0 +1,8 @@
apiVersion: 1
contactPoints:
- name: cp_1
receivers:
- uid: ""
type: prometheus-alertmanager
settings:
url: http://test:9000

View File

@ -0,0 +1,7 @@
apiVersion: 1
contactPoints:
- name: cp_1
receivers:
- type: prometheus-alertmanager
settings:
url: http://test:9000

View File

@ -0,0 +1,14 @@
apiVersion: 1
contactPoints:
- name: cp_1
receivers:
- uid: first_uid
type: prometheus-alertmanager
settings:
url: http://test:9000
- name: cp_2
receivers:
- uid: second_uid
type: prometheus-alertmanager
settings:
url: http://test:9000

View File

@ -0,0 +1,8 @@
apiVersion: 1
contactPoints:
- name: cp_1
receivers:
- uid: " "
type: prometheus-alertmanager
settings:
url: http://test:9000

View File

@ -0,0 +1,77 @@
package alerting
import (
"fmt"
"github.com/grafana/grafana/pkg/services/provisioning/values"
)
type configVersion struct {
APIVersion values.Int64Value `json:"apiVersion" yaml:"apiVersion"`
}
type AlertingFile struct {
configVersion
Groups []AlertRuleGroup
DeleteRules []RuleDelete
ContactPoints []ContactPoint
DeleteContactPoints []DeleteContactPoint
}
type AlertingFileV1 struct {
configVersion
Filename string
Groups []AlertRuleGroupV1 `json:"groups" yaml:"groups"`
DeleteRules []RuleDeleteV1 `json:"deleteRules" yaml:"deleteRules"`
ContactPoints []ContactPointV1 `json:"contactPoints" yaml:"contactPoints"`
DeleteContactPoints []DeleteContactPointV1 `json:"deleteContactPoints" yaml:"deleteContactPoints"`
}
func (fileV1 *AlertingFileV1) MapToModel() (AlertingFile, error) {
alertingFile := AlertingFile{}
err := fileV1.mapRules(&alertingFile)
if err != nil {
return AlertingFile{}, fmt.Errorf("failure parsing rules: %w", err)
}
err = fileV1.mapContactPoint(&alertingFile)
if err != nil {
return AlertingFile{}, fmt.Errorf("failure parsing contact points: %w", err)
}
return alertingFile, nil
}
func (fileV1 *AlertingFileV1) mapContactPoint(alertingFile *AlertingFile) error {
for _, dcp := range fileV1.DeleteContactPoints {
alertingFile.DeleteContactPoints = append(alertingFile.DeleteContactPoints, dcp.MapToModel())
}
for _, contactPointV1 := range fileV1.ContactPoints {
contactPoint, err := contactPointV1.MapToModel()
if err != nil {
return err
}
alertingFile.ContactPoints = append(alertingFile.ContactPoints, contactPoint)
}
return nil
}
func (fileV1 *AlertingFileV1) mapRules(alertingFile *AlertingFile) error {
for _, groupV1 := range fileV1.Groups {
group, err := groupV1.MapToModel()
if err != nil {
return err
}
alertingFile.Groups = append(alertingFile.Groups, group)
}
for _, ruleDeleteV1 := range fileV1.DeleteRules {
orgID := ruleDeleteV1.OrgID.Value()
if orgID < 1 {
orgID = 1
}
ruleDelete := RuleDelete{
UID: ruleDeleteV1.UID.Value(),
OrgID: orgID,
}
alertingFile.DeleteRules = append(alertingFile.DeleteRules, ruleDelete)
}
return nil
}

View File

@ -19,7 +19,7 @@ import (
"github.com/grafana/grafana/pkg/services/ngalert/store" "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" prov_alerting "github.com/grafana/grafana/pkg/services/provisioning/alerting"
"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"
@ -27,6 +27,7 @@ import (
"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/quota"
"github.com/grafana/grafana/pkg/services/searchV2" "github.com/grafana/grafana/pkg/services/searchV2"
"github.com/grafana/grafana/pkg/services/secrets"
"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"
) )
@ -47,6 +48,7 @@ func ProvideService(
pluginSettings pluginsettings.Service, pluginSettings pluginsettings.Service,
searchService searchV2.SearchService, searchService searchV2.SearchService,
quotaService quota.Service, quotaService quota.Service,
secrectService secrets.Service,
) (*ProvisioningServiceImpl, error) { ) (*ProvisioningServiceImpl, error) {
s := &ProvisioningServiceImpl{ s := &ProvisioningServiceImpl{
Cfg: cfg, Cfg: cfg,
@ -59,6 +61,7 @@ func ProvideService(
provisionNotifiers: notifiers.Provision, provisionNotifiers: notifiers.Provision,
provisionDatasources: datasources.Provision, provisionDatasources: datasources.Provision,
provisionPlugins: plugins.Provision, provisionPlugins: plugins.Provision,
provisionAlerting: prov_alerting.Provision,
dashboardProvisioningService: dashboardProvisioningService, dashboardProvisioningService: dashboardProvisioningService,
dashboardService: dashboardService, dashboardService: dashboardService,
datasourceService: datasourceService, datasourceService: datasourceService,
@ -67,6 +70,7 @@ func ProvideService(
pluginsSettings: pluginSettings, pluginsSettings: pluginSettings,
searchService: searchService, searchService: searchService,
quotaService: quotaService, quotaService: quotaService,
secretService: secrectService,
log: log.New("provisioning"), log: log.New("provisioning"),
} }
return s, nil return s, nil
@ -79,7 +83,7 @@ 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 ProvisionAlerting(ctx context.Context) error
GetDashboardProvisionerResolvedPath(name string) string GetDashboardProvisionerResolvedPath(name string) string
GetAllowUIUpdatesFromConfig(name string) bool GetAllowUIUpdatesFromConfig(name string) bool
} }
@ -93,7 +97,6 @@ func NewProvisioningServiceImpl() *ProvisioningServiceImpl {
provisionNotifiers: notifiers.Provision, provisionNotifiers: notifiers.Provision,
provisionDatasources: datasources.Provision, provisionDatasources: datasources.Provision,
provisionPlugins: plugins.Provision, provisionPlugins: plugins.Provision,
provisionRules: rules.Provision,
} }
} }
@ -127,7 +130,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, datasources.CorrelationsStore, utils.OrgStore) error provisionDatasources func(context.Context, string, datasources.Store, datasources.CorrelationsStore, 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 provisionAlerting func(context.Context, prov_alerting.ProvisionerConfig) error
mutex sync.Mutex mutex sync.Mutex
dashboardProvisioningService dashboardservice.DashboardProvisioningService dashboardProvisioningService dashboardservice.DashboardProvisioningService
dashboardService dashboardservice.DashboardService dashboardService dashboardservice.DashboardService
@ -137,6 +140,7 @@ type ProvisioningServiceImpl struct {
pluginsSettings pluginsettings.Service pluginsSettings pluginsettings.Service
searchService searchV2.SearchService searchService searchV2.SearchService
quotaService quota.Service quotaService quota.Service
secretService secrets.Service
} }
func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error { func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) error {
@ -155,7 +159,7 @@ func (ps *ProvisioningServiceImpl) RunInitProvisioners(ctx context.Context) erro
return err return err
} }
err = ps.ProvisionAlertRules(ctx) err = ps.ProvisionAlerting(ctx)
if err != nil { if err != nil {
return err return err
} }
@ -248,8 +252,8 @@ func (ps *ProvisioningServiceImpl) ProvisionDashboards(ctx context.Context) erro
return nil return nil
} }
func (ps *ProvisioningServiceImpl) ProvisionAlertRules(ctx context.Context) error { func (ps *ProvisioningServiceImpl) ProvisionAlerting(ctx context.Context) error {
alertRulesPath := filepath.Join(ps.Cfg.ProvisioningPath, "alerting") alertingPath := filepath.Join(ps.Cfg.ProvisioningPath, "alerting")
st := store.DBstore{ st := store.DBstore{
Cfg: ps.Cfg.UnifiedAlerting, Cfg: ps.Cfg.UnifiedAlerting,
SQLStore: ps.SQLStore, SQLStore: ps.SQLStore,
@ -266,8 +270,16 @@ func (ps *ProvisioningServiceImpl) ProvisionAlertRules(ctx context.Context) erro
int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()), int64(ps.Cfg.UnifiedAlerting.DefaultRuleEvaluationInterval.Seconds()),
int64(ps.Cfg.UnifiedAlerting.BaseInterval.Seconds()), int64(ps.Cfg.UnifiedAlerting.BaseInterval.Seconds()),
ps.log) ps.log)
return rules.Provision(ctx, alertRulesPath, ps.dashboardService, contactPointService := provisioning.NewContactPointService(&st, ps.secretService,
ps.dashboardProvisioningService, *ruleService) st, ps.SQLStore, ps.log)
cfg := prov_alerting.ProvisionerConfig{
Path: alertingPath,
RuleService: *ruleService,
DashboardService: ps.dashboardService,
DashboardProvService: ps.dashboardProvisioningService,
ContactPointService: *contactPointService,
}
return ps.provisionAlerting(ctx, cfg)
} }
func (ps *ProvisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string { func (ps *ProvisioningServiceImpl) GetDashboardProvisionerResolvedPath(name string) string {

View File

@ -8,7 +8,7 @@ type Calls struct {
ProvisionPlugins []interface{} ProvisionPlugins []interface{}
ProvisionNotifications []interface{} ProvisionNotifications []interface{}
ProvisionDashboards []interface{} ProvisionDashboards []interface{}
ProvisionAlertRules []interface{} ProvisionAlerting []interface{}
GetDashboardProvisionerResolvedPath []interface{} GetDashboardProvisionerResolvedPath []interface{}
GetAllowUIUpdatesFromConfig []interface{} GetAllowUIUpdatesFromConfig []interface{}
Run []interface{} Run []interface{}
@ -72,8 +72,8 @@ func (mock *ProvisioningServiceMock) ProvisionDashboards(ctx context.Context) er
return nil return nil
} }
func (mock *ProvisioningServiceMock) ProvisionAlertRules(ctx context.Context) error { func (mock *ProvisioningServiceMock) ProvisionAlerting(ctx context.Context) error {
mock.Calls.ProvisionAlertRules = append(mock.Calls.ProvisionAlertRules, nil) mock.Calls.ProvisionAlerting = append(mock.Calls.ProvisionAlerting, nil)
return nil return nil
} }