From 6e3e9a337dbbcbf20e4990f0492364244ee58b40 Mon Sep 17 00:00:00 2001
From: Pavel Bakulev
Date: Wed, 28 Nov 2018 16:35:42 +0200
Subject: [PATCH] Added alert_notification configuration
---
.../alert_notifications/sample.yaml | 20 ++
docs/sources/administration/provisioning.md | 178 ++++++++++++++
pkg/services/alerting/rule.go | 18 +-
pkg/services/alerting/rule_test.go | 156 +++++++++++++
.../alert_notifications.go | 151 ++++++++++++
.../alert_notifications/config_reader.go | 115 +++++++++
.../alert_notifications/config_reader_test.go | 219 ++++++++++++++++++
.../test-configs/broken-yaml/broken.yaml | 9 +
.../test-configs/broken-yaml/not.yaml.text | 6 +
.../correct-properties.yaml | 26 +++
.../test-configs/double-default/default-1.yml | 4 +
.../double-default/default-2.yaml | 4 +
.../test-configs/empty/empty.yaml | 0
.../test-configs/empty_folder/.gitignore | 4 +
.../two-notifications/two-notifications.yaml | 5 +
.../unknown-notifier/notification.yaml | 3 +
.../provisioning/alert_notifications/types.go | 19 ++
pkg/services/provisioning/provisioning.go | 6 +
18 files changed, 941 insertions(+), 2 deletions(-)
create mode 100644 conf/provisioning/alert_notifications/sample.yaml
create mode 100644 pkg/services/provisioning/alert_notifications/alert_notifications.go
create mode 100644 pkg/services/provisioning/alert_notifications/config_reader.go
create mode 100644 pkg/services/provisioning/alert_notifications/config_reader_test.go
create mode 100644 pkg/services/provisioning/alert_notifications/test-configs/broken-yaml/broken.yaml
create mode 100644 pkg/services/provisioning/alert_notifications/test-configs/broken-yaml/not.yaml.text
create mode 100644 pkg/services/provisioning/alert_notifications/test-configs/correct-properties/correct-properties.yaml
create mode 100644 pkg/services/provisioning/alert_notifications/test-configs/double-default/default-1.yml
create mode 100644 pkg/services/provisioning/alert_notifications/test-configs/double-default/default-2.yaml
create mode 100644 pkg/services/provisioning/alert_notifications/test-configs/empty/empty.yaml
create mode 100644 pkg/services/provisioning/alert_notifications/test-configs/empty_folder/.gitignore
create mode 100644 pkg/services/provisioning/alert_notifications/test-configs/two-notifications/two-notifications.yaml
create mode 100644 pkg/services/provisioning/alert_notifications/test-configs/unknown-notifier/notification.yaml
create mode 100644 pkg/services/provisioning/alert_notifications/types.go
diff --git a/conf/provisioning/alert_notifications/sample.yaml b/conf/provisioning/alert_notifications/sample.yaml
new file mode 100644
index 00000000000..92d03a81720
--- /dev/null
+++ b/conf/provisioning/alert_notifications/sample.yaml
@@ -0,0 +1,20 @@
+# # config file version
+apiVersion: 1
+
+# alert_notifications:
+# - name: default-slack
+# type: slack
+# org_id: 1
+# is_default: true
+# settings:
+# recipient: "XXX"
+# token: "xoxb"
+# uploadImage: true
+# - name: default-email
+# type: email
+# org_id: 1
+# is_default: false
+# delete_alert_notifications:
+# - name: default-slack
+# org_id: 1
+# - name: default-email
\ No newline at end of file
diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md
index b2a1b1f42e7..6b8a1a992da 100644
--- a/docs/sources/administration/provisioning.md
+++ b/docs/sources/administration/provisioning.md
@@ -230,4 +230,182 @@ By default Grafana will delete dashboards in the database if the file is removed
> **Note.** Provisioning allows you to overwrite existing dashboards
> which leads to problems if you re-use settings that are supposed to be unique.
> Be careful not to re-use the same `title` multiple times within a folder
+<<<<<<< HEAD
> or `uid` within the same installation as this will cause weird behaviors.
+=======
+> or `uid` within the same installation as this will cause weird behaviours.
+
+## Alert Notification Channels
+
+Alert Notification Channels can be provisionned by adding one or more yaml config files in the [`provisioning/alert_notifications`](/installation/configuration/#provisioning) directory.
+
+Each config file can contain the following top-level fields:
+- `alert_notifications`, a list of alert notifications that will be added or updated during start up. If the notification channel already exists, Grafana will update it to match the configuration file.
+- `delete_alert_notifications`, a list of alert notifications to be deleted before before inserting/updating those in the `alert_notifications` list.
+
+Provisionning looks up alert notifications by name, and will update any existing notification with the provided name.
+
+By default, exporting a dashboard as JSON will use a sequential identifier to refer to alert notifications. The field `name` can be optionally specified to specify a string identifier for the alert name.
+
+```json
+{
+ ...
+ "alert": {
+ ...,
+ "conditions": [...],
+ "frequency": "24h",
+ "noDataState": "ok",
+ "notifications": [
+ {"name": "notification-channel-1"},
+ {"name": "notification-channel-2"},
+ ]
+ }
+ ...
+}
+```
+
+### Example Alert Notification Channels Config File
+
+```yaml
+alert_notifications:
+ - name: notification-channel-1
+ type: slack
+ org_id: 2
+ is_default: true
+ # See `Supported Settings` section for settings supporter for each
+ # alert notification type.
+ settings:
+ recipient: "XXX"
+ token: "xoxb"
+ uploadImage: true
+
+delete_alert_notifications:
+ - name: notification-channel-1
+ org_id: 2
+ - name: notification-channel-2
+```
+
+### Supported Settings
+
+The following sections detail the supported settings for each alert notification type.
+
+#### Alert notification `pushover`
+
+| Name |
+| ---- |
+| apiToken |
+| userKey |
+| device |
+| retry |
+| expire |
+
+#### Alert notification `slack`
+
+| Name |
+| ---- |
+| url |
+| recipient |
+| username |
+| iconEmoji |
+| iconUrl |
+| uploadImage |
+| mention |
+| token |
+
+#### Alert notification `victorops`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `kafka`
+
+| Name |
+| ---- |
+| kafkaRestProxy |
+| kafkaTopic |
+
+#### Alert notification `LINE`
+
+| Name |
+| ---- |
+| token |
+
+#### Alert notification `pagerduty`
+
+| Name |
+| ---- |
+| integrationKey |
+
+#### Alert notification `sensu`
+
+| Name |
+| ---- |
+| url |
+| source |
+| handler |
+| username |
+| password |
+
+#### Alert notification `prometheus-alertmanager`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `teams`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `dingding`
+
+| Name |
+| ---- |
+| url |
+
+#### Alert notification `email`
+
+| Name |
+| ---- |
+| addresses |
+
+#### Alert notification `hipchat`
+
+| Name |
+| ---- |
+| url |
+| apikey |
+| roomid |
+
+#### Alert notification `opsgenie`
+
+| Name |
+| ---- |
+| apiKey |
+| apiUrl |
+
+#### Alert notification `telegram`
+
+| Name |
+| ---- |
+| bottoken |
+| chatid |
+
+#### Alert notification `threema`
+
+| Name |
+| ---- |
+| gateway_id |
+| recipient_id |
+| api_secret |
+
+#### Alert notification `webhook`
+
+| Name |
+| ---- |
+| url |
+| username |
+| password |
+>>>>>>> Added alert_notification configuration
diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go
index 4423046d600..180e013a856 100644
--- a/pkg/services/alerting/rule.go
+++ b/pkg/services/alerting/rule.go
@@ -7,6 +7,8 @@ import (
"strconv"
"time"
+ "github.com/grafana/grafana/pkg/bus"
+
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
)
@@ -128,9 +130,21 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
jsonModel := simplejson.NewFromAny(v)
id, err := jsonModel.Get("id").Int64()
if err != nil {
- return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+ notificationName, notificationNameErr := jsonModel.Get("name").String()
+ if notificationNameErr != nil {
+ return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+ }
+ cmd := &m.GetAlertNotificationsQuery{OrgId: ruleDef.OrgId, Name: notificationName}
+ nameErr := bus.Dispatch(cmd)
+ if nameErr != nil || cmd.Result == nil {
+ errorMsg := fmt.Sprintf("Cannot lookup notification by name %s and orgId %d", notificationName, ruleDef.OrgId)
+ return nil, ValidationError{Reason: errorMsg, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
+ }
+ model.Notifications = append(model.Notifications, cmd.Result.Id)
+ } else {
+ model.Notifications = append(model.Notifications, id)
}
- model.Notifications = append(model.Notifications, id)
+
}
for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
diff --git a/pkg/services/alerting/rule_test.go b/pkg/services/alerting/rule_test.go
index cf25cc118f4..c81af138f6e 100644
--- a/pkg/services/alerting/rule_test.go
+++ b/pkg/services/alerting/rule_test.go
@@ -3,11 +3,17 @@ package alerting
import (
"testing"
+ "github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/models"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
+var (
+ fakeRepo *fakeRepository
+)
+
type FakeCondition struct{}
func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
@@ -129,5 +135,155 @@ func TestAlertRuleModel(t *testing.T) {
So(err, ShouldBeNil)
So(alertRule.Frequency, ShouldEqual, 60)
})
+ Convey("can construct alert rule model mixed notification ids and names", func() {
+ json := `
+ {
+ "name": "name2",
+ "description": "desc2",
+ "handler": 0,
+ "noDataMode": "critical",
+ "enabled": true,
+ "frequency": "60s",
+ "conditions": [
+ {
+ "type": "test",
+ "prop": 123
+ }
+ ],
+ "notifications": [
+ {"id": 1134},
+ {"id": 22},
+ {"name": "channel1"},
+ {"name": "channel2"}
+ ]
+ }
+ `
+
+ alertJSON, jsonErr := simplejson.NewJson([]byte(json))
+ So(jsonErr, ShouldBeNil)
+
+ alert := &m.Alert{
+ Id: 1,
+ OrgId: 1,
+ DashboardId: 1,
+ PanelId: 1,
+
+ Settings: alertJSON,
+ }
+
+ fakeRepo = &fakeRepository{}
+ bus.ClearBusHandlers()
+ bus.AddHandler("test", mockGet)
+
+ fakeRepo.loadAll = []*models.AlertNotification{
+ {Name: "channel1", OrgId: 1, Id: 1},
+ {Name: "channel2", OrgId: 1, Id: 2},
+ }
+
+ alertRule, err := NewRuleFromDBAlert(alert)
+ So(err, ShouldBeNil)
+
+ Convey("Can read notifications", func() {
+ So(len(alertRule.Notifications), ShouldEqual, 4)
+ So(alertRule.Notifications, ShouldResemble, []int64{1134, 22, 1, 2})
+ })
+ })
+
+ Convey("raise error in case of left id", func() {
+ json := `
+ {
+ "name": "name2",
+ "description": "desc2",
+ "handler": 0,
+ "noDataMode": "critical",
+ "enabled": true,
+ "frequency": "60s",
+ "conditions": [
+ {
+ "type": "test",
+ "prop": 123
+ }
+ ],
+ "notifications": [
+ {"not_id": 1134}
+ ]
+ }
+ `
+
+ alertJSON, jsonErr := simplejson.NewJson([]byte(json))
+ So(jsonErr, ShouldBeNil)
+
+ alert := &m.Alert{
+ Id: 1,
+ OrgId: 1,
+ DashboardId: 1,
+ PanelId: 1,
+
+ Settings: alertJSON,
+ }
+
+ _, err := NewRuleFromDBAlert(alert)
+ So(err, ShouldNotBeNil)
+ })
+
+ Convey("raise error in case of left id but existed name with alien orgId", func() {
+ json := `
+ {
+ "name": "name2",
+ "description": "desc2",
+ "handler": 0,
+ "noDataMode": "critical",
+ "enabled": true,
+ "frequency": "60s",
+ "conditions": [
+ {
+ "type": "test",
+ "prop": 123
+ }
+ ],
+ "notifications": [
+ {"not_id": 1134, "name": "channel1"}
+ ]
+ }
+ `
+
+ alertJSON, jsonErr := simplejson.NewJson([]byte(json))
+ So(jsonErr, ShouldBeNil)
+
+ alert := &m.Alert{
+ Id: 1,
+ OrgId: 1,
+ DashboardId: 1,
+ PanelId: 1,
+
+ Settings: alertJSON,
+ }
+
+ fakeRepo = &fakeRepository{}
+ bus.ClearBusHandlers()
+ bus.AddHandler("test", mockGet)
+
+ fakeRepo.loadAll = []*models.AlertNotification{
+ {Name: "channel1", OrgId: 2, Id: 1},
+ }
+
+ _, err := NewRuleFromDBAlert(alert)
+
+ So(err, ShouldNotBeNil)
+ })
})
}
+
+type fakeRepository struct {
+ loadAll []*models.AlertNotification
+}
+
+func mockGet(cmd *models.GetAlertNotificationsQuery) error {
+ for _, v := range fakeRepo.loadAll {
+ if cmd.Name == v.Name && cmd.OrgId == v.OrgId {
+ cmd.Result = v
+ return nil
+ }
+ }
+ return nil
+}
diff --git a/pkg/services/provisioning/alert_notifications/alert_notifications.go b/pkg/services/provisioning/alert_notifications/alert_notifications.go
new file mode 100644
index 00000000000..eae5b4bb44e
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/alert_notifications.go
@@ -0,0 +1,151 @@
+package alert_notifications
+
+import (
+ "errors"
+
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/components/simplejson"
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
+)
+
+var (
+ ErrInvalidConfigTooManyDefault = errors.New("Alert notification provisioning config is invalid. Only one alert notification can be marked as default")
+ ErrInvalidNotifierType = errors.New("Unknown notifier type")
+)
+
+func Provision(configDirectory string) error {
+ dc := newNotificationProvisioner(log.New("provisioning.alert_notifications"))
+ return dc.applyChanges(configDirectory)
+}
+
+type NotificationProvisioner struct {
+ log log.Logger
+ cfgProvider *configReader
+}
+
+func newNotificationProvisioner(log log.Logger) NotificationProvisioner {
+ return NotificationProvisioner{
+ log: log,
+ cfgProvider: &configReader{log: log},
+ }
+}
+
+func (dc *NotificationProvisioner) apply(cfg *notificationsAsConfig) error {
+ if err := dc.deleteNotifications(cfg.DeleteNotifications); err != nil {
+ return err
+ }
+
+ if err := dc.mergeNotifications(cfg.Notifications); err != nil {
+ return err
+ }
+
+ return nil
+}
+
+func (dc *NotificationProvisioner) deleteNotifications(notificationToDelete []*deleteNotificationConfig) error {
+ for _, notification := range notificationToDelete {
+ dc.log.Info("Deleting alert notification", "name", notification.Name)
+
+ getNotification := &models.GetAlertNotificationsQuery{Name: notification.Name, OrgId: notification.OrgId}
+
+ if err := bus.Dispatch(getNotification); err != nil {
+ return err
+ }
+
+ if getNotification.Result != nil {
+ cmd := &models.DeleteAlertNotificationCommand{Id: getNotification.Result.Id, OrgId: getNotification.OrgId}
+ if err := bus.Dispatch(cmd); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+
+func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*notificationFromConfig) error {
+ for _, notification := range notificationToMerge {
+ cmd := &models.GetAlertNotificationsQuery{OrgId: notification.OrgId, Name: notification.Name}
+ err := bus.Dispatch(cmd)
+ if err != nil {
+ return err
+ }
+
+ settings := simplejson.New()
+ if len(notification.Settings) > 0 {
+ for k, v := range notification.Settings {
+ settings.Set(k, v)
+ }
+ }
+
+ if cmd.Result == nil {
+ dc.log.Info("Inserting alert notification from configuration ", "name", notification.Name)
+ insertCmd := &models.CreateAlertNotificationCommand{
+ Name: notification.Name,
+ Type: notification.Type,
+ IsDefault: notification.IsDefault,
+ Settings: settings,
+ OrgId: notification.OrgId,
+ }
+ if err := bus.Dispatch(insertCmd); err != nil {
+ return err
+ }
+ } else {
+ dc.log.Info("Updating alert notification from configuration", "name", notification.Name)
+ updateCmd := &models.UpdateAlertNotificationCommand{
+ Id: cmd.Result.Id,
+ Name: notification.Name,
+ Type: notification.Type,
+ IsDefault: notification.IsDefault,
+ Settings: settings,
+ OrgId: notification.OrgId,
+ }
+ if err := bus.Dispatch(updateCmd); err != nil {
+ return err
+ }
+ }
+ }
+
+ return nil
+}
+func (cfg *notificationsAsConfig) mapToNotificationFromConfig() *notificationsAsConfig {
+ r := ¬ificationsAsConfig{}
+ if cfg == nil {
+ return r
+ }
+
+ for _, notification := range cfg.Notifications {
+ r.Notifications = append(r.Notifications, ¬ificationFromConfig{
+ OrgId: notification.OrgId,
+ Name: notification.Name,
+ Type: notification.Type,
+ IsDefault: notification.IsDefault,
+ Settings: notification.Settings,
+ })
+ }
+
+ for _, notification := range cfg.DeleteNotifications {
+ r.DeleteNotifications = append(r.DeleteNotifications, &deleteNotificationConfig{
+ OrgId: notification.OrgId,
+ Name: notification.Name,
+ })
+ }
+
+ return r
+}
+
+func (dc *NotificationProvisioner) applyChanges(configPath string) error {
+ configs, err := dc.cfgProvider.readConfig(configPath)
+ if err != nil {
+ return err
+ }
+
+ for _, cfg := range configs {
+ if err := dc.apply(cfg); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/services/provisioning/alert_notifications/config_reader.go b/pkg/services/provisioning/alert_notifications/config_reader.go
new file mode 100644
index 00000000000..b69f9bf15b1
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/config_reader.go
@@ -0,0 +1,115 @@
+package alert_notifications
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/services/alerting"
+ "gopkg.in/yaml.v2"
+)
+
+type configReader struct {
+ log log.Logger
+}
+
+func (cr *configReader) readConfig(path string) ([]*notificationsAsConfig, error) {
+ var notifications []*notificationsAsConfig
+ cr.log.Debug("Looking for alert notification provisioning files", "path", path)
+
+ files, err := ioutil.ReadDir(path)
+ if err != nil {
+ cr.log.Error("Can't read alert notification provisioning files from directory", "path", path)
+ return notifications, nil
+ }
+
+ for _, file := range files {
+ if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
+ cr.log.Debug("Parsing alert notifications provisioning file", "path", path, "file.Name", file.Name())
+ notifs, err := cr.parseNotificationConfig(path, file)
+ if err != nil {
+ return nil, err
+ }
+
+ if notifs != nil {
+ notifications = append(notifications, notifs)
+ }
+ }
+ }
+
+ cr.log.Debug("Validating alert notifications")
+ err = validateDefaultUniqueness(notifications)
+ if err != nil {
+ return nil, err
+ }
+
+ err = validateType(notifications)
+ if err != nil {
+ return nil, err
+ }
+
+ return notifications, nil
+}
+
+func (cr *configReader) parseNotificationConfig(path string, file os.FileInfo) (*notificationsAsConfig, error) {
+ filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
+ yamlFile, err := ioutil.ReadFile(filename)
+ if err != nil {
+ return nil, err
+ }
+
+ var cfg *notificationsAsConfig
+ err = yaml.Unmarshal(yamlFile, &cfg)
+ if err != nil {
+ return nil, err
+ }
+
+ return cfg.mapToNotificationFromConfig(), nil
+}
+
+func validateDefaultUniqueness(notifications []*notificationsAsConfig) error {
+ for i := range notifications {
+ for _, notification := range notifications[i].Notifications {
+ if notification.OrgId < 1 {
+ notification.OrgId = 1
+ }
+ }
+
+ for _, notification := range notifications[i].DeleteNotifications {
+ if notification.OrgId < 1 {
+ notification.OrgId = 1
+ }
+ }
+ }
+
+ return nil
+}
+
+func validateType(notifications []*notificationsAsConfig) error {
+ notifierTypes := alerting.GetNotifiers()
+
+ for i := range notifications {
+ if notifications[i].Notifications == nil {
+ continue
+ }
+
+ for _, notification := range notifications[i].Notifications {
+ foundNotifier := false
+
+ for _, notifier := range notifierTypes {
+ if notifier.Type == notification.Type {
+ foundNotifier = true
+ break
+ }
+ }
+
+ if !foundNotifier {
+ return ErrInvalidNotifierType
+ }
+ }
+ }
+
+ return nil
+}
diff --git a/pkg/services/provisioning/alert_notifications/config_reader_test.go b/pkg/services/provisioning/alert_notifications/config_reader_test.go
new file mode 100644
index 00000000000..e028f5bac40
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/config_reader_test.go
@@ -0,0 +1,219 @@
+package alert_notifications
+
+import (
+ "testing"
+
+ "github.com/grafana/grafana/pkg/bus"
+ "github.com/grafana/grafana/pkg/log"
+ "github.com/grafana/grafana/pkg/models"
+ "github.com/grafana/grafana/pkg/services/alerting"
+ . "github.com/smartystreets/goconvey/convey"
+)
+
+var (
+ logger = log.New("fake.log")
+
+ correct_properties = "./test-configs/correct-properties"
+ brokenYaml = "./test-configs/broken-yaml"
+ doubleNotificationsConfig = "./test-configs/double-default"
+ emptyFolder = "./test-configs/empty_folder"
+ emptyFile = "./test-configs/empty"
+ twoNotificationsConfig = "./test-configs/two-notifications"
+ unknownNotifier = "./test-configs/unknown-notifier"
+
+ fakeRepo *fakeRepository
+)
+
+func TestNotificationAsConfig(t *testing.T) {
+ Convey("Testing notification as configuration", t, func() {
+ fakeRepo = &fakeRepository{}
+ bus.ClearBusHandlers()
+ bus.AddHandler("test", mockDelete)
+ bus.AddHandler("test", mockInsert)
+ bus.AddHandler("test", mockUpdate)
+ bus.AddHandler("test", mockGet)
+
+ alerting.RegisterNotifier(&alerting.NotifierPlugin{
+ Type: "slack",
+ Name: "slack",
+ })
+ alerting.RegisterNotifier(&alerting.NotifierPlugin{
+ Type: "email",
+ Name: "email",
+ })
+ Convey("Can read correct properties", func() {
+ cfgProvifer := &configReader{log: log.New("test logger")}
+ cfg, err := cfgProvifer.readConfig(correct_properties)
+ if err != nil {
+ t.Fatalf("readConfig return an error %v", err)
+ }
+ So(len(cfg), ShouldEqual, 1)
+
+ ntCfg := cfg[0]
+ nts := ntCfg.Notifications
+ So(len(nts), ShouldEqual, 4)
+
+ nt := nts[0]
+ So(nt.Name, ShouldEqual, "default-slack-notification")
+ So(nt.Type, ShouldEqual, "slack")
+ So(nt.OrgId, ShouldEqual, 2)
+ So(nt.IsDefault, ShouldBeTrue)
+ So(nt.Settings, ShouldResemble, map[string]interface{}{
+ "recipient": "XXX", "token": "xoxb", "uploadImage": true,
+ })
+
+ nt = nts[1]
+ So(nt.Name, ShouldEqual, "another-not-default-notification")
+ So(nt.Type, ShouldEqual, "email")
+ So(nt.OrgId, ShouldEqual, 3)
+ So(nt.IsDefault, ShouldBeFalse)
+
+ nt = nts[2]
+ So(nt.Name, ShouldEqual, "check-unset-is_default-is-false")
+ So(nt.Type, ShouldEqual, "slack")
+ So(nt.OrgId, ShouldEqual, 3)
+ So(nt.IsDefault, ShouldBeFalse)
+
+ nt = nts[3]
+ So(nt.Name, ShouldEqual, "Added notification with whitespaces in name")
+ So(nt.Type, ShouldEqual, "email")
+ So(nt.OrgId, ShouldEqual, 3)
+
+ deleteNts := ntCfg.DeleteNotifications
+ So(len(deleteNts), ShouldEqual, 4)
+
+ deleteNt := deleteNts[0]
+ So(deleteNt.Name, ShouldEqual, "default-slack-notification")
+ So(deleteNt.OrgId, ShouldEqual, 2)
+
+ deleteNt = deleteNts[1]
+ So(deleteNt.Name, ShouldEqual, "deleted-notification-without-orgId")
+ So(deleteNt.OrgId, ShouldEqual, 1)
+
+ deleteNt = deleteNts[2]
+ So(deleteNt.Name, ShouldEqual, "deleted-notification-with-0-orgId")
+ So(deleteNt.OrgId, ShouldEqual, 1)
+
+ deleteNt = deleteNts[3]
+ So(deleteNt.Name, ShouldEqual, "Deleted notification with whitespaces in name")
+ So(deleteNt.OrgId, ShouldEqual, 1)
+ })
+
+ Convey("One configured notification", func() {
+ Convey("no notification in database", func() {
+ dc := newNotificationProvisioner(logger)
+ err := dc.applyChanges(twoNotificationsConfig)
+ if err != nil {
+ t.Fatalf("applyChanges return an error %v", err)
+ }
+ So(len(fakeRepo.deleted), ShouldEqual, 0)
+ So(len(fakeRepo.inserted), ShouldEqual, 2)
+ So(len(fakeRepo.updated), ShouldEqual, 0)
+ })
+ Convey("One notification in database with same name", func() {
+ fakeRepo.loadAll = []*models.AlertNotification{
+ {Name: "channel1", OrgId: 1, Id: 1},
+ }
+ Convey("should update one notification", func() {
+ dc := newNotificationProvisioner(logger)
+ err := dc.applyChanges(twoNotificationsConfig)
+ if err != nil {
+ t.Fatalf("applyChanges return an error %v", err)
+ }
+ So(len(fakeRepo.deleted), ShouldEqual, 0)
+ So(len(fakeRepo.inserted), ShouldEqual, 1)
+ So(len(fakeRepo.updated), ShouldEqual, 1)
+ })
+ })
+ Convey("Two notifications with is_default", func() {
+ dc := newNotificationProvisioner(logger)
+ err := dc.applyChanges(doubleNotificationsConfig)
+ Convey("should both be inserted", func() {
+ So(err, ShouldBeNil)
+ So(len(fakeRepo.deleted), ShouldEqual, 0)
+ So(len(fakeRepo.inserted), ShouldEqual, 2)
+ So(len(fakeRepo.updated), ShouldEqual, 0)
+ })
+ })
+ })
+
+ Convey("Two configured notification", func() {
+ Convey("two other notifications in database", func() {
+ fakeRepo.loadAll = []*models.AlertNotification{
+ {Name: "channel1", OrgId: 1, Id: 1},
+ {Name: "channel3", OrgId: 1, Id: 2},
+ }
+ Convey("should have two new notifications", func() {
+ dc := newNotificationProvisioner(logger)
+ err := dc.applyChanges(twoNotificationsConfig)
+ if err != nil {
+ t.Fatalf("applyChanges return an error %v", err)
+ }
+ So(len(fakeRepo.deleted), ShouldEqual, 0)
+ So(len(fakeRepo.inserted), ShouldEqual, 1)
+ So(len(fakeRepo.updated), ShouldEqual, 1)
+ })
+ })
+ })
+ Convey("Empty yaml file", func() {
+ Convey("should have not changed repo", func() {
+ dc := newNotificationProvisioner(logger)
+ err := dc.applyChanges(emptyFile)
+ if err != nil {
+ t.Fatalf("applyChanges return an error %v", err)
+ }
+ So(len(fakeRepo.deleted), ShouldEqual, 0)
+ So(len(fakeRepo.inserted), ShouldEqual, 0)
+ So(len(fakeRepo.updated), ShouldEqual, 0)
+ })
+ })
+ Convey("Broken yaml should return error", func() {
+ reader := &configReader{log: log.New("test logger")}
+ _, err := reader.readConfig(brokenYaml)
+ So(err, ShouldNotBeNil)
+ })
+ Convey("Skip invalid directory", func() {
+ cfgProvifer := &configReader{log: log.New("test logger")}
+ cfg, err := cfgProvifer.readConfig(emptyFolder)
+ if err != nil {
+ t.Fatalf("readConfig return an error %v", err)
+ }
+ So(len(cfg), ShouldEqual, 0)
+ })
+ Convey("Unknown notifier should return error", func() {
+ cfgProvifer := &configReader{log: log.New("test logger")}
+ _, err := cfgProvifer.readConfig(unknownNotifier)
+ So(err, ShouldEqual, ErrInvalidNotifierType)
+ })
+
+ })
+}
+
+type fakeRepository struct {
+ inserted []*models.CreateAlertNotificationCommand
+ deleted []*models.DeleteAlertNotificationCommand
+ updated []*models.UpdateAlertNotificationCommand
+ loadAll []*models.AlertNotification
+}
+
+func mockDelete(cmd *models.DeleteAlertNotificationCommand) error {
+ fakeRepo.deleted = append(fakeRepo.deleted, cmd)
+ return nil
+}
+func mockUpdate(cmd *models.UpdateAlertNotificationCommand) error {
+ fakeRepo.updated = append(fakeRepo.updated, cmd)
+ return nil
+}
+func mockInsert(cmd *models.CreateAlertNotificationCommand) error {
+ fakeRepo.inserted = append(fakeRepo.inserted, cmd)
+ return nil
+}
+func mockGet(cmd *models.GetAlertNotificationsQuery) error {
+ for _, v := range fakeRepo.loadAll {
+ if cmd.Name == v.Name && cmd.OrgId == v.OrgId {
+ cmd.Result = v
+ return nil
+ }
+ }
+ return nil
+}
diff --git a/pkg/services/provisioning/alert_notifications/test-configs/broken-yaml/broken.yaml b/pkg/services/provisioning/alert_notifications/test-configs/broken-yaml/broken.yaml
new file mode 100644
index 00000000000..e7c38d22f2c
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/test-configs/broken-yaml/broken.yaml
@@ -0,0 +1,9 @@
+alert_notifications:
+ - name: notification-channel-1
+ type: slack
+ org_id: 2
+ is_default: true
+ settings:
+ recipient: "XXX"
+ token: "xoxb"
+ uploadImage: true
\ No newline at end of file
diff --git a/pkg/services/provisioning/alert_notifications/test-configs/broken-yaml/not.yaml.text b/pkg/services/provisioning/alert_notifications/test-configs/broken-yaml/not.yaml.text
new file mode 100644
index 00000000000..9050f543cef
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/test-configs/broken-yaml/not.yaml.text
@@ -0,0 +1,6 @@
+#sfxzgnsxzcvnbzcvn
+cvbn
+cvbn
+c
+vbn
+cvbncvbn
\ No newline at end of file
diff --git a/pkg/services/provisioning/alert_notifications/test-configs/correct-properties/correct-properties.yaml b/pkg/services/provisioning/alert_notifications/test-configs/correct-properties/correct-properties.yaml
new file mode 100644
index 00000000000..d18f538d4ee
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/test-configs/correct-properties/correct-properties.yaml
@@ -0,0 +1,26 @@
+alert_notifications:
+ - name: default-slack-notification
+ type: slack
+ org_id: 2
+ is_default: true
+ settings:
+ recipient: "XXX"
+ token: "xoxb"
+ uploadImage: true
+ - name: another-not-default-notification
+ type: email
+ org_id: 3
+ is_default: false
+ - name: check-unset-is_default-is-false
+ type: slack
+ org_id: 3
+ - name: Added notification with whitespaces in name
+ type: email
+ org_id: 3
+delete_alert_notifications:
+ - name: default-slack-notification
+ org_id: 2
+ - name: deleted-notification-without-orgId
+ - name: deleted-notification-with-0-orgId
+ org_id: 0
+ - name: Deleted notification with whitespaces in name
\ No newline at end of file
diff --git a/pkg/services/provisioning/alert_notifications/test-configs/double-default/default-1.yml b/pkg/services/provisioning/alert_notifications/test-configs/double-default/default-1.yml
new file mode 100644
index 00000000000..aaa4541b993
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/test-configs/double-default/default-1.yml
@@ -0,0 +1,4 @@
+alert_notifications:
+ - name: first-default
+ type: slack
+ is_default: true
\ No newline at end of file
diff --git a/pkg/services/provisioning/alert_notifications/test-configs/double-default/default-2.yaml b/pkg/services/provisioning/alert_notifications/test-configs/double-default/default-2.yaml
new file mode 100644
index 00000000000..0636316614a
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/test-configs/double-default/default-2.yaml
@@ -0,0 +1,4 @@
+alert_notifications:
+ - name: second-default
+ type: email
+ is_default: true
\ No newline at end of file
diff --git a/pkg/services/provisioning/alert_notifications/test-configs/empty/empty.yaml b/pkg/services/provisioning/alert_notifications/test-configs/empty/empty.yaml
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/pkg/services/provisioning/alert_notifications/test-configs/empty_folder/.gitignore b/pkg/services/provisioning/alert_notifications/test-configs/empty_folder/.gitignore
new file mode 100644
index 00000000000..86d0cb2726c
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/test-configs/empty_folder/.gitignore
@@ -0,0 +1,4 @@
+# Ignore everything in this directory
+*
+# Except this file
+!.gitignore
\ No newline at end of file
diff --git a/pkg/services/provisioning/alert_notifications/test-configs/two-notifications/two-notifications.yaml b/pkg/services/provisioning/alert_notifications/test-configs/two-notifications/two-notifications.yaml
new file mode 100644
index 00000000000..a5e34d92fd5
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/test-configs/two-notifications/two-notifications.yaml
@@ -0,0 +1,5 @@
+alert_notifications:
+ - name: channel1
+ type: slack
+ - name: channel2
+ type: email
\ No newline at end of file
diff --git a/pkg/services/provisioning/alert_notifications/test-configs/unknown-notifier/notification.yaml b/pkg/services/provisioning/alert_notifications/test-configs/unknown-notifier/notification.yaml
new file mode 100644
index 00000000000..a59c38ea84b
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/test-configs/unknown-notifier/notification.yaml
@@ -0,0 +1,3 @@
+alert_notifications:
+ - name: unknown-notifier
+ type: nonexisting
\ No newline at end of file
diff --git a/pkg/services/provisioning/alert_notifications/types.go b/pkg/services/provisioning/alert_notifications/types.go
new file mode 100644
index 00000000000..7718605cd67
--- /dev/null
+++ b/pkg/services/provisioning/alert_notifications/types.go
@@ -0,0 +1,19 @@
+package alert_notifications
+
+type notificationsAsConfig struct {
+ Notifications []*notificationFromConfig `json:"alert_notifications" yaml:"alert_notifications"`
+ DeleteNotifications []*deleteNotificationConfig `json:"delete_alert_notifications" yaml:"delete_alert_notifications"`
+}
+
+type deleteNotificationConfig struct {
+ Name string `json:"name" yaml:"name"`
+ OrgId int64 `json:"org_id" yaml:"org_id"`
+}
+
+type notificationFromConfig struct {
+ OrgId int64 `json:"org_id" yaml:"org_id"`
+ Name string `json:"name" yaml:"name"`
+ Type string `json:"type" yaml:"type"`
+ IsDefault bool `json:"is_default" yaml:"is_default"`
+ Settings map[string]interface{} `json:"settings" yaml:"settings"`
+}
diff --git a/pkg/services/provisioning/provisioning.go b/pkg/services/provisioning/provisioning.go
index 9044ae97389..fdecd03a3da 100644
--- a/pkg/services/provisioning/provisioning.go
+++ b/pkg/services/provisioning/provisioning.go
@@ -6,6 +6,7 @@ import (
"path"
"github.com/grafana/grafana/pkg/registry"
+ "github.com/grafana/grafana/pkg/services/provisioning/alert_notifications"
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
"github.com/grafana/grafana/pkg/setting"
@@ -25,6 +26,11 @@ func (ps *ProvisioningService) Init() error {
return fmt.Errorf("Datasource provisioning error: %v", err)
}
+ alertNotificationsPath := path.Join(ps.Cfg.ProvisioningPath, "alert_notifications")
+ if err := alert_notifications.Provision(alertNotificationsPath); err != nil {
+ return fmt.Errorf("Alert notification provisioning error: %v", err)
+ }
+
return nil
}