From a5e95823b21bd4f8d15964a65ed9cd5667d2503f Mon Sep 17 00:00:00 2001 From: Sofia Papagiannaki Date: Wed, 31 Mar 2021 23:00:56 +0300 Subject: [PATCH] [Alerting]: Alertmanager API implementation (#32174) * Add validation for grafana recipient * Alertmanager API implementation (WIP) * Fix encoding/decoding receiver settings from/to YAML * Save templates together with the configuration * update POST to apply latest config * Alertmanager service enabled by the ngalert toggle * Silence API integration with Alertmanager * Apply suggestions from code review Co-authored-by: gotjosh Co-authored-by: Ganesh Vernekar <15064823+codesome@users.noreply.github.com> --- go.mod | 2 +- go.sum | 4 + pkg/components/simplejson/simplejson.go | 15 + pkg/services/ngalert/api/api.go | 7 +- pkg/services/ngalert/api/api_alertmanager.go | 359 +++++++ .../ngalert/api/api_alertmanager_base.go | 62 +- .../ngalert/api/api_alertmanager_mock.go | 891 ------------------ pkg/services/ngalert/api/forked_am.go | 2 +- pkg/services/ngalert/api/lotex_am.go | 2 +- pkg/services/ngalert/api/test-data/am.http | 104 +- .../api/test-data/post-user-config.json | 6 +- pkg/services/ngalert/api/util.go | 4 - pkg/services/ngalert/models/alertmanager.go | 17 + pkg/services/ngalert/models/silence.go | 119 +++ pkg/services/ngalert/ngalert.go | 11 +- pkg/services/ngalert/notifier/alertmanager.go | 31 +- pkg/services/ngalert/notifier/config.go | 4 +- pkg/services/ngalert/store/alertmanager.go | 65 +- pkg/services/ngalert/store/database.go | 18 +- .../ngalert/{ => store}/database_mig.go | 32 +- pkg/services/ngalert/store/silence.go | 165 ++++ 21 files changed, 948 insertions(+), 972 deletions(-) create mode 100644 pkg/services/ngalert/api/api_alertmanager.go delete mode 100644 pkg/services/ngalert/api/api_alertmanager_mock.go create mode 100644 pkg/services/ngalert/models/silence.go rename pkg/services/ngalert/{ => store}/database_mig.go (81%) create mode 100644 pkg/services/ngalert/store/silence.go diff --git a/go.mod b/go.mod index ab8bac58ba5..0d75d717874 100644 --- a/go.mod +++ b/go.mod @@ -40,7 +40,7 @@ require ( github.com/google/go-cmp v0.5.5 github.com/google/uuid v1.2.0 github.com/gosimple/slug v1.9.0 - github.com/grafana/alerting-api v0.0.0-20210330162237-0b5408c529a8 + github.com/grafana/alerting-api v0.0.0-20210331130828-17c19ddf88ee github.com/grafana/grafana-aws-sdk v0.4.0 github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 github.com/grafana/grafana-plugin-sdk-go v0.90.0 diff --git a/go.sum b/go.sum index cac189c5fd3..8b91e70478d 100644 --- a/go.sum +++ b/go.sum @@ -798,6 +798,8 @@ github.com/gosimple/slug v1.9.0 h1:r5vDcYrFz9BmfIAMC829un9hq7hKM4cHUrsv36LbEqs= github.com/gosimple/slug v1.9.0/go.mod h1:AMZ+sOVe65uByN3kgEyf9WEBKBCSS+dJjMX9x4vDJbg= github.com/grafana/alerting-api v0.0.0-20210330162237-0b5408c529a8 h1:okhEX26LU7AGN/3C8NDWfdjBmKclvoFvJz9o/LsNcK8= github.com/grafana/alerting-api v0.0.0-20210330162237-0b5408c529a8/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= +github.com/grafana/alerting-api v0.0.0-20210331130828-17c19ddf88ee h1:jpZdUOta4PK3CH3+2UCuzqn1SGZ+dQj+dWH45B0c1aI= +github.com/grafana/alerting-api v0.0.0-20210331130828-17c19ddf88ee/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036 h1:GplhUk6Xes5JIhUUrggPcPBhOn+eT8+WsHiebvq7GgA= github.com/grafana/go-mssqldb v0.0.0-20210326084033-d0ce3c521036/go.mod h1:xbL0rPBG9cCiLr28tMa8zpbdarY27NDyej4t/EjAShU= github.com/grafana/grafana v1.9.2-0.20210308201921-4ce0a49eac03/go.mod h1:AHRRvd4utJGY25J5nW8aL7wZzn/LcJ0z2za9oOp14j4= @@ -1335,6 +1337,7 @@ github.com/pquerna/cachecontrol v0.0.0-20200819021114-67c6ae64274f/go.mod h1:hoL github.com/prometheus/alertmanager v0.18.0/go.mod h1:WcxHBl40VSPuOaqWae6l6HpnEOVRIycEJ7i9iYkadEE= github.com/prometheus/alertmanager v0.19.0/go.mod h1:Eyp94Yi/T+kdeb2qvq66E3RGuph5T/jm/RBVh4yz1xo= github.com/prometheus/alertmanager v0.20.0/go.mod h1:9g2i48FAyZW6BtbsnvHtMHQXl2aVtrORKwKVCQ+nbrg= +github.com/prometheus/alertmanager v0.21.0 h1:qK51JcUR9l/unhawGA9F9B64OCYfcGewhPNprem/Acc= github.com/prometheus/alertmanager v0.21.0/go.mod h1:h7tJ81NA0VLWvWEayi1QltevFkLF3KxmC/malTcT8Go= github.com/prometheus/alertmanager v0.21.1-0.20200911160112-1fdff6b3f939/go.mod h1:imXRHOP6QTsE0fFsIsAV/cXimS32m7gVZOiUj11m6Ig= github.com/prometheus/alertmanager v0.21.1-0.20210211203738-a7ca7b1d2951/go.mod h1:6Yc2n2ap5/oP99x1yN6Ho+yL0w8a0oClIR5xxW/JLGs= @@ -1421,6 +1424,7 @@ github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17 h1:VN3p3Nb github.com/prometheus/prometheus v1.8.2-0.20210217141258-a6be548dbc17/go.mod h1:dv3B1syqmkrkmo665MPCU6L8PbTXIiUeg/OEQULLNxA= github.com/prometheus/statsd_exporter v0.15.0/go.mod h1:Dv8HnkoLQkeEjkIE4/2ndAA7WL1zHKK7WMqFQqu72rw= github.com/prometheus/tsdb v0.7.1/go.mod h1:qhTCs0VvXwvX/y3TZrWD7rabWM+ijKTux40TwIPHuXU= +github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3 h1:eL7x4/zMnlquMxYe7V078BD7MGskZ0daGln+SJCVzuY= github.com/quasilyte/go-ruleguard/dsl/fluent v0.0.0-20201222093424-5d7e62a465d3/go.mod h1:P7JlQWFT7jDcFZMtUPQbtGzzzxva3rBn6oIF+LPwFcM= github.com/rafaeljusto/redigomock v0.0.0-20190202135759-257e089e14a1/go.mod h1:JaY6n2sDr+z2WTsXkOmNRUfDy6FN0L6Nk7x06ndm4tY= github.com/rainycape/unidecode v0.0.0-20150907023854-cb7f23ec59be h1:ta7tUOvsPHVHGom5hKW5VXNc2xZIkfCKP8iaqOyYtUQ= diff --git a/pkg/components/simplejson/simplejson.go b/pkg/components/simplejson/simplejson.go index 177a4434a56..e76653ebdbc 100644 --- a/pkg/components/simplejson/simplejson.go +++ b/pkg/components/simplejson/simplejson.go @@ -482,3 +482,18 @@ func (j *Json) MustUint64(args ...uint64) uint64 { return def } + +// MarshalYAML implements yaml.Marshaller. +func (j *Json) MarshalYAML() (interface{}, error) { + return j.data, nil +} + +// UnmarshalYAML implements yaml.Unmarshaller. +func (j *Json) UnmarshalYAML(unmarshal func(interface{}) error) error { + var data interface{} + if err := unmarshal(&data); err != nil { + return err + } + j.data = data + return nil +} diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 2afe687dda0..5ff786512bf 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -30,6 +30,10 @@ var timeNow = time.Now type Alertmanager interface { ApplyConfig(config *apimodels.PostableUserConfig) error + CreateSilence(ps *apimodels.PostableSilence) (string, error) + DeleteSilence(silenceID string) error + GetSilence(silenceID string) (apimodels.GettableSilence, error) + ListSilences(filters []string) (apimodels.GettableSilences, error) } // API handlers. @@ -40,6 +44,7 @@ type API struct { DataService *tsdb.Service Schedule schedule.ScheduleService Store store.Store + AlertingStore store.AlertingStore DataProxy *datasourceproxy.DatasourceProxyService Alertmanager Alertmanager } @@ -53,7 +58,7 @@ func (api *API) RegisterAPIEndpoints() { api.RegisterAlertmanagerApiEndpoints(NewForkedAM( api.DatasourceCache, NewLotexAM(proxy, logger), - AlertmanagerApiMock{log: logger}, + AlertmanagerSrv{store: api.AlertingStore, am: api.Alertmanager, log: logger}, )) api.RegisterPrometheusApiEndpoints(NewForkedProm( api.DatasourceCache, diff --git a/pkg/services/ngalert/api/api_alertmanager.go b/pkg/services/ngalert/api/api_alertmanager.go new file mode 100644 index 00000000000..f4874d64d01 --- /dev/null +++ b/pkg/services/ngalert/api/api_alertmanager.go @@ -0,0 +1,359 @@ +package api + +import ( + "errors" + "fmt" + "net/http" + "time" + + "gopkg.in/yaml.v3" + + "github.com/go-openapi/strfmt" + apimodels "github.com/grafana/alerting-api/pkg/api" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/ngalert/notifier" + "github.com/grafana/grafana/pkg/services/ngalert/store" + "github.com/grafana/grafana/pkg/util" + amv2 "github.com/prometheus/alertmanager/api/v2/models" +) + +type AlertmanagerSrv struct { + am Alertmanager + store store.AlertingStore + log log.Logger +} + +func (srv AlertmanagerSrv) RouteCreateSilence(c *models.ReqContext, postableSilence apimodels.PostableSilence) response.Response { + silenceID, err := srv.am.CreateSilence(&postableSilence) + if err != nil { + if errors.Is(err, notifier.ErrSilenceNotFound) { + return response.Error(http.StatusNotFound, err.Error(), nil) + } + + if errors.Is(err, notifier.ErrCreateSilenceBadPayload) { + return response.Error(http.StatusBadRequest, err.Error(), nil) + } + + return response.Error(http.StatusInternalServerError, "failed to create silence", err) + } + return response.JSON(http.StatusAccepted, util.DynMap{"message": "silence created", "id": silenceID}) +} + +func (srv AlertmanagerSrv) RouteDeleteAlertingConfig(c *models.ReqContext) response.Response { + // not implemented + return response.Error(http.StatusNotImplemented, "", nil) +} + +func (srv AlertmanagerSrv) RouteDeleteSilence(c *models.ReqContext) response.Response { + silenceID := c.Params(":SilenceId") + if err := srv.am.DeleteSilence(silenceID); err != nil { + if errors.Is(err, notifier.ErrSilenceNotFound) { + return response.Error(http.StatusNotFound, err.Error(), nil) + } + return response.Error(http.StatusInternalServerError, err.Error(), nil) + } + return response.JSON(http.StatusOK, util.DynMap{"message": "silence deleted"}) +} + +func (srv AlertmanagerSrv) RouteGetAlertingConfig(c *models.ReqContext) response.Response { + query := ngmodels.GetLatestAlertmanagerConfigurationQuery{} + err := srv.store.GetLatestAlertmanagerConfiguration(&query) + if err != nil { + return response.Error(http.StatusInternalServerError, "failed to get latest configuration", err) + } + + cfg := apimodels.PostableUserConfig{} + err = yaml.Unmarshal([]byte(query.Result.AlertmanagerConfiguration), &cfg) + if err != nil { + return response.Error(http.StatusInternalServerError, "failed to unmarshal alertmanager configuration", err) + } + + var apiReceiverName string + var receivers []*apimodels.GettableGrafanaReceiver + alertmanagerCfg := cfg.AlertmanagerConfig + if len(alertmanagerCfg.Receivers) > 0 { + apiReceiverName = alertmanagerCfg.Receivers[0].Name + receivers = make([]*apimodels.GettableGrafanaReceiver, 0, len(alertmanagerCfg.Receivers[0].PostableGrafanaReceivers.GrafanaManagedReceivers)) + for _, pr := range alertmanagerCfg.Receivers[0].PostableGrafanaReceivers.GrafanaManagedReceivers { + secureFields := make(map[string]bool, len(pr.SecureSettings)) + for k := range pr.SecureSettings { + secureFields[k] = true + } + gr := apimodels.GettableGrafanaReceiver{ + Uid: pr.Uid, + Name: pr.Name, + Type: pr.Type, + IsDefault: pr.IsDefault, + SendReminder: pr.SendReminder, + DisableResolveMessage: pr.DisableResolveMessage, + Frequency: pr.Frequency, + Settings: pr.Settings, + SecureFields: secureFields, + } + receivers = append(receivers, &gr) + } + } + + gettableApiReceiver := apimodels.GettableApiReceiver{ + GettableGrafanaReceivers: apimodels.GettableGrafanaReceivers{ + GrafanaManagedReceivers: receivers, + }, + } + gettableApiReceiver.Name = apiReceiverName + result := apimodels.GettableUserConfig{ + TemplateFiles: cfg.TemplateFiles, + AlertmanagerConfig: apimodels.GettableApiAlertingConfig{ + Config: alertmanagerCfg.Config, + Receivers: []*apimodels.GettableApiReceiver{ + &gettableApiReceiver, + }, + }, + } + + return response.JSON(http.StatusOK, result) +} + +func (srv AlertmanagerSrv) RouteGetAMAlertGroups(c *models.ReqContext) response.Response { + recipient := c.Params(":Recipient") + srv.log.Info("RouteGetAMAlertGroups: ", "Recipient", recipient) + now := time.Now() + result := apimodels.AlertGroups{ + &amv2.AlertGroup{ + Alerts: []*amv2.GettableAlert{ + { + Annotations: amv2.LabelSet{ + "annotation1-1": "value1", + "annotation1-2": "value2", + }, + EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), + Fingerprint: stringPtr("fingerprint 1"), + Receivers: []*amv2.Receiver{ + { + Name: stringPtr("receiver identifier 1-1"), + }, + { + Name: stringPtr("receiver identifier 1-2"), + }, + }, + StartsAt: timePtr(strfmt.DateTime(now)), + Status: &amv2.AlertStatus{ + InhibitedBy: []string{"inhibitedBy 1"}, + SilencedBy: []string{"silencedBy 1"}, + State: stringPtr(amv2.AlertStatusStateActive), + }, + UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), + Alert: amv2.Alert{ + GeneratorURL: strfmt.URI("a URL"), + Labels: amv2.LabelSet{ + "label1-1": "value1", + "label1-2": "value2", + }, + }, + }, + { + Annotations: amv2.LabelSet{ + "annotation2-1": "value1", + "annotation2-2": "value2", + }, + EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), + Fingerprint: stringPtr("fingerprint 2"), + Receivers: []*amv2.Receiver{ + { + Name: stringPtr("receiver identifier 2-1"), + }, + { + Name: stringPtr("receiver identifier 2-2"), + }, + }, + StartsAt: timePtr(strfmt.DateTime(now)), + Status: &amv2.AlertStatus{ + InhibitedBy: []string{"inhibitedBy 2"}, + SilencedBy: []string{"silencedBy 2"}, + State: stringPtr(amv2.AlertStatusStateActive), + }, + UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), + Alert: amv2.Alert{ + GeneratorURL: strfmt.URI("a URL"), + Labels: amv2.LabelSet{ + "label2-1": "value1", + "label2-2": "value2", + }, + }, + }, + }, + Labels: amv2.LabelSet{ + "label1-1": "value1", + "label1-2": "value2", + }, + Receiver: &amv2.Receiver{ + Name: stringPtr("receiver identifier 2-1"), + }, + }, + &amv2.AlertGroup{ + Alerts: []*amv2.GettableAlert{ + { + Annotations: amv2.LabelSet{ + "annotation2-1": "value1", + "annotation2-2": "value2", + }, + EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), + Fingerprint: stringPtr("fingerprint 2"), + Receivers: []*amv2.Receiver{ + { + Name: stringPtr("receiver identifier 2-1"), + }, + { + Name: stringPtr("receiver identifier 2-2"), + }, + }, + StartsAt: timePtr(strfmt.DateTime(now)), + Status: &amv2.AlertStatus{ + InhibitedBy: []string{"inhibitedBy 2"}, + SilencedBy: []string{"silencedBy 2"}, + State: stringPtr(amv2.AlertStatusStateActive), + }, + UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), + Alert: amv2.Alert{ + GeneratorURL: strfmt.URI("a URL"), + Labels: amv2.LabelSet{ + "label2-1": "value1", + "label2-2": "value2", + }, + }, + }, + }, + Labels: amv2.LabelSet{ + "label2-1": "value1", + "label2-2": "value2", + }, + Receiver: &amv2.Receiver{ + Name: stringPtr("receiver identifier 2-1"), + }, + }, + } + return response.JSON(http.StatusOK, result) +} + +func (srv AlertmanagerSrv) RouteGetAMAlerts(c *models.ReqContext) response.Response { + recipient := c.Params(":Recipient") + srv.log.Info("RouteGetAMAlerts: ", "Recipient", recipient) + now := time.Now() + result := apimodels.GettableAlerts{ + &amv2.GettableAlert{ + Annotations: amv2.LabelSet{ + "annotation1-1": "value1", + "annotation1-2": "value2", + }, + EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), + Fingerprint: stringPtr("fingerprint 1"), + Receivers: []*amv2.Receiver{ + { + Name: stringPtr("receiver identifier 1-1"), + }, + { + Name: stringPtr("receiver identifier 1-2"), + }, + }, + StartsAt: timePtr(strfmt.DateTime(now)), + Status: &amv2.AlertStatus{ + InhibitedBy: []string{"inhibitedBy 1"}, + SilencedBy: []string{"silencedBy 1"}, + State: stringPtr(amv2.AlertStatusStateActive), + }, + UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), + Alert: amv2.Alert{ + GeneratorURL: strfmt.URI("a URL"), + Labels: amv2.LabelSet{ + "label1-1": "value1", + "label1-2": "value2", + }, + }, + }, + &amv2.GettableAlert{ + Annotations: amv2.LabelSet{ + "annotation2-1": "value1", + "annotation2-2": "value2", + }, + EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), + Fingerprint: stringPtr("fingerprint 2"), + Receivers: []*amv2.Receiver{ + { + Name: stringPtr("receiver identifier 2-1"), + }, + { + Name: stringPtr("receiver identifier 2-2"), + }, + }, + StartsAt: timePtr(strfmt.DateTime(now)), + Status: &amv2.AlertStatus{ + InhibitedBy: []string{"inhibitedBy 2"}, + SilencedBy: []string{"silencedBy 2"}, + State: stringPtr(amv2.AlertStatusStateActive), + }, + UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), + Alert: amv2.Alert{ + GeneratorURL: strfmt.URI("a URL"), + Labels: amv2.LabelSet{ + "label2-1": "value1", + "label2-2": "value2", + }, + }, + }, + } + return response.JSON(http.StatusOK, result) +} + +func (srv AlertmanagerSrv) RouteGetSilence(c *models.ReqContext) response.Response { + silenceID := c.Params(":SilenceId") + gettableSilence, err := srv.am.GetSilence(silenceID) + if err != nil { + if errors.Is(err, notifier.ErrSilenceNotFound) { + return response.Error(http.StatusNotFound, err.Error(), nil) + } + // any other error here should be an unexpected failure and thus an internal error + return response.Error(http.StatusInternalServerError, err.Error(), nil) + } + return response.JSON(http.StatusOK, gettableSilence) +} + +func (srv AlertmanagerSrv) RouteGetSilences(c *models.ReqContext) response.Response { + filters := c.QueryStrings("Filter") + gettableSilences, err := srv.am.ListSilences(filters) + if err != nil { + if errors.Is(err, notifier.ErrListSilencesBadPayload) { + return response.Error(http.StatusBadRequest, err.Error(), nil) + } + // any other error here should be an unexpected failure and thus an internal error + return response.Error(http.StatusInternalServerError, err.Error(), nil) + } + return response.JSON(http.StatusOK, gettableSilences) +} + +func (srv AlertmanagerSrv) RoutePostAlertingConfig(c *models.ReqContext, body apimodels.PostableUserConfig) response.Response { + config, err := yaml.Marshal(&body) + if err != nil { + return response.Error(http.StatusInternalServerError, "failed to serialize to the Alertmanager configuration", err) + } + + cmd := ngmodels.SaveAlertmanagerConfigurationCmd{ + AlertmanagerConfiguration: string(config), + ConfigurationVersion: fmt.Sprintf("v%d", ngmodels.AlertConfigurationVersion), + } + if err := srv.store.SaveAlertmanagerConfiguration(&cmd); err != nil { + return response.Error(http.StatusInternalServerError, "failed to save Alertmanager configuration", err) + } + + if err := srv.am.ApplyConfig(&body); err != nil { + return response.Error(http.StatusInternalServerError, "failed to apply Alertmanager configuration", err) + } + + return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"}) +} + +func (srv AlertmanagerSrv) RoutePostAMAlerts(c *models.ReqContext, body apimodels.PostableAlerts) response.Response { + // not implemented + return response.Error(http.StatusNotImplemented, "", nil) +} diff --git a/pkg/services/ngalert/api/api_alertmanager_base.go b/pkg/services/ngalert/api/api_alertmanager_base.go index 63659cfdbf2..f327ddf1d13 100644 --- a/pkg/services/ngalert/api/api_alertmanager_base.go +++ b/pkg/services/ngalert/api/api_alertmanager_base.go @@ -18,15 +18,14 @@ import ( ) type AlertmanagerApiService interface { - RouteCreateSilence(*models.ReqContext, apimodels.CreateSilenceParams) response.Response - RouteGetSilences(*models.ReqContext) response.Response - RouteGetSilence(*models.ReqContext) response.Response - RouteDeleteSilence(*models.ReqContext) response.Response - + RouteCreateSilence(*models.ReqContext, apimodels.PostableSilence) response.Response RouteDeleteAlertingConfig(*models.ReqContext) response.Response + RouteDeleteSilence(*models.ReqContext) response.Response RouteGetAMAlertGroups(*models.ReqContext) response.Response RouteGetAMAlerts(*models.ReqContext) response.Response RouteGetAlertingConfig(*models.ReqContext) response.Response + RouteGetSilence(*models.ReqContext) response.Response + RouteGetSilences(*models.ReqContext) response.Response RoutePostAMAlerts(*models.ReqContext, apimodels.PostableAlerts) response.Response RoutePostAlertingConfig(*models.ReqContext, apimodels.PostableUserConfig) response.Response } @@ -37,44 +36,29 @@ type AlertmanagerApiBase struct { func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApiService) { api.RouteRegister.Group("", func(group routing.RouteRegister) { - // Silences - group.Post(toMacaronPath("/alertmanager/{Recipient}/api/v2/silences"), binding.Bind(apimodels.CreateSilenceParams{}), routing.Wrap(srv.RouteCreateSilence)) - group.Get(toMacaronPath("/alertmanager/{Recipient}/api/v2/silences"), routing.Wrap(srv.RouteGetSilences)) - group.Get(toMacaronPath("/alertmanager/{Recipient}/api/v2/silence/{SilenceId}"), routing.Wrap(srv.RouteGetSilence)) + group.Post(toMacaronPath("/alertmanager/{Recipient}/api/v2/silences"), binding.Bind(apimodels.PostableSilence{}), routing.Wrap(srv.RouteCreateSilence)) + group.Delete(toMacaronPath("/alertmanager/{Recipient}/config/api/v1/alerts"), routing.Wrap(srv.RouteDeleteAlertingConfig)) group.Delete(toMacaronPath("/alertmanager/{Recipient}/api/v2/silence/{SilenceId}"), routing.Wrap(srv.RouteDeleteSilence)) - - // Alerts group.Get(toMacaronPath("/alertmanager/{Recipient}/api/v2/alerts/groups"), routing.Wrap(srv.RouteGetAMAlertGroups)) group.Get(toMacaronPath("/alertmanager/{Recipient}/api/v2/alerts"), routing.Wrap(srv.RouteGetAMAlerts)) - group.Post(toMacaronPath("/alertmanager/{Recipient}/api/v2/alerts"), binding.Bind(apimodels.PostableAlerts{}), routing.Wrap(srv.RoutePostAMAlerts)) - - // Configuration - group.Delete(toMacaronPath("/alertmanager/{Recipient}/config/api/v1/alerts"), routing.Wrap(srv.RouteDeleteAlertingConfig)) group.Get(toMacaronPath("/alertmanager/{Recipient}/config/api/v1/alerts"), routing.Wrap(srv.RouteGetAlertingConfig)) + group.Get(toMacaronPath("/alertmanager/{Recipient}/api/v2/silence/{SilenceId}"), routing.Wrap(srv.RouteGetSilence)) + group.Get(toMacaronPath("/alertmanager/{Recipient}/api/v2/silences"), routing.Wrap(srv.RouteGetSilences)) + group.Post(toMacaronPath("/alertmanager/{Recipient}/api/v2/alerts"), binding.Bind(apimodels.PostableAlerts{}), routing.Wrap(srv.RoutePostAMAlerts)) group.Post(toMacaronPath("/alertmanager/{Recipient}/config/api/v1/alerts"), binding.Bind(apimodels.PostableUserConfig{}), routing.Wrap(srv.RoutePostAlertingConfig)) }) } -func (base AlertmanagerApiBase) RouteCreateSilence(c *models.ReqContext, params apimodels.CreateSilenceParams) response.Response { +func (base AlertmanagerApiBase) RouteCreateSilence(c *models.ReqContext, body apimodels.PostableSilence) response.Response { recipient := c.Params(":Recipient") base.log.Info("RouteCreateSilence: ", "Recipient", recipient) - base.log.Info("RouteCreateSilence: ", "params", params) + base.log.Info("RouteCreateSilence: ", "body", body) return response.Error(http.StatusNotImplemented, "", nil) } -func (base AlertmanagerApiBase) RouteGetSilences(c *models.ReqContext) response.Response { +func (base AlertmanagerApiBase) RouteDeleteAlertingConfig(c *models.ReqContext) response.Response { recipient := c.Params(":Recipient") - base.log.Info("RouteGetSilences: ", "Recipient", recipient) - filter := c.Params(":Filter") - base.log.Info("RouteGetSilences: ", "params", filter) - return response.Error(http.StatusNotImplemented, "", nil) -} - -func (base AlertmanagerApiBase) RouteGetSilence(c *models.ReqContext) response.Response { - silenceId := c.Params(":SilenceId") - base.log.Info("RouteGetSilence: ", "SilenceId", silenceId) - recipient := c.Params(":Recipient") - base.log.Info("RouteGetSilence: ", "Recipient", recipient) + base.log.Info("RouteDeleteAlertingConfig: ", "Recipient", recipient) return response.Error(http.StatusNotImplemented, "", nil) } @@ -86,12 +70,6 @@ func (base AlertmanagerApiBase) RouteDeleteSilence(c *models.ReqContext) respons return response.Error(http.StatusNotImplemented, "", nil) } -func (base AlertmanagerApiBase) RouteDeleteAlertingConfig(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - base.log.Info("RouteDeleteAlertingConfig: ", "Recipient", recipient) - return response.Error(http.StatusNotImplemented, "", nil) -} - func (base AlertmanagerApiBase) RouteGetAMAlertGroups(c *models.ReqContext) response.Response { recipient := c.Params(":Recipient") base.log.Info("RouteGetAMAlertGroups: ", "Recipient", recipient) @@ -110,6 +88,20 @@ func (base AlertmanagerApiBase) RouteGetAlertingConfig(c *models.ReqContext) res return response.Error(http.StatusNotImplemented, "", nil) } +func (base AlertmanagerApiBase) RouteGetSilence(c *models.ReqContext) response.Response { + silenceId := c.Params(":SilenceId") + base.log.Info("RouteGetSilence: ", "SilenceId", silenceId) + recipient := c.Params(":Recipient") + base.log.Info("RouteGetSilence: ", "Recipient", recipient) + return response.Error(http.StatusNotImplemented, "", nil) +} + +func (base AlertmanagerApiBase) RouteGetSilences(c *models.ReqContext) response.Response { + recipient := c.Params(":Recipient") + base.log.Info("RouteGetSilences: ", "Recipient", recipient) + return response.Error(http.StatusNotImplemented, "", nil) +} + func (base AlertmanagerApiBase) RoutePostAMAlerts(c *models.ReqContext, body apimodels.PostableAlerts) response.Response { recipient := c.Params(":Recipient") base.log.Info("RoutePostAMAlerts: ", "Recipient", recipient) diff --git a/pkg/services/ngalert/api/api_alertmanager_mock.go b/pkg/services/ngalert/api/api_alertmanager_mock.go deleted file mode 100644 index fd0d0019628..00000000000 --- a/pkg/services/ngalert/api/api_alertmanager_mock.go +++ /dev/null @@ -1,891 +0,0 @@ -/*Package api contains mock API implementation of unified alerting - * - * Generated by: Swagger Codegen (https://github.com/swagger-api/swagger-codegen.git) - * - * Need to remove unused imports. - */ -package api - -import ( - "net/http" - "time" - - "github.com/grafana/grafana/pkg/api/dtos" - - "github.com/grafana/grafana/pkg/components/securejsondata" - "github.com/grafana/grafana/pkg/components/simplejson" - - "github.com/go-openapi/strfmt" - apimodels "github.com/grafana/alerting-api/pkg/api" - "github.com/grafana/grafana/pkg/api/response" - "github.com/grafana/grafana/pkg/infra/log" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/util" - amv2 "github.com/prometheus/alertmanager/api/v2/models" - "github.com/prometheus/alertmanager/config" -) - -func toSimpleJSON(blob string) *simplejson.Json { - json, _ := simplejson.NewJson([]byte(blob)) - return json -} - -var alertmanagerReceiver = models.AlertNotification{ - Id: 1, - Uid: "alertmanager UID", - OrgId: 1, - Name: "an alert manager receiver", - Type: "prometheus-alertmanager", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "basicAuthUser": "user", - "httpMethod": "POST", - "severity": "critical", - "uploadImage": false, - "url": "http://localhost:9093" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "basicAuthPassword": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var alertmanagerReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&alertmanagerReceiver)) - -var dingdingReceiver = models.AlertNotification{ - Id: 2, - Uid: "dingding UID", - OrgId: 1, - Name: "a dingding receiver", - Type: "dingding", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "httpMethod": "POST", - "msgType": "link", - "severity": "critical", - "uploadImage": false, - "url": "https://oapi.dingtalk.com/robot/send?access_token=xxxxxxxxx" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{}), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var dingdingReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&dingdingReceiver)) - -var discordReceiver = models.AlertNotification{ - Id: 3, - Uid: "discord UID", - OrgId: 1, - Name: "a discord receiver", - Type: "discord", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "content": "@user", - "httpMethod": "POST", - "severity": "critical", - "uploadImage": false, - "url": "http://" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{}), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var discordReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&discordReceiver)) - -var emailReceiver = models.AlertNotification{ - Id: 4, - Uid: "email UID", - OrgId: 1, - Name: "an email receiver", - Type: "email", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "addresses": "", - "autoResolve": true, - "httpMethod": "POST", - "severity": "critical", - "singleEmail": true, - "uploadImage": false - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{}), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var emailReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&emailReceiver)) - -var googlechatReceiver = models.AlertNotification{ - Id: 5, - Uid: "googlechatReceiver UID", - OrgId: 1, - Name: "a googlechat receiver", - Type: "googlechat", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "httpMethod": "POST", - "severity": "critical", - "uploadImage": false, - "url": "http://" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{}), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var googlechatReceiverDTOs = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&googlechatReceiver)) - -var hipchatReceiver = models.AlertNotification{ - Id: 6, - Uid: "hipchat UID", - OrgId: 1, - Name: "a hipchat receiver", - Type: "hipchat", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "apiKey": "", - "autoResolve": true, - "httpMethod": "POST", - "roomid": "12345", - "severity": "critical", - "uploadImage": false, - "url": "http://" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{}), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var hipchatReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&hipchatReceiver)) - -var kafkaReceiver = models.AlertNotification{ - Id: 7, - Uid: "kafka UID", - OrgId: 1, - Name: "a kafka receiver", - Type: "kafka", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "httpMethod": "POST", - "kafkaRestProxy": "http://localhost:8082", - "kafkaTopic": "topic1", - "severity": "critical", - "uploadImage": false - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{}), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var kafkaReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&kafkaReceiver)) - -var lineReceiver = models.AlertNotification{ - Id: 8, - Uid: "line UID", - OrgId: 1, - Name: "a line receiver", - Type: "line", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(` "settings": { - "autoResolve": true, - "httpMethod": "POST", - "severity": "critical", - "uploadImage": false - },`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "token": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var lineReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&lineReceiver)) - -var opsgenieReceiver = models.AlertNotification{ - Id: 9, - Uid: "opsgenie UID", - OrgId: 1, - Name: "a opsgenie receiver", - Type: "opsgenie", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(` "settings": { - "apiUrl": "https://api.opsgenie.com/v2/alerts", - "autoClose": true, - "autoResolve": true, - "httpMethod": "POST", - "overridePriority": true, - "severity": "critical", - "uploadImage": false - },`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "apiKey": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var opsgenieReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&opsgenieReceiver)) - -var pagerdutyReceiver = models.AlertNotification{ - Id: 10, - Uid: "pagerduty UID", - OrgId: 1, - Name: "a pagerduty receiver", - Type: "pagerduty", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "httpMethod": "POST", - "severity": "critical", - "uploadImage": true - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "integrationKey": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var pagerdutyReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&pagerdutyReceiver)) - -var pushoverReceiver = models.AlertNotification{ - Id: 11, - Uid: "pushover UID", - OrgId: 1, - Name: "a pushover receiver", - Type: "pushover", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "apiToken": "", - "autoResolve": true, - "device": "", - "expire": "", - "httpMethod": "POST", - "okPriority": "0", - "okSound": "cosmic", - "priority": "1", - "retry": "30", - "severity": "critical", - "sound": "pushover", - "uploadImage": true, - "userKey": "" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "apiToken": "", - "userKey": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var pushoverReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&pushoverReceiver)) - -var sensuReceiver = models.AlertNotification{ - Id: 12, - Uid: "sensu UID", - OrgId: 1, - Name: "a sensu receiver", - Type: "sensu", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "handler": "", - "httpMethod": "POST", - "severity": "critical", - "source": "", - "uploadImage": false, - "url": "http://sensu-api.local:4567/results", - "username": "" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{}), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var sensuReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&sensuReceiver)) - -var sensugoReceiver = models.AlertNotification{ - Id: 13, - Uid: "sensugo UID", - OrgId: 1, - Name: "a sensugo receiver", - Type: "sensugo", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "check": "", - "entity": "", - "handler": "", - "httpMethod": "POST", - "namespace": "", - "severity": "critical", - "uploadImage": false, - "url": "http://sensu-api.local:8080" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "apikey": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var sensugoReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&sensugoReceiver)) - -var slackReceiver = models.AlertNotification{ - Id: 14, - Uid: "slack UID", - OrgId: 1, - Name: "a slack receiver", - Type: "slack", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "httpMethod": "POST", - "iconEmoji": "", - "iconUrl": "", - "mentionGroups": "", - "mentionUsers": "", - "recipient": "", - "severity": "critical", - "uploadImage": false, - "username": "" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "token": "", - "url": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var slackReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&slackReceiver)) - -var teamsReceiver = models.AlertNotification{ - Id: 15, - Uid: "teams UID", - OrgId: 1, - Name: "a teams receiver", - Type: "teams", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "httpMethod": "POST", - "severity": "critical", - "uploadImage": false, - "url": "http://" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{}), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var teamsReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&teamsReceiver)) - -var telegramReceiver = models.AlertNotification{ - Id: 16, - Uid: "telegram UID", - OrgId: 1, - Name: "a telegram receiver", - Type: "telegram", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "chatid": "12345", - "httpMethod": "POST", - "severity": "critical", - "uploadImage": false - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "bottoken": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var telegramReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&telegramReceiver)) - -var threemaReceiver = models.AlertNotification{ - Id: 17, - Uid: "threema UID", - OrgId: 1, - Name: "a threema receiver", - Type: "threema", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "gateway_id": "*3MAGWID", - "httpMethod": "POST", - "recipient_id": "YOUR3MID", - "severity": "critical", - "uploadImage": false - },`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "api_secret": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var threemaDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&threemaReceiver)) - -var victoropsReceiver = models.AlertNotification{ - Id: 18, - Uid: "victorops UID", - OrgId: 1, - Name: "a victorops receiver", - Type: "victorops", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`{ - "autoResolve": true, - "httpMethod": "POST", - "severity": "critical", - "uploadImage": false, - "url": "http://" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{}), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var victoropsReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&victoropsReceiver)) - -var webhookReceiver = models.AlertNotification{ - Id: 19, - Uid: "webhook UID", - OrgId: 1, - Name: "a webhook receiver", - Type: "webhook", - SendReminder: false, - DisableResolveMessage: false, - Frequency: 5 * time.Minute, - IsDefault: false, - Settings: toSimpleJSON(`x{ - "autoResolve": true, - "httpMethod": "POST", - "password": "", - "severity": "critical", - "uploadImage": true, - "url": "http://localhost:3010", - "username": "" - }`), - SecureSettings: securejsondata.GetEncryptedJsonData(map[string]string{ - "password": "", - }), - Created: time.Now().Add(-time.Hour), - Updated: time.Now().Add(-5 * time.Minute), -} -var webhookReceiverDTO = apimodels.GettableGrafanaReceiver(*dtos.NewAlertNotification(&webhookReceiver)) - -type AlertmanagerApiMock struct { - log log.Logger -} - -func (mock AlertmanagerApiMock) RouteCreateSilence(c *models.ReqContext, body apimodels.CreateSilenceParams) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteCreateSilence: ", "Recipient", recipient) - mock.log.Info("RouteCreateSilence: ", "body", body) - return response.JSON(http.StatusAccepted, util.DynMap{"message": "silence created"}) -} - -func (mock AlertmanagerApiMock) RouteDeleteAlertingConfig(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteDeleteAlertingConfig: ", "Recipient", recipient) - return response.JSON(http.StatusOK, util.DynMap{"message": "config deleted"}) -} - -func (mock AlertmanagerApiMock) RouteDeleteSilence(c *models.ReqContext) response.Response { - silenceID := c.Params(":SilenceId") - mock.log.Info("RouteDeleteSilence: ", "SilenceId", silenceID) - recipient := c.Params(":Recipient") - mock.log.Info("RouteDeleteSilence: ", "Recipient", recipient) - return response.JSON(http.StatusOK, util.DynMap{"message": "silence deleted"}) -} - -func (mock AlertmanagerApiMock) RouteGetAlertingConfig(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteGetAlertingConfig: ", "Recipient", recipient) - // now := time.Now() - result := apimodels.GettableUserConfig{ - TemplateFiles: map[string]string{ - "tmpl1": "val1", - "tmpl2": "val2", - }, - AlertmanagerConfig: apimodels.GettableApiAlertingConfig{ - Config: apimodels.Config{ - Global: &config.GlobalConfig{}, - Route: &config.Route{}, - InhibitRules: []*config.InhibitRule{}, - Receivers: []*config.Receiver{}, - Templates: []string{}, - }, - Receivers: []*apimodels.GettableApiReceiver{ - { - GettableGrafanaReceivers: apimodels.GettableGrafanaReceivers{ - GrafanaManagedReceivers: []*apimodels.GettableGrafanaReceiver{ - &alertmanagerReceiverDTO, - &dingdingReceiverDTO, - &discordReceiverDTO, - &emailReceiverDTO, - &googlechatReceiverDTOs, - &hipchatReceiverDTO, - &kafkaReceiverDTO, - &lineReceiverDTO, - &opsgenieReceiverDTO, - &pagerdutyReceiverDTO, - &pushoverReceiverDTO, - &sensuReceiverDTO, - &sensugoReceiverDTO, - &slackReceiverDTO, - &teamsReceiverDTO, - &telegramReceiverDTO, - &threemaDTO, - &victoropsReceiverDTO, - &webhookReceiverDTO, - }, - }, - }, - }, - }, - } - return response.JSON(http.StatusOK, result) -} - -func (mock AlertmanagerApiMock) RouteGetAMAlertGroups(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteGetAMAlertGroups: ", "Recipient", recipient) - now := time.Now() - result := apimodels.AlertGroups{ - &amv2.AlertGroup{ - Alerts: []*amv2.GettableAlert{ - { - Annotations: amv2.LabelSet{ - "annotation1-1": "value1", - "annotation1-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 1"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 1-1"), - }, - { - Name: stringPtr("receiver identifier 1-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 1"}, - SilencedBy: []string{"silencedBy 1"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label1-1": "value1", - "label1-2": "value2", - }, - }, - }, - { - Annotations: amv2.LabelSet{ - "annotation2-1": "value1", - "annotation2-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 2"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 2-1"), - }, - { - Name: stringPtr("receiver identifier 2-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 2"}, - SilencedBy: []string{"silencedBy 2"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label2-1": "value1", - "label2-2": "value2", - }, - }, - }, - }, - Labels: amv2.LabelSet{ - "label1-1": "value1", - "label1-2": "value2", - }, - Receiver: &amv2.Receiver{ - Name: stringPtr("receiver identifier 2-1"), - }, - }, - &amv2.AlertGroup{ - Alerts: []*amv2.GettableAlert{ - { - Annotations: amv2.LabelSet{ - "annotation2-1": "value1", - "annotation2-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 2"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 2-1"), - }, - { - Name: stringPtr("receiver identifier 2-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 2"}, - SilencedBy: []string{"silencedBy 2"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label2-1": "value1", - "label2-2": "value2", - }, - }, - }, - }, - Labels: amv2.LabelSet{ - "label2-1": "value1", - "label2-2": "value2", - }, - Receiver: &amv2.Receiver{ - Name: stringPtr("receiver identifier 2-1"), - }, - }, - } - return response.JSON(http.StatusOK, result) -} - -func (mock AlertmanagerApiMock) RouteGetAMAlerts(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteGetAMAlerts: ", "Recipient", recipient) - now := time.Now() - result := apimodels.GettableAlerts{ - &amv2.GettableAlert{ - Annotations: amv2.LabelSet{ - "annotation1-1": "value1", - "annotation1-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 1"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 1-1"), - }, - { - Name: stringPtr("receiver identifier 1-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 1"}, - SilencedBy: []string{"silencedBy 1"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label1-1": "value1", - "label1-2": "value2", - }, - }, - }, - &amv2.GettableAlert{ - Annotations: amv2.LabelSet{ - "annotation2-1": "value1", - "annotation2-2": "value2", - }, - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - Fingerprint: stringPtr("fingerprint 2"), - Receivers: []*amv2.Receiver{ - { - Name: stringPtr("receiver identifier 2-1"), - }, - { - Name: stringPtr("receiver identifier 2-2"), - }, - }, - StartsAt: timePtr(strfmt.DateTime(now)), - Status: &amv2.AlertStatus{ - InhibitedBy: []string{"inhibitedBy 2"}, - SilencedBy: []string{"silencedBy 2"}, - State: stringPtr(amv2.AlertStatusStateActive), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Alert: amv2.Alert{ - GeneratorURL: strfmt.URI("a URL"), - Labels: amv2.LabelSet{ - "label2-1": "value1", - "label2-2": "value2", - }, - }, - }, - } - return response.JSON(http.StatusOK, result) -} - -func (mock AlertmanagerApiMock) RouteGetSilence(c *models.ReqContext) response.Response { - silenceID := c.Params(":SilenceId") - mock.log.Info("RouteGetSilence: ", "SilenceId", silenceID) - recipient := c.Params(":Recipient") - mock.log.Info("RouteGetSilence: ", "Recipient", recipient) - now := time.Now() - result := apimodels.GettableSilence{ - ID: stringPtr("id"), - Status: &amv2.SilenceStatus{ - State: stringPtr("active"), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Silence: amv2.Silence{ - Comment: stringPtr("comment"), - CreatedBy: stringPtr("created by"), - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - StartsAt: timePtr(strfmt.DateTime(now)), - Matchers: []*amv2.Matcher{ - { - IsRegex: boolPtr(false), - Name: stringPtr("name"), - Value: stringPtr("value"), - }, - { - IsRegex: boolPtr(false), - Name: stringPtr("name2"), - Value: stringPtr("value2"), - }, - }, - }, - } - return response.JSON(http.StatusOK, result) -} - -func (mock AlertmanagerApiMock) RouteGetSilences(c *models.ReqContext) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RouteGetSilences: ", "Recipient", recipient) - now := time.Now() - result := apimodels.GettableSilences{ - &amv2.GettableSilence{ - ID: stringPtr("silence1"), - Status: &amv2.SilenceStatus{ - State: stringPtr("active"), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Silence: amv2.Silence{ - Comment: stringPtr("silence1 comment"), - CreatedBy: stringPtr("silence1 created by"), - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - StartsAt: timePtr(strfmt.DateTime(now)), - Matchers: []*amv2.Matcher{ - { - IsRegex: boolPtr(false), - Name: stringPtr("silence1 name"), - Value: stringPtr("silence1 value"), - }, - { - IsRegex: boolPtr(true), - Name: stringPtr("silence1 name2"), - Value: stringPtr("silence1 value2"), - }, - }, - }, - }, - &amv2.GettableSilence{ - ID: stringPtr("silence2"), - Status: &amv2.SilenceStatus{ - State: stringPtr("pending"), - }, - UpdatedAt: timePtr(strfmt.DateTime(now.Add(-time.Hour))), - Silence: amv2.Silence{ - Comment: stringPtr("silence2 comment"), - CreatedBy: stringPtr("silence2 created by"), - EndsAt: timePtr(strfmt.DateTime(now.Add(time.Hour))), - StartsAt: timePtr(strfmt.DateTime(now)), - Matchers: []*amv2.Matcher{ - { - IsRegex: boolPtr(false), - Name: stringPtr("silence2 name"), - Value: stringPtr("silence2 value"), - }, - { - IsRegex: boolPtr(true), - Name: stringPtr("silence2 name2"), - Value: stringPtr("silence2 value2"), - }, - }, - }, - }, - } - return response.JSON(http.StatusOK, result) -} - -func (mock AlertmanagerApiMock) RoutePostAlertingConfig(c *models.ReqContext, body apimodels.PostableUserConfig) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RoutePostAlertingConfig: ", "Recipient", recipient) - mock.log.Info("RoutePostAlertingConfig: ", "body", body) - return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"}) -} - -func (mock AlertmanagerApiMock) RoutePostAMAlerts(c *models.ReqContext, body apimodels.PostableAlerts) response.Response { - recipient := c.Params(":Recipient") - mock.log.Info("RoutePostAMAlerts: ", "Recipient", recipient) - mock.log.Info("RoutePostAMAlerts: ", "body", body) - return response.JSON(http.StatusOK, util.DynMap{"message": "alerts created"}) -} diff --git a/pkg/services/ngalert/api/forked_am.go b/pkg/services/ngalert/api/forked_am.go index 091dfa4183d..dff9e391b11 100644 --- a/pkg/services/ngalert/api/forked_am.go +++ b/pkg/services/ngalert/api/forked_am.go @@ -38,7 +38,7 @@ func (am *ForkedAMSvc) getService(ctx *models.ReqContext) (AlertmanagerApiServic } } -func (am *ForkedAMSvc) RouteCreateSilence(ctx *models.ReqContext, body apimodels.CreateSilenceParams) response.Response { +func (am *ForkedAMSvc) RouteCreateSilence(ctx *models.ReqContext, body apimodels.PostableSilence) response.Response { s, err := am.getService(ctx) if err != nil { return response.Error(400, err.Error(), nil) diff --git a/pkg/services/ngalert/api/lotex_am.go b/pkg/services/ngalert/api/lotex_am.go index 3b84ca74796..d5532f9fb5e 100644 --- a/pkg/services/ngalert/api/lotex_am.go +++ b/pkg/services/ngalert/api/lotex_am.go @@ -32,7 +32,7 @@ func NewLotexAM(proxy *AlertingProxy, log log.Logger) *LotexAM { } } -func (am *LotexAM) RouteCreateSilence(ctx *models.ReqContext, silenceBody apimodels.CreateSilenceParams) response.Response { +func (am *LotexAM) RouteCreateSilence(ctx *models.ReqContext, silenceBody apimodels.PostableSilence) response.Response { blob, err := json.Marshal(silenceBody) if err != nil { return response.Error(500, "Failed marshal silence", err) diff --git a/pkg/services/ngalert/api/test-data/am.http b/pkg/services/ngalert/api/test-data/am.http index 643ca41d59a..c2261bf78c0 100644 --- a/pkg/services/ngalert/api/test-data/am.http +++ b/pkg/services/ngalert/api/test-data/am.http @@ -10,23 +10,109 @@ GET http://admin:admin@localhost:3000/alertmanager/{{lokiDatasourceID}}/config/a GET http://admin:admin@localhost:3000/alertmanager/{{prometheusDatasourceID}}/config/api/v1/alerts ### -# grafana requests -GET http://admin:admin@localhost:3000/alertmanager/{{grafana}}/config/api/v1/alerts - -### -DELETE http://admin:admin@localhost:3000/alertmanager/{{grafana}}/config/api/v1/alerts - -### +# create AM configuration POST http://admin:admin@localhost:3000/alertmanager/{{grafana}}/config/api/v1/alerts content-type: application/json < ./post-user-config.json ### +# get latest AM configuration +GET http://admin:admin@localhost:3000/alertmanager/{{grafana}}/config/api/v1/alerts +content-type: application/json + +### +# delete AM configuration +DELETE http://admin:admin@localhost:3000/alertmanager/{{grafana}}/config/api/v1/alerts + +### +# create AM alerts +POST http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/alerts +content-type: application/json + +### +# get silences - no silences +GET http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silences?Filter=foo="bar"&Filter=bar="foo" + +### +# create silence POST http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silences content-type: application/json -< ./post-silence-data.json +{ + "comment": "string", + "createdBy": "string", + "endsAt": "2023-03-31T14:17:04.419Z", + "matchers": [ + { + "isRegex": true, + "name": "string", + "value": "string" + } + ], + "startsAt": "2021-03-31T13:17:04.419Z" +} ### -GET http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silences \ No newline at end of file +# update silence - does not exist +POST http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silences +content-type: application/json + +{ + "id": "something", + "comment": "string", + "createdBy": "string", + "endsAt": "2023-03-31T14:17:04.419Z", + "matchers": [ + { + "isRegex": true, + "name": "string", + "value": "string" + } + ], + "startsAt": "2021-03-31T13:17:04.419Z" +} + +### +# create silence - bad paylaad - start time must be before end time +POST http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silences +content-type: application/json + +{ + "comment": "string", + "createdBy": "string", + "endsAt": "2019-03-31T14:17:04.419Z", + "matchers": [ + { + "isRegex": true, + "name": "string", + "value": "string" + } + ], + "startsAt": "2021-03-31T13:17:04.419Z" +} + +### +# get silences +# @name getSilences +GET http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silences + + +### +@silenceID = {{getSilences.response.body.$.[0].id}} + +### +# get silence +GET http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silence/{{silenceID}} + +### +# get silence - unknown +GET http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silence/unknown + +### +# delete silence +DELETE http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silence/{{silenceID}} + +### +# delete silence - unknown +DELETE http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silence/unknown diff --git a/pkg/services/ngalert/api/test-data/post-user-config.json b/pkg/services/ngalert/api/test-data/post-user-config.json index a845a34fbc3..986ddbeb883 100644 --- a/pkg/services/ngalert/api/test-data/post-user-config.json +++ b/pkg/services/ngalert/api/test-data/post-user-config.json @@ -8,11 +8,13 @@ "resolve_timeout": "1s", "smtp_smarthost": "" }, - "route": {}, + "route": { + "receiver": "grafana_managed" + }, "templates": [], "receivers": [ { - "name": "", + "name": "grafana_managed", "grafana_managed_receiver_configs": [ { "uid": "alertmanager UID", diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index 693e2877583..5b3d17ac7f9 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -35,10 +35,6 @@ func stringPtr(s string) *string { return &s } -func boolPtr(b bool) *bool { - return &b -} - func backendType(ctx *models.ReqContext, cache datasources.CacheService) (apimodels.Backend, error) { recipient := ctx.Params("Recipient") if recipient == apimodels.GrafanaBackend.String() { diff --git a/pkg/services/ngalert/models/alertmanager.go b/pkg/services/ngalert/models/alertmanager.go index d2370103f6b..3dad0ed2b05 100644 --- a/pkg/services/ngalert/models/alertmanager.go +++ b/pkg/services/ngalert/models/alertmanager.go @@ -2,6 +2,8 @@ package models import "time" +const AlertConfigurationVersion = 1 + // AlertConfiguration represents a single version of the Alerting Engine Configuration. type AlertConfiguration struct { ID int64 `xorm:"pk autoincr 'id'"` @@ -13,7 +15,22 @@ type AlertConfiguration struct { // GetLatestAlertmanagerConfigurationQuery is the query to get the latest alertmanager configuration. type GetLatestAlertmanagerConfigurationQuery struct { + Result *AlertConfiguration +} + +// GetAlertmanagerConfigurationQuery is the query to get the latest alertmanager configuration. +type GetAlertmanagerConfigurationQuery struct { ID int64 Result *AlertConfiguration } + +// SaveAlertmanagerConfigurationCmd is the command to save an alertmanager configuration. +type SaveAlertmanagerConfigurationCmd struct { + AlertmanagerConfiguration string + ConfigurationVersion string +} + +type DeleteAlertmanagerConfigurationCmd struct { + ID int64 +} diff --git a/pkg/services/ngalert/models/silence.go b/pkg/services/ngalert/models/silence.go new file mode 100644 index 00000000000..56f52db3971 --- /dev/null +++ b/pkg/services/ngalert/models/silence.go @@ -0,0 +1,119 @@ +package models + +import ( + "encoding/json" + "errors" + "fmt" + + "github.com/go-openapi/strfmt" + + amv2 "github.com/prometheus/alertmanager/api/v2/models" +) + +var ( + // ErrSilenceNotFound is an error for an unknown silence. + ErrSilenceNotFound = fmt.Errorf("could not find silence") + // ErrSilenceFailedGenerateUniqueUID is an error for failure to generate silence UID + ErrSilenceFailedGenerateUniqueUID = errors.New("failed to generate silence UID") +) + +type SilenceSettings amv2.GettableSilence + +type SilenceStatus amv2.SilenceStatus + +// FromDB loads silence status stored in the database. +// FromDB is part of the xorm Conversion interface. +func (st *SilenceStatus) FromDB(b []byte) error { + str := string(b) + *st = SilenceStatus{State: &str} + return nil +} + +// ToDB serializes silence status to be stored in the database. +// ToDB is part of the xorm Conversion interface. +func (st *SilenceStatus) ToDB() ([]byte, error) { + return []byte(*st.State), nil +} + +type Matchers amv2.Matchers + +// FromDB loads matchers stored in the database. +// FromDB is part of the xorm Conversion interface. +func (m *Matchers) FromDB(b []byte) error { + err := json.Unmarshal(b, &m) + if err != nil { + return fmt.Errorf("failed to convert matchers from database: %w", err) + } + return nil +} + +// ToDB serializes matchers to be stored in the database. +// ToDB is part of the xorm Conversion interface. +func (m *Matchers) ToDB() ([]byte, error) { + blobMatchers, err := json.Marshal(m) + if err != nil { + return nil, fmt.Errorf("failed to convert matchers to send to the database: %w", err) + } + return blobMatchers, nil +} + +type Silence struct { + ID int64 `xorm:"pk autoincr 'id'"` + OrgID int64 `xorm:"org_id" json:"orgId"` + UID string `xorm:"uid" json:"uid"` + Status SilenceStatus `json:"status"` + UpdatedAt strfmt.DateTime `json:"updatedAt"` + Comment string `json:"comment"` + CreatedBy string `json:"createdBy"` + EndsAt strfmt.DateTime `json:"endsAt"` + Matchers Matchers `json:"matchers"` + StartsAt strfmt.DateTime `json:"startsAt"` +} + +func (s Silence) ToGettableSilence() amv2.GettableSilence { + gettableSilence := amv2.GettableSilence{ + ID: &s.UID, + Status: &amv2.SilenceStatus{State: s.Status.State}, + UpdatedAt: &s.UpdatedAt, + } + gettableSilence.Comment = &s.Comment + gettableSilence.CreatedBy = &s.CreatedBy + gettableSilence.EndsAt = &s.EndsAt + gettableSilence.Matchers = amv2.Matchers(s.Matchers) + gettableSilence.StartsAt = &s.StartsAt + return gettableSilence +} + +type SaveSilenceCommand struct { + amv2.Silence + UID string + OrgID int64 +} + +type DeleteSilenceByUIDCommand struct { + UID string + OrgID int64 +} + +type DeleteSilenceByIDCommand struct { + ID int64 +} + +type GetSilenceByUIDQuery struct { + UID string + OrgID int64 + + Result *Silence +} + +type GetSilenceByIDQuery struct { + ID int64 + + Result *Silence +} + +type GetSilencesQuery struct { + OrgID int64 + + Result []*Silence +} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 7b4c8e95d13..325cc3dd9fd 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -81,6 +81,7 @@ func (ng *AlertNG) Init() error { Schedule: ng.schedule, DataProxy: ng.DataProxy, Store: store, + AlertingStore: store, Alertmanager: ng.Alertmanager, } api.RegisterAPIEndpoints() @@ -99,7 +100,6 @@ func (ng *AlertNG) IsDisabled() bool { if ng.Cfg == nil { return true } - // Check also about expressions? return !ng.Cfg.IsNgAlertEnabled() } @@ -109,8 +109,11 @@ func (ng *AlertNG) AddMigration(mg *migrator.Migrator) { if ng.IsDisabled() { return } - addAlertDefinitionMigrations(mg) - addAlertDefinitionVersionMigrations(mg) + store.AddAlertDefinitionMigrations(mg, defaultIntervalSeconds) + store.AddAlertDefinitionVersionMigrations(mg) // Create alert_instance table - alertInstanceMigration(mg) + store.AlertInstanceMigration(mg) + + // Create silence table + store.SilenceMigration(mg) } diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 43df284aa41..3a7cea81d0f 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -2,10 +2,15 @@ package notifier import ( "context" + "fmt" "path/filepath" "sync" "time" + "github.com/grafana/grafana/pkg/components/securejsondata" + + "github.com/grafana/grafana/pkg/models" + gokit_log "github.com/go-kit/kit/log" "github.com/grafana/alerting-api/pkg/api" "github.com/pkg/errors" @@ -20,7 +25,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/services/ngalert/models" + ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/notifier/channels" "github.com/grafana/grafana/pkg/services/ngalert/store" "github.com/grafana/grafana/pkg/services/sqlstore" @@ -61,7 +66,10 @@ func init() { } func (am *Alertmanager) IsDisabled() bool { - return !setting.AlertingEnabled || !setting.ExecuteAlerts + if am.Settings == nil { + return true + } + return !am.Settings.IsNgAlertEnabled() } func (am *Alertmanager) Init() (err error) { @@ -146,7 +154,7 @@ func (am *Alertmanager) SyncAndApplyConfigFromDatabase() error { func (am *Alertmanager) getConfigFromDatabase() (*api.PostableUserConfig, error) { // First, let's get the configuration we need from the database. - q := &models.GetLatestAlertmanagerConfigurationQuery{} + q := &ngmodels.GetLatestAlertmanagerConfigurationQuery{} if err := am.Store.GetLatestAlertmanagerConfiguration(q); err != nil { return nil, err } @@ -233,7 +241,22 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *api.PostableApiRecei for i, r := range receiver.GrafanaManagedReceivers { switch r.Type { case "email": - n, err := channels.NewEmailNotifier(r.Result) + frequency, err := time.ParseDuration(r.Frequency) + if err != nil { + return nil, fmt.Errorf("unable to parse receiver frequency %s, %w", r.Frequency, err) + } + notification := models.AlertNotification{ + Uid: r.Uid, + Name: r.Name, + Type: r.Type, + IsDefault: r.IsDefault, + SendReminder: r.SendReminder, + DisableResolveMessage: r.DisableResolveMessage, + Frequency: frequency, + Settings: r.Settings, + SecureSettings: securejsondata.GetEncryptedJsonData(r.SecureSettings), + } + n, err := channels.NewEmailNotifier(¬ification) if err != nil { return nil, err } diff --git a/pkg/services/ngalert/notifier/config.go b/pkg/services/ngalert/notifier/config.go index 9b30c5a6e8f..17e00c74036 100644 --- a/pkg/services/ngalert/notifier/config.go +++ b/pkg/services/ngalert/notifier/config.go @@ -10,7 +10,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" "github.com/pkg/errors" - "gopkg.in/yaml.v2" + "gopkg.in/yaml.v3" ) func PersistTemplates(cfg *api.PostableUserConfig, path string) ([]string, bool, error) { @@ -78,7 +78,7 @@ func PersistTemplates(cfg *api.PostableUserConfig, path string) ([]string, bool, func Load(rawConfig string) (*api.PostableUserConfig, error) { cfg := &api.PostableUserConfig{} - if err := yaml.UnmarshalStrict([]byte(rawConfig), cfg); err != nil { + if err := yaml.Unmarshal([]byte(rawConfig), cfg); err != nil { return nil, errors.Wrap(err, "unable to parse Alertmanager configuration") } diff --git a/pkg/services/ngalert/store/alertmanager.go b/pkg/services/ngalert/store/alertmanager.go index bf40cf01e08..0848ae27113 100644 --- a/pkg/services/ngalert/store/alertmanager.go +++ b/pkg/services/ngalert/store/alertmanager.go @@ -13,22 +13,71 @@ var ( ErrNoAlertmanagerConfiguration = fmt.Errorf("could not find an Alertmanager configuration") ) +func getAlertmanagerConfigurationByID(sess *sqlstore.DBSession, id int64) (*models.AlertConfiguration, error) { + c := &models.AlertConfiguration{} + + has, err := sess.ID(id).Get(c) + if err != nil { + return nil, err + } + if !has { + return nil, ErrNoAlertmanagerConfiguration + } + + return c, nil +} + +func getLatestAlertmanagerConfiguration(sess *sqlstore.DBSession) (*models.AlertConfiguration, error) { + c := &models.AlertConfiguration{} + // The ID is already an auto incremental column, using the ID as an order should guarantee the latest. + ok, err := sess.Desc("id").Limit(1).Get(c) + if err != nil { + return nil, err + } + + if !ok { + return nil, ErrNoAlertmanagerConfiguration + } + + return c, nil +} + // GetLatestAlertmanagerConfiguration returns the lastest version of the alertmanager configuration. // It returns ErrNoAlertmanagerConfiguration if no configuration is found. func (st DBstore) GetLatestAlertmanagerConfiguration(query *models.GetLatestAlertmanagerConfigurationQuery) error { return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { - c := &models.AlertConfiguration{} - // The ID is already an auto incremental column, using the ID as an order should guarantee the latest. - ok, err := sess.Desc("id").Limit(1).Get(c) + c, err := getLatestAlertmanagerConfiguration(sess) if err != nil { return err } - - if !ok { - return ErrNoAlertmanagerConfiguration - } - query.Result = c return nil }) } + +// GetAlertmanagerConfiguration returns the alertmanager configuration identified by the query. +// It returns ErrNoAlertmanagerConfiguration if no such configuration is found. +func (st DBstore) GetAlertmanagerConfiguration(query *models.GetAlertmanagerConfigurationQuery) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + c, err := getAlertmanagerConfigurationByID(sess, query.ID) + if err != nil { + return err + } + query.Result = c + return nil + }) +} + +// SaveAlertmanagerConfiguration creates an alertmanager configuration. +func (st DBstore) SaveAlertmanagerConfiguration(cmd *models.SaveAlertmanagerConfigurationCmd) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + config := models.AlertConfiguration{ + AlertmanagerConfiguration: cmd.AlertmanagerConfiguration, + ConfigurationVersion: cmd.ConfigurationVersion, + } + if _, err := sess.Insert(config); err != nil { + return err + } + return nil + }) +} diff --git a/pkg/services/ngalert/store/database.go b/pkg/services/ngalert/store/database.go index 1400ee1ef18..4d3146fd301 100644 --- a/pkg/services/ngalert/store/database.go +++ b/pkg/services/ngalert/store/database.go @@ -26,20 +26,28 @@ var ErrEmptyTitleError = errors.New("title is empty") type Store interface { DeleteAlertDefinitionByUID(*models.DeleteAlertDefinitionByUIDCommand) error GetAlertDefinitionByUID(*models.GetAlertDefinitionByUIDQuery) error - GetAlertDefinitions(query *models.ListAlertDefinitionsQuery) error - GetOrgAlertDefinitions(query *models.ListAlertDefinitionsQuery) error + GetAlertDefinitions(*models.ListAlertDefinitionsQuery) error + GetOrgAlertDefinitions(*models.ListAlertDefinitionsQuery) error SaveAlertDefinition(*models.SaveAlertDefinitionCommand) error UpdateAlertDefinition(*models.UpdateAlertDefinitionCommand) error GetAlertInstance(*models.GetAlertInstanceQuery) error - ListAlertInstances(cmd *models.ListAlertInstancesQuery) error - SaveAlertInstance(cmd *models.SaveAlertInstanceCommand) error + ListAlertInstances(*models.ListAlertInstancesQuery) error + SaveAlertInstance(*models.SaveAlertInstanceCommand) error ValidateAlertDefinition(*models.AlertDefinition, bool) error UpdateAlertDefinitionPaused(*models.UpdateAlertDefinitionPausedCommand) error } // AlertingStore is the database interface used by the Alertmanager service. type AlertingStore interface { - GetLatestAlertmanagerConfiguration(cmd *models.GetLatestAlertmanagerConfigurationQuery) error + GetLatestAlertmanagerConfiguration(*models.GetLatestAlertmanagerConfigurationQuery) error + GetAlertmanagerConfiguration(*models.GetAlertmanagerConfigurationQuery) error + SaveAlertmanagerConfiguration(*models.SaveAlertmanagerConfigurationCmd) error + GetOrgSilences(*models.GetSilencesQuery) error + GetSilenceByUID(*models.GetSilenceByUIDQuery) error + GetSilenceByID(*models.GetSilenceByIDQuery) error + SaveSilence(*models.SaveSilenceCommand) error + DeleteSilenceByUID(*models.DeleteSilenceByUIDCommand) error + DeleteSilenceByID(*models.DeleteSilenceByIDCommand) error } // DBstore stores the alert definitions and instances in the database. diff --git a/pkg/services/ngalert/database_mig.go b/pkg/services/ngalert/store/database_mig.go similarity index 81% rename from pkg/services/ngalert/database_mig.go rename to pkg/services/ngalert/store/database_mig.go index ec130328244..a44afba3a77 100644 --- a/pkg/services/ngalert/database_mig.go +++ b/pkg/services/ngalert/store/database_mig.go @@ -1,4 +1,4 @@ -package ngalert +package store import ( "fmt" @@ -6,7 +6,7 @@ import ( "github.com/grafana/grafana/pkg/services/sqlstore/migrator" ) -func addAlertDefinitionMigrations(mg *migrator.Migrator) { +func AddAlertDefinitionMigrations(mg *migrator.Migrator, defaultIntervalSeconds int64) { mg.AddMigration("delete alert_definition table", migrator.NewDropTableMigration("alert_definition")) alertDefinition := migrator.Table{ @@ -52,7 +52,7 @@ func addAlertDefinitionMigrations(mg *migrator.Migrator) { })) } -func addAlertDefinitionVersionMigrations(mg *migrator.Migrator) { +func AddAlertDefinitionVersionMigrations(mg *migrator.Migrator) { mg.AddMigration("delete alert_definition_version table", migrator.NewDropTableMigration("alert_definition_version")) alertDefinitionVersion := migrator.Table{ @@ -83,7 +83,7 @@ func addAlertDefinitionVersionMigrations(mg *migrator.Migrator) { Mysql("ALTER TABLE alert_definition_version MODIFY data MEDIUMTEXT;")) } -func alertInstanceMigration(mg *migrator.Migrator) { +func AlertInstanceMigration(mg *migrator.Migrator) { alertInstance := migrator.Table{ Name: "alert_instance", Columns: []*migrator.Column{ @@ -107,3 +107,27 @@ func alertInstanceMigration(mg *migrator.Migrator) { mg.AddMigration("add index in alert_instance table on def_org_id, def_uid and current_state columns", migrator.NewAddIndexMigration(alertInstance, alertInstance.Indices[0])) mg.AddMigration("add index in alert_instance table on def_org_id, current_state columns", migrator.NewAddIndexMigration(alertInstance, alertInstance.Indices[1])) } + +func SilenceMigration(mg *migrator.Migrator) { + silence := migrator.Table{ + Name: "silence", + Columns: []*migrator.Column{ + {Name: "id", Type: migrator.DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: migrator.DB_BigInt, Nullable: false}, + {Name: "uid", Type: migrator.DB_NVarchar, Length: 190, Nullable: false, Default: "0"}, + {Name: "comment", Type: migrator.DB_NVarchar, Length: 190, Nullable: true}, + {Name: "created_by", Type: migrator.DB_NVarchar, Length: 190, Nullable: true}, + {Name: "matchers", Type: migrator.DB_Text, Nullable: false}, + {Name: "ends_at", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "starts_at", Type: migrator.DB_DateTime, Nullable: false}, + {Name: "updated_at", Type: migrator.DB_DateTime, Nullable: true}, + {Name: "status", Type: migrator.DB_NVarchar, Length: 8, Nullable: false}, + }, + Indices: []*migrator.Index{ + {Cols: []string{"org_id", "uid"}, Type: migrator.IndexType}, + }, + } + + mg.AddMigration("create_silence_table", migrator.NewAddTableMigration(silence)) + mg.AddMigration("add unique index in silence on org_id and uid columns", migrator.NewAddIndexMigration(silence, silence.Indices[0])) +} diff --git a/pkg/services/ngalert/store/silence.go b/pkg/services/ngalert/store/silence.go new file mode 100644 index 00000000000..2beb92cbfed --- /dev/null +++ b/pkg/services/ngalert/store/silence.go @@ -0,0 +1,165 @@ +package store + +import ( + "context" + "errors" + "fmt" + + "github.com/go-openapi/strfmt" + "github.com/grafana/grafana/pkg/services/ngalert/models" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/util" + amv2 "github.com/prometheus/alertmanager/api/v2/models" +) + +func getSilenceByUID(sess *sqlstore.DBSession, silenceUID string, orgID int64) (*models.Silence, error) { + silence := &models.Silence{ + OrgID: orgID, + UID: silenceUID, + } + has, err := sess.Get(silence) + if err != nil { + return nil, err + } + if !has { + return nil, models.ErrSilenceNotFound + } + return silence, nil +} + +func getSilenceByID(sess *sqlstore.DBSession, id int64) (*models.Silence, error) { + silence := &models.Silence{} + + has, err := sess.ID(id).Get(silence) + if err != nil { + return nil, err + } + if !has { + return nil, ErrNoAlertmanagerConfiguration + } + + return silence, nil +} + +func (st DBstore) GetOrgSilences(query *models.GetSilencesQuery) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + silences := make([]*models.Silence, 0) + q := "SELECT * FROM silence WHERE org_id = ?" + if err := sess.SQL(q, query.OrgID).Find(&silences); err != nil { + return err + } + + query.Result = silences + return nil + }) +} + +func (st DBstore) GetSilenceByUID(query *models.GetSilenceByUIDQuery) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + silence, err := getSilenceByUID(sess, query.UID, query.OrgID) + if err != nil { + return err + } + query.Result = silence + return nil + }) +} + +func (st DBstore) GetSilenceByID(query *models.GetSilenceByIDQuery) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + silence, err := getSilenceByID(sess, query.ID) + if err != nil { + return err + } + query.Result = silence + return nil + }) +} + +func (st DBstore) DeleteSilenceByUID(cmd *models.DeleteSilenceByUIDCommand) error { + return st.SQLStore.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + _, err := getSilenceByUID(sess, cmd.UID, cmd.OrgID) + if err != nil && errors.Is(err, models.ErrSilenceNotFound) { + return err + } + + _, err = sess.Exec("DELETE FROM silence WHERE uid = ? AND org_id = ?", cmd.UID, cmd.OrgID) + if err != nil { + return err + } + return nil + }) +} + +func (st DBstore) DeleteSilenceByID(cmd *models.DeleteSilenceByIDCommand) error { + return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + _, err := getSilenceByID(sess, cmd.ID) + if err != nil && errors.Is(err, models.ErrSilenceNotFound) { + return err + } + + _, err = sess.Exec("DELETE FROM silence WHERE id ?", cmd.ID) + if err != nil { + return err + } + return nil + }) +} + +func (st DBstore) SaveSilence(cmd *models.SaveSilenceCommand) error { + return st.SQLStore.WithTransactionalDbSession(context.Background(), func(sess *sqlstore.DBSession) error { + var firstSeen bool + existingSilence, err := getSilenceByUID(sess, cmd.UID, cmd.OrgID) + if err != nil { + if !errors.Is(err, models.ErrSilenceNotFound) { + return err + } + firstSeen = true + } + + statusPending := amv2.SilenceStatusStatePending + updatedAt := strfmt.DateTime(TimeNow()) + + silenceModel := models.Silence{ + OrgID: cmd.OrgID, + UID: cmd.UID, + Status: models.SilenceStatus{State: &statusPending}, + UpdatedAt: updatedAt, + Comment: *cmd.Comment, + CreatedBy: *cmd.CreatedBy, + EndsAt: *cmd.EndsAt, + Matchers: models.Matchers(cmd.Matchers), + StartsAt: *cmd.StartsAt, + } + + switch firstSeen { + case true: + if _, err := sess.Insert(&silenceModel); err != nil { + return fmt.Errorf("failed to insert silence: %w", err) + } + default: + if _, err := sess.ID(existingSilence.ID).Update(&silenceModel); err != nil { + return fmt.Errorf("failed to update silence: %w", err) + } + } + + return nil + }) +} + +func GenerateNewSilenceUID(sess *sqlstore.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(&models.Silence{}) + if err != nil { + return "", err + } + + if !exists { + return uid, nil + } + } + + return "", models.ErrSilenceFailedGenerateUniqueUID +}