diff --git a/go.mod b/go.mod index d60567bc9a9..e20113a681e 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-20210316151414-4987b85e57ee + github.com/grafana/alerting-api v0.0.0-20210318231719-9499804fc548 github.com/grafana/grafana-aws-sdk v0.2.0 github.com/grafana/grafana-plugin-model v0.0.0-20190930120109-1fc953a61fb4 github.com/grafana/grafana-plugin-sdk-go v0.88.0 @@ -97,6 +97,7 @@ require ( gopkg.in/redis.v5 v5.2.9 gopkg.in/square/go-jose.v2 v2.5.1 gopkg.in/yaml.v2 v2.4.0 + gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b xorm.io/core v0.7.3 xorm.io/xorm v0.8.2 ) diff --git a/go.sum b/go.sum index 242b1f5e232..5f27a03e05d 100644 --- a/go.sum +++ b/go.sum @@ -794,8 +794,8 @@ github.com/gorilla/websocket v1.4.2 h1:+/TMaTYc4QFitKJxsQ7Yye35DkWvkdLcvGKqM+x0U github.com/gorilla/websocket v1.4.2/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= 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-20210316151414-4987b85e57ee h1:LZo5p1wyV4RbQa28QBJZoS+VtnH1ruh4K4k1fOuHxJs= -github.com/grafana/alerting-api v0.0.0-20210316151414-4987b85e57ee/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= +github.com/grafana/alerting-api v0.0.0-20210318231719-9499804fc548 h1:KjyaZJhPJ15Ul/+OQr8mbO7kDpU5i7G3r5FGVZKClTQ= +github.com/grafana/alerting-api v0.0.0-20210318231719-9499804fc548/go.mod h1:5IppnPguSHcCbVLGCVzVjBvuQZNbYgVJ4KyXXjhCyWY= github.com/grafana/grafana v1.9.2-0.20210308201921-4ce0a49eac03/go.mod h1:AHRRvd4utJGY25J5nW8aL7wZzn/LcJ0z2za9oOp14j4= github.com/grafana/grafana-aws-sdk v0.1.0/go.mod h1:+pPo5U+pX0zWimR7YBc7ASeSQfbRkcTyQYqMiAj7G5U= github.com/grafana/grafana-aws-sdk v0.2.0 h1:UTBBYwye+ad5YUIlwN7TGxLdz1wXN3Ezhl0pseDGRVA= diff --git a/pkg/api/response/response.go b/pkg/api/response/response.go index 46e5aa4cf13..57d751a407c 100644 --- a/pkg/api/response/response.go +++ b/pkg/api/response/response.go @@ -34,7 +34,6 @@ type NormalResponse struct { header http.Header errMessage string err error - http.ResponseWriter } // Write implements http.ResponseWriter diff --git a/pkg/services/ngalert/api/api.go b/pkg/services/ngalert/api/api.go index 48194fd8b41..3b8f8946c06 100644 --- a/pkg/services/ngalert/api/api.go +++ b/pkg/services/ngalert/api/api.go @@ -6,6 +6,7 @@ import ( "github.com/grafana/grafana/pkg/infra/log" + "github.com/grafana/grafana/pkg/services/datasourceproxy" ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models" "github.com/grafana/grafana/pkg/services/ngalert/schedule" @@ -36,6 +37,7 @@ type API struct { DataService *tsdb.Service Schedule schedule.ScheduleService Store store.Store + DataProxy *datasourceproxy.DatasourceProxyService } // RegisterAPIEndpoints registers API handlers @@ -43,7 +45,10 @@ func (api *API) RegisterAPIEndpoints() { logger := log.New("ngalert.api") api.RegisterAlertmanagerApiEndpoints(AlertmanagerApiMock{log: logger}) api.RegisterPrometheusApiEndpoints(PrometheusApiMock{log: logger}) - api.RegisterRulerApiEndpoints(RulerApiMock{log: logger}) + api.RegisterRulerApiEndpoints(NewForkedRuler( + &LotexRuler{DataProxy: api.DataProxy, log: logger}, + RulerApiMock{log: logger}, + )) api.RegisterTestingApiEndpoints(TestingApiMock{log: logger}) // Legacy routes; they will be removed in v8 diff --git a/pkg/services/ngalert/api/fork_ruler.go b/pkg/services/ngalert/api/fork_ruler.go new file mode 100644 index 00000000000..5487fcb643a --- /dev/null +++ b/pkg/services/ngalert/api/fork_ruler.go @@ -0,0 +1,107 @@ +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" +) + +// ForkedRuler will validate and proxy requests to the correct backend type depending on the datasource. +type ForkedRuler struct { + LotexRuler, GrafanaRuler RulerApiService +} + +func NewForkedRuler(lotex, grafana RulerApiService) *ForkedRuler { + return &ForkedRuler{ + LotexRuler: lotex, + GrafanaRuler: grafana, + } +} + +func (r *ForkedRuler) backendType(ctx *models.ReqContext) apimodels.Backend { + // TODO: implement, hardcoded for now + return apimodels.LoTexRulerBackend +} + +func (r *ForkedRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { + switch t := r.backendType(ctx); t { + case apimodels.GrafanaBackend: + return r.GrafanaRuler.RouteDeleteNamespaceRulesConfig(ctx) + case apimodels.LoTexRulerBackend: + return r.LotexRuler.RouteDeleteNamespaceRulesConfig(ctx) + default: + return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil) + } +} + +func (r *ForkedRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { + switch t := r.backendType(ctx); t { + case apimodels.GrafanaBackend: + return r.GrafanaRuler.RouteDeleteRuleGroupConfig(ctx) + case apimodels.LoTexRulerBackend: + return r.LotexRuler.RouteDeleteRuleGroupConfig(ctx) + default: + return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil) + } +} + +func (r *ForkedRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { + switch t := r.backendType(ctx); t { + case apimodels.GrafanaBackend: + return r.GrafanaRuler.RouteGetNamespaceRulesConfig(ctx) + case apimodels.LoTexRulerBackend: + return r.LotexRuler.RouteGetNamespaceRulesConfig(ctx) + default: + return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil) + } +} + +func (r *ForkedRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { + switch t := r.backendType(ctx); t { + case apimodels.GrafanaBackend: + return r.GrafanaRuler.RouteGetRulegGroupConfig(ctx) + case apimodels.LoTexRulerBackend: + return r.LotexRuler.RouteGetRulegGroupConfig(ctx) + default: + return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil) + } +} + +func (r *ForkedRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { + switch t := r.backendType(ctx); t { + case apimodels.GrafanaBackend: + return r.GrafanaRuler.RouteGetRulesConfig(ctx) + case apimodels.LoTexRulerBackend: + return r.LotexRuler.RouteGetRulesConfig(ctx) + default: + return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", t), nil) + } +} + +func (r *ForkedRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.RuleGroupConfig) response.Response { + backendType := r.backendType(ctx) + payloadType := conf.Type() + + if backendType != payloadType { + return response.Error( + 400, + fmt.Sprintf( + "unexpected backend type (%v) vs payload type (%v)", + backendType, + payloadType, + ), + nil, + ) + } + + switch backendType { + case apimodels.GrafanaBackend: + return r.GrafanaRuler.RoutePostNameRulesConfig(ctx, conf) + case apimodels.LoTexRulerBackend: + return r.LotexRuler.RoutePostNameRulesConfig(ctx, conf) + default: + return response.Error(400, fmt.Sprintf("unexpected backend type (%v)", backendType), nil) + } +} diff --git a/pkg/services/ngalert/api/lotex.go b/pkg/services/ngalert/api/lotex.go new file mode 100644 index 00000000000..ea30c82594f --- /dev/null +++ b/pkg/services/ngalert/api/lotex.go @@ -0,0 +1,212 @@ +package api + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + + apimodels "github.com/grafana/alerting-api/pkg/api" + "gopkg.in/macaron.v1" + "gopkg.in/yaml.v3" + + "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/services/datasourceproxy" +) + +const legacyRulerPrefix = "/api/prom/rules" + +type LotexRuler struct { + DataProxy *datasourceproxy.DatasourceProxyService + log log.Logger +} + +// macaron unsafely asserts the http.ResponseWriter is an http.CloseNotifier, which will panic. +// Here we impl it, which will ensure this no longer happens, but neither will we take +// advantage cancelling upstream requests when the downstream has closed. +// NB: http.CloseNotifier is a deprecated ifc from before the context pkg. +type safeMacaronWrapper struct { + http.ResponseWriter +} + +func (w *safeMacaronWrapper) CloseNotify() <-chan bool { + return make(chan bool) +} + +// replacedResponseWriter overwrites the underlying responsewriter used by a *models.ReqContext. +// It's ugly because it needs to replace a value behind a few nested pointers. +func replacedResponseWriter(ctx *models.ReqContext) (*models.ReqContext, *response.NormalResponse) { + resp := response.CreateNormalResponse(make(http.Header), nil, 0) + cpy := *ctx + cpyMCtx := *cpy.Context + cpyMCtx.Resp = macaron.NewResponseWriter(ctx.Req.Method, &safeMacaronWrapper{resp}) + cpy.Context = &cpyMCtx + return &cpy, resp +} + +// withReq proxies a different request +func (r *LotexRuler) withReq( + ctx *models.ReqContext, + req *http.Request, + extractor func([]byte) (interface{}, error), +) response.Response { + newCtx, resp := replacedResponseWriter(ctx) + newCtx.Req.Request = req + r.DataProxy.ProxyDatasourceRequestWithID(newCtx, ctx.ParamsInt64("DatasourceId")) + + status := resp.Status() + if status >= 400 { + return response.Error(status, string(resp.Body()), nil) + } + + t, err := extractor(resp.Body()) + if err != nil { + return response.Error(500, err.Error(), nil) + } + + b, err := json.Marshal(t) + if err != nil { + return response.Error(500, err.Error(), nil) + } + + return response.JSON(status, b) +} + +func yamlExtractor(v interface{}) func([]byte) (interface{}, error) { + return func(b []byte) (interface{}, error) { + decoder := yaml.NewDecoder(bytes.NewReader(b)) + decoder.KnownFields(true) + + err := decoder.Decode(v) + + return v, err + } +} + +func jsonExtractor(v interface{}) func([]byte) (interface{}, error) { + if v == nil { + // json unmarshal expects a pointer + v = &map[string]interface{}{} + } + return func(b []byte) (interface{}, error) { + return v, json.Unmarshal(b, v) + } +} + +func messageExtractor(b []byte) (interface{}, error) { + return map[string]string{"message": string(b)}, nil +} + +func (r *LotexRuler) RouteDeleteNamespaceRulesConfig(ctx *models.ReqContext) response.Response { + return r.withReq( + ctx, + &http.Request{ + Method: "DELETE", + URL: withPath( + *ctx.Req.URL, + fmt.Sprintf("/api/prom/rules/%s", ctx.Params("Namespace")), + ), + }, + messageExtractor, + ) +} + +func (r *LotexRuler) RouteDeleteRuleGroupConfig(ctx *models.ReqContext) response.Response { + return r.withReq( + ctx, + &http.Request{ + Method: "DELETE", + URL: withPath( + *ctx.Req.URL, + fmt.Sprintf( + "%s/%s/%s", + legacyRulerPrefix, + ctx.Params("Namespace"), + ctx.Params("Groupname"), + ), + ), + }, + messageExtractor, + ) +} + +func (r *LotexRuler) RouteGetNamespaceRulesConfig(ctx *models.ReqContext) response.Response { + return r.withReq( + ctx, &http.Request{ + URL: withPath( + *ctx.Req.URL, + fmt.Sprintf( + "%s/%s", + legacyRulerPrefix, + ctx.Params("Namespace"), + ), + ), + }, + yamlExtractor(apimodels.NamespaceConfigResponse{}), + ) +} + +func (r *LotexRuler) RouteGetRulegGroupConfig(ctx *models.ReqContext) response.Response { + return r.withReq( + ctx, + &http.Request{ + URL: withPath( + *ctx.Req.URL, + fmt.Sprintf( + "%s/%s/%s", + legacyRulerPrefix, + ctx.Params("Namespace"), + ctx.Params("Groupname"), + ), + ), + }, + yamlExtractor(apimodels.RuleGroupConfigResponse{}), + ) +} + +func (r *LotexRuler) RouteGetRulesConfig(ctx *models.ReqContext) response.Response { + return r.withReq( + ctx, + &http.Request{ + URL: withPath( + *ctx.Req.URL, + legacyRulerPrefix, + ), + }, + yamlExtractor(apimodels.NamespaceConfigResponse{}), + ) +} + +func (r *LotexRuler) RoutePostNameRulesConfig(ctx *models.ReqContext, conf apimodels.RuleGroupConfig) response.Response { + yml, err := yaml.Marshal(conf) + if err != nil { + return response.Error(500, "Failed marshal rule group", err) + } + body, ln := payload(yml) + + ns := ctx.Params("Namespace") + + u := withPath(*ctx.Req.URL, fmt.Sprintf("%s/%s", legacyRulerPrefix, ns)) + req := &http.Request{ + Method: "POST", + URL: u, + Body: body, + ContentLength: ln, + } + return r.withReq(ctx, req, jsonExtractor(nil)) +} + +func withPath(u url.URL, newPath string) *url.URL { + // TODO: handle path escaping + u.Path = newPath + return &u +} + +func payload(b []byte) (io.ReadCloser, int64) { + return ioutil.NopCloser(bytes.NewBuffer(b)), int64(len(b)) +} diff --git a/pkg/services/ngalert/ngalert.go b/pkg/services/ngalert/ngalert.go index 0a25656a462..5f301023baf 100644 --- a/pkg/services/ngalert/ngalert.go +++ b/pkg/services/ngalert/ngalert.go @@ -4,6 +4,7 @@ import ( "context" "time" + "github.com/grafana/grafana/pkg/services/datasourceproxy" "github.com/grafana/grafana/pkg/services/ngalert/api" "github.com/grafana/grafana/pkg/services/ngalert/schedule" @@ -36,11 +37,12 @@ const ( // AlertNG is the service for evaluating the condition of an alert definition. type AlertNG struct { - Cfg *setting.Cfg `inject:""` - DatasourceCache datasources.CacheService `inject:""` - RouteRegister routing.RouteRegister `inject:""` - SQLStore *sqlstore.SQLStore `inject:""` - DataService *tsdb.Service `inject:""` + Cfg *setting.Cfg `inject:""` + DatasourceCache datasources.CacheService `inject:""` + RouteRegister routing.RouteRegister `inject:""` + SQLStore *sqlstore.SQLStore `inject:""` + DataService *tsdb.Service `inject:""` + DataProxy *datasourceproxy.DatasourceProxyService `inject:""` Log log.Logger schedule schedule.ScheduleService } @@ -73,6 +75,7 @@ func (ng *AlertNG) Init() error { RouteRegister: ng.RouteRegister, DataService: ng.DataService, Schedule: ng.schedule, + DataProxy: ng.DataProxy, Store: store} api.RegisterAPIEndpoints()