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. | ||||
| > 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. | ||||
| 
 | ||||
| ## 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 { | ||||
| 	return &AlertNotification{ | ||||
| 		Id:                    notification.Id, | ||||
| 		Uid:                   notification.Uid, | ||||
| 		Name:                  notification.Name, | ||||
| 		Type:                  notification.Type, | ||||
| 		IsDefault:             notification.IsDefault, | ||||
|  | @ -64,6 +65,7 @@ func NewAlertNotification(notification *models.AlertNotification) *AlertNotifica | |||
| 
 | ||||
| type AlertNotification struct { | ||||
| 	Id                    int64            `json:"id"` | ||||
| 	Uid                   string           `json:"uid"` | ||||
| 	Name                  string           `json:"name"` | ||||
| 	Type                  string           `json:"type"` | ||||
| 	IsDefault             bool             `json:"isDefault"` | ||||
|  |  | |||
|  | @ -8,10 +8,11 @@ import ( | |||
| ) | ||||
| 
 | ||||
| var ( | ||||
| 	ErrNotificationFrequencyNotFound         = errors.New("Notification frequency not specified") | ||||
| 	ErrAlertNotificationStateNotFound        = errors.New("alert notification state not found") | ||||
| 	ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict") | ||||
| 	ErrAlertNotificationStateAlreadyExist    = errors.New("alert notification state already exists.") | ||||
| 	ErrNotificationFrequencyNotFound            = errors.New("Notification frequency not specified") | ||||
| 	ErrAlertNotificationStateNotFound           = errors.New("alert notification state not found") | ||||
| 	ErrAlertNotificationStateVersionConflict    = errors.New("alert notification state update version conflict") | ||||
| 	ErrAlertNotificationStateAlreadyExist       = errors.New("alert notification state already exists.") | ||||
| 	ErrAlertNotificationFailedGenerateUniqueUid = errors.New("Failed to generate unique alert notification uid") | ||||
| ) | ||||
| 
 | ||||
| type AlertNotificationStateType string | ||||
|  | @ -24,6 +25,7 @@ var ( | |||
| 
 | ||||
| type AlertNotification struct { | ||||
| 	Id                    int64            `json:"id"` | ||||
| 	Uid                   string           `json:"-"` | ||||
| 	OrgId                 int64            `json:"-"` | ||||
| 	Name                  string           `json:"name"` | ||||
| 	Type                  string           `json:"type"` | ||||
|  | @ -37,6 +39,7 @@ type AlertNotification struct { | |||
| } | ||||
| 
 | ||||
| type CreateAlertNotificationCommand struct { | ||||
| 	Uid                   string           `json:"-"` | ||||
| 	Name                  string           `json:"name"  binding:"Required"` | ||||
| 	Type                  string           `json:"type"  binding:"Required"` | ||||
| 	SendReminder          bool             `json:"sendReminder"` | ||||
|  | @ -63,10 +66,28 @@ type UpdateAlertNotificationCommand struct { | |||
| 	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 { | ||||
| 	Id    int64 | ||||
| 	OrgId int64 | ||||
| } | ||||
| type DeleteAlertNotificationWithUidCommand struct { | ||||
| 	Uid   string | ||||
| 	OrgId int64 | ||||
| } | ||||
| 
 | ||||
| type GetAlertNotificationsQuery struct { | ||||
| 	Name  string | ||||
|  | @ -76,8 +97,15 @@ type GetAlertNotificationsQuery struct { | |||
| 	Result *AlertNotification | ||||
| } | ||||
| 
 | ||||
| type GetAlertNotificationsToSendQuery struct { | ||||
| 	Ids   []int64 | ||||
| type GetAlertNotificationsWithUidQuery struct { | ||||
| 	Uid   string | ||||
| 	OrgId int64 | ||||
| 
 | ||||
| 	Result *AlertNotification | ||||
| } | ||||
| 
 | ||||
| type GetAlertNotificationsWithUidToSendQuery struct { | ||||
| 	Uids  []string | ||||
| 	OrgId int64 | ||||
| 
 | ||||
| 	Result []*AlertNotification | ||||
|  |  | |||
|  | @ -8,6 +8,7 @@ import ( | |||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	"github.com/grafana/grafana/pkg/components/simplejson" | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| ) | ||||
| 
 | ||||
|  | @ -197,74 +198,84 @@ func TestAlertRuleExtraction(t *testing.T) { | |||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		Convey("Parse and validate dashboard containing influxdb alert", func() { | ||||
| 			json, err := ioutil.ReadFile("./testdata/influxdb-alert.json") | ||||
| 		Convey("Alert notifications are in DB", func() { | ||||
| 			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) | ||||
| 
 | ||||
| 			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() { | ||||
| 			Convey("Parse and validate dashboard containing influxdb alert", func() { | ||||
| 				json, err := ioutil.ReadFile("./testdata/influxdb-alert.json") | ||||
| 				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 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() { | ||||
| 				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 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() { | ||||
| 				So(len(alerts), ShouldEqual, 4) | ||||
| 			}) | ||||
| 		}) | ||||
| 
 | ||||
| 		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() { | ||||
| 			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) | ||||
| 				}) | ||||
| 
 | ||||
| 				Convey("should be able to extract collapsed alerts", func() { | ||||
| 					So(len(alerts), ShouldEqual, 4) | ||||
| 				}) | ||||
| 			}) | ||||
| 
 | ||||
| 			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") | ||||
| 			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) | ||||
| 				}) | ||||
| 
 | ||||
| 				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(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool | ||||
| 
 | ||||
| 	GetNotifierId() int64 | ||||
| 	GetNotifierUid() string | ||||
| 	GetIsDefault() bool | ||||
| 	GetSendReminder() bool | ||||
| 	GetDisableResolveMessage() bool | ||||
|  |  | |||
|  | @ -60,13 +60,13 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error { | |||
| func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *notifierState) error { | ||||
| 	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() | ||||
| 
 | ||||
| 	err := notifier.Notify(evalContext) | ||||
| 
 | ||||
| 	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 { | ||||
|  | @ -110,7 +110,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi | |||
| 	for _, notifierState := range notifierStates { | ||||
| 		err := n.sendNotification(evalContext, notifierState) | ||||
| 		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 | ||||
| } | ||||
| 
 | ||||
| func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (notifierStateSlice, error) { | ||||
| 	query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} | ||||
| func (n *notificationService) getNeededNotifiers(orgId int64, notificationUids []string, evalContext *EvalContext) (notifierStateSlice, error) { | ||||
| 	query := &m.GetAlertNotificationsWithUidToSendQuery{OrgId: orgId, Uids: notificationUids} | ||||
| 
 | ||||
| 	if err := bus.Dispatch(query); err != nil { | ||||
| 		return nil, err | ||||
|  | @ -168,7 +168,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] | |||
| 	for _, notification := range query.Result { | ||||
| 		not, err := InitNotifier(notification) | ||||
| 		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 | ||||
| 		} | ||||
| 
 | ||||
|  |  | |||
|  | @ -16,7 +16,7 @@ const ( | |||
| type NotifierBase struct { | ||||
| 	Name                  string | ||||
| 	Type                  string | ||||
| 	Id                    int64 | ||||
| 	Uid                   string | ||||
| 	IsDeault              bool | ||||
| 	UploadImage           bool | ||||
| 	SendReminder          bool | ||||
|  | @ -34,7 +34,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { | |||
| 	} | ||||
| 
 | ||||
| 	return NotifierBase{ | ||||
| 		Id:                    model.Id, | ||||
| 		Uid:                   model.Uid, | ||||
| 		Name:                  model.Name, | ||||
| 		IsDeault:              model.IsDefault, | ||||
| 		Type:                  model.Type, | ||||
|  | @ -110,8 +110,8 @@ func (n *NotifierBase) NeedsImage() bool { | |||
| 	return n.UploadImage | ||||
| } | ||||
| 
 | ||||
| func (n *NotifierBase) GetNotifierId() int64 { | ||||
| 	return n.Id | ||||
| func (n *NotifierBase) GetNotifierUid() string { | ||||
| 	return n.Uid | ||||
| } | ||||
| 
 | ||||
| func (n *NotifierBase) GetIsDefault() bool { | ||||
|  |  | |||
|  | @ -173,7 +173,7 @@ func TestBaseNotifier(t *testing.T) { | |||
| 		bJson := simplejson.New() | ||||
| 
 | ||||
| 		model := &m.AlertNotification{ | ||||
| 			Id:       1, | ||||
| 			Uid:      "1", | ||||
| 			Name:     "name", | ||||
| 			Type:     "email", | ||||
| 			Settings: bJson, | ||||
|  |  | |||
|  | @ -30,7 +30,7 @@ type Rule struct { | |||
| 	ExecutionErrorState m.ExecutionErrorOption | ||||
| 	State               m.AlertStateType | ||||
| 	Conditions          []Condition | ||||
| 	Notifications       []int64 | ||||
| 	Notifications       []string | ||||
| 
 | ||||
| 	StateChanges int64 | ||||
| } | ||||
|  | @ -126,11 +126,15 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { | |||
| 
 | ||||
| 	for _, v := range ruleDef.Settings.Get("notifications").MustArray() { | ||||
| 		jsonModel := simplejson.NewFromAny(v) | ||||
| 		id, err := jsonModel.Get("id").Int64() | ||||
| 		if err != nil { | ||||
| 			return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId} | ||||
| 		if id, err := jsonModel.Get("id").Int64(); err == nil { | ||||
| 			model.Notifications = append(model.Notifications, fmt.Sprintf("%09d", id)) | ||||
| 		} 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() { | ||||
|  |  | |||
|  | @ -5,6 +5,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/components/simplejson" | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/services/sqlstore" | ||||
| 	. "github.com/smartystreets/goconvey/convey" | ||||
| ) | ||||
| 
 | ||||
|  | @ -45,6 +46,7 @@ func TestAlertRuleFrequencyParsing(t *testing.T) { | |||
| } | ||||
| 
 | ||||
| func TestAlertRuleModel(t *testing.T) { | ||||
| 	sqlstore.InitTestDB(t) | ||||
| 	Convey("Testing alert rule", t, func() { | ||||
| 
 | ||||
| 		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() { | ||||
| 			json := ` | ||||
| 			{ | ||||
| 				"name": "name2", | ||||
| 				"description": "desc2", | ||||
| 				"handler": 0, | ||||
| 				"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) | ||||
| 			firstNotification := m.CreateAlertNotificationCommand{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(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() { | ||||
| 				So(len(alertRule.Notifications), ShouldEqual, 2) | ||||
| 				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(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", | ||||
| 				"enabled": true, | ||||
| 				"frequency": "0s", | ||||
|         		"conditions": [ { "type": "test", "prop": 123 } ], | ||||
|         		"notifications": [] | ||||
| 				"conditions": [ { "type": "test", "prop": 123 } ], | ||||
| 				"notifications": [] | ||||
| 			}` | ||||
| 
 | ||||
| 			alertJSON, jsonErr := simplejson.NewJson([]byte(json)) | ||||
|  | @ -129,5 +142,43 @@ func TestAlertRuleModel(t *testing.T) { | |||
| 			So(err, ShouldBeNil) | ||||
| 			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", | ||||
|               "notifications": [ | ||||
|                 { | ||||
|                   "id": 6 | ||||
|                   "uid": "notifier1" | ||||
|                 }, | ||||
|                 { | ||||
|                   "id": 2 | ||||
|                 } | ||||
|               ] | ||||
|             }, | ||||
|  |  | |||
|  | @ -45,7 +45,10 @@ | |||
|               "noDataState": "no_data", | ||||
|               "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/services/provisioning/dashboards" | ||||
| 	"github.com/grafana/grafana/pkg/services/provisioning/datasources" | ||||
| 	"github.com/grafana/grafana/pkg/services/provisioning/notifiers" | ||||
| 	"github.com/grafana/grafana/pkg/setting" | ||||
| ) | ||||
| 
 | ||||
|  | @ -25,6 +26,11 @@ func (ps *ProvisioningService) Init() error { | |||
| 		return fmt.Errorf("Datasource provisioning error: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	alertNotificationsPath := path.Join(ps.Cfg.ProvisioningPath, "notifiers") | ||||
| 	if err := notifiers.Provision(alertNotificationsPath); err != nil { | ||||
| 		return fmt.Errorf("Alert notification provisioning error: %v", err) | ||||
| 	} | ||||
| 
 | ||||
| 	return nil | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -10,6 +10,7 @@ import ( | |||
| 
 | ||||
| 	"github.com/grafana/grafana/pkg/bus" | ||||
| 	m "github.com/grafana/grafana/pkg/models" | ||||
| 	"github.com/grafana/grafana/pkg/util" | ||||
| ) | ||||
| 
 | ||||
| func init() { | ||||
|  | @ -17,11 +18,15 @@ func init() { | |||
| 	bus.AddHandler("sql", CreateAlertNotificationCommand) | ||||
| 	bus.AddHandler("sql", UpdateAlertNotification) | ||||
| 	bus.AddHandler("sql", DeleteAlertNotification) | ||||
| 	bus.AddHandler("sql", GetAlertNotificationsToSend) | ||||
| 	bus.AddHandler("sql", GetAllAlertNotifications) | ||||
| 	bus.AddHandlerCtx("sql", GetOrCreateAlertNotificationState) | ||||
| 	bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand) | ||||
| 	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 { | ||||
|  | @ -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 { | ||||
| 	return getAlertNotificationInternal(query, newSession()) | ||||
| } | ||||
| 
 | ||||
| func GetAlertNotificationsWithUid(query *m.GetAlertNotificationsWithUidQuery) error { | ||||
| 	return getAlertNotificationWithUidInternal(query, newSession()) | ||||
| } | ||||
| 
 | ||||
| func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error { | ||||
| 	results := make([]*m.AlertNotification, 0) | ||||
| 	if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil { | ||||
|  | @ -53,12 +81,13 @@ func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error { | |||
| 	return nil | ||||
| } | ||||
| 
 | ||||
| func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) error { | ||||
| func GetAlertNotificationsWithUidToSend(query *m.GetAlertNotificationsWithUidToSendQuery) error { | ||||
| 	var sql bytes.Buffer | ||||
| 	params := make([]interface{}, 0) | ||||
| 
 | ||||
| 	sql.WriteString(`SELECT | ||||
| 	sql.WriteString(`SELECT										 | ||||
| 										alert_notification.id, | ||||
| 										alert_notification.uid, | ||||
| 										alert_notification.org_id, | ||||
| 										alert_notification.name, | ||||
| 										alert_notification.type, | ||||
|  | @ -77,9 +106,10 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro | |||
| 
 | ||||
| 	sql.WriteString(` AND ((alert_notification.is_default = ?)`) | ||||
| 	params = append(params, dialect.BooleanStr(true)) | ||||
| 	if len(query.Ids) > 0 { | ||||
| 		sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")") | ||||
| 		for _, v := range query.Ids { | ||||
| 
 | ||||
| 	if len(query.Uids) > 0 { | ||||
| 		sql.WriteString(` OR alert_notification.uid IN (?` + strings.Repeat(",?", len(query.Uids)-1) + ")") | ||||
| 		for _, v := range query.Uids { | ||||
| 			params = append(params, v) | ||||
| 		} | ||||
| 	} | ||||
|  | @ -142,16 +172,70 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS | |||
| 	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 { | ||||
| 	return inTransaction(func(sess *DBSession) error { | ||||
| 		existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name} | ||||
| 		err := getAlertNotificationInternal(existingQuery, sess) | ||||
| 		if cmd.Uid == "" { | ||||
| 			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 { | ||||
| 			return err | ||||
| 		} | ||||
| 
 | ||||
| 		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) | ||||
| 		} | ||||
| 
 | ||||
|  | @ -168,6 +252,7 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error | |||
| 		} | ||||
| 
 | ||||
| 		alertNotification := &m.AlertNotification{ | ||||
| 			Uid:                   cmd.Uid, | ||||
| 			OrgId:                 cmd.OrgId, | ||||
| 			Name:                  cmd.Name, | ||||
| 			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 { | ||||
| 	return inTransaction(func(sess *DBSession) (err error) { | ||||
| 		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 { | ||||
| 	return inTransactionCtx(ctx, func(sess *DBSession) error { | ||||
| 		version := cmd.Version | ||||
|  |  | |||
|  | @ -220,11 +220,38 @@ func TestAlertNotificationSQLAccess(t *testing.T) { | |||
| 			So(cmd.Result.Type, ShouldEqual, "email") | ||||
| 			So(cmd.Result.Frequency, ShouldEqual, 10*time.Second) | ||||
| 			So(cmd.Result.DisableResolveMessage, ShouldBeFalse) | ||||
| 			So(cmd.Result.Uid, ShouldNotBeEmpty) | ||||
| 
 | ||||
| 			Convey("Cannot save Alert Notification with the same name", func() { | ||||
| 				err = CreateAlertNotificationCommand(cmd) | ||||
| 				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() { | ||||
| 				newCmd := &models.UpdateAlertNotificationCommand{ | ||||
|  | @ -274,12 +301,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { | |||
| 			So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil) | ||||
| 
 | ||||
| 			Convey("search", func() { | ||||
| 				query := &models.GetAlertNotificationsToSendQuery{ | ||||
| 					Ids:   []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231}, | ||||
| 				query := &models.GetAlertNotificationsWithUidToSendQuery{ | ||||
| 					Uids:  []string{cmd1.Result.Uid, cmd2.Result.Uid, "112341231"}, | ||||
| 					OrgId: 1, | ||||
| 				} | ||||
| 
 | ||||
| 				err := GetAlertNotificationsToSend(query) | ||||
| 				err := GetAlertNotificationsWithUidToSend(query) | ||||
| 				So(err, ShouldBeNil) | ||||
| 				So(len(query.Result), ShouldEqual, 3) | ||||
| 			}) | ||||
|  |  | |||
|  | @ -137,4 +137,21 @@ func addAlertMigrations(mg *Migrator) { | |||
| 	mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{ | ||||
| 		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, | ||||
|       iconClass: this.getNotificationIcon(model.type), | ||||
|       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
 | ||||
|     this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value; | ||||
|  | @ -149,9 +154,11 @@ export class AlertTabCtrl { | |||
|     this.addNotificationSegment.fake = true; | ||||
|   } | ||||
| 
 | ||||
|   removeNotification(index) { | ||||
|     this.alert.notifications.splice(index, 1); | ||||
|     this.alertNotifications.splice(index, 1); | ||||
|   removeNotification(an) { | ||||
|     // remove notifiers refeered to by id and uid to support notifiers added
 | ||||
|     // 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() { | ||||
|  | @ -187,7 +194,14 @@ export class AlertTabCtrl { | |||
|     ThresholdMapper.alertToGraphThresholds(this.panel); | ||||
| 
 | ||||
|     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) { | ||||
|         model.iconClass = this.getNotificationIcon(model.type); | ||||
|         this.alertNotifications.push(model); | ||||
|  |  | |||
|  | @ -135,7 +135,7 @@ | |||
|         <div class="gf-form" ng-repeat="nc in ctrl.alertNotifications"> | ||||
|           <span class="gf-form-label" ng-style="{'background-color': nc.bgColor }"> | ||||
|             <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> | ||||
|         </div> | ||||
|         <div class="gf-form"> | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue