mirror of
https://github.com/grafana/grafana.git
synced 2025-07-28 21:12:30 +08:00

* parse via sse I need to figure out how to handle the pipeline.execute with our own client. I think this is important for MT reasons, just like using our own cache (via legacy) is important. parsing is done though! * WIP nonsense * horrible code but i think it works * Add support for sql expressions config settings * Cleanup: - remove spew from nodes.go - uncomment out plugin context and use in single tenant flow - make code more readable and add comments * Cleanup: - create separate file for mt ds client builder - ensure error handling is the same for both expressions and regular queries - other cleanup * not working but good thoughts * WIP, vector not working for non sse * super hacky but i think vectors work now * delete delete delete * Comments for future ref * break out query handling and start test * add prom debugger * clean up: remove comments and commented out bits * fix query_test * add prom debugger * create table-driven tests with testsdata files * Fix test * Add test * go mod?? * idk * Remove comment * go enterprise issue maybe * Fix codeowners * Delete * Remove test data * Clean up * logger * Remove go changes hopefully * idk go man * sad * idk i ran go mod tidy and this is what it wants * Fix readme, with much help from adam * some linting and testing errors * lint * fix lint * fix lint register.go * another lint * address lint in test * fix dead code and linters for query_test * Go mod? * Struggling with go mod * Fix test * Fix another test * Revert headers change * Its difficult to test this in OSS as it depends on functionality defined in enterprise, let's bring these tests back in some form in enterprise * Fix codeowners --------- Co-authored-by: Adam Simpson <adam@adamsimpson.net>
311 lines
8.3 KiB
Go
311 lines
8.3 KiB
Go
package api
|
|
|
|
import (
|
|
"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/apimachinery/errutil"
|
|
"github.com/grafana/grafana/pkg/infra/db/dbtest"
|
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
|
"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/mtdsclient"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
|
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
|
"github.com/grafana/grafana/pkg/services/query"
|
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
|
secretstest "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/setting"
|
|
"github.com/grafana/grafana/pkg/web/webtest"
|
|
)
|
|
|
|
type fakeDataSourceRequestValidator struct {
|
|
err error
|
|
}
|
|
|
|
func (rv *fakeDataSourceRequestValidator) Validate(ds *datasources.DataSource, req *http.Request) error {
|
|
return rv.err
|
|
}
|
|
|
|
// `/ds/query` endpoint test
|
|
func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
|
cfg := setting.NewCfg()
|
|
qds := query.ProvideService(
|
|
cfg,
|
|
nil,
|
|
nil,
|
|
&fakeDataSourceRequestValidator{},
|
|
&fakePluginClient{
|
|
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
resp := backend.Responses{
|
|
"A": backend.DataResponse{
|
|
Error: errors.New("query failed"),
|
|
},
|
|
}
|
|
return &backend.QueryDataResponse{Responses: resp}, nil
|
|
},
|
|
},
|
|
plugincontext.ProvideService(
|
|
cfg,
|
|
localcache.ProvideService(),
|
|
&pluginstore.FakePluginStore{
|
|
PluginList: []pluginstore.Plugin{
|
|
{
|
|
JSONData: plugins.JSONData{
|
|
ID: "grafana",
|
|
},
|
|
},
|
|
},
|
|
},
|
|
&fakeDatasources.FakeCacheService{},
|
|
&fakeDatasources.FakeDataSourceService{},
|
|
pluginSettings.ProvideService(
|
|
dbtest.NewFakeDB(),
|
|
secretstest.NewFakeSecretsService(),
|
|
),
|
|
pluginconfig.NewFakePluginRequestConfigProvider(),
|
|
),
|
|
mtdsclient.NewNullMTDatasourceClientBuilder(),
|
|
)
|
|
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
|
hs.queryDataService = qds
|
|
hs.QuotaService = quotatest.New(false, nil)
|
|
})
|
|
|
|
t.Run("Status code is 400 when data source response has an error", func(t *testing.T) {
|
|
req := server.NewPostRequest("/api/ds/query", strings.NewReader(reqValid))
|
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {datasources.ActionQuery: []string{datasources.ScopeAll}}}})
|
|
resp, err := server.SendJSON(req)
|
|
require.NoError(t, err)
|
|
require.NoError(t, resp.Body.Close())
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
})
|
|
}
|
|
|
|
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) {
|
|
type body struct {
|
|
Message string `json:"message"`
|
|
MessageId string `json:"messageId"`
|
|
StatusCode int `json:"statusCode"`
|
|
}
|
|
|
|
tcs := []struct {
|
|
request string
|
|
clientErr error
|
|
expectedStatus int
|
|
expectedBody body
|
|
}{
|
|
{
|
|
request: reqValid,
|
|
clientErr: plugins.ErrPluginUnavailable,
|
|
expectedStatus: http.StatusInternalServerError,
|
|
expectedBody: body{
|
|
Message: "Plugin unavailable",
|
|
MessageId: "plugin.unavailable",
|
|
StatusCode: 500,
|
|
},
|
|
},
|
|
{
|
|
request: reqValid,
|
|
clientErr: plugins.ErrMethodNotImplemented,
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedBody: body{
|
|
Message: "Method not implemented",
|
|
MessageId: "plugin.notImplemented",
|
|
StatusCode: 404,
|
|
},
|
|
},
|
|
{
|
|
request: reqValid,
|
|
clientErr: errors.New("surprise surprise"),
|
|
expectedStatus: errutil.StatusInternal.HTTPStatus(),
|
|
expectedBody: body{
|
|
Message: "An error occurred within the plugin",
|
|
MessageId: "plugin.requestFailureError",
|
|
StatusCode: 500,
|
|
},
|
|
},
|
|
{
|
|
request: reqNoQueries,
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedBody: body{
|
|
Message: "No queries found",
|
|
MessageId: "query.noQueries",
|
|
StatusCode: 400,
|
|
},
|
|
},
|
|
{
|
|
request: reqQueryWithInvalidDatasourceID,
|
|
expectedStatus: http.StatusBadRequest,
|
|
expectedBody: body{
|
|
Message: "Query does not contain a valid data source identifier",
|
|
MessageId: "query.invalidDatasourceId",
|
|
StatusCode: 400,
|
|
},
|
|
},
|
|
{
|
|
request: reqDatasourceByUidNotFound,
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedBody: body{
|
|
Message: "Data source not found",
|
|
},
|
|
},
|
|
{
|
|
request: reqDatasourceByIdNotFound,
|
|
expectedStatus: http.StatusNotFound,
|
|
expectedBody: body{
|
|
Message: "Data source not found",
|
|
},
|
|
},
|
|
}
|
|
|
|
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) {
|
|
cfg := setting.NewCfg()
|
|
r := registry.NewInMemory()
|
|
err := r.Add(context.Background(), p)
|
|
require.NoError(t, err)
|
|
ds := &fakeDatasources.FakeDataSourceService{}
|
|
hs.queryDataService = query.ProvideService(
|
|
cfg,
|
|
&fakeDatasources.FakeCacheService{},
|
|
nil,
|
|
&fakeDataSourceRequestValidator{},
|
|
pluginClient.ProvideService(r),
|
|
plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{
|
|
PluginList: []pluginstore.Plugin{pluginstore.ToGrafanaDTO(p)},
|
|
},
|
|
&fakeDatasources.FakeCacheService{}, ds,
|
|
pluginSettings.ProvideService(dbtest.NewFakeDB(),
|
|
secretstest.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider()),
|
|
mtdsclient.NewNullMTDatasourceClientBuilder(),
|
|
)
|
|
hs.QuotaService = quotatest.New(false, nil)
|
|
})
|
|
req := srv.NewPostRequest("/api/ds/query", strings.NewReader(tc.request))
|
|
|
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {datasources.ActionQuery: []string{datasources.ScopeAll}}}})
|
|
resp, err := srv.SendJSON(req)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc.expectedStatus, resp.StatusCode)
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
var responseBody body
|
|
err = json.Unmarshal(bodyBytes, &responseBody)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, tc.expectedBody.Message, responseBody.Message)
|
|
require.Equal(t, tc.expectedBody.MessageId, responseBody.MessageId)
|
|
require.Equal(t, tc.expectedBody.StatusCode, responseBody.StatusCode)
|
|
|
|
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
|
|
}
|