diff --git a/pkg/tsdb/prometheus/force_http_get_middleware.go b/pkg/tsdb/prometheus/force_http_get_middleware.go new file mode 100644 index 00000000000..b3211515506 --- /dev/null +++ b/pkg/tsdb/prometheus/force_http_get_middleware.go @@ -0,0 +1,29 @@ +package prometheus + +import ( + "net/http" + + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" +) + +func forceHttpGetMiddleware(logger log.Logger) sdkhttpclient.Middleware { + return sdkhttpclient.NamedMiddlewareFunc("force-http-get", func(opts sdkhttpclient.Options, next http.RoundTripper) http.RoundTripper { + // the prometheus library we use does not allow us to set the http method. + // it's behavior is to first try POST, and if it fails in certain ways + // (for example, by returning a method-not-allowed error), it will try GET. + // so here, we check if the http-method is POST, and if it is, we + // return an artificial method-not-allowed response. + // this will cause the prometheus library to retry with GET. + return sdkhttpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { + if req.Method == http.MethodPost { + resp := &http.Response{ + StatusCode: http.StatusMethodNotAllowed, + } + return resp, nil + } + + return next.RoundTrip(req) + }) + }) +} diff --git a/pkg/tsdb/prometheus/force_http_get_middleware_test.go b/pkg/tsdb/prometheus/force_http_get_middleware_test.go new file mode 100644 index 00000000000..c500059b9bd --- /dev/null +++ b/pkg/tsdb/prometheus/force_http_get_middleware_test.go @@ -0,0 +1,84 @@ +package prometheus + +import ( + "net/http" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana/pkg/infra/log" + "github.com/stretchr/testify/require" +) + +func TestForceGet(t *testing.T) { + t.Run("With nil jsonOpts, should not force get-method", func(t *testing.T) { + var jsonOpts map[string]interface{} + require.False(t, forceHttpGet(jsonOpts)) + }) + + t.Run("With empty jsonOpts, should not force get-method", func(t *testing.T) { + jsonOpts := make(map[string]interface{}) + require.False(t, forceHttpGet(jsonOpts)) + }) + + t.Run("With httpMethod=nil, should not not force get-method", func(t *testing.T) { + jsonOpts := map[string]interface{}{ + "httpMethod": nil, + } + require.False(t, forceHttpGet(jsonOpts)) + }) + + t.Run("With httpMethod=post, should not force get-method", func(t *testing.T) { + jsonOpts := map[string]interface{}{ + "httpMethod": "POST", + } + require.False(t, forceHttpGet(jsonOpts)) + }) + + t.Run("With httpMethod=get, should force get-method", func(t *testing.T) { + jsonOpts := map[string]interface{}{ + "httpMethod": "get", + } + require.True(t, forceHttpGet(jsonOpts)) + }) + + t.Run("With httpMethod=GET, should force get-method", func(t *testing.T) { + jsonOpts := map[string]interface{}{ + "httpMethod": "GET", + } + require.True(t, forceHttpGet(jsonOpts)) + }) +} + +func TestEnsureHttpMethodMiddleware(t *testing.T) { + t.Run("Name should be correct", func(t *testing.T) { + finalRoundTripper := httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK}, nil + }) + mw := forceHttpGetMiddleware(log.New("test")) + rt := mw.CreateMiddleware(httpclient.Options{}, finalRoundTripper) + require.NotNil(t, rt) + middlewareName, ok := mw.(httpclient.MiddlewareName) + require.True(t, ok) + require.Equal(t, "force-http-get", middlewareName.MiddlewareName()) + }) + + t.Run("Should force GET method", func(t *testing.T) { + finalRoundTripper := httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { + return &http.Response{StatusCode: http.StatusOK}, nil + }) + + mw := forceHttpGetMiddleware(log.New("test")) + rt := mw.CreateMiddleware(httpclient.Options{}, finalRoundTripper) + require.NotNil(t, rt) + + req, err := http.NewRequest(http.MethodPost, "http://example.com", nil) + require.NoError(t, err) + res, err := rt.RoundTrip(req) + require.NoError(t, err) + require.NotNil(t, res) + require.Equal(t, res.StatusCode, http.StatusMethodNotAllowed) + if res.Body != nil { + require.NoError(t, res.Body.Close()) + } + }) +} diff --git a/pkg/tsdb/prometheus/prometheus.go b/pkg/tsdb/prometheus/prometheus.go index 62e77849b02..40f12e56929 100644 --- a/pkg/tsdb/prometheus/prometheus.go +++ b/pkg/tsdb/prometheus/prometheus.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "regexp" + "strings" "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" @@ -55,6 +56,24 @@ func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider, pl return s, nil } +func forceHttpGet(settingsJson map[string]interface{}) bool { + methodInterface, exists := settingsJson["httpMethod"] + if !exists { + return false + } + + method, ok := methodInterface.(string) + if !ok { + return false + } + + if strings.ToLower(method) != "get" { + return false + } + + return true +} + func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { jsonData := map[string]interface{}{} @@ -86,7 +105,7 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst } } - client, err := createClient(settings.URL, httpCliOpts, httpClientProvider) + client, err := createClient(settings.URL, httpCliOpts, httpClientProvider, forceHttpGet(jsonData)) if err != nil { return nil, err } @@ -124,9 +143,13 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) return result, err } -func createClient(url string, httpOpts sdkhttpclient.Options, clientProvider httpclient.Provider) (apiv1.API, error) { - customMiddlewares := customQueryParametersMiddleware(plog) - httpOpts.Middlewares = []sdkhttpclient.Middleware{customMiddlewares} +func createClient(url string, httpOpts sdkhttpclient.Options, clientProvider httpclient.Provider, forceHttpGet bool) (apiv1.API, error) { + customParamsMiddleware := customQueryParametersMiddleware(plog) + middlewares := []sdkhttpclient.Middleware{customParamsMiddleware} + if forceHttpGet { + middlewares = append(middlewares, forceHttpGetMiddleware(plog)) + } + httpOpts.Middlewares = middlewares roundTripper, err := clientProvider.GetTransport(httpOpts) if err != nil {