mirror of
https://github.com/grafana/grafana.git
synced 2025-01-27 16:57:14 -06: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:
parent
deb86e3250
commit
29327cbba2
@ -10,11 +10,8 @@ import (
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/api/response"
|
||||
"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/featuremgmt"
|
||||
"github.com/grafana/grafana/pkg/services/query"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"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)
|
||||
}
|
||||
|
||||
var badQuery query.ErrBadQuery
|
||||
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)
|
||||
return response.ErrOrFallback(http.StatusInternalServerError, "Query data error", err)
|
||||
}
|
||||
|
||||
// QueryMetricsV2 returns query metrics.
|
||||
|
@ -4,42 +4,32 @@ import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"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"
|
||||
|
||||
"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"
|
||||
"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/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 {
|
||||
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) {
|
||||
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})
|
||||
resp, err := serverFeatureDisabled.SendJSON(req)
|
||||
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) {
|
||||
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})
|
||||
resp, err := serverFeatureEnabled.SendJSON(req)
|
||||
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) {
|
||||
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})
|
||||
resp, err := httpServer.SendJSON(req)
|
||||
require.NoError(t, err)
|
||||
@ -157,3 +147,168 @@ func TestAPIEndpoint_Metrics_PluginDecryptionFailure(t *testing.T) {
|
||||
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/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/dtos"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/infra/log/logtest"
|
||||
@ -319,11 +320,10 @@ func Test_GetPluginAssets(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestMakePluginResourceRequest(t *testing.T) {
|
||||
pluginClient := &fakePluginClient{}
|
||||
hs := HTTPServer{
|
||||
Cfg: setting.NewCfg(),
|
||||
log: log.New(),
|
||||
pluginClient: pluginClient,
|
||||
pluginClient: &fakePluginClient{},
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodGet, "/", nil)
|
||||
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) {
|
||||
plugin, exists := s.plugin(ctx, req.PluginContext.PluginID)
|
||||
if !exists {
|
||||
return nil, backendplugin.ErrPluginNotRegistered
|
||||
return nil, plugins.ErrPluginNotRegistered.Errorf("%w", backendplugin.ErrPluginNotRegistered)
|
||||
}
|
||||
|
||||
var resp *backend.QueryDataResponse
|
||||
@ -39,14 +39,14 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, backendplugin.ErrMethodNotImplemented) {
|
||||
return nil, err
|
||||
return nil, plugins.ErrMethodNotImplemented.Errorf("%w", backendplugin.ErrMethodNotImplemented)
|
||||
}
|
||||
|
||||
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 {
|
||||
|
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/setting"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
"github.com/grafana/grafana/pkg/web"
|
||||
|
||||
"github.com/centrifugal/centrifuge"
|
||||
@ -598,8 +599,8 @@ func (g *GrafanaLive) handleOnRPC(client *centrifuge.Client, e centrifuge.RPCEve
|
||||
if errors.Is(err, datasources.ErrDataSourceAccessDenied) {
|
||||
return centrifuge.RPCReply{}, ¢rifuge.Error{Code: uint32(http.StatusForbidden), Message: http.StatusText(http.StatusForbidden)}
|
||||
}
|
||||
var badQuery *query.ErrBadQuery
|
||||
if errors.As(err, &badQuery) {
|
||||
var gfErr *errutil.Error
|
||||
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{}, centrifuge.ErrorInternal
|
||||
|
@ -1,17 +1,12 @@
|
||||
package query
|
||||
|
||||
import "fmt"
|
||||
import (
|
||||
"github.com/grafana/grafana/pkg/util/errutil"
|
||||
)
|
||||
|
||||
// ErrBadQuery returned whenever request is malformed and must contain a message
|
||||
// suitable to return in API response.
|
||||
type ErrBadQuery struct {
|
||||
Message string
|
||||
}
|
||||
|
||||
func NewErrBadQuery(msg string) *ErrBadQuery {
|
||||
return &ErrBadQuery{Message: msg}
|
||||
}
|
||||
|
||||
func (e ErrBadQuery) Error() string {
|
||||
return fmt.Sprintf("bad query: %s", e.Message)
|
||||
}
|
||||
var (
|
||||
ErrNoQueriesFound = errutil.NewBase(errutil.StatusBadRequest, "query.noQueries", errutil.WithPublicMessage("No queries found")).Errorf("no queries found")
|
||||
ErrInvalidDatasourceID = errutil.NewBase(errutil.StatusBadRequest, "query.invalidDatasourceId", errutil.WithPublicMessage("Query does not contain a valid data source identifier")).Errorf("invalid data source identifier")
|
||||
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"))
|
||||
)
|
||||
|
@ -21,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb/grafanads"
|
||||
"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-plugin-sdk-go/backend"
|
||||
@ -117,7 +118,11 @@ func (s *Service) handleExpressions(ctx context.Context, user *user.SignedInUser
|
||||
|
||||
for _, pq := range parsedReq.parsedQueries {
|
||||
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{
|
||||
@ -211,7 +216,7 @@ type parsedRequest struct {
|
||||
|
||||
func (s *Service) parseMetricRequest(ctx context.Context, user *user.SignedInUser, skipCache bool, reqDTO dtos.MetricRequest) (*parsedRequest, error) {
|
||||
if len(reqDTO.Queries) == 0 {
|
||||
return nil, NewErrBadQuery("no queries found")
|
||||
return nil, ErrNoQueriesFound
|
||||
}
|
||||
|
||||
timeRange := legacydata.NewDataTimeRange(reqDTO.From, reqDTO.To)
|
||||
@ -228,7 +233,7 @@ func (s *Service) parseMetricRequest(ctx context.Context, user *user.SignedInUse
|
||||
return nil, err
|
||||
}
|
||||
if ds == nil {
|
||||
return nil, NewErrBadQuery("invalid data source ID")
|
||||
return nil, ErrInvalidDatasourceID
|
||||
}
|
||||
|
||||
datasourcesByUid[ds.Uid] = ds
|
||||
@ -262,7 +267,7 @@ func (s *Service) parseMetricRequest(ctx context.Context, user *user.SignedInUse
|
||||
if !req.hasExpression {
|
||||
if len(datasourcesByUid) > 1 {
|
||||
// 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 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) {
|
||||
|
@ -166,12 +166,16 @@ func (e Error) Is(other error) bool {
|
||||
o, isGrafanaError := other.(Error)
|
||||
//nolint:errorlint
|
||||
base, isBase := other.(Base)
|
||||
//nolint:errorlint
|
||||
templateErr, isTemplateErr := other.(Template)
|
||||
|
||||
switch {
|
||||
case isGrafanaError:
|
||||
return o.Reason == e.Reason && o.MessageID == e.MessageID && o.Error() == e.Error()
|
||||
case isBase:
|
||||
return base.Is(e)
|
||||
case isTemplateErr:
|
||||
return templateErr.Base.Is(e)
|
||||
default:
|
||||
return false
|
||||
}
|
||||
|
@ -115,3 +115,7 @@ func (t Template) Build(data TemplateData) error {
|
||||
|
||||
return e
|
||||
}
|
||||
|
||||
func (t Template) Error() string {
|
||||
return t.Base.Error()
|
||||
}
|
||||
|
@ -3,10 +3,30 @@ package errutil_test
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"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() {
|
||||
// Initialization, this is typically done on a package or global
|
||||
// level.
|
||||
|
Loading…
Reference in New Issue
Block a user