mirror of https://github.com/grafana/grafana.git
				
				
				
			Merge pull request #14229 from pbakulev/configurable-alert-notification
Configurable alert notification
This commit is contained in:
		
						commit
						c6f80ecec2
					
				|  | @ -0,0 +1,25 @@ | ||||||
|  | # # config file version | ||||||
|  | apiVersion: 1 | ||||||
|  | 
 | ||||||
|  | # notifiers: | ||||||
|  | #   - name: default-slack-temp | ||||||
|  | #     type: slack | ||||||
|  | #     org_name: Main Org. | ||||||
|  | #     is_default: true | ||||||
|  | #     uid: notifier1 | ||||||
|  | #     settings: | ||||||
|  | #       recipient: "XXX" | ||||||
|  | #       token: "xoxb" | ||||||
|  | #       uploadImage: true | ||||||
|  | #       url: https://slack.com | ||||||
|  | #   - name: default-email | ||||||
|  | #     type: email | ||||||
|  | #     org_id: 1 | ||||||
|  | #     uid: notifier2 | ||||||
|  | #     is_default: false   | ||||||
|  | #     settings: | ||||||
|  | #       addresses: example11111@example.com | ||||||
|  | # delete_notifiers: | ||||||
|  | #   - name: default-slack-temp | ||||||
|  | #     org_name: Main Org. | ||||||
|  | #     uid: notifier1 | ||||||
|  | @ -231,3 +231,187 @@ By default Grafana will delete dashboards in the database if the file is removed | ||||||
| > which leads to problems if you re-use settings that are supposed to be unique. | > 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 | > Be careful not to re-use the same `title` multiple times within a folder | ||||||
| > or `uid` within the same installation as this will cause weird behaviors. | > or `uid` within the same installation as this will cause weird behaviors. | ||||||
|  | 
 | ||||||
|  | ## Alert Notification Channels | ||||||
|  | 
 | ||||||
|  | Alert Notification Channels can be provisioned by adding one or more yaml config files in the [`provisioning/notifiers`](/installation/configuration/#provisioning) directory. | ||||||
|  | 
 | ||||||
|  | Each config file can contain the following top-level fields: | ||||||
|  | - `notifiers`, 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_notifiers`, a list of alert notifications to be deleted before before inserting/updating those in the `notifiers` list. | ||||||
|  | 
 | ||||||
|  | Provisioning looks up alert notifications by uid, and will update any existing notification with the provided uid. | ||||||
|  | 
 | ||||||
|  | By default, exporting a dashboard as JSON will use a sequential identifier to refer to alert notifications. The field `uid` can be optionally specified to specify a string identifier for the alert name. | ||||||
|  | 
 | ||||||
|  | ```json | ||||||
|  | { | ||||||
|  |   ... | ||||||
|  |       "alert": { | ||||||
|  |         ..., | ||||||
|  |         "conditions": [...], | ||||||
|  |         "frequency": "24h", | ||||||
|  |         "noDataState": "ok", | ||||||
|  |         "notifications": [ | ||||||
|  |            {"uid": "notifier1"}, | ||||||
|  |            {"uid": "notifier2"}, | ||||||
|  |         ] | ||||||
|  |       } | ||||||
|  |   ... | ||||||
|  | } | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### Example Alert Notification Channels Config File | ||||||
|  | 
 | ||||||
|  | ```yaml | ||||||
|  | notifiers: | ||||||
|  |   - name: notification-channel-1 | ||||||
|  |     type: slack | ||||||
|  |     uid: notifier1 | ||||||
|  |     # either | ||||||
|  |     org_id: 2 | ||||||
|  |     # or | ||||||
|  |     org_name: Main Org. | ||||||
|  |     is_default: true | ||||||
|  |     # See `Supported Settings` section for settings supporter for each | ||||||
|  |     # alert notification type. | ||||||
|  |     settings: | ||||||
|  |       recipient: "XXX" | ||||||
|  |       token: "xoxb" | ||||||
|  |       uploadImage: true | ||||||
|  |       url: https://slack.com | ||||||
|  | 
 | ||||||
|  | delete_notifiers: | ||||||
|  |   - name: notification-channel-1 | ||||||
|  |     uid: notifier1 | ||||||
|  |     # either | ||||||
|  |     org_id: 2 | ||||||
|  |     # or  | ||||||
|  |     org_name: Main Org. | ||||||
|  |   - name: notification-channel-2 | ||||||
|  |     # default org_id: 1 | ||||||
|  | ``` | ||||||
|  | 
 | ||||||
|  | ### 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 | | ||||||
|  | @ -50,6 +50,7 @@ func formatShort(interval time.Duration) string { | ||||||
| func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { | func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { | ||||||
| 	return &AlertNotification{ | 	return &AlertNotification{ | ||||||
| 		Id:                    notification.Id, | 		Id:                    notification.Id, | ||||||
|  | 		Uid:                   notification.Uid, | ||||||
| 		Name:                  notification.Name, | 		Name:                  notification.Name, | ||||||
| 		Type:                  notification.Type, | 		Type:                  notification.Type, | ||||||
| 		IsDefault:             notification.IsDefault, | 		IsDefault:             notification.IsDefault, | ||||||
|  | @ -64,6 +65,7 @@ func NewAlertNotification(notification *models.AlertNotification) *AlertNotifica | ||||||
| 
 | 
 | ||||||
| type AlertNotification struct { | type AlertNotification struct { | ||||||
| 	Id                    int64            `json:"id"` | 	Id                    int64            `json:"id"` | ||||||
|  | 	Uid                   string           `json:"uid"` | ||||||
| 	Name                  string           `json:"name"` | 	Name                  string           `json:"name"` | ||||||
| 	Type                  string           `json:"type"` | 	Type                  string           `json:"type"` | ||||||
| 	IsDefault             bool             `json:"isDefault"` | 	IsDefault             bool             `json:"isDefault"` | ||||||
|  |  | ||||||
|  | @ -8,10 +8,11 @@ import ( | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| var ( | var ( | ||||||
| 	ErrNotificationFrequencyNotFound         = errors.New("Notification frequency not specified") | 	ErrNotificationFrequencyNotFound            = errors.New("Notification frequency not specified") | ||||||
| 	ErrAlertNotificationStateNotFound        = errors.New("alert notification state not found") | 	ErrAlertNotificationStateNotFound           = errors.New("alert notification state not found") | ||||||
| 	ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict") | 	ErrAlertNotificationStateVersionConflict    = errors.New("alert notification state update version conflict") | ||||||
| 	ErrAlertNotificationStateAlreadyExist    = errors.New("alert notification state already exists.") | 	ErrAlertNotificationStateAlreadyExist       = errors.New("alert notification state already exists.") | ||||||
|  | 	ErrAlertNotificationFailedGenerateUniqueUid = errors.New("Failed to generate unique alert notification uid") | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| type AlertNotificationStateType string | type AlertNotificationStateType string | ||||||
|  | @ -24,6 +25,7 @@ var ( | ||||||
| 
 | 
 | ||||||
| type AlertNotification struct { | type AlertNotification struct { | ||||||
| 	Id                    int64            `json:"id"` | 	Id                    int64            `json:"id"` | ||||||
|  | 	Uid                   string           `json:"-"` | ||||||
| 	OrgId                 int64            `json:"-"` | 	OrgId                 int64            `json:"-"` | ||||||
| 	Name                  string           `json:"name"` | 	Name                  string           `json:"name"` | ||||||
| 	Type                  string           `json:"type"` | 	Type                  string           `json:"type"` | ||||||
|  | @ -37,6 +39,7 @@ type AlertNotification struct { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type CreateAlertNotificationCommand struct { | type CreateAlertNotificationCommand struct { | ||||||
|  | 	Uid                   string           `json:"-"` | ||||||
| 	Name                  string           `json:"name"  binding:"Required"` | 	Name                  string           `json:"name"  binding:"Required"` | ||||||
| 	Type                  string           `json:"type"  binding:"Required"` | 	Type                  string           `json:"type"  binding:"Required"` | ||||||
| 	SendReminder          bool             `json:"sendReminder"` | 	SendReminder          bool             `json:"sendReminder"` | ||||||
|  | @ -63,10 +66,28 @@ type UpdateAlertNotificationCommand struct { | ||||||
| 	Result *AlertNotification | 	Result *AlertNotification | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | type UpdateAlertNotificationWithUidCommand struct { | ||||||
|  | 	Uid                   string | ||||||
|  | 	Name                  string | ||||||
|  | 	Type                  string | ||||||
|  | 	SendReminder          bool | ||||||
|  | 	DisableResolveMessage bool | ||||||
|  | 	Frequency             string | ||||||
|  | 	IsDefault             bool | ||||||
|  | 	Settings              *simplejson.Json | ||||||
|  | 
 | ||||||
|  | 	OrgId  int64 | ||||||
|  | 	Result *AlertNotification | ||||||
|  | } | ||||||
|  | 
 | ||||||
| type DeleteAlertNotificationCommand struct { | type DeleteAlertNotificationCommand struct { | ||||||
| 	Id    int64 | 	Id    int64 | ||||||
| 	OrgId int64 | 	OrgId int64 | ||||||
| } | } | ||||||
|  | type DeleteAlertNotificationWithUidCommand struct { | ||||||
|  | 	Uid   string | ||||||
|  | 	OrgId int64 | ||||||
|  | } | ||||||
| 
 | 
 | ||||||
| type GetAlertNotificationsQuery struct { | type GetAlertNotificationsQuery struct { | ||||||
| 	Name  string | 	Name  string | ||||||
|  | @ -76,8 +97,15 @@ type GetAlertNotificationsQuery struct { | ||||||
| 	Result *AlertNotification | 	Result *AlertNotification | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| type GetAlertNotificationsToSendQuery struct { | type GetAlertNotificationsWithUidQuery struct { | ||||||
| 	Ids   []int64 | 	Uid   string | ||||||
|  | 	OrgId int64 | ||||||
|  | 
 | ||||||
|  | 	Result *AlertNotification | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type GetAlertNotificationsWithUidToSendQuery struct { | ||||||
|  | 	Uids  []string | ||||||
| 	OrgId int64 | 	OrgId int64 | ||||||
| 
 | 
 | ||||||
| 	Result []*AlertNotification | 	Result []*AlertNotification | ||||||
|  |  | ||||||
|  | @ -8,6 +8,7 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/bus" | 	"github.com/grafana/grafana/pkg/bus" | ||||||
| 	"github.com/grafana/grafana/pkg/components/simplejson" | 	"github.com/grafana/grafana/pkg/components/simplejson" | ||||||
| 	m "github.com/grafana/grafana/pkg/models" | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||||
| 	. "github.com/smartystreets/goconvey/convey" | 	. "github.com/smartystreets/goconvey/convey" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -197,74 +198,84 @@ func TestAlertRuleExtraction(t *testing.T) { | ||||||
| 			}) | 			}) | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		Convey("Parse and validate dashboard containing influxdb alert", func() { | 		Convey("Alert notifications are in DB", func() { | ||||||
| 			json, err := ioutil.ReadFile("./testdata/influxdb-alert.json") | 			sqlstore.InitTestDB(t) | ||||||
|  | 			firstNotification := m.CreateAlertNotificationCommand{Uid: "notifier1", OrgId: 1, Name: "1"} | ||||||
|  | 			err = sqlstore.CreateAlertNotificationCommand(&firstNotification) | ||||||
|  | 			So(err, ShouldBeNil) | ||||||
|  | 			secondNotification := m.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"} | ||||||
|  | 			err = sqlstore.CreateAlertNotificationCommand(&secondNotification) | ||||||
| 			So(err, ShouldBeNil) | 			So(err, ShouldBeNil) | ||||||
| 
 | 
 | ||||||
| 			dashJson, err := simplejson.NewJson(json) | 			Convey("Parse and validate dashboard containing influxdb alert", func() { | ||||||
| 			So(err, ShouldBeNil) | 				json, err := ioutil.ReadFile("./testdata/influxdb-alert.json") | ||||||
| 			dash := m.NewDashboardFromJson(dashJson) |  | ||||||
| 			extractor := NewDashAlertExtractor(dash, 1, nil) |  | ||||||
| 
 |  | ||||||
| 			alerts, err := extractor.GetAlerts() |  | ||||||
| 
 |  | ||||||
| 			Convey("Get rules without error", func() { |  | ||||||
| 				So(err, ShouldBeNil) | 				So(err, ShouldBeNil) | ||||||
| 			}) |  | ||||||
| 
 | 
 | ||||||
| 			Convey("should be able to read interval", func() { | 				dashJson, err := simplejson.NewJson(json) | ||||||
| 				So(len(alerts), ShouldEqual, 1) |  | ||||||
| 
 |  | ||||||
| 				for _, alert := range alerts { |  | ||||||
| 					So(alert.DashboardId, ShouldEqual, 4) |  | ||||||
| 
 |  | ||||||
| 					conditions := alert.Settings.Get("conditions").MustArray() |  | ||||||
| 					cond := simplejson.NewFromAny(conditions[0]) |  | ||||||
| 
 |  | ||||||
| 					So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s") |  | ||||||
| 				} |  | ||||||
| 			}) |  | ||||||
| 		}) |  | ||||||
| 
 |  | ||||||
| 		Convey("Should be able to extract collapsed panels", func() { |  | ||||||
| 			json, err := ioutil.ReadFile("./testdata/collapsed-panels.json") |  | ||||||
| 			So(err, ShouldBeNil) |  | ||||||
| 
 |  | ||||||
| 			dashJson, err := simplejson.NewJson(json) |  | ||||||
| 			So(err, ShouldBeNil) |  | ||||||
| 
 |  | ||||||
| 			dash := m.NewDashboardFromJson(dashJson) |  | ||||||
| 			extractor := NewDashAlertExtractor(dash, 1, nil) |  | ||||||
| 
 |  | ||||||
| 			alerts, err := extractor.GetAlerts() |  | ||||||
| 
 |  | ||||||
| 			Convey("Get rules without error", func() { |  | ||||||
| 				So(err, ShouldBeNil) | 				So(err, ShouldBeNil) | ||||||
|  | 				dash := m.NewDashboardFromJson(dashJson) | ||||||
|  | 				extractor := NewDashAlertExtractor(dash, 1, nil) | ||||||
|  | 
 | ||||||
|  | 				alerts, err := extractor.GetAlerts() | ||||||
|  | 
 | ||||||
|  | 				Convey("Get rules without error", func() { | ||||||
|  | 					So(err, ShouldBeNil) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				Convey("should be able to read interval", func() { | ||||||
|  | 					So(len(alerts), ShouldEqual, 1) | ||||||
|  | 
 | ||||||
|  | 					for _, alert := range alerts { | ||||||
|  | 						So(alert.DashboardId, ShouldEqual, 4) | ||||||
|  | 
 | ||||||
|  | 						conditions := alert.Settings.Get("conditions").MustArray() | ||||||
|  | 						cond := simplejson.NewFromAny(conditions[0]) | ||||||
|  | 
 | ||||||
|  | 						So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s") | ||||||
|  | 					} | ||||||
|  | 				}) | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			Convey("should be able to extract collapsed alerts", func() { | 			Convey("Should be able to extract collapsed panels", func() { | ||||||
| 				So(len(alerts), ShouldEqual, 4) | 				json, err := ioutil.ReadFile("./testdata/collapsed-panels.json") | ||||||
| 			}) |  | ||||||
| 		}) |  | ||||||
| 
 |  | ||||||
| 		Convey("Parse and validate dashboard without id and containing an alert", func() { |  | ||||||
| 			json, err := ioutil.ReadFile("./testdata/dash-without-id.json") |  | ||||||
| 			So(err, ShouldBeNil) |  | ||||||
| 
 |  | ||||||
| 			dashJSON, err := simplejson.NewJson(json) |  | ||||||
| 			So(err, ShouldBeNil) |  | ||||||
| 			dash := m.NewDashboardFromJson(dashJSON) |  | ||||||
| 			extractor := NewDashAlertExtractor(dash, 1, nil) |  | ||||||
| 
 |  | ||||||
| 			err = extractor.ValidateAlerts() |  | ||||||
| 
 |  | ||||||
| 			Convey("Should validate without error", func() { |  | ||||||
| 				So(err, ShouldBeNil) | 				So(err, ShouldBeNil) | ||||||
|  | 
 | ||||||
|  | 				dashJson, err := simplejson.NewJson(json) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 
 | ||||||
|  | 				dash := m.NewDashboardFromJson(dashJson) | ||||||
|  | 				extractor := NewDashAlertExtractor(dash, 1, nil) | ||||||
|  | 
 | ||||||
|  | 				alerts, err := extractor.GetAlerts() | ||||||
|  | 
 | ||||||
|  | 				Convey("Get rules without error", func() { | ||||||
|  | 					So(err, ShouldBeNil) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				Convey("should be able to extract collapsed alerts", func() { | ||||||
|  | 					So(len(alerts), ShouldEqual, 4) | ||||||
|  | 				}) | ||||||
| 			}) | 			}) | ||||||
| 
 | 
 | ||||||
| 			Convey("Should fail on save", func() { | 			Convey("Parse and validate dashboard without id and containing an alert", func() { | ||||||
| 				_, err := extractor.GetAlerts() | 				json, err := ioutil.ReadFile("./testdata/dash-without-id.json") | ||||||
| 				So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1") | 				So(err, ShouldBeNil) | ||||||
|  | 
 | ||||||
|  | 				dashJSON, err := simplejson.NewJson(json) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 				dash := m.NewDashboardFromJson(dashJSON) | ||||||
|  | 				extractor := NewDashAlertExtractor(dash, 1, nil) | ||||||
|  | 
 | ||||||
|  | 				err = extractor.ValidateAlerts() | ||||||
|  | 
 | ||||||
|  | 				Convey("Should validate without error", func() { | ||||||
|  | 					So(err, ShouldBeNil) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 				Convey("Should fail on save", func() { | ||||||
|  | 					_, err := extractor.GetAlerts() | ||||||
|  | 					So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1") | ||||||
|  | 				}) | ||||||
| 			}) | 			}) | ||||||
| 		}) | 		}) | ||||||
| 	}) | 	}) | ||||||
|  |  | ||||||
|  | @ -24,7 +24,7 @@ type Notifier interface { | ||||||
| 	// ShouldNotify checks this evaluation should send an alert notification
 | 	// ShouldNotify checks this evaluation should send an alert notification
 | ||||||
| 	ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool | 	ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool | ||||||
| 
 | 
 | ||||||
| 	GetNotifierId() int64 | 	GetNotifierUid() string | ||||||
| 	GetIsDefault() bool | 	GetIsDefault() bool | ||||||
| 	GetSendReminder() bool | 	GetSendReminder() bool | ||||||
| 	GetDisableResolveMessage() bool | 	GetDisableResolveMessage() bool | ||||||
|  |  | ||||||
|  | @ -60,13 +60,13 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error { | ||||||
| func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *notifierState) error { | func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *notifierState) error { | ||||||
| 	notifier := notifierState.notifier | 	notifier := notifierState.notifier | ||||||
| 
 | 
 | ||||||
| 	n.log.Debug("Sending notification", "type", notifier.GetType(), "id", notifier.GetNotifierId(), "isDefault", notifier.GetIsDefault()) | 	n.log.Debug("Sending notification", "type", notifier.GetType(), "uid", notifier.GetNotifierUid(), "isDefault", notifier.GetIsDefault()) | ||||||
| 	metrics.M_Alerting_Notification_Sent.WithLabelValues(notifier.GetType()).Inc() | 	metrics.M_Alerting_Notification_Sent.WithLabelValues(notifier.GetType()).Inc() | ||||||
| 
 | 
 | ||||||
| 	err := notifier.Notify(evalContext) | 	err := notifier.Notify(evalContext) | ||||||
| 
 | 
 | ||||||
| 	if err != nil { | 	if err != nil { | ||||||
| 		n.log.Error("failed to send notification", "id", notifier.GetNotifierId(), "error", err) | 		n.log.Error("failed to send notification", "uid", notifier.GetNotifierUid(), "error", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	if evalContext.IsTestRun { | 	if evalContext.IsTestRun { | ||||||
|  | @ -110,7 +110,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi | ||||||
| 	for _, notifierState := range notifierStates { | 	for _, notifierState := range notifierStates { | ||||||
| 		err := n.sendNotification(evalContext, notifierState) | 		err := n.sendNotification(evalContext, notifierState) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			n.log.Error("failed to send notification", "id", notifierState.notifier.GetNotifierId(), "error", err) | 			n.log.Error("failed to send notification", "uid", notifierState.notifier.GetNotifierUid(), "error", err) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | @ -157,8 +157,8 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (notifierStateSlice, error) { | func (n *notificationService) getNeededNotifiers(orgId int64, notificationUids []string, evalContext *EvalContext) (notifierStateSlice, error) { | ||||||
| 	query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} | 	query := &m.GetAlertNotificationsWithUidToSendQuery{OrgId: orgId, Uids: notificationUids} | ||||||
| 
 | 
 | ||||||
| 	if err := bus.Dispatch(query); err != nil { | 	if err := bus.Dispatch(query); err != nil { | ||||||
| 		return nil, err | 		return nil, err | ||||||
|  | @ -168,7 +168,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] | ||||||
| 	for _, notification := range query.Result { | 	for _, notification := range query.Result { | ||||||
| 		not, err := InitNotifier(notification) | 		not, err := InitNotifier(notification) | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err) | 			n.log.Error("Could not create notifier", "notifier", notification.Uid, "error", err) | ||||||
| 			continue | 			continue | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -16,7 +16,7 @@ const ( | ||||||
| type NotifierBase struct { | type NotifierBase struct { | ||||||
| 	Name                  string | 	Name                  string | ||||||
| 	Type                  string | 	Type                  string | ||||||
| 	Id                    int64 | 	Uid                   string | ||||||
| 	IsDeault              bool | 	IsDeault              bool | ||||||
| 	UploadImage           bool | 	UploadImage           bool | ||||||
| 	SendReminder          bool | 	SendReminder          bool | ||||||
|  | @ -34,7 +34,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	return NotifierBase{ | 	return NotifierBase{ | ||||||
| 		Id:                    model.Id, | 		Uid:                   model.Uid, | ||||||
| 		Name:                  model.Name, | 		Name:                  model.Name, | ||||||
| 		IsDeault:              model.IsDefault, | 		IsDeault:              model.IsDefault, | ||||||
| 		Type:                  model.Type, | 		Type:                  model.Type, | ||||||
|  | @ -110,8 +110,8 @@ func (n *NotifierBase) NeedsImage() bool { | ||||||
| 	return n.UploadImage | 	return n.UploadImage | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (n *NotifierBase) GetNotifierId() int64 { | func (n *NotifierBase) GetNotifierUid() string { | ||||||
| 	return n.Id | 	return n.Uid | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func (n *NotifierBase) GetIsDefault() bool { | func (n *NotifierBase) GetIsDefault() bool { | ||||||
|  |  | ||||||
|  | @ -173,7 +173,7 @@ func TestBaseNotifier(t *testing.T) { | ||||||
| 		bJson := simplejson.New() | 		bJson := simplejson.New() | ||||||
| 
 | 
 | ||||||
| 		model := &m.AlertNotification{ | 		model := &m.AlertNotification{ | ||||||
| 			Id:       1, | 			Uid:      "1", | ||||||
| 			Name:     "name", | 			Name:     "name", | ||||||
| 			Type:     "email", | 			Type:     "email", | ||||||
| 			Settings: bJson, | 			Settings: bJson, | ||||||
|  |  | ||||||
|  | @ -30,7 +30,7 @@ type Rule struct { | ||||||
| 	ExecutionErrorState m.ExecutionErrorOption | 	ExecutionErrorState m.ExecutionErrorOption | ||||||
| 	State               m.AlertStateType | 	State               m.AlertStateType | ||||||
| 	Conditions          []Condition | 	Conditions          []Condition | ||||||
| 	Notifications       []int64 | 	Notifications       []string | ||||||
| 
 | 
 | ||||||
| 	StateChanges int64 | 	StateChanges int64 | ||||||
| } | } | ||||||
|  | @ -126,11 +126,15 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { | ||||||
| 
 | 
 | ||||||
| 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() { | 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() { | ||||||
| 		jsonModel := simplejson.NewFromAny(v) | 		jsonModel := simplejson.NewFromAny(v) | ||||||
| 		id, err := jsonModel.Get("id").Int64() | 		if id, err := jsonModel.Get("id").Int64(); err == nil { | ||||||
| 		if err != nil { | 			model.Notifications = append(model.Notifications, fmt.Sprintf("%09d", id)) | ||||||
| 			return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} | 		} else { | ||||||
|  | 			if uid, err := jsonModel.Get("uid").String(); err != nil { | ||||||
|  | 				return nil, ValidationError{Reason: "Neither id nor uid is specified, " + err.Error(), DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} | ||||||
|  | 			} else { | ||||||
|  | 				model.Notifications = append(model.Notifications, uid) | ||||||
|  | 			} | ||||||
| 		} | 		} | ||||||
| 		model.Notifications = append(model.Notifications, id) |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	for index, condition := range ruleDef.Settings.Get("conditions").MustArray() { | 	for index, condition := range ruleDef.Settings.Get("conditions").MustArray() { | ||||||
|  |  | ||||||
|  | @ -5,6 +5,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/grafana/grafana/pkg/components/simplejson" | 	"github.com/grafana/grafana/pkg/components/simplejson" | ||||||
| 	m "github.com/grafana/grafana/pkg/models" | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||||
| 	. "github.com/smartystreets/goconvey/convey" | 	. "github.com/smartystreets/goconvey/convey" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -45,6 +46,7 @@ func TestAlertRuleFrequencyParsing(t *testing.T) { | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func TestAlertRuleModel(t *testing.T) { | func TestAlertRuleModel(t *testing.T) { | ||||||
|  | 	sqlstore.InitTestDB(t) | ||||||
| 	Convey("Testing alert rule", t, func() { | 	Convey("Testing alert rule", t, func() { | ||||||
| 
 | 
 | ||||||
| 		RegisterCondition("test", func(model *simplejson.Json, index int) (Condition, error) { | 		RegisterCondition("test", func(model *simplejson.Json, index int) (Condition, error) { | ||||||
|  | @ -57,46 +59,57 @@ func TestAlertRuleModel(t *testing.T) { | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
| 		Convey("can construct alert rule model", func() { | 		Convey("can construct alert rule model", func() { | ||||||
| 			json := ` | 			firstNotification := m.CreateAlertNotificationCommand{OrgId: 1, Name: "1"} | ||||||
| 			{ | 			err := sqlstore.CreateAlertNotificationCommand(&firstNotification) | ||||||
| 				"name": "name2", | 			So(err, ShouldBeNil) | ||||||
| 				"description": "desc2", | 			secondNotification := m.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"} | ||||||
| 				"handler": 0, | 			err = sqlstore.CreateAlertNotificationCommand(&secondNotification) | ||||||
| 				"noDataMode": "critical", |  | ||||||
| 				"enabled": true, |  | ||||||
| 				"frequency": "60s", |  | ||||||
|         "conditions": [ |  | ||||||
|           { |  | ||||||
|             "type": "test", |  | ||||||
|             "prop": 123 |  | ||||||
| 					} |  | ||||||
|         ], |  | ||||||
|         "notifications": [ |  | ||||||
| 					{"id": 1134}, |  | ||||||
| 					{"id": 22} |  | ||||||
| 				] |  | ||||||
| 			} |  | ||||||
| 			` |  | ||||||
| 
 |  | ||||||
| 			alertJSON, jsonErr := simplejson.NewJson([]byte(json)) |  | ||||||
| 			So(jsonErr, ShouldBeNil) |  | ||||||
| 
 |  | ||||||
| 			alert := &m.Alert{ |  | ||||||
| 				Id:          1, |  | ||||||
| 				OrgId:       1, |  | ||||||
| 				DashboardId: 1, |  | ||||||
| 				PanelId:     1, |  | ||||||
| 
 |  | ||||||
| 				Settings: alertJSON, |  | ||||||
| 			} |  | ||||||
| 
 |  | ||||||
| 			alertRule, err := NewRuleFromDBAlert(alert) |  | ||||||
| 			So(err, ShouldBeNil) | 			So(err, ShouldBeNil) | ||||||
| 
 | 
 | ||||||
| 			So(len(alertRule.Conditions), ShouldEqual, 1) | 			Convey("with notification id and uid", func() { | ||||||
|  | 				json := ` | ||||||
|  | 				{ | ||||||
|  | 					"name": "name2", | ||||||
|  | 					"description": "desc2", | ||||||
|  | 					"handler": 0, | ||||||
|  | 					"noDataMode": "critical", | ||||||
|  | 					"enabled": true, | ||||||
|  | 					"frequency": "60s", | ||||||
|  | 					"conditions": [ | ||||||
|  | 						{ | ||||||
|  | 							"type": "test", | ||||||
|  | 							"prop": 123 | ||||||
|  | 						} | ||||||
|  | 					], | ||||||
|  | 					"notifications": [ | ||||||
|  | 						{"id": 1}, | ||||||
|  | 						{"uid": "notifier2"} | ||||||
|  | 					] | ||||||
|  | 				} | ||||||
|  | 				` | ||||||
| 
 | 
 | ||||||
| 			Convey("Can read notifications", func() { | 				alertJSON, jsonErr := simplejson.NewJson([]byte(json)) | ||||||
| 				So(len(alertRule.Notifications), ShouldEqual, 2) | 				So(jsonErr, ShouldBeNil) | ||||||
|  | 
 | ||||||
|  | 				alert := &m.Alert{ | ||||||
|  | 					Id:          1, | ||||||
|  | 					OrgId:       1, | ||||||
|  | 					DashboardId: 1, | ||||||
|  | 					PanelId:     1, | ||||||
|  | 
 | ||||||
|  | 					Settings: alertJSON, | ||||||
|  | 				} | ||||||
|  | 
 | ||||||
|  | 				alertRule, err := NewRuleFromDBAlert(alert) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 
 | ||||||
|  | 				So(len(alertRule.Conditions), ShouldEqual, 1) | ||||||
|  | 
 | ||||||
|  | 				Convey("Can read notifications", func() { | ||||||
|  | 					So(len(alertRule.Notifications), ShouldEqual, 2) | ||||||
|  | 					So(alertRule.Notifications, ShouldContain, "000000001") | ||||||
|  | 					So(alertRule.Notifications, ShouldContain, "notifier2") | ||||||
|  | 				}) | ||||||
| 			}) | 			}) | ||||||
| 		}) | 		}) | ||||||
| 
 | 
 | ||||||
|  | @ -108,8 +121,8 @@ func TestAlertRuleModel(t *testing.T) { | ||||||
| 				"noDataMode": "critical", | 				"noDataMode": "critical", | ||||||
| 				"enabled": true, | 				"enabled": true, | ||||||
| 				"frequency": "0s", | 				"frequency": "0s", | ||||||
|         		"conditions": [ { "type": "test", "prop": 123 } ], | 				"conditions": [ { "type": "test", "prop": 123 } ], | ||||||
|         		"notifications": [] | 				"notifications": [] | ||||||
| 			}` | 			}` | ||||||
| 
 | 
 | ||||||
| 			alertJSON, jsonErr := simplejson.NewJson([]byte(json)) | 			alertJSON, jsonErr := simplejson.NewJson([]byte(json)) | ||||||
|  | @ -129,5 +142,43 @@ func TestAlertRuleModel(t *testing.T) { | ||||||
| 			So(err, ShouldBeNil) | 			So(err, ShouldBeNil) | ||||||
| 			So(alertRule.Frequency, ShouldEqual, 60) | 			So(alertRule.Frequency, ShouldEqual, 60) | ||||||
| 		}) | 		}) | ||||||
|  | 
 | ||||||
|  | 		Convey("raise error in case of missing notification id and uid", func() { | ||||||
|  | 			json := ` | ||||||
|  | 			{ | ||||||
|  | 				"name": "name2", | ||||||
|  | 				"description": "desc2", | ||||||
|  | 				"noDataMode": "critical", | ||||||
|  | 				"enabled": true, | ||||||
|  | 				"frequency": "60s", | ||||||
|  | 				"conditions": [ | ||||||
|  | 					{ | ||||||
|  | 						"type": "test", | ||||||
|  | 						"prop": 123 | ||||||
|  | 					} | ||||||
|  | 				], | ||||||
|  | 				"notifications": [ | ||||||
|  | 					{"not_id_uid": "1134"} | ||||||
|  | 				] | ||||||
|  | 			} | ||||||
|  | 			` | ||||||
|  | 
 | ||||||
|  | 			alertJSON, jsonErr := simplejson.NewJson([]byte(json)) | ||||||
|  | 			So(jsonErr, ShouldBeNil) | ||||||
|  | 
 | ||||||
|  | 			alert := &m.Alert{ | ||||||
|  | 				Id:          1, | ||||||
|  | 				OrgId:       1, | ||||||
|  | 				DashboardId: 1, | ||||||
|  | 				PanelId:     1, | ||||||
|  | 				Frequency:   0, | ||||||
|  | 
 | ||||||
|  | 				Settings: alertJSON, | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			_, err := NewRuleFromDBAlert(alert) | ||||||
|  | 			So(err, ShouldNotBeNil) | ||||||
|  | 			So(err.Error(), ShouldEqual, "Alert validation error: Neither id nor uid is specified, type assertion to string failed AlertId: 1 PanelId: 1 DashboardId: 1") | ||||||
|  | 		}) | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -44,7 +44,10 @@ | ||||||
|               "noDataState": "no_data", |               "noDataState": "no_data", | ||||||
|               "notifications": [ |               "notifications": [ | ||||||
|                 { |                 { | ||||||
|                   "id": 6 |                   "uid": "notifier1" | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   "id": 2 | ||||||
|                 } |                 } | ||||||
|               ] |               ] | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  | @ -45,7 +45,10 @@ | ||||||
|               "noDataState": "no_data", |               "noDataState": "no_data", | ||||||
|               "notifications": [ |               "notifications": [ | ||||||
|                 { |                 { | ||||||
|                   "id": 6 |                   "id": 1 | ||||||
|  |                 }, | ||||||
|  |                 { | ||||||
|  |                   "uid": "notifier2" | ||||||
|                 } |                 } | ||||||
|               ] |               ] | ||||||
|             }, |             }, | ||||||
|  |  | ||||||
|  | @ -0,0 +1,180 @@ | ||||||
|  | package notifiers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"errors" | ||||||
|  | 
 | ||||||
|  | 	"github.com/grafana/grafana/pkg/bus" | ||||||
|  | 	"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") | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func Provision(configDirectory string) error { | ||||||
|  | 	dc := newNotificationProvisioner(log.New("provisioning.notifiers")) | ||||||
|  | 	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, "uid", notification.Uid) | ||||||
|  | 
 | ||||||
|  | 		if notification.OrgId == 0 && notification.OrgName != "" { | ||||||
|  | 			getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName} | ||||||
|  | 			if err := bus.Dispatch(getOrg); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			notification.OrgId = getOrg.Result.Id | ||||||
|  | 		} else if notification.OrgId < 0 { | ||||||
|  | 			notification.OrgId = 1 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		getNotification := &models.GetAlertNotificationsWithUidQuery{Uid: notification.Uid, OrgId: notification.OrgId} | ||||||
|  | 
 | ||||||
|  | 		if err := bus.Dispatch(getNotification); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if getNotification.Result != nil { | ||||||
|  | 			cmd := &models.DeleteAlertNotificationWithUidCommand{Uid: getNotification.Result.Uid, 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 { | ||||||
|  | 
 | ||||||
|  | 		if notification.OrgId == 0 && notification.OrgName != "" { | ||||||
|  | 			getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName} | ||||||
|  | 			if err := bus.Dispatch(getOrg); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 			notification.OrgId = getOrg.Result.Id | ||||||
|  | 		} else if notification.OrgId < 0 { | ||||||
|  | 			notification.OrgId = 1 | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		cmd := &models.GetAlertNotificationsWithUidQuery{OrgId: notification.OrgId, Uid: notification.Uid} | ||||||
|  | 		err := bus.Dispatch(cmd) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if cmd.Result == nil { | ||||||
|  | 			dc.log.Info("Inserting alert notification from configuration ", "name", notification.Name, "uid", notification.Uid) | ||||||
|  | 			insertCmd := &models.CreateAlertNotificationCommand{ | ||||||
|  | 				Uid:                   notification.Uid, | ||||||
|  | 				Name:                  notification.Name, | ||||||
|  | 				Type:                  notification.Type, | ||||||
|  | 				IsDefault:             notification.IsDefault, | ||||||
|  | 				Settings:              notification.SettingsToJson(), | ||||||
|  | 				OrgId:                 notification.OrgId, | ||||||
|  | 				DisableResolveMessage: notification.DisableResolveMessage, | ||||||
|  | 				Frequency:             notification.Frequency, | ||||||
|  | 				SendReminder:          notification.SendReminder, | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if err := bus.Dispatch(insertCmd); err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} else { | ||||||
|  | 			dc.log.Info("Updating alert notification from configuration", "name", notification.Name) | ||||||
|  | 			updateCmd := &models.UpdateAlertNotificationWithUidCommand{ | ||||||
|  | 				Uid:                   notification.Uid, | ||||||
|  | 				Name:                  notification.Name, | ||||||
|  | 				Type:                  notification.Type, | ||||||
|  | 				IsDefault:             notification.IsDefault, | ||||||
|  | 				Settings:              notification.SettingsToJson(), | ||||||
|  | 				OrgId:                 notification.OrgId, | ||||||
|  | 				DisableResolveMessage: notification.DisableResolveMessage, | ||||||
|  | 				Frequency:             notification.Frequency, | ||||||
|  | 				SendReminder:          notification.SendReminder, | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			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{ | ||||||
|  | 			Uid:                   notification.Uid, | ||||||
|  | 			OrgId:                 notification.OrgId, | ||||||
|  | 			OrgName:               notification.OrgName, | ||||||
|  | 			Name:                  notification.Name, | ||||||
|  | 			Type:                  notification.Type, | ||||||
|  | 			IsDefault:             notification.IsDefault, | ||||||
|  | 			Settings:              notification.Settings, | ||||||
|  | 			DisableResolveMessage: notification.DisableResolveMessage, | ||||||
|  | 			Frequency:             notification.Frequency, | ||||||
|  | 			SendReminder:          notification.SendReminder, | ||||||
|  | 		}) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	for _, notification := range cfg.DeleteNotifications { | ||||||
|  | 		r.DeleteNotifications = append(r.DeleteNotifications, &deleteNotificationConfig{ | ||||||
|  | 			Uid:     notification.Uid, | ||||||
|  | 			OrgId:   notification.OrgId, | ||||||
|  | 			OrgName: notification.OrgName, | ||||||
|  | 			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 | ||||||
|  | } | ||||||
|  | @ -0,0 +1,163 @@ | ||||||
|  | package notifiers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"fmt" | ||||||
|  | 	"io/ioutil" | ||||||
|  | 	"os" | ||||||
|  | 	"path/filepath" | ||||||
|  | 	"strings" | ||||||
|  | 
 | ||||||
|  | 	"github.com/grafana/grafana/pkg/log" | ||||||
|  | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | 	"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") | ||||||
|  | 	if err = validateRequiredField(notifications); err != nil { | ||||||
|  | 		return nil, err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	checkOrgIdAndOrgName(notifications) | ||||||
|  | 
 | ||||||
|  | 	err = validateNotifications(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 checkOrgIdAndOrgName(notifications []*notificationsAsConfig) { | ||||||
|  | 	for i := range notifications { | ||||||
|  | 		for _, notification := range notifications[i].Notifications { | ||||||
|  | 			if notification.OrgId < 1 { | ||||||
|  | 				if notification.OrgName == "" { | ||||||
|  | 					notification.OrgId = 1 | ||||||
|  | 				} else { | ||||||
|  | 					notification.OrgId = 0 | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for _, notification := range notifications[i].DeleteNotifications { | ||||||
|  | 			if notification.OrgId < 1 { | ||||||
|  | 				if notification.OrgName == "" { | ||||||
|  | 					notification.OrgId = 1 | ||||||
|  | 				} else { | ||||||
|  | 					notification.OrgId = 0 | ||||||
|  | 				} | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateRequiredField(notifications []*notificationsAsConfig) error { | ||||||
|  | 	for i := range notifications { | ||||||
|  | 		var errStrings []string | ||||||
|  | 		for index, notification := range notifications[i].Notifications { | ||||||
|  | 			if notification.Name == "" { | ||||||
|  | 				errStrings = append( | ||||||
|  | 					errStrings, | ||||||
|  | 					fmt.Sprintf("Added alert notification item %d in configuration doesn't contain required field name", index+1), | ||||||
|  | 				) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if notification.Uid == "" { | ||||||
|  | 				errStrings = append( | ||||||
|  | 					errStrings, | ||||||
|  | 					fmt.Sprintf("Added alert notification item %d in configuration doesn't contain required field uid", index+1), | ||||||
|  | 				) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for index, notification := range notifications[i].DeleteNotifications { | ||||||
|  | 			if notification.Name == "" { | ||||||
|  | 				errStrings = append( | ||||||
|  | 					errStrings, | ||||||
|  | 					fmt.Sprintf("Deleted alert notification item %d in configuration doesn't contain required field name", index+1), | ||||||
|  | 				) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			if notification.Uid == "" { | ||||||
|  | 				errStrings = append( | ||||||
|  | 					errStrings, | ||||||
|  | 					fmt.Sprintf("Deleted alert notification item %d in configuration doesn't contain required field uid", index+1), | ||||||
|  | 				) | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if len(errStrings) != 0 { | ||||||
|  | 			return fmt.Errorf(strings.Join(errStrings, "\n")) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func validateNotifications(notifications []*notificationsAsConfig) error { | ||||||
|  | 
 | ||||||
|  | 	for i := range notifications { | ||||||
|  | 		if notifications[i].Notifications == nil { | ||||||
|  | 			continue | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		for _, notification := range notifications[i].Notifications { | ||||||
|  | 			_, err := alerting.InitNotifier(&m.AlertNotification{ | ||||||
|  | 				Name:     notification.Name, | ||||||
|  | 				Settings: notification.SettingsToJson(), | ||||||
|  | 				Type:     notification.Type, | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			if err != nil { | ||||||
|  | 				return err | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | @ -0,0 +1,313 @@ | ||||||
|  | package notifiers | ||||||
|  | 
 | ||||||
|  | import ( | ||||||
|  | 	"testing" | ||||||
|  | 
 | ||||||
|  | 	"github.com/grafana/grafana/pkg/log" | ||||||
|  | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | 	"github.com/grafana/grafana/pkg/services/alerting" | ||||||
|  | 	"github.com/grafana/grafana/pkg/services/alerting/notifiers" | ||||||
|  | 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||||
|  | 	. "github.com/smartystreets/goconvey/convey" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | var ( | ||||||
|  | 	correct_properties              = "./testdata/test-configs/correct-properties" | ||||||
|  | 	incorrect_settings              = "./testdata/test-configs/incorrect-settings" | ||||||
|  | 	no_required_fields              = "./testdata/test-configs/no-required-fields" | ||||||
|  | 	correct_properties_with_orgName = "./testdata/test-configs/correct-properties-with-orgName" | ||||||
|  | 	brokenYaml                      = "./testdata/test-configs/broken-yaml" | ||||||
|  | 	doubleNotificationsConfig       = "./testdata/test-configs/double-default" | ||||||
|  | 	emptyFolder                     = "./testdata/test-configs/empty_folder" | ||||||
|  | 	emptyFile                       = "./testdata/test-configs/empty" | ||||||
|  | 	twoNotificationsConfig          = "./testdata/test-configs/two-notifications" | ||||||
|  | 	unknownNotifier                 = "./testdata/test-configs/unknown-notifier" | ||||||
|  | ) | ||||||
|  | 
 | ||||||
|  | func TestNotificationAsConfig(t *testing.T) { | ||||||
|  | 	logger := log.New("fake.log") | ||||||
|  | 
 | ||||||
|  | 	Convey("Testing notification as configuration", t, func() { | ||||||
|  | 		sqlstore.InitTestDB(t) | ||||||
|  | 
 | ||||||
|  | 		alerting.RegisterNotifier(&alerting.NotifierPlugin{ | ||||||
|  | 			Type:    "slack", | ||||||
|  | 			Name:    "slack", | ||||||
|  | 			Factory: notifiers.NewSlackNotifier, | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		alerting.RegisterNotifier(&alerting.NotifierPlugin{ | ||||||
|  | 			Type:    "email", | ||||||
|  | 			Name:    "email", | ||||||
|  | 			Factory: notifiers.NewEmailNotifier, | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		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.Uid, ShouldEqual, "notifier1") | ||||||
|  | 			So(nt.IsDefault, ShouldBeTrue) | ||||||
|  | 			So(nt.Settings, ShouldResemble, map[string]interface{}{ | ||||||
|  | 				"recipient": "XXX", "token": "xoxb", "uploadImage": true, "url": "https://slack.com", | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			nt = nts[1] | ||||||
|  | 			So(nt.Name, ShouldEqual, "another-not-default-notification") | ||||||
|  | 			So(nt.Type, ShouldEqual, "email") | ||||||
|  | 			So(nt.OrgId, ShouldEqual, 3) | ||||||
|  | 			So(nt.Uid, ShouldEqual, "notifier2") | ||||||
|  | 			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.Uid, ShouldEqual, "notifier3") | ||||||
|  | 			So(nt.IsDefault, ShouldBeFalse) | ||||||
|  | 
 | ||||||
|  | 			nt = nts[3] | ||||||
|  | 			So(nt.Name, ShouldEqual, "Added notification with whitespaces in name") | ||||||
|  | 			So(nt.Type, ShouldEqual, "email") | ||||||
|  | 			So(nt.Uid, ShouldEqual, "notifier4") | ||||||
|  | 			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.Uid, ShouldEqual, "notifier1") | ||||||
|  | 			So(deleteNt.OrgId, ShouldEqual, 2) | ||||||
|  | 
 | ||||||
|  | 			deleteNt = deleteNts[1] | ||||||
|  | 			So(deleteNt.Name, ShouldEqual, "deleted-notification-without-orgId") | ||||||
|  | 			So(deleteNt.OrgId, ShouldEqual, 1) | ||||||
|  | 			So(deleteNt.Uid, ShouldEqual, "notifier2") | ||||||
|  | 
 | ||||||
|  | 			deleteNt = deleteNts[2] | ||||||
|  | 			So(deleteNt.Name, ShouldEqual, "deleted-notification-with-0-orgId") | ||||||
|  | 			So(deleteNt.OrgId, ShouldEqual, 1) | ||||||
|  | 			So(deleteNt.Uid, ShouldEqual, "notifier3") | ||||||
|  | 
 | ||||||
|  | 			deleteNt = deleteNts[3] | ||||||
|  | 			So(deleteNt.Name, ShouldEqual, "Deleted notification with whitespaces in name") | ||||||
|  | 			So(deleteNt.OrgId, ShouldEqual, 1) | ||||||
|  | 			So(deleteNt.Uid, ShouldEqual, "notifier4") | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		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) | ||||||
|  | 				} | ||||||
|  | 				notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1} | ||||||
|  | 				err = sqlstore.GetAllAlertNotifications(¬ificationsQuery) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 				So(notificationsQuery.Result, ShouldNotBeNil) | ||||||
|  | 				So(len(notificationsQuery.Result), ShouldEqual, 2) | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 			Convey("One notification in database with same name and uid", func() { | ||||||
|  | 				existingNotificationCmd := m.CreateAlertNotificationCommand{ | ||||||
|  | 					Name:  "channel1", | ||||||
|  | 					OrgId: 1, | ||||||
|  | 					Uid:   "notifier1", | ||||||
|  | 					Type:  "slack", | ||||||
|  | 				} | ||||||
|  | 				err := sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 				So(existingNotificationCmd.Result, ShouldNotBeNil) | ||||||
|  | 				notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1} | ||||||
|  | 				err = sqlstore.GetAllAlertNotifications(¬ificationsQuery) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 				So(notificationsQuery.Result, ShouldNotBeNil) | ||||||
|  | 				So(len(notificationsQuery.Result), ShouldEqual, 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) | ||||||
|  | 					} | ||||||
|  | 					err = sqlstore.GetAllAlertNotifications(¬ificationsQuery) | ||||||
|  | 					So(err, ShouldBeNil) | ||||||
|  | 					So(notificationsQuery.Result, ShouldNotBeNil) | ||||||
|  | 					So(len(notificationsQuery.Result), ShouldEqual, 2) | ||||||
|  | 
 | ||||||
|  | 					nts := notificationsQuery.Result | ||||||
|  | 					nt1 := nts[0] | ||||||
|  | 					So(nt1.Type, ShouldEqual, "email") | ||||||
|  | 					So(nt1.Name, ShouldEqual, "channel1") | ||||||
|  | 					So(nt1.Uid, ShouldEqual, "notifier1") | ||||||
|  | 
 | ||||||
|  | 					nt2 := nts[1] | ||||||
|  | 					So(nt2.Type, ShouldEqual, "slack") | ||||||
|  | 					So(nt2.Name, ShouldEqual, "channel2") | ||||||
|  | 					So(nt2.Uid, ShouldEqual, "notifier2") | ||||||
|  | 				}) | ||||||
|  | 			}) | ||||||
|  | 			Convey("Two notifications with is_default", func() { | ||||||
|  | 				dc := newNotificationProvisioner(logger) | ||||||
|  | 				err := dc.applyChanges(doubleNotificationsConfig) | ||||||
|  | 				Convey("should both be inserted", func() { | ||||||
|  | 					So(err, ShouldBeNil) | ||||||
|  | 					notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1} | ||||||
|  | 					err = sqlstore.GetAllAlertNotifications(¬ificationsQuery) | ||||||
|  | 					So(err, ShouldBeNil) | ||||||
|  | 					So(notificationsQuery.Result, ShouldNotBeNil) | ||||||
|  | 					So(len(notificationsQuery.Result), ShouldEqual, 2) | ||||||
|  | 
 | ||||||
|  | 					So(notificationsQuery.Result[0].IsDefault, ShouldBeTrue) | ||||||
|  | 					So(notificationsQuery.Result[1].IsDefault, ShouldBeTrue) | ||||||
|  | 				}) | ||||||
|  | 			}) | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		Convey("Two configured notification", func() { | ||||||
|  | 			Convey("two other notifications in database", func() { | ||||||
|  | 				existingNotificationCmd := m.CreateAlertNotificationCommand{ | ||||||
|  | 					Name:  "channel0", | ||||||
|  | 					OrgId: 1, | ||||||
|  | 					Uid:   "notifier0", | ||||||
|  | 					Type:  "slack", | ||||||
|  | 				} | ||||||
|  | 				err := sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 				existingNotificationCmd = m.CreateAlertNotificationCommand{ | ||||||
|  | 					Name:  "channel3", | ||||||
|  | 					OrgId: 1, | ||||||
|  | 					Uid:   "notifier3", | ||||||
|  | 					Type:  "slack", | ||||||
|  | 				} | ||||||
|  | 				err = sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 
 | ||||||
|  | 				notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1} | ||||||
|  | 				err = sqlstore.GetAllAlertNotifications(¬ificationsQuery) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 				So(notificationsQuery.Result, ShouldNotBeNil) | ||||||
|  | 				So(len(notificationsQuery.Result), ShouldEqual, 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) | ||||||
|  | 					} | ||||||
|  | 					notificationsQuery = m.GetAllAlertNotificationsQuery{OrgId: 1} | ||||||
|  | 					err = sqlstore.GetAllAlertNotifications(¬ificationsQuery) | ||||||
|  | 					So(err, ShouldBeNil) | ||||||
|  | 					So(notificationsQuery.Result, ShouldNotBeNil) | ||||||
|  | 					So(len(notificationsQuery.Result), ShouldEqual, 4) | ||||||
|  | 				}) | ||||||
|  | 			}) | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		Convey("Can read correct properties with orgName instead of orgId", func() { | ||||||
|  | 			existingOrg1 := m.CreateOrgCommand{Name: "Main Org. 1"} | ||||||
|  | 			err := sqlstore.CreateOrg(&existingOrg1) | ||||||
|  | 			So(err, ShouldBeNil) | ||||||
|  | 			So(existingOrg1.Result, ShouldNotBeNil) | ||||||
|  | 			existingOrg2 := m.CreateOrgCommand{Name: "Main Org. 2"} | ||||||
|  | 			err = sqlstore.CreateOrg(&existingOrg2) | ||||||
|  | 			So(err, ShouldBeNil) | ||||||
|  | 			So(existingOrg2.Result, ShouldNotBeNil) | ||||||
|  | 
 | ||||||
|  | 			existingNotificationCmd := m.CreateAlertNotificationCommand{ | ||||||
|  | 				Name:  "default-notification-delete", | ||||||
|  | 				OrgId: existingOrg2.Result.Id, | ||||||
|  | 				Uid:   "notifier2", | ||||||
|  | 				Type:  "slack", | ||||||
|  | 			} | ||||||
|  | 			err = sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd) | ||||||
|  | 			So(err, ShouldBeNil) | ||||||
|  | 
 | ||||||
|  | 			dc := newNotificationProvisioner(logger) | ||||||
|  | 			err = dc.applyChanges(correct_properties_with_orgName) | ||||||
|  | 			if err != nil { | ||||||
|  | 				t.Fatalf("applyChanges return an error %v", err) | ||||||
|  | 			} | ||||||
|  | 
 | ||||||
|  | 			notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: existingOrg2.Result.Id} | ||||||
|  | 			err = sqlstore.GetAllAlertNotifications(¬ificationsQuery) | ||||||
|  | 			So(err, ShouldBeNil) | ||||||
|  | 			So(notificationsQuery.Result, ShouldNotBeNil) | ||||||
|  | 			So(len(notificationsQuery.Result), ShouldEqual, 1) | ||||||
|  | 
 | ||||||
|  | 			nt := notificationsQuery.Result[0] | ||||||
|  | 			So(nt.Name, ShouldEqual, "default-notification-create") | ||||||
|  | 			So(nt.OrgId, ShouldEqual, existingOrg2.Result.Id) | ||||||
|  | 
 | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		Convey("Config doesn't contain required field", func() { | ||||||
|  | 			dc := newNotificationProvisioner(logger) | ||||||
|  | 			err := dc.applyChanges(no_required_fields) | ||||||
|  | 			So(err, ShouldNotBeNil) | ||||||
|  | 
 | ||||||
|  | 			errString := err.Error() | ||||||
|  | 			So(errString, ShouldContainSubstring, "Deleted alert notification item 1 in configuration doesn't contain required field uid") | ||||||
|  | 			So(errString, ShouldContainSubstring, "Deleted alert notification item 2 in configuration doesn't contain required field name") | ||||||
|  | 			So(errString, ShouldContainSubstring, "Added alert notification item 1 in configuration doesn't contain required field name") | ||||||
|  | 			So(errString, ShouldContainSubstring, "Added alert notification item 2 in configuration doesn't contain required field uid") | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		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) | ||||||
|  | 				} | ||||||
|  | 				notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1} | ||||||
|  | 				err = sqlstore.GetAllAlertNotifications(¬ificationsQuery) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 				So(notificationsQuery.Result, ShouldBeEmpty) | ||||||
|  | 			}) | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		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, ShouldNotBeNil) | ||||||
|  | 			So(err.Error(), ShouldEqual, "Unsupported notification type") | ||||||
|  | 		}) | ||||||
|  | 
 | ||||||
|  | 		Convey("Read incorrect properties", func() { | ||||||
|  | 			cfgProvifer := &configReader{log: log.New("test logger")} | ||||||
|  | 			_, err := cfgProvifer.readConfig(incorrect_settings) | ||||||
|  | 			So(err, ShouldNotBeNil) | ||||||
|  | 			So(err.Error(), ShouldEqual, "Alert validation error: Could not find url property in settings") | ||||||
|  | 		}) | ||||||
|  | 	}) | ||||||
|  | } | ||||||
							
								
								
									
										9
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/broken.yaml
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										9
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/broken.yaml
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,9 @@ | ||||||
|  | notifiers: | ||||||
|  |   - name: notification-channel-1 | ||||||
|  |      type: slack | ||||||
|  |     org_id: 2 | ||||||
|  |      is_default: true | ||||||
|  |    settings: | ||||||
|  |       recipient: "XXX" | ||||||
|  |       token: "xoxb" | ||||||
|  |       uploadImage: true | ||||||
							
								
								
									
										6
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/not.yaml.text
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										6
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/not.yaml.text
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,6 @@ | ||||||
|  | #sfxzgnsxzcvnbzcvn | ||||||
|  | cvbn | ||||||
|  | cvbn | ||||||
|  | c | ||||||
|  | vbn | ||||||
|  | cvbncvbn | ||||||
|  | @ -0,0 +1,12 @@ | ||||||
|  | notifiers: | ||||||
|  |   - name: default-notification-create | ||||||
|  |     type: email | ||||||
|  |     uid: notifier2 | ||||||
|  |     settings: | ||||||
|  |       addresses: example@example.com | ||||||
|  |     org_name: Main Org. 2 | ||||||
|  |     is_default: false   | ||||||
|  | delete_notifiers: | ||||||
|  |   - name: default-notification-delete | ||||||
|  |     org_name: Main Org. 2 | ||||||
|  |     uid: notifier2 | ||||||
|  | @ -0,0 +1,42 @@ | ||||||
|  | notifiers: | ||||||
|  |   - name: default-slack-notification | ||||||
|  |     type: slack | ||||||
|  |     uid: notifier1 | ||||||
|  |     org_id: 2 | ||||||
|  |     uid: "notifier1" | ||||||
|  |     is_default: true | ||||||
|  |     settings: | ||||||
|  |       recipient: "XXX" | ||||||
|  |       token: "xoxb" | ||||||
|  |       uploadImage: true | ||||||
|  |       url: https://slack.com | ||||||
|  |   - name: another-not-default-notification | ||||||
|  |     type: email | ||||||
|  |     settings: | ||||||
|  |       addresses: example@exmaple.com | ||||||
|  |     org_id: 3 | ||||||
|  |     uid: "notifier2" | ||||||
|  |     is_default: false | ||||||
|  |   - name: check-unset-is_default-is-false | ||||||
|  |     type: slack | ||||||
|  |     org_id: 3 | ||||||
|  |     uid: "notifier3" | ||||||
|  |     settings: | ||||||
|  |       url: https://slack.com | ||||||
|  |   - name: Added notification with whitespaces in name | ||||||
|  |     type: email | ||||||
|  |     org_id: 3 | ||||||
|  |     uid: "notifier4" | ||||||
|  |     settings: | ||||||
|  |       addresses: example@exmaple.com | ||||||
|  | delete_notifiers: | ||||||
|  |   - name: default-slack-notification | ||||||
|  |     org_id: 2 | ||||||
|  |     uid: notifier1 | ||||||
|  |   - name: deleted-notification-without-orgId | ||||||
|  |     uid: "notifier2" | ||||||
|  |   - name: deleted-notification-with-0-orgId | ||||||
|  |     org_id: 0 | ||||||
|  |     uid: "notifier3" | ||||||
|  |   - name: Deleted notification with whitespaces in name | ||||||
|  |     uid: "notifier4" | ||||||
							
								
								
									
										7
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-1.yml
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										7
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-1.yml
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | notifiers: | ||||||
|  |   - name: first-default | ||||||
|  |     type: slack | ||||||
|  |     uid: notifier1 | ||||||
|  |     is_default: true | ||||||
|  |     settings: | ||||||
|  |       url: https://slack.com | ||||||
							
								
								
									
										7
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-2.yaml
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										7
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-2.yaml
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,7 @@ | ||||||
|  | notifiers: | ||||||
|  |   - name: second-default | ||||||
|  |     type: email | ||||||
|  |     uid: notifier2 | ||||||
|  |     is_default: true | ||||||
|  |     settings: | ||||||
|  |       addresses: example@example.com | ||||||
							
								
								
									
										4
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/empty_folder/.gitignore
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										4
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/empty_folder/.gitignore
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | # Ignore everything in this directory | ||||||
|  | * | ||||||
|  | # Except this file | ||||||
|  | !.gitignore | ||||||
|  | @ -0,0 +1,10 @@ | ||||||
|  | notifiers: | ||||||
|  |   - name: slack-notification-without-url-in-settings | ||||||
|  |     type: slack | ||||||
|  |     org_id: 2 | ||||||
|  |     uid: notifier1 | ||||||
|  |     is_default: true | ||||||
|  |     settings: | ||||||
|  |       recipient: "XXX" | ||||||
|  |       token: "xoxb" | ||||||
|  |       uploadImage: true | ||||||
|  | @ -0,0 +1,35 @@ | ||||||
|  | notifiers: | ||||||
|  |   - type: slack | ||||||
|  |     org_id: 2 | ||||||
|  |     uid: no-name_added-notification | ||||||
|  |     is_default: true | ||||||
|  |     settings: | ||||||
|  |       recipient: "XXX" | ||||||
|  |       token: "xoxb" | ||||||
|  |       uploadImage: true | ||||||
|  |   - name: no-uid  | ||||||
|  |     type: slack | ||||||
|  |     org_id: 2     | ||||||
|  |     is_default: true | ||||||
|  |     settings: | ||||||
|  |       recipient: "XXX" | ||||||
|  |       token: "xoxb" | ||||||
|  |       uploadImage: true | ||||||
|  | delete_notifiers: | ||||||
|  |   - name: no-uid  | ||||||
|  |     type: slack | ||||||
|  |     org_id: 2     | ||||||
|  |     is_default: true | ||||||
|  |     settings: | ||||||
|  |       recipient: "XXX" | ||||||
|  |       token: "xoxb" | ||||||
|  |       uploadImage: true | ||||||
|  |   - type: slack | ||||||
|  |     org_id: 2 | ||||||
|  |     uid: no-name_added-notification | ||||||
|  |     is_default: true | ||||||
|  |     settings: | ||||||
|  |       recipient: "XXX" | ||||||
|  |       token: "xoxb" | ||||||
|  |       uploadImage: true | ||||||
|  |        | ||||||
|  | @ -0,0 +1,12 @@ | ||||||
|  | notifiers:   | ||||||
|  |   - name: channel1 | ||||||
|  |     type: email | ||||||
|  |     uid: notifier1 | ||||||
|  |     org_id: 1 | ||||||
|  |     settings: | ||||||
|  |       addresses: example@example.com | ||||||
|  |   - name: channel2 | ||||||
|  |     type: slack | ||||||
|  |     uid: notifier2 | ||||||
|  |     settings: | ||||||
|  |       url: http://slack.com | ||||||
							
								
								
									
										4
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/unknown-notifier/notification.yaml
								
								
								
									vendored
								
								
									Normal file
								
							
							
						
						
									
										4
									
								
								pkg/services/provisioning/notifiers/testdata/test-configs/unknown-notifier/notification.yaml
								
								
								
									vendored
								
								
									Normal file
								
							|  | @ -0,0 +1,4 @@ | ||||||
|  | notifiers: | ||||||
|  |   - name: unknown-notifier | ||||||
|  |     type: nonexisting | ||||||
|  |     uid: notifier1 | ||||||
|  | @ -0,0 +1,38 @@ | ||||||
|  | package notifiers | ||||||
|  | 
 | ||||||
|  | import "github.com/grafana/grafana/pkg/components/simplejson" | ||||||
|  | 
 | ||||||
|  | type notificationsAsConfig struct { | ||||||
|  | 	Notifications       []*notificationFromConfig   `json:"notifiers" yaml:"notifiers"` | ||||||
|  | 	DeleteNotifications []*deleteNotificationConfig `json:"delete_notifiers" yaml:"delete_notifiers"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type deleteNotificationConfig struct { | ||||||
|  | 	Uid     string `json:"uid" yaml:"uid"` | ||||||
|  | 	Name    string `json:"name" yaml:"name"` | ||||||
|  | 	OrgId   int64  `json:"org_id" yaml:"org_id"` | ||||||
|  | 	OrgName string `json:"org_name" yaml:"org_name"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | type notificationFromConfig struct { | ||||||
|  | 	Uid                   string                 `json:"uid" yaml:"uid"` | ||||||
|  | 	OrgId                 int64                  `json:"org_id" yaml:"org_id"` | ||||||
|  | 	OrgName               string                 `json:"org_name" yaml:"org_name"` | ||||||
|  | 	Name                  string                 `json:"name" yaml:"name"` | ||||||
|  | 	Type                  string                 `json:"type" yaml:"type"` | ||||||
|  | 	SendReminder          bool                   `json:"send_reminder" yaml:"send_reminder"` | ||||||
|  | 	DisableResolveMessage bool                   `json:"disable_resolve_message" yaml:"disable_resolve_message"` | ||||||
|  | 	Frequency             string                 `json:"frequency" yaml:"frequency"` | ||||||
|  | 	IsDefault             bool                   `json:"is_default" yaml:"is_default"` | ||||||
|  | 	Settings              map[string]interface{} `json:"settings" yaml:"settings"` | ||||||
|  | } | ||||||
|  | 
 | ||||||
|  | func (notification notificationFromConfig) SettingsToJson() *simplejson.Json { | ||||||
|  | 	settings := simplejson.New() | ||||||
|  | 	if len(notification.Settings) > 0 { | ||||||
|  | 		for k, v := range notification.Settings { | ||||||
|  | 			settings.Set(k, v) | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 	return settings | ||||||
|  | } | ||||||
|  | @ -8,6 +8,7 @@ import ( | ||||||
| 	"github.com/grafana/grafana/pkg/registry" | 	"github.com/grafana/grafana/pkg/registry" | ||||||
| 	"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/setting" | 	"github.com/grafana/grafana/pkg/setting" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
|  | @ -25,6 +26,11 @@ func (ps *ProvisioningService) Init() error { | ||||||
| 		return fmt.Errorf("Datasource provisioning error: %v", err) | 		return fmt.Errorf("Datasource provisioning error: %v", err) | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
|  | 	alertNotificationsPath := path.Join(ps.Cfg.ProvisioningPath, "notifiers") | ||||||
|  | 	if err := notifiers.Provision(alertNotificationsPath); err != nil { | ||||||
|  | 		return fmt.Errorf("Alert notification provisioning error: %v", err) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  |  | ||||||
|  | @ -10,6 +10,7 @@ import ( | ||||||
| 
 | 
 | ||||||
| 	"github.com/grafana/grafana/pkg/bus" | 	"github.com/grafana/grafana/pkg/bus" | ||||||
| 	m "github.com/grafana/grafana/pkg/models" | 	m "github.com/grafana/grafana/pkg/models" | ||||||
|  | 	"github.com/grafana/grafana/pkg/util" | ||||||
| ) | ) | ||||||
| 
 | 
 | ||||||
| func init() { | func init() { | ||||||
|  | @ -17,11 +18,15 @@ func init() { | ||||||
| 	bus.AddHandler("sql", CreateAlertNotificationCommand) | 	bus.AddHandler("sql", CreateAlertNotificationCommand) | ||||||
| 	bus.AddHandler("sql", UpdateAlertNotification) | 	bus.AddHandler("sql", UpdateAlertNotification) | ||||||
| 	bus.AddHandler("sql", DeleteAlertNotification) | 	bus.AddHandler("sql", DeleteAlertNotification) | ||||||
| 	bus.AddHandler("sql", GetAlertNotificationsToSend) |  | ||||||
| 	bus.AddHandler("sql", GetAllAlertNotifications) | 	bus.AddHandler("sql", GetAllAlertNotifications) | ||||||
| 	bus.AddHandlerCtx("sql", GetOrCreateAlertNotificationState) | 	bus.AddHandlerCtx("sql", GetOrCreateAlertNotificationState) | ||||||
| 	bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand) | 	bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand) | ||||||
| 	bus.AddHandlerCtx("sql", SetAlertNotificationStateToPendingCommand) | 	bus.AddHandlerCtx("sql", SetAlertNotificationStateToPendingCommand) | ||||||
|  | 
 | ||||||
|  | 	bus.AddHandler("sql", GetAlertNotificationsWithUid) | ||||||
|  | 	bus.AddHandler("sql", UpdateAlertNotificationWithUid) | ||||||
|  | 	bus.AddHandler("sql", DeleteAlertNotificationWithUid) | ||||||
|  | 	bus.AddHandler("sql", GetAlertNotificationsWithUidToSend) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { | func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { | ||||||
|  | @ -39,10 +44,33 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func DeleteAlertNotificationWithUid(cmd *m.DeleteAlertNotificationWithUidCommand) error { | ||||||
|  | 	existingNotification := &m.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid} | ||||||
|  | 	if err := getAlertNotificationWithUidInternal(existingNotification, newSession()); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if existingNotification.Result != nil { | ||||||
|  | 		deleteCommand := &m.DeleteAlertNotificationCommand{ | ||||||
|  | 			Id:    existingNotification.Result.Id, | ||||||
|  | 			OrgId: existingNotification.Result.OrgId, | ||||||
|  | 		} | ||||||
|  | 		if err := bus.Dispatch(deleteCommand); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error { | func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error { | ||||||
| 	return getAlertNotificationInternal(query, newSession()) | 	return getAlertNotificationInternal(query, newSession()) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func GetAlertNotificationsWithUid(query *m.GetAlertNotificationsWithUidQuery) error { | ||||||
|  | 	return getAlertNotificationWithUidInternal(query, newSession()) | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error { | func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error { | ||||||
| 	results := make([]*m.AlertNotification, 0) | 	results := make([]*m.AlertNotification, 0) | ||||||
| 	if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil { | 	if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil { | ||||||
|  | @ -53,12 +81,13 @@ func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error { | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) error { | func GetAlertNotificationsWithUidToSend(query *m.GetAlertNotificationsWithUidToSendQuery) error { | ||||||
| 	var sql bytes.Buffer | 	var sql bytes.Buffer | ||||||
| 	params := make([]interface{}, 0) | 	params := make([]interface{}, 0) | ||||||
| 
 | 
 | ||||||
| 	sql.WriteString(`SELECT | 	sql.WriteString(`SELECT										 | ||||||
| 										alert_notification.id, | 										alert_notification.id, | ||||||
|  | 										alert_notification.uid, | ||||||
| 										alert_notification.org_id, | 										alert_notification.org_id, | ||||||
| 										alert_notification.name, | 										alert_notification.name, | ||||||
| 										alert_notification.type, | 										alert_notification.type, | ||||||
|  | @ -77,9 +106,10 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro | ||||||
| 
 | 
 | ||||||
| 	sql.WriteString(` AND ((alert_notification.is_default = ?)`) | 	sql.WriteString(` AND ((alert_notification.is_default = ?)`) | ||||||
| 	params = append(params, dialect.BooleanStr(true)) | 	params = append(params, dialect.BooleanStr(true)) | ||||||
| 	if len(query.Ids) > 0 { | 
 | ||||||
| 		sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")") | 	if len(query.Uids) > 0 { | ||||||
| 		for _, v := range query.Ids { | 		sql.WriteString(` OR alert_notification.uid IN (?` + strings.Repeat(",?", len(query.Uids)-1) + ")") | ||||||
|  | 		for _, v := range query.Uids { | ||||||
| 			params = append(params, v) | 			params = append(params, v) | ||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
|  | @ -142,16 +172,70 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS | ||||||
| 	return nil | 	return nil | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func getAlertNotificationWithUidInternal(query *m.GetAlertNotificationsWithUidQuery, sess *DBSession) error { | ||||||
|  | 	var sql bytes.Buffer | ||||||
|  | 	params := make([]interface{}, 0) | ||||||
|  | 
 | ||||||
|  | 	sql.WriteString(`SELECT | ||||||
|  | 										alert_notification.id, | ||||||
|  | 										alert_notification.uid, | ||||||
|  | 										alert_notification.org_id, | ||||||
|  | 										alert_notification.name, | ||||||
|  | 										alert_notification.type, | ||||||
|  | 										alert_notification.created, | ||||||
|  | 										alert_notification.updated, | ||||||
|  | 										alert_notification.settings, | ||||||
|  | 										alert_notification.is_default, | ||||||
|  | 										alert_notification.disable_resolve_message, | ||||||
|  | 										alert_notification.send_reminder, | ||||||
|  | 										alert_notification.frequency | ||||||
|  | 										FROM alert_notification | ||||||
|  | 	  							`) | ||||||
|  | 
 | ||||||
|  | 	sql.WriteString(` WHERE alert_notification.org_id = ? AND alert_notification.uid = ?`) | ||||||
|  | 	params = append(params, query.OrgId, query.Uid) | ||||||
|  | 
 | ||||||
|  | 	results := make([]*m.AlertNotification, 0) | ||||||
|  | 	if err := sess.SQL(sql.String(), params...).Find(&results); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if len(results) == 0 { | ||||||
|  | 		query.Result = nil | ||||||
|  | 	} else { | ||||||
|  | 		query.Result = results[0] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error { | func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error { | ||||||
| 	return inTransaction(func(sess *DBSession) error { | 	return inTransaction(func(sess *DBSession) error { | ||||||
| 		existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name} | 		if cmd.Uid == "" { | ||||||
| 		err := getAlertNotificationInternal(existingQuery, sess) | 			if uid, uidGenerationErr := generateNewAlertNotificationUid(sess, cmd.OrgId); uidGenerationErr != nil { | ||||||
|  | 				return uidGenerationErr | ||||||
|  | 			} else { | ||||||
|  | 				cmd.Uid = uid | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 		existingQuery := &m.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid} | ||||||
|  | 		err := getAlertNotificationWithUidInternal(existingQuery, sess) | ||||||
| 
 | 
 | ||||||
| 		if err != nil { | 		if err != nil { | ||||||
| 			return err | 			return err | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if existingQuery.Result != nil { | 		if existingQuery.Result != nil { | ||||||
|  | 			return fmt.Errorf("Alert notification uid %s already exists", cmd.Uid) | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		// check if name exists
 | ||||||
|  | 		sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name} | ||||||
|  | 		if err := getAlertNotificationInternal(sameNameQuery, sess); err != nil { | ||||||
|  | 			return err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if sameNameQuery.Result != nil { | ||||||
| 			return fmt.Errorf("Alert notification name %s already exists", cmd.Name) | 			return fmt.Errorf("Alert notification name %s already exists", cmd.Name) | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
|  | @ -168,6 +252,7 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error | ||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		alertNotification := &m.AlertNotification{ | 		alertNotification := &m.AlertNotification{ | ||||||
|  | 			Uid:                   cmd.Uid, | ||||||
| 			OrgId:                 cmd.OrgId, | 			OrgId:                 cmd.OrgId, | ||||||
| 			Name:                  cmd.Name, | 			Name:                  cmd.Name, | ||||||
| 			Type:                  cmd.Type, | 			Type:                  cmd.Type, | ||||||
|  | @ -189,6 +274,22 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func generateNewAlertNotificationUid(sess *DBSession, orgId int64) (string, error) { | ||||||
|  | 	for i := 0; i < 3; i++ { | ||||||
|  | 		uid := util.GenerateShortUid() | ||||||
|  | 		exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&m.AlertNotification{}) | ||||||
|  | 		if err != nil { | ||||||
|  | 			return "", err | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if !exists { | ||||||
|  | 			return uid, nil | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return "", m.ErrAlertNotificationFailedGenerateUniqueUid | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { | func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { | ||||||
| 	return inTransaction(func(sess *DBSession) (err error) { | 	return inTransaction(func(sess *DBSession) (err error) { | ||||||
| 		current := m.AlertNotification{} | 		current := m.AlertNotification{} | ||||||
|  | @ -241,6 +342,39 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { | ||||||
| 	}) | 	}) | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
|  | func UpdateAlertNotificationWithUid(cmd *m.UpdateAlertNotificationWithUidCommand) error { | ||||||
|  | 	getAlertNotificationWithUidQuery := &m.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid} | ||||||
|  | 
 | ||||||
|  | 	if err := getAlertNotificationWithUidInternal(getAlertNotificationWithUidQuery, newSession()); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	current := getAlertNotificationWithUidQuery.Result | ||||||
|  | 
 | ||||||
|  | 	if current == nil { | ||||||
|  | 		return fmt.Errorf("Cannot update, alert notification uid %s doesn't exist", cmd.Uid) | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	updateNotification := &m.UpdateAlertNotificationCommand{ | ||||||
|  | 		Id:                    current.Id, | ||||||
|  | 		Name:                  cmd.Name, | ||||||
|  | 		Type:                  cmd.Type, | ||||||
|  | 		SendReminder:          cmd.SendReminder, | ||||||
|  | 		DisableResolveMessage: cmd.DisableResolveMessage, | ||||||
|  | 		Frequency:             cmd.Frequency, | ||||||
|  | 		IsDefault:             cmd.IsDefault, | ||||||
|  | 		Settings:              cmd.Settings, | ||||||
|  | 
 | ||||||
|  | 		OrgId: cmd.OrgId, | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	if err := bus.Dispatch(updateNotification); err != nil { | ||||||
|  | 		return err | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	return nil | ||||||
|  | } | ||||||
|  | 
 | ||||||
| func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error { | func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error { | ||||||
| 	return inTransactionCtx(ctx, func(sess *DBSession) error { | 	return inTransactionCtx(ctx, func(sess *DBSession) error { | ||||||
| 		version := cmd.Version | 		version := cmd.Version | ||||||
|  |  | ||||||
|  | @ -220,11 +220,38 @@ func TestAlertNotificationSQLAccess(t *testing.T) { | ||||||
| 			So(cmd.Result.Type, ShouldEqual, "email") | 			So(cmd.Result.Type, ShouldEqual, "email") | ||||||
| 			So(cmd.Result.Frequency, ShouldEqual, 10*time.Second) | 			So(cmd.Result.Frequency, ShouldEqual, 10*time.Second) | ||||||
| 			So(cmd.Result.DisableResolveMessage, ShouldBeFalse) | 			So(cmd.Result.DisableResolveMessage, ShouldBeFalse) | ||||||
|  | 			So(cmd.Result.Uid, ShouldNotBeEmpty) | ||||||
| 
 | 
 | ||||||
| 			Convey("Cannot save Alert Notification with the same name", func() { | 			Convey("Cannot save Alert Notification with the same name", func() { | ||||||
| 				err = CreateAlertNotificationCommand(cmd) | 				err = CreateAlertNotificationCommand(cmd) | ||||||
| 				So(err, ShouldNotBeNil) | 				So(err, ShouldNotBeNil) | ||||||
| 			}) | 			}) | ||||||
|  | 			Convey("Cannot save Alert Notification with the same name and another uid", func() { | ||||||
|  | 				anotherUidCmd := &models.CreateAlertNotificationCommand{ | ||||||
|  | 					Name:         cmd.Name, | ||||||
|  | 					Type:         cmd.Type, | ||||||
|  | 					OrgId:        1, | ||||||
|  | 					SendReminder: cmd.SendReminder, | ||||||
|  | 					Frequency:    cmd.Frequency, | ||||||
|  | 					Settings:     cmd.Settings, | ||||||
|  | 					Uid:          "notifier1", | ||||||
|  | 				} | ||||||
|  | 				err = CreateAlertNotificationCommand(anotherUidCmd) | ||||||
|  | 				So(err, ShouldNotBeNil) | ||||||
|  | 			}) | ||||||
|  | 			Convey("Can save Alert Notification with another name and another uid", func() { | ||||||
|  | 				anotherUidCmd := &models.CreateAlertNotificationCommand{ | ||||||
|  | 					Name:         "another ops", | ||||||
|  | 					Type:         cmd.Type, | ||||||
|  | 					OrgId:        1, | ||||||
|  | 					SendReminder: cmd.SendReminder, | ||||||
|  | 					Frequency:    cmd.Frequency, | ||||||
|  | 					Settings:     cmd.Settings, | ||||||
|  | 					Uid:          "notifier2", | ||||||
|  | 				} | ||||||
|  | 				err = CreateAlertNotificationCommand(anotherUidCmd) | ||||||
|  | 				So(err, ShouldBeNil) | ||||||
|  | 			}) | ||||||
| 
 | 
 | ||||||
| 			Convey("Can update alert notification", func() { | 			Convey("Can update alert notification", func() { | ||||||
| 				newCmd := &models.UpdateAlertNotificationCommand{ | 				newCmd := &models.UpdateAlertNotificationCommand{ | ||||||
|  | @ -274,12 +301,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { | ||||||
| 			So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil) | 			So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil) | ||||||
| 
 | 
 | ||||||
| 			Convey("search", func() { | 			Convey("search", func() { | ||||||
| 				query := &models.GetAlertNotificationsToSendQuery{ | 				query := &models.GetAlertNotificationsWithUidToSendQuery{ | ||||||
| 					Ids:   []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231}, | 					Uids:  []string{cmd1.Result.Uid, cmd2.Result.Uid, "112341231"}, | ||||||
| 					OrgId: 1, | 					OrgId: 1, | ||||||
| 				} | 				} | ||||||
| 
 | 
 | ||||||
| 				err := GetAlertNotificationsToSend(query) | 				err := GetAlertNotificationsWithUidToSend(query) | ||||||
| 				So(err, ShouldBeNil) | 				So(err, ShouldBeNil) | ||||||
| 				So(len(query.Result), ShouldEqual, 3) | 				So(len(query.Result), ShouldEqual, 3) | ||||||
| 			}) | 			}) | ||||||
|  |  | ||||||
|  | @ -137,4 +137,21 @@ func addAlertMigrations(mg *Migrator) { | ||||||
| 	mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{ | 	mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{ | ||||||
| 		Name: "for", Type: DB_BigInt, Nullable: true, | 		Name: "for", Type: DB_BigInt, Nullable: true, | ||||||
| 	})) | 	})) | ||||||
|  | 
 | ||||||
|  | 	mg.AddMigration("Add column uid in alert_notification", NewAddColumnMigration(alert_notification, &Column{ | ||||||
|  | 		Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: true, | ||||||
|  | 	})) | ||||||
|  | 
 | ||||||
|  | 	mg.AddMigration("Update uid column values in alert_notification", new(RawSqlMigration). | ||||||
|  | 		Sqlite("UPDATE alert_notification SET uid=printf('%09d',id) WHERE uid IS NULL;"). | ||||||
|  | 		Postgres("UPDATE alert_notification SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;"). | ||||||
|  | 		Mysql("UPDATE alert_notification SET uid=lpad(id,9,'0') WHERE uid IS NULL;")) | ||||||
|  | 
 | ||||||
|  | 	mg.AddMigration("Add unique index alert_notification_org_id_uid", NewAddIndexMigration(alert_notification, &Index{ | ||||||
|  | 		Cols: []string{"org_id", "uid"}, Type: UniqueIndex, | ||||||
|  | 	})) | ||||||
|  | 
 | ||||||
|  | 	mg.AddMigration("Remove unique index org_id_name", NewDropIndexMigration(alert_notification, &Index{ | ||||||
|  | 		Cols: []string{"org_id", "name"}, Type: UniqueIndex, | ||||||
|  | 	})) | ||||||
| } | } | ||||||
|  |  | ||||||
|  | @ -140,8 +140,13 @@ export class AlertTabCtrl { | ||||||
|       name: model.name, |       name: model.name, | ||||||
|       iconClass: this.getNotificationIcon(model.type), |       iconClass: this.getNotificationIcon(model.type), | ||||||
|       isDefault: false, |       isDefault: false, | ||||||
|  |       uid: model.uid | ||||||
|     }); |     }); | ||||||
|     this.alert.notifications.push({ id: model.id }); | 
 | ||||||
|  |     // avoid duplicates using both id and uid to be backwards compatible.
 | ||||||
|  |     if (!_.find(this.alert.notifications, n => n.id === model.id || n.uid === model.uid)) { | ||||||
|  |       this.alert.notifications.push({ uid: model.uid }); | ||||||
|  |     } | ||||||
| 
 | 
 | ||||||
|     // reset plus button
 |     // reset plus button
 | ||||||
|     this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value; |     this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value; | ||||||
|  | @ -149,9 +154,11 @@ export class AlertTabCtrl { | ||||||
|     this.addNotificationSegment.fake = true; |     this.addNotificationSegment.fake = true; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   removeNotification(index) { |   removeNotification(an) { | ||||||
|     this.alert.notifications.splice(index, 1); |     // remove notifiers refeered to by id and uid to support notifiers added
 | ||||||
|     this.alertNotifications.splice(index, 1); |     // before and after we added support for uid
 | ||||||
|  |     _.remove(this.alert.notifications, n =>  n.uid === an.uid || n.id === an.id); | ||||||
|  |     _.remove(this.alertNotifications, n =>  n.uid === an.uid || n.id === an.id); | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   initModel() { |   initModel() { | ||||||
|  | @ -187,7 +194,14 @@ export class AlertTabCtrl { | ||||||
|     ThresholdMapper.alertToGraphThresholds(this.panel); |     ThresholdMapper.alertToGraphThresholds(this.panel); | ||||||
| 
 | 
 | ||||||
|     for (const addedNotification of alert.notifications) { |     for (const addedNotification of alert.notifications) { | ||||||
|       const model = _.find(this.notifications, { id: addedNotification.id }); |       // lookup notifier type by uid
 | ||||||
|  |       let model = _.find(this.notifications, { uid: addedNotification.uid }); | ||||||
|  | 
 | ||||||
|  |       // fallback to using id if uid is missing
 | ||||||
|  |       if (!model) { | ||||||
|  |         model = _.find(this.notifications, { id: addedNotification.id }); | ||||||
|  |       } | ||||||
|  | 
 | ||||||
|       if (model && model.isDefault === false) { |       if (model && model.isDefault === false) { | ||||||
|         model.iconClass = this.getNotificationIcon(model.type); |         model.iconClass = this.getNotificationIcon(model.type); | ||||||
|         this.alertNotifications.push(model); |         this.alertNotifications.push(model); | ||||||
|  |  | ||||||
|  | @ -135,7 +135,7 @@ | ||||||
|         <div class="gf-form" ng-repeat="nc in ctrl.alertNotifications"> |         <div class="gf-form" ng-repeat="nc in ctrl.alertNotifications"> | ||||||
|           <span class="gf-form-label" ng-style="{'background-color': nc.bgColor }"> |           <span class="gf-form-label" ng-style="{'background-color': nc.bgColor }"> | ||||||
|             <i class="{{nc.iconClass}}"></i> {{nc.name}}  |             <i class="{{nc.iconClass}}"></i> {{nc.name}}  | ||||||
|             <i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i> |             <i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification(nc)" ng-if="nc.isDefault === false"></i> | ||||||
|           </span> |           </span> | ||||||
|         </div> |         </div> | ||||||
|         <div class="gf-form"> |         <div class="gf-form"> | ||||||
|  |  | ||||||
		Loading…
	
		Reference in New Issue