mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 09:52:35 +08:00
Plugins: Use error plane for api/ds/query (#54750)
* plugin client returns error base * fix api test * add plugin client test * add fallback err * fix linting * wip * replace bad query * template is an error * failing test of templated error * add one test passing * fix failing test * move test * rename ErrBadQuery to ErrQueryValidationFailure * tidy diff * Change to one error per specific error kind * last err + fix test * fix imports * more tests * keep req vars together Co-authored-by: Marcus Efraimsson <marcus.efraimsson@gmail.com>
This commit is contained in:
@ -10,11 +10,8 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/api/response"
|
"github.com/grafana/grafana/pkg/api/response"
|
||||||
"github.com/grafana/grafana/pkg/models"
|
"github.com/grafana/grafana/pkg/models"
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/services/query"
|
|
||||||
"github.com/grafana/grafana/pkg/util"
|
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -31,16 +28,7 @@ func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalRespons
|
|||||||
return response.Error(http.StatusInternalServerError, fmt.Sprint("Secrets Plugin error: ", err.Error()), err)
|
return response.Error(http.StatusInternalServerError, fmt.Sprint("Secrets Plugin error: ", err.Error()), err)
|
||||||
}
|
}
|
||||||
|
|
||||||
var badQuery query.ErrBadQuery
|
return response.ErrOrFallback(http.StatusInternalServerError, "Query data error", err)
|
||||||
if errors.As(err, &badQuery) {
|
|
||||||
return response.Error(http.StatusBadRequest, util.Capitalize(badQuery.Message), err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if errors.Is(err, backendplugin.ErrPluginNotRegistered) {
|
|
||||||
return response.Error(http.StatusNotFound, "Plugin not found", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return response.Error(http.StatusInternalServerError, "Query data error", err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// QueryMetricsV2 returns query metrics.
|
// QueryMetricsV2 returns query metrics.
|
||||||
|
@ -4,42 +4,32 @@ import (
|
|||||||
"bytes"
|
"bytes"
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/services/datasources"
|
|
||||||
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
|
||||||
"github.com/grafana/grafana/pkg/services/org"
|
|
||||||
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
|
||||||
"github.com/grafana/grafana/pkg/services/user"
|
|
||||||
"github.com/grafana/grafana/pkg/web/webtest"
|
|
||||||
|
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||||
|
pluginClient "github.com/grafana/grafana/pkg/plugins/manager/client"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/registry"
|
||||||
|
"github.com/grafana/grafana/pkg/services/datasources"
|
||||||
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
|
"github.com/grafana/grafana/pkg/services/org"
|
||||||
"github.com/grafana/grafana/pkg/services/query"
|
"github.com/grafana/grafana/pkg/services/query"
|
||||||
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
||||||
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
"github.com/grafana/grafana/pkg/web/webtest"
|
||||||
)
|
)
|
||||||
|
|
||||||
var queryDatasourceInput = `{
|
|
||||||
"from": "",
|
|
||||||
"to": "",
|
|
||||||
"queries": [
|
|
||||||
{
|
|
||||||
"datasource": {
|
|
||||||
"type": "datasource",
|
|
||||||
"uid": "grafana"
|
|
||||||
},
|
|
||||||
"queryType": "randomWalk",
|
|
||||||
"refId": "A"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`
|
|
||||||
|
|
||||||
type fakePluginRequestValidator struct {
|
type fakePluginRequestValidator struct {
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
@ -98,7 +88,7 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Status code is 400 when data source response has an error and feature toggle is disabled", func(t *testing.T) {
|
t.Run("Status code is 400 when data source response has an error and feature toggle is disabled", func(t *testing.T) {
|
||||||
req := serverFeatureDisabled.NewPostRequest("/api/ds/query", strings.NewReader(queryDatasourceInput))
|
req := serverFeatureDisabled.NewPostRequest("/api/ds/query", strings.NewReader(reqValid))
|
||||||
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer})
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer})
|
||||||
resp, err := serverFeatureDisabled.SendJSON(req)
|
resp, err := serverFeatureDisabled.SendJSON(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -107,7 +97,7 @@ func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Status code is 207 when data source response has an error and feature toggle is enabled", func(t *testing.T) {
|
t.Run("Status code is 207 when data source response has an error and feature toggle is enabled", func(t *testing.T) {
|
||||||
req := serverFeatureEnabled.NewPostRequest("/api/ds/query", strings.NewReader(queryDatasourceInput))
|
req := serverFeatureEnabled.NewPostRequest("/api/ds/query", strings.NewReader(reqValid))
|
||||||
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer})
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer})
|
||||||
resp, err := serverFeatureEnabled.SendJSON(req)
|
resp, err := serverFeatureEnabled.SendJSON(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -141,7 +131,7 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Status code is 500 and a secrets plugin error is returned if there is a problem getting secrets from the remote plugin", func(t *testing.T) {
|
t.Run("Status code is 500 and a secrets plugin error is returned if there is a problem getting secrets from the remote plugin", func(t *testing.T) {
|
||||||
req := httpServer.NewPostRequest("/api/ds/query", strings.NewReader(queryDatasourceInput))
|
req := httpServer.NewPostRequest("/api/ds/query", strings.NewReader(reqValid))
|
||||||
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer})
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer})
|
||||||
resp, err := httpServer.SendJSON(req)
|
resp, err := httpServer.SendJSON(req)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -157,3 +147,168 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
|
|||||||
require.Contains(t, resObj.Message, "Secrets Plugin error:")
|
require.Contains(t, resObj.Message, "Secrets Plugin error:")
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var reqValid = `{
|
||||||
|
"from": "",
|
||||||
|
"to": "",
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "grafana"
|
||||||
|
},
|
||||||
|
"queryType": "randomWalk",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
var reqNoQueries = `{
|
||||||
|
"from": "",
|
||||||
|
"to": "",
|
||||||
|
"queries": []
|
||||||
|
}`
|
||||||
|
|
||||||
|
var reqQueryWithInvalidDatasourceID = `{
|
||||||
|
"from": "",
|
||||||
|
"to": "",
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"queryType": "randomWalk",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
var reqDatasourceByUidNotFound = `{
|
||||||
|
"from": "",
|
||||||
|
"to": "",
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"datasource": {
|
||||||
|
"type": "datasource",
|
||||||
|
"uid": "not-found"
|
||||||
|
},
|
||||||
|
"queryType": "randomWalk",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
var reqDatasourceByIdNotFound = `{
|
||||||
|
"from": "",
|
||||||
|
"to": "",
|
||||||
|
"queries": [
|
||||||
|
{
|
||||||
|
"datasourceId": 1,
|
||||||
|
"queryType": "randomWalk",
|
||||||
|
"refId": "A"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}`
|
||||||
|
|
||||||
|
func TestDataSourceQueryError(t *testing.T) {
|
||||||
|
tcs := []struct {
|
||||||
|
request string
|
||||||
|
clientErr error
|
||||||
|
expectedStatus int
|
||||||
|
expectedBody string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
request: reqValid,
|
||||||
|
clientErr: backendplugin.ErrPluginUnavailable,
|
||||||
|
expectedStatus: http.StatusInternalServerError,
|
||||||
|
expectedBody: `{"message":"Internal server error","messageId":"plugin.unavailable","statusCode":500,"traceID":""}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: reqValid,
|
||||||
|
clientErr: backendplugin.ErrMethodNotImplemented,
|
||||||
|
expectedStatus: http.StatusNotImplemented,
|
||||||
|
expectedBody: `{"message":"Not implemented","messageId":"plugin.notImplemented","statusCode":501,"traceID":""}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: reqValid,
|
||||||
|
clientErr: errors.New("surprise surprise"),
|
||||||
|
expectedStatus: errutil.StatusInternal.HTTPStatus(),
|
||||||
|
expectedBody: `{"message":"An error occurred within the plugin","messageId":"plugin.downstreamError","statusCode":500,"traceID":""}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: reqNoQueries,
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedBody: `{"message":"No queries found","messageId":"query.noQueries","statusCode":400,"traceID":""}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: reqQueryWithInvalidDatasourceID,
|
||||||
|
expectedStatus: http.StatusBadRequest,
|
||||||
|
expectedBody: `{"message":"Query does not contain a valid data source identifier","messageId":"query.invalidDatasourceId","statusCode":400,"traceID":""}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: reqDatasourceByUidNotFound,
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
expectedBody: `{"error":"data source not found","message":"Data source not found","traceID":""}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
request: reqDatasourceByIdNotFound,
|
||||||
|
expectedStatus: http.StatusNotFound,
|
||||||
|
expectedBody: `{"error":"data source not found","message":"Data source not found","traceID":""}`,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(fmt.Sprintf("Plugin client error %q should propagate to API", tc.clientErr), func(t *testing.T) {
|
||||||
|
p := &plugins.Plugin{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "grafana",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.RegisterClient(&fakePluginBackend{
|
||||||
|
qdr: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
return nil, tc.clientErr
|
||||||
|
},
|
||||||
|
})
|
||||||
|
srv := SetupAPITestServer(t, func(hs *HTTPServer) {
|
||||||
|
r := registry.NewInMemory()
|
||||||
|
err := r.Add(context.Background(), p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
hs.queryDataService = query.ProvideService(
|
||||||
|
nil,
|
||||||
|
&fakeDatasources.FakeCacheService{},
|
||||||
|
nil,
|
||||||
|
&fakePluginRequestValidator{},
|
||||||
|
&fakeDatasources.FakeDataSourceService{},
|
||||||
|
pluginClient.ProvideService(r),
|
||||||
|
&fakeOAuthTokenService{},
|
||||||
|
)
|
||||||
|
hs.QuotaService = quotatest.NewQuotaServiceFake()
|
||||||
|
})
|
||||||
|
req := srv.NewPostRequest("/api/ds/query", strings.NewReader(tc.request))
|
||||||
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, OrgRole: org.RoleViewer})
|
||||||
|
resp, err := srv.SendJSON(req)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
require.Equal(t, tc.expectedStatus, resp.StatusCode)
|
||||||
|
require.Equal(t, tc.expectedStatus, resp.StatusCode)
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tc.expectedBody, string(body))
|
||||||
|
require.NoError(t, resp.Body.Close())
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakePluginBackend struct {
|
||||||
|
qdr backend.QueryDataHandlerFunc
|
||||||
|
|
||||||
|
backendplugin.Plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginBackend) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
if f.qdr != nil {
|
||||||
|
return f.qdr(ctx, req)
|
||||||
|
}
|
||||||
|
return backend.NewQueryDataResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginBackend) IsDecommissioned() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
@ -16,6 +16,7 @@ import (
|
|||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/api/dtos"
|
"github.com/grafana/grafana/pkg/api/dtos"
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||||
@ -319,11 +320,10 @@ func Test_GetPluginAssets(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestMakePluginResourceRequest(t *testing.T) {
|
func TestMakePluginResourceRequest(t *testing.T) {
|
||||||
pluginClient := &fakePluginClient{}
|
|
||||||
hs := HTTPServer{
|
hs := HTTPServer{
|
||||||
Cfg: setting.NewCfg(),
|
Cfg: setting.NewCfg(),
|
||||||
log: log.New(),
|
log: log.New(),
|
||||||
pluginClient: pluginClient,
|
pluginClient: &fakePluginClient{},
|
||||||
}
|
}
|
||||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||||
resp := httptest.NewRecorder()
|
resp := httptest.NewRecorder()
|
||||||
|
16
pkg/plugins/errors.go
Normal file
16
pkg/plugins/errors.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package plugins
|
||||||
|
|
||||||
|
import "github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrPluginNotRegistered error returned when a plugin is not registered.
|
||||||
|
ErrPluginNotRegistered = errutil.NewBase(errutil.StatusNotFound, "plugin.notRegistered")
|
||||||
|
// ErrHealthCheckFailed error returned when a plugin health check failed.
|
||||||
|
ErrHealthCheckFailed = errutil.NewBase(errutil.StatusInternal, "plugin.failedHealthCheck")
|
||||||
|
// ErrPluginUnavailable error returned when a plugin is unavailable.
|
||||||
|
ErrPluginUnavailable = errutil.NewBase(errutil.StatusInternal, "plugin.unavailable")
|
||||||
|
// ErrMethodNotImplemented error returned when a plugin method is not implemented.
|
||||||
|
ErrMethodNotImplemented = errutil.NewBase(errutil.StatusNotImplemented, "plugin.notImplemented")
|
||||||
|
// ErrPluginDownstreamError error returned when a plugin method is not implemented.
|
||||||
|
ErrPluginDownstreamError = errutil.NewBase(errutil.StatusInternal, "plugin.downstreamError", errutil.WithPublicMessage("An error occurred within the plugin"))
|
||||||
|
)
|
@ -28,7 +28,7 @@ func ProvideService(pluginRegistry registry.Service) *Service {
|
|||||||
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID)
|
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID)
|
||||||
if !exists {
|
if !exists {
|
||||||
return nil, backendplugin.ErrPluginNotRegistered
|
return nil, plugins.ErrPluginNotRegistered.Errorf("%w", backendplugin.ErrPluginNotRegistered)
|
||||||
}
|
}
|
||||||
|
|
||||||
var resp *backend.QueryDataResponse
|
var resp *backend.QueryDataResponse
|
||||||
@ -39,14 +39,14 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
|||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
||||||
return nil, err
|
return nil, plugins.ErrMethodNotImplemented.Errorf("%w", backendplugin.ErrMethodNotImplemented)
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
if errors.Is(err, backendplugin.ErrPluginUnavailable) {
|
||||||
return nil, err
|
return nil, plugins.ErrPluginUnavailable.Errorf("%w", backendplugin.ErrPluginUnavailable)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, fmt.Errorf("%v: %w", "failed to query data", err)
|
return nil, plugins.ErrPluginDownstreamError.Errorf("%v: %w", "failed to query data", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for refID, res := range resp.Responses {
|
for refID, res := range resp.Responses {
|
||||||
|
88
pkg/plugins/manager/client/client_test.go
Normal file
88
pkg/plugins/manager/client/client_test.go
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
package client
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestQueryData(t *testing.T) {
|
||||||
|
t.Run("Empty registry should return not registered error", func(t *testing.T) {
|
||||||
|
registry := fakes.NewFakePluginRegistry()
|
||||||
|
client := ProvideService(registry)
|
||||||
|
_, err := client.QueryData(context.Background(), &backend.QueryDataRequest{})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, plugins.ErrPluginNotRegistered)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Non-empty registry", func(t *testing.T) {
|
||||||
|
tcs := []struct {
|
||||||
|
err error
|
||||||
|
expectedError error
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
err: backendplugin.ErrPluginUnavailable,
|
||||||
|
expectedError: plugins.ErrPluginUnavailable,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: backendplugin.ErrMethodNotImplemented,
|
||||||
|
expectedError: plugins.ErrMethodNotImplemented,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
err: errors.New("surprise surprise"),
|
||||||
|
expectedError: plugins.ErrPluginDownstreamError,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tcs {
|
||||||
|
t.Run(fmt.Sprintf("Plugin client error %q should return expected error", tc.err), func(t *testing.T) {
|
||||||
|
registry := fakes.NewFakePluginRegistry()
|
||||||
|
p := &plugins.Plugin{
|
||||||
|
JSONData: plugins.JSONData{
|
||||||
|
ID: "grafana",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.RegisterClient(&fakePluginBackend{
|
||||||
|
qdr: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
return nil, tc.err
|
||||||
|
},
|
||||||
|
})
|
||||||
|
err := registry.Add(context.Background(), p)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
client := ProvideService(registry)
|
||||||
|
_, err = client.QueryData(context.Background(), &backend.QueryDataRequest{
|
||||||
|
PluginContext: backend.PluginContext{
|
||||||
|
PluginID: "grafana",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
require.Error(t, err)
|
||||||
|
require.ErrorIs(t, err, tc.expectedError)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type fakePluginBackend struct {
|
||||||
|
qdr backend.QueryDataHandlerFunc
|
||||||
|
|
||||||
|
backendplugin.Plugin
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginBackend) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||||
|
if f.qdr != nil {
|
||||||
|
return f.qdr(ctx, req)
|
||||||
|
}
|
||||||
|
return backend.NewQueryDataResponse(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakePluginBackend) IsDecommissioned() bool {
|
||||||
|
return false
|
||||||
|
}
|
@ -46,6 +46,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/services/user"
|
"github.com/grafana/grafana/pkg/services/user"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/util"
|
"github.com/grafana/grafana/pkg/util"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
"github.com/grafana/grafana/pkg/web"
|
"github.com/grafana/grafana/pkg/web"
|
||||||
|
|
||||||
"github.com/centrifugal/centrifuge"
|
"github.com/centrifugal/centrifuge"
|
||||||
@ -598,8 +599,8 @@ func (g *GrafanaLive) handleOnRPC(client *centrifuge.Client, e centrifuge.RPCEve
|
|||||||
if errors.Is(err, datasources.ErrDataSourceAccessDenied) {
|
if errors.Is(err, datasources.ErrDataSourceAccessDenied) {
|
||||||
return centrifuge.RPCReply{}, ¢rifuge.Error{Code: uint32(http.StatusForbidden), Message: http.StatusText(http.StatusForbidden)}
|
return centrifuge.RPCReply{}, ¢rifuge.Error{Code: uint32(http.StatusForbidden), Message: http.StatusText(http.StatusForbidden)}
|
||||||
}
|
}
|
||||||
var badQuery *query.ErrBadQuery
|
var gfErr *errutil.Error
|
||||||
if errors.As(err, &badQuery) {
|
if errors.As(err, &gfErr) && gfErr.Reason.Status() == errutil.StatusBadRequest {
|
||||||
return centrifuge.RPCReply{}, ¢rifuge.Error{Code: uint32(http.StatusBadRequest), Message: http.StatusText(http.StatusBadRequest)}
|
return centrifuge.RPCReply{}, ¢rifuge.Error{Code: uint32(http.StatusBadRequest), Message: http.StatusText(http.StatusBadRequest)}
|
||||||
}
|
}
|
||||||
return centrifuge.RPCReply{}, centrifuge.ErrorInternal
|
return centrifuge.RPCReply{}, centrifuge.ErrorInternal
|
||||||
|
@ -1,17 +1,12 @@
|
|||||||
package query
|
package query
|
||||||
|
|
||||||
import "fmt"
|
import (
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
)
|
||||||
|
|
||||||
// ErrBadQuery returned whenever request is malformed and must contain a message
|
var (
|
||||||
// suitable to return in API response.
|
ErrNoQueriesFound = errutil.NewBase(errutil.StatusBadRequest, "query.noQueries", errutil.WithPublicMessage("No queries found")).Errorf("no queries found")
|
||||||
type ErrBadQuery struct {
|
ErrInvalidDatasourceID = errutil.NewBase(errutil.StatusBadRequest, "query.invalidDatasourceId", errutil.WithPublicMessage("Query does not contain a valid data source identifier")).Errorf("invalid data source identifier")
|
||||||
Message string
|
ErrMultipleDatasources = errutil.NewBase(errutil.StatusBadRequest, "query.differentDatasources", errutil.WithPublicMessage("All queries must use the same datasource")).Errorf("all queries must use the same datasource")
|
||||||
}
|
ErrMissingDataSourceInfo = errutil.NewBase(errutil.StatusBadRequest, "query.missingDataSourceInfo").MustTemplate("query missing datasource info: {{ .Public.RefId }}", errutil.WithPublic("Query {{ .Public.RefId }} is missing datasource information"))
|
||||||
|
)
|
||||||
func NewErrBadQuery(msg string) *ErrBadQuery {
|
|
||||||
return &ErrBadQuery{Message: msg}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e ErrBadQuery) Error() string {
|
|
||||||
return fmt.Sprintf("bad query: %s", e.Message)
|
|
||||||
}
|
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/grafanads"
|
"github.com/grafana/grafana/pkg/tsdb/grafanads"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
"github.com/grafana/grafana/pkg/tsdb/legacydata"
|
||||||
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
"github.com/grafana/grafana/pkg/util/proxyutil"
|
"github.com/grafana/grafana/pkg/util/proxyutil"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
@ -117,7 +118,11 @@ func (s *Service) handleExpressions(ctx context.Context, user *user.SignedInUser
|
|||||||
|
|
||||||
for _, pq := range parsedReq.parsedQueries {
|
for _, pq := range parsedReq.parsedQueries {
|
||||||
if pq.datasource == nil {
|
if pq.datasource == nil {
|
||||||
return nil, NewErrBadQuery(fmt.Sprintf("query mising datasource info: %s", pq.query.RefID))
|
return nil, ErrMissingDataSourceInfo.Build(errutil.TemplateData{
|
||||||
|
Public: map[string]interface{}{
|
||||||
|
"RefId": pq.query.RefID,
|
||||||
|
},
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
exprReq.Queries = append(exprReq.Queries, expr.Query{
|
exprReq.Queries = append(exprReq.Queries, expr.Query{
|
||||||
@ -211,7 +216,7 @@ type parsedRequest struct {
|
|||||||
|
|
||||||
func (s *Service) parseMetricRequest(ctx context.Context, user *user.SignedInUser, skipCache bool, reqDTO dtos.MetricRequest) (*parsedRequest, error) {
|
func (s *Service) parseMetricRequest(ctx context.Context, user *user.SignedInUser, skipCache bool, reqDTO dtos.MetricRequest) (*parsedRequest, error) {
|
||||||
if len(reqDTO.Queries) == 0 {
|
if len(reqDTO.Queries) == 0 {
|
||||||
return nil, NewErrBadQuery("no queries found")
|
return nil, ErrNoQueriesFound
|
||||||
}
|
}
|
||||||
|
|
||||||
timeRange := legacydata.NewDataTimeRange(reqDTO.From, reqDTO.To)
|
timeRange := legacydata.NewDataTimeRange(reqDTO.From, reqDTO.To)
|
||||||
@ -228,7 +233,7 @@ func (s *Service) parseMetricRequest(ctx context.Context, user *user.SignedInUse
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if ds == nil {
|
if ds == nil {
|
||||||
return nil, NewErrBadQuery("invalid data source ID")
|
return nil, ErrInvalidDatasourceID
|
||||||
}
|
}
|
||||||
|
|
||||||
datasourcesByUid[ds.Uid] = ds
|
datasourcesByUid[ds.Uid] = ds
|
||||||
@ -262,7 +267,7 @@ func (s *Service) parseMetricRequest(ctx context.Context, user *user.SignedInUse
|
|||||||
if !req.hasExpression {
|
if !req.hasExpression {
|
||||||
if len(datasourcesByUid) > 1 {
|
if len(datasourcesByUid) > 1 {
|
||||||
// We do not (yet) support mixed query type
|
// We do not (yet) support mixed query type
|
||||||
return nil, NewErrBadQuery("all queries must use the same datasource")
|
return nil, ErrMultipleDatasources
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,7 +319,7 @@ func (s *Service) getDataSourceFromQuery(ctx context.Context, user *user.SignedI
|
|||||||
return ds, nil
|
return ds, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil, NewErrBadQuery("missing data source ID/UID")
|
return nil, ErrInvalidDatasourceID
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(ds *datasources.DataSource) (map[string]string, error) {
|
func (s *Service) decryptSecureJsonDataFn(ctx context.Context) func(ds *datasources.DataSource) (map[string]string, error) {
|
||||||
|
@ -166,12 +166,16 @@ func (e Error) Is(other error) bool {
|
|||||||
o, isGrafanaError := other.(Error)
|
o, isGrafanaError := other.(Error)
|
||||||
//nolint:errorlint
|
//nolint:errorlint
|
||||||
base, isBase := other.(Base)
|
base, isBase := other.(Base)
|
||||||
|
//nolint:errorlint
|
||||||
|
templateErr, isTemplateErr := other.(Template)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case isGrafanaError:
|
case isGrafanaError:
|
||||||
return o.Reason == e.Reason && o.MessageID == e.MessageID && o.Error() == e.Error()
|
return o.Reason == e.Reason && o.MessageID == e.MessageID && o.Error() == e.Error()
|
||||||
case isBase:
|
case isBase:
|
||||||
return base.Is(e)
|
return base.Is(e)
|
||||||
|
case isTemplateErr:
|
||||||
|
return templateErr.Base.Is(e)
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
@ -115,3 +115,7 @@ func (t Template) Build(data TemplateData) error {
|
|||||||
|
|
||||||
return e
|
return e
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (t Template) Error() string {
|
||||||
|
return t.Base.Error()
|
||||||
|
}
|
||||||
|
@ -3,10 +3,30 @@ package errutil_test
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"testing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/util/errutil"
|
"github.com/grafana/grafana/pkg/util/errutil"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func TestTemplate(t *testing.T) {
|
||||||
|
tmpl := errutil.NewBase(errutil.StatusInternal, "template.sample-error").MustTemplate("[{{ .Public.user }}] got error: {{ .Error }}")
|
||||||
|
err := tmpl.Build(errutil.TemplateData{
|
||||||
|
Public: map[string]interface{}{
|
||||||
|
"user": "grot the bot",
|
||||||
|
},
|
||||||
|
Error: errors.New("oh noes"),
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Built error should return true when compared with templated error ", func(t *testing.T) {
|
||||||
|
require.True(t, errors.Is(err, tmpl))
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Built error should return true when compared with templated error base ", func(t *testing.T) {
|
||||||
|
require.True(t, errors.Is(err, tmpl.Base))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
func ExampleTemplate() {
|
func ExampleTemplate() {
|
||||||
// Initialization, this is typically done on a package or global
|
// Initialization, this is typically done on a package or global
|
||||||
// level.
|
// level.
|
||||||
|
Reference in New Issue
Block a user