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 }