diff --git a/go.sum b/go.sum index 4f623ae288f..d995cdfb296 100644 --- a/go.sum +++ b/go.sum @@ -1417,6 +1417,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/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 8d2d1da1858..2afe687dda0 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -50,7 +50,11 @@ func (api *API) RegisterAPIEndpoints() { proxy := &AlertingProxy{ DataProxy: api.DataProxy, } - api.RegisterAlertmanagerApiEndpoints(AlertmanagerApiMock{log: logger}) + api.RegisterAlertmanagerApiEndpoints(NewForkedAM( + api.DatasourceCache, + NewLotexAM(proxy, logger), + AlertmanagerApiMock{log: logger}, + )) api.RegisterPrometheusApiEndpoints(NewForkedProm( api.DatasourceCache, NewLotexProm(proxy, logger), diff --git a/pkg/services/ngalert/api/api_alertmanager_base.go b/pkg/services/ngalert/api/api_alertmanager_base.go index 2ac1048a32c..feae04a42f1 100644 --- a/pkg/services/ngalert/api/api_alertmanager_base.go +++ b/pkg/services/ngalert/api/api_alertmanager_base.go @@ -21,13 +21,13 @@ type AlertmanagerApiService interface { RouteCreateSilence(*models.ReqContext, apimodels.SilenceBody) 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 - RouteGetAmAlertGroups(*models.ReqContext) response.Response - RouteGetAmAlerts(*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 - RoutePostAmAlerts(*models.ReqContext, apimodels.PostableAlerts) response.Response } type AlertmanagerApiBase struct { @@ -39,13 +39,13 @@ func (api *API) RegisterAlertmanagerApiEndpoints(srv AlertmanagerApiService) { group.Post(toMacaronPath("/alertmanager/{Recipient}/api/v2/silences"), binding.Bind(apimodels.SilenceBody{}), 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)) + 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.Get(toMacaronPath("/alertmanager/{Recipient}/config/api/v1/alerts"), routing.Wrap(srv.RouteGetAlertingConfig)) - 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.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)) - group.Post(toMacaronPath("/alertmanager/{Recipient}/api/v2/alerts"), binding.Bind(apimodels.PostableAlerts{}), routing.Wrap(srv.RoutePostAmAlerts)) }) } @@ -70,21 +70,21 @@ func (base AlertmanagerApiBase) RouteDeleteSilence(c *models.ReqContext) respons return response.Error(http.StatusNotImplemented, "", nil) } -func (base AlertmanagerApiBase) RouteGetAlertingConfig(c *models.ReqContext) response.Response { +func (base AlertmanagerApiBase) RouteGetAMAlertGroups(c *models.ReqContext) response.Response { recipient := c.Params(":Recipient") - base.log.Info("RouteGetAlertingConfig: ", "Recipient", recipient) + base.log.Info("RouteGetAMAlertGroups: ", "Recipient", recipient) return response.Error(http.StatusNotImplemented, "", nil) } -func (base AlertmanagerApiBase) RouteGetAmAlertGroups(c *models.ReqContext) response.Response { +func (base AlertmanagerApiBase) RouteGetAMAlerts(c *models.ReqContext) response.Response { recipient := c.Params(":Recipient") - base.log.Info("RouteGetAmAlertGroups: ", "Recipient", recipient) + base.log.Info("RouteGetAMAlerts: ", "Recipient", recipient) return response.Error(http.StatusNotImplemented, "", nil) } -func (base AlertmanagerApiBase) RouteGetAmAlerts(c *models.ReqContext) response.Response { +func (base AlertmanagerApiBase) RouteGetAlertingConfig(c *models.ReqContext) response.Response { recipient := c.Params(":Recipient") - base.log.Info("RouteGetAmAlerts: ", "Recipient", recipient) + base.log.Info("RouteGetAlertingConfig: ", "Recipient", recipient) return response.Error(http.StatusNotImplemented, "", nil) } @@ -102,16 +102,16 @@ func (base AlertmanagerApiBase) RouteGetSilences(c *models.ReqContext) response. return response.Error(http.StatusNotImplemented, "", nil) } -func (base AlertmanagerApiBase) RoutePostAlertingConfig(c *models.ReqContext, body apimodels.PostableUserConfig) response.Response { +func (base AlertmanagerApiBase) RoutePostAMAlerts(c *models.ReqContext, body apimodels.PostableAlerts) response.Response { recipient := c.Params(":Recipient") - base.log.Info("RoutePostAlertingConfig: ", "Recipient", recipient) - base.log.Info("RoutePostAlertingConfig: ", "body", body) + base.log.Info("RoutePostAMAlerts: ", "Recipient", recipient) + base.log.Info("RoutePostAMAlerts: ", "body", body) return response.Error(http.StatusNotImplemented, "", nil) } -func (base AlertmanagerApiBase) RoutePostAmAlerts(c *models.ReqContext, body apimodels.PostableAlerts) response.Response { +func (base AlertmanagerApiBase) RoutePostAlertingConfig(c *models.ReqContext, body apimodels.PostableUserConfig) response.Response { recipient := c.Params(":Recipient") - base.log.Info("RoutePostAmAlerts: ", "Recipient", recipient) - base.log.Info("RoutePostAmAlerts: ", "body", body) + base.log.Info("RoutePostAlertingConfig: ", "Recipient", recipient) + base.log.Info("RoutePostAlertingConfig: ", "body", body) return response.Error(http.StatusNotImplemented, "", nil) } diff --git a/pkg/services/ngalert/api/api_alertmanager_mock.go b/pkg/services/ngalert/api/api_alertmanager_mock.go index 27f57ea31a1..61e62e23f88 100644 --- a/pkg/services/ngalert/api/api_alertmanager_mock.go +++ b/pkg/services/ngalert/api/api_alertmanager_mock.go @@ -593,9 +593,9 @@ func (mock AlertmanagerApiMock) RouteGetAlertingConfig(c *models.ReqContext) res return response.JSON(http.StatusOK, result) } -func (mock AlertmanagerApiMock) RouteGetAmAlertGroups(c *models.ReqContext) response.Response { +func (mock AlertmanagerApiMock) RouteGetAMAlertGroups(c *models.ReqContext) response.Response { recipient := c.Params(":Recipient") - mock.log.Info("RouteGetAmAlertGroups: ", "Recipient", recipient) + mock.log.Info("RouteGetAMAlertGroups: ", "Recipient", recipient) now := time.Now() result := apimodels.AlertGroups{ &amv2.AlertGroup{ @@ -714,9 +714,9 @@ func (mock AlertmanagerApiMock) RouteGetAmAlertGroups(c *models.ReqContext) resp return response.JSON(http.StatusOK, result) } -func (mock AlertmanagerApiMock) RouteGetAmAlerts(c *models.ReqContext) response.Response { +func (mock AlertmanagerApiMock) RouteGetAMAlerts(c *models.ReqContext) response.Response { recipient := c.Params(":Recipient") - mock.log.Info("RouteGetAmAlerts: ", "Recipient", recipient) + mock.log.Info("RouteGetAMAlerts: ", "Recipient", recipient) now := time.Now() result := apimodels.GettableAlerts{ &amv2.GettableAlert{ @@ -883,9 +883,9 @@ func (mock AlertmanagerApiMock) RoutePostAlertingConfig(c *models.ReqContext, bo return response.JSON(http.StatusAccepted, util.DynMap{"message": "configuration created"}) } -func (mock AlertmanagerApiMock) RoutePostAmAlerts(c *models.ReqContext, body apimodels.PostableAlerts) response.Response { +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) + 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 new file mode 100644 index 00000000000..be1775fb177 --- /dev/null +++ b/pkg/services/ngalert/api/forked_am.go @@ -0,0 +1,129 @@ +package api + +import ( + "fmt" + + apimodels "github.com/grafana/alerting-api/pkg/api" + "github.com/grafana/grafana/pkg/api/response" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/datasources" +) + +type ForkedAMSvc struct { + AMSvc, GrafanaSvc AlertmanagerApiService + DatasourceCache datasources.CacheService +} + +func NewForkedAM(datasourceCache datasources.CacheService, proxy, grafana AlertmanagerApiService) *ForkedAMSvc { + return &ForkedAMSvc{ + AMSvc: proxy, + GrafanaSvc: grafana, + DatasourceCache: datasourceCache, + } +} + +func (am *ForkedAMSvc) getService(ctx *models.ReqContext) (AlertmanagerApiService, error) { + t, err := backendType(ctx, am.DatasourceCache) + if err != nil { + return nil, err + } + + switch t { + case apimodels.GrafanaBackend: + return am.GrafanaSvc, nil + case apimodels.AlertmanagerBackend: + return am.AMSvc, nil + default: + return nil, fmt.Errorf("unexpected backend type (%v)", t) + } +} + +func (am *ForkedAMSvc) RouteCreateSilence(ctx *models.ReqContext, body apimodels.SilenceBody) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RouteCreateSilence(ctx, body) +} + +func (am *ForkedAMSvc) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RouteDeleteAlertingConfig(ctx) +} + +func (am *ForkedAMSvc) RouteDeleteSilence(ctx *models.ReqContext) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RouteDeleteSilence(ctx) +} + +func (am *ForkedAMSvc) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RouteGetAlertingConfig(ctx) +} + +func (am *ForkedAMSvc) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RouteGetAMAlertGroups(ctx) +} + +func (am *ForkedAMSvc) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RouteGetAMAlerts(ctx) +} + +func (am *ForkedAMSvc) RouteGetSilence(ctx *models.ReqContext) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RouteGetSilence(ctx) +} + +func (am *ForkedAMSvc) RouteGetSilences(ctx *models.ReqContext) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RouteGetSilences(ctx) +} + +func (am *ForkedAMSvc) RoutePostAlertingConfig(ctx *models.ReqContext, body apimodels.PostableUserConfig) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RoutePostAlertingConfig(ctx, body) +} + +func (am *ForkedAMSvc) RoutePostAMAlerts(ctx *models.ReqContext, body apimodels.PostableAlerts) response.Response { + s, err := am.getService(ctx) + if err != nil { + return response.Error(400, err.Error(), nil) + } + + return s.RoutePostAMAlerts(ctx, body) +} diff --git a/pkg/services/ngalert/api/lotex_am.go b/pkg/services/ngalert/api/lotex_am.go new file mode 100644 index 00000000000..2c702ddab94 --- /dev/null +++ b/pkg/services/ngalert/api/lotex_am.go @@ -0,0 +1,170 @@ +package api + +import ( + "encoding/json" + "fmt" + "net/http" + + 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" + "gopkg.in/yaml.v3" +) + +const ( + amSilencesPath = "/api/v2/silences" + amSilencePath = "/api/v2/silence/%s" + amAlertGroupsPath = "/api/v2/alerts/groups" + amAlertsPath = "/api/v2/alerts" + amConfigPath = "/api/v1/alerts" +) + +type LotexAM struct { + log log.Logger + *AlertingProxy +} + +func NewLotexAM(proxy *AlertingProxy, log log.Logger) *LotexAM { + return &LotexAM{ + log: log, + AlertingProxy: proxy, + } +} + +func (am *LotexAM) RouteCreateSilence(ctx *models.ReqContext, silenceBody apimodels.SilenceBody) response.Response { + blob, err := json.Marshal(silenceBody) + if err != nil { + return response.Error(500, "Failed marshal silence", err) + } + body, ln := payload(blob) + return am.withReq( + ctx, &http.Request{ + Method: "POST", + URL: withPath(*ctx.Req.URL, amSilencesPath), + Body: body, + ContentLength: ln, + }, + jsonExtractor(&apimodels.GettableSilence{}), + ) +} + +func (am *LotexAM) RouteDeleteAlertingConfig(ctx *models.ReqContext) response.Response { + return am.withReq( + ctx, &http.Request{ + Method: "DELETE", + URL: withPath( + *ctx.Req.URL, + amConfigPath, + ), + }, + messageExtractor, + ) +} + +func (am *LotexAM) RouteDeleteSilence(ctx *models.ReqContext) response.Response { + return am.withReq( + ctx, &http.Request{ + Method: "DELETE", + URL: withPath( + *ctx.Req.URL, + fmt.Sprintf(amSilencePath, ctx.Params(":SilenceId")), + ), + }, + messageExtractor, + ) +} + +func (am *LotexAM) RouteGetAlertingConfig(ctx *models.ReqContext) response.Response { + return am.withReq( + ctx, &http.Request{ + URL: withPath( + *ctx.Req.URL, + amConfigPath, + ), + }, + jsonExtractor(&apimodels.GettableUserConfig{}), + ) +} + +func (am *LotexAM) RouteGetAMAlertGroups(ctx *models.ReqContext) response.Response { + return am.withReq( + ctx, &http.Request{ + URL: withPath( + *ctx.Req.URL, + amAlertGroupsPath, + ), + }, + jsonExtractor(&apimodels.AlertGroups{}), + ) +} + +func (am *LotexAM) RouteGetAMAlerts(ctx *models.ReqContext) response.Response { + return am.withReq( + ctx, &http.Request{ + URL: withPath( + *ctx.Req.URL, + amAlertsPath, + ), + }, + jsonExtractor(&apimodels.GettableAlerts{}), + ) +} + +func (am *LotexAM) RouteGetSilence(ctx *models.ReqContext) response.Response { + return am.withReq( + ctx, &http.Request{ + URL: withPath( + *ctx.Req.URL, + fmt.Sprintf(amSilencePath, ctx.Params(":SilenceId")), + ), + }, + jsonExtractor(&apimodels.GettableSilence{}), + ) +} + +func (am *LotexAM) RouteGetSilences(ctx *models.ReqContext) response.Response { + return am.withReq( + ctx, &http.Request{ + URL: withPath( + *ctx.Req.URL, + amSilencesPath, + ), + }, + jsonExtractor(&apimodels.GettableSilences{}), + ) +} + +func (am *LotexAM) RoutePostAlertingConfig(ctx *models.ReqContext, config apimodels.PostableUserConfig) response.Response { + yml, err := yaml.Marshal(config) + if err != nil { + return response.Error(500, "Failed marshal alert manager configuration ", err) + } + body, ln := payload(yml) + + u := withPath(*ctx.Req.URL, amConfigPath) + req := &http.Request{ + Method: "POST", + URL: u, + Body: body, + ContentLength: ln, + } + return am.withReq(ctx, req, messageExtractor) +} + +func (am *LotexAM) RoutePostAMAlerts(ctx *models.ReqContext, alerts apimodels.PostableAlerts) response.Response { + yml, err := yaml.Marshal(alerts) + if err != nil { + return response.Error(500, "Failed marshal postable alerts", err) + } + body, ln := payload(yml) + + u := withPath(*ctx.Req.URL, amAlertsPath) + req := &http.Request{ + Method: "POST", + URL: u, + Body: body, + ContentLength: ln, + } + return am.withReq(ctx, req, messageExtractor) +} diff --git a/pkg/services/ngalert/api/test-data/am.http b/pkg/services/ngalert/api/test-data/am.http new file mode 100644 index 00000000000..643ca41d59a --- /dev/null +++ b/pkg/services/ngalert/api/test-data/am.http @@ -0,0 +1,32 @@ +@lokiDatasourceID = 32 +@prometheusDatasourceID = 875 +@grafana = grafana + +# unsupported loki backend +GET http://admin:admin@localhost:3000/alertmanager/{{lokiDatasourceID}}/config/api/v1/alerts + +### +# unsupported cortex backend +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 + +### +POST http://admin:admin@localhost:3000/alertmanager/{{grafana}}/config/api/v1/alerts +content-type: application/json + +< ./post-user-config.json + +### +POST http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silences +content-type: application/json + +< ./post-silence-data.json + +### +GET http://admin:admin@localhost:3000/alertmanager/{{grafana}}/api/v2/silences \ No newline at end of file diff --git a/pkg/services/ngalert/api/util.go b/pkg/services/ngalert/api/util.go index 6a5b08b9a01..693e2877583 100644 --- a/pkg/services/ngalert/api/util.go +++ b/pkg/services/ngalert/api/util.go @@ -49,6 +49,8 @@ func backendType(ctx *models.ReqContext, cache datasources.CacheService) (apimod switch ds.Type { case "loki", "prometheus": return apimodels.LoTexRulerBackend, nil + case "grafana-alertmanager-datasource": + return apimodels.AlertmanagerBackend, nil default: return 0, fmt.Errorf("unexpected backend type (%v)", ds.Type) }