2022-03-08 02:33:01 +08:00
|
|
|
package api
|
|
|
|
|
|
|
|
import (
|
|
|
|
"context"
|
2022-07-13 21:27:03 +08:00
|
|
|
"encoding/json"
|
2022-09-15 00:19:57 +08:00
|
|
|
"errors"
|
2022-03-08 02:33:01 +08:00
|
|
|
"fmt"
|
2022-09-15 00:19:57 +08:00
|
|
|
"io"
|
2022-03-08 02:33:01 +08:00
|
|
|
"net/http"
|
|
|
|
"strings"
|
|
|
|
"testing"
|
|
|
|
|
2022-05-18 02:52:22 +08:00
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
2022-06-28 00:23:15 +08:00
|
|
|
"github.com/stretchr/testify/require"
|
|
|
|
|
2024-06-13 12:11:35 +08:00
|
|
|
"github.com/grafana/grafana/pkg/apimachinery/errutil"
|
2023-06-08 19:59:51 +08:00
|
|
|
"github.com/grafana/grafana/pkg/infra/db/dbtest"
|
|
|
|
"github.com/grafana/grafana/pkg/infra/localcache"
|
2022-09-15 00:19:57 +08:00
|
|
|
"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"
|
2023-05-23 22:29:20 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
2022-09-15 00:19:57 +08:00
|
|
|
fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes"
|
2025-08-19 18:37:56 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/dsquerierclient"
|
2024-02-27 19:38:02 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginconfig"
|
2023-06-08 19:59:51 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/plugincontext"
|
|
|
|
pluginSettings "github.com/grafana/grafana/pkg/services/pluginsintegration/pluginsettings/service"
|
2023-09-11 19:59:24 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/pluginsintegration/pluginstore"
|
2022-09-15 00:19:57 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/query"
|
2022-07-16 00:06:44 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/quota/quotatest"
|
2023-06-08 19:59:51 +08:00
|
|
|
secretstest "github.com/grafana/grafana/pkg/services/secrets/fakes"
|
2022-08-10 17:56:48 +08:00
|
|
|
"github.com/grafana/grafana/pkg/services/user"
|
2022-10-21 19:54:55 +08:00
|
|
|
"github.com/grafana/grafana/pkg/setting"
|
2022-06-14 07:23:56 +08:00
|
|
|
"github.com/grafana/grafana/pkg/web/webtest"
|
2022-03-08 02:33:01 +08:00
|
|
|
)
|
|
|
|
|
2025-01-24 23:01:46 +08:00
|
|
|
type fakeDataSourceRequestValidator struct {
|
2022-03-08 02:33:01 +08:00
|
|
|
err error
|
|
|
|
}
|
|
|
|
|
2025-01-24 23:01:46 +08:00
|
|
|
func (rv *fakeDataSourceRequestValidator) Validate(ds *datasources.DataSource, req *http.Request) error {
|
2022-03-08 02:33:01 +08:00
|
|
|
return rv.err
|
|
|
|
}
|
|
|
|
|
2022-05-04 00:02:20 +08:00
|
|
|
// `/ds/query` endpoint test
|
|
|
|
func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) {
|
2023-09-21 17:33:31 +08:00
|
|
|
cfg := setting.NewCfg()
|
2022-05-04 00:02:20 +08:00
|
|
|
qds := query.ProvideService(
|
2023-09-21 17:33:31 +08:00
|
|
|
cfg,
|
2022-05-04 00:02:20 +08:00
|
|
|
nil,
|
|
|
|
nil,
|
2025-01-24 23:01:46 +08:00
|
|
|
&fakeDataSourceRequestValidator{},
|
2022-05-04 00:02:20 +08:00
|
|
|
&fakePluginClient{
|
|
|
|
QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
|
|
resp := backend.Responses{
|
|
|
|
"A": backend.DataResponse{
|
2022-12-02 03:51:12 +08:00
|
|
|
Error: errors.New("query failed"),
|
2022-05-04 00:02:20 +08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
return &backend.QueryDataResponse{Responses: resp}, nil
|
|
|
|
},
|
|
|
|
},
|
2025-07-18 05:22:55 +08:00
|
|
|
plugincontext.ProvideService(
|
|
|
|
cfg,
|
|
|
|
localcache.ProvideService(),
|
|
|
|
&pluginstore.FakePluginStore{
|
|
|
|
PluginList: []pluginstore.Plugin{
|
|
|
|
{
|
|
|
|
JSONData: plugins.JSONData{
|
|
|
|
ID: "grafana",
|
|
|
|
},
|
2023-06-08 19:59:51 +08:00
|
|
|
},
|
|
|
|
},
|
|
|
|
},
|
2025-07-18 05:22:55 +08:00
|
|
|
&fakeDatasources.FakeCacheService{},
|
|
|
|
&fakeDatasources.FakeDataSourceService{},
|
|
|
|
pluginSettings.ProvideService(
|
|
|
|
dbtest.NewFakeDB(),
|
|
|
|
secretstest.NewFakeSecretsService(),
|
|
|
|
),
|
|
|
|
pluginconfig.NewFakePluginRequestConfigProvider(),
|
|
|
|
),
|
2025-08-19 18:37:56 +08:00
|
|
|
dsquerierclient.NewNullQSDatasourceClientBuilder(),
|
2022-05-04 00:02:20 +08:00
|
|
|
)
|
2024-07-10 17:15:10 +08:00
|
|
|
server := SetupAPITestServer(t, func(hs *HTTPServer) {
|
2022-05-04 00:02:20 +08:00
|
|
|
hs.queryDataService = qds
|
2022-11-15 03:08:10 +08:00
|
|
|
hs.QuotaService = quotatest.New(false, nil)
|
2022-05-04 00:02:20 +08:00
|
|
|
})
|
|
|
|
|
2024-07-10 17:15:10 +08:00
|
|
|
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))
|
2023-05-23 22:29:20 +08:00
|
|
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {datasources.ActionQuery: []string{datasources.ScopeAll}}}})
|
2024-07-10 17:15:10 +08:00
|
|
|
resp, err := server.SendJSON(req)
|
2022-05-04 00:02:20 +08:00
|
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, resp.Body.Close())
|
|
|
|
require.Equal(t, http.StatusBadRequest, resp.StatusCode)
|
|
|
|
})
|
|
|
|
}
|
2022-07-13 21:27:03 +08:00
|
|
|
|
2022-09-15 00:19:57 +08:00
|
|
|
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) {
|
2024-08-17 06:08:19 +08:00
|
|
|
type body struct {
|
|
|
|
Message string `json:"message"`
|
|
|
|
MessageId string `json:"messageId"`
|
|
|
|
StatusCode int `json:"statusCode"`
|
|
|
|
}
|
|
|
|
|
2022-09-15 00:19:57 +08:00
|
|
|
tcs := []struct {
|
|
|
|
request string
|
|
|
|
clientErr error
|
|
|
|
expectedStatus int
|
2024-08-17 06:08:19 +08:00
|
|
|
expectedBody body
|
2022-09-15 00:19:57 +08:00
|
|
|
}{
|
|
|
|
{
|
|
|
|
request: reqValid,
|
2023-09-25 17:56:03 +08:00
|
|
|
clientErr: plugins.ErrPluginUnavailable,
|
2022-09-15 00:19:57 +08:00
|
|
|
expectedStatus: http.StatusInternalServerError,
|
2024-08-17 06:08:19 +08:00
|
|
|
expectedBody: body{
|
|
|
|
Message: "Plugin unavailable",
|
|
|
|
MessageId: "plugin.unavailable",
|
|
|
|
StatusCode: 500,
|
|
|
|
},
|
2022-09-15 00:19:57 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
request: reqValid,
|
2023-09-25 17:56:03 +08:00
|
|
|
clientErr: plugins.ErrMethodNotImplemented,
|
|
|
|
expectedStatus: http.StatusNotFound,
|
2024-08-17 06:08:19 +08:00
|
|
|
expectedBody: body{
|
|
|
|
Message: "Method not implemented",
|
|
|
|
MessageId: "plugin.notImplemented",
|
|
|
|
StatusCode: 404,
|
|
|
|
},
|
2022-09-15 00:19:57 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
request: reqValid,
|
|
|
|
clientErr: errors.New("surprise surprise"),
|
|
|
|
expectedStatus: errutil.StatusInternal.HTTPStatus(),
|
2024-08-17 06:08:19 +08:00
|
|
|
expectedBody: body{
|
|
|
|
Message: "An error occurred within the plugin",
|
2025-02-20 17:23:53 +08:00
|
|
|
MessageId: "plugin.requestFailureError",
|
2024-08-17 06:08:19 +08:00
|
|
|
StatusCode: 500,
|
|
|
|
},
|
2022-09-15 00:19:57 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
request: reqNoQueries,
|
|
|
|
expectedStatus: http.StatusBadRequest,
|
2024-08-17 06:08:19 +08:00
|
|
|
expectedBody: body{
|
|
|
|
Message: "No queries found",
|
|
|
|
MessageId: "query.noQueries",
|
|
|
|
StatusCode: 400,
|
|
|
|
},
|
2022-09-15 00:19:57 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
request: reqQueryWithInvalidDatasourceID,
|
|
|
|
expectedStatus: http.StatusBadRequest,
|
2024-08-17 06:08:19 +08:00
|
|
|
expectedBody: body{
|
|
|
|
Message: "Query does not contain a valid data source identifier",
|
|
|
|
MessageId: "query.invalidDatasourceId",
|
|
|
|
StatusCode: 400,
|
|
|
|
},
|
2022-09-15 00:19:57 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
request: reqDatasourceByUidNotFound,
|
|
|
|
expectedStatus: http.StatusNotFound,
|
2024-08-17 06:08:19 +08:00
|
|
|
expectedBody: body{
|
|
|
|
Message: "Data source not found",
|
|
|
|
},
|
2022-09-15 00:19:57 +08:00
|
|
|
},
|
|
|
|
{
|
|
|
|
request: reqDatasourceByIdNotFound,
|
|
|
|
expectedStatus: http.StatusNotFound,
|
2024-08-17 06:08:19 +08:00
|
|
|
expectedBody: body{
|
|
|
|
Message: "Data source not found",
|
|
|
|
},
|
2022-09-15 00:19:57 +08:00
|
|
|
},
|
|
|
|
}
|
|
|
|
|
|
|
|
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) {
|
2023-09-21 17:33:31 +08:00
|
|
|
cfg := setting.NewCfg()
|
2022-09-15 00:19:57 +08:00
|
|
|
r := registry.NewInMemory()
|
|
|
|
err := r.Add(context.Background(), p)
|
|
|
|
require.NoError(t, err)
|
2023-06-08 19:59:51 +08:00
|
|
|
ds := &fakeDatasources.FakeDataSourceService{}
|
2022-09-15 00:19:57 +08:00
|
|
|
hs.queryDataService = query.ProvideService(
|
2023-09-21 17:33:31 +08:00
|
|
|
cfg,
|
2022-09-15 00:19:57 +08:00
|
|
|
&fakeDatasources.FakeCacheService{},
|
|
|
|
nil,
|
2025-01-24 23:01:46 +08:00
|
|
|
&fakeDataSourceRequestValidator{},
|
2024-03-11 23:28:46 +08:00
|
|
|
pluginClient.ProvideService(r),
|
2023-09-21 17:33:31 +08:00
|
|
|
plugincontext.ProvideService(cfg, localcache.ProvideService(), &pluginstore.FakePluginStore{
|
2023-09-11 19:59:24 +08:00
|
|
|
PluginList: []pluginstore.Plugin{pluginstore.ToGrafanaDTO(p)},
|
2023-06-08 19:59:51 +08:00
|
|
|
},
|
2024-01-19 22:56:52 +08:00
|
|
|
&fakeDatasources.FakeCacheService{}, ds,
|
|
|
|
pluginSettings.ProvideService(dbtest.NewFakeDB(),
|
2024-02-27 19:38:02 +08:00
|
|
|
secretstest.NewFakeSecretsService()), pluginconfig.NewFakePluginRequestConfigProvider()),
|
2025-08-19 18:37:56 +08:00
|
|
|
dsquerierclient.NewNullQSDatasourceClientBuilder(),
|
2022-09-15 00:19:57 +08:00
|
|
|
)
|
2022-11-15 03:08:10 +08:00
|
|
|
hs.QuotaService = quotatest.New(false, nil)
|
2022-09-15 00:19:57 +08:00
|
|
|
})
|
|
|
|
req := srv.NewPostRequest("/api/ds/query", strings.NewReader(tc.request))
|
2024-08-17 06:08:19 +08:00
|
|
|
|
2023-05-23 22:29:20 +08:00
|
|
|
webtest.RequestWithSignedInUser(req, &user.SignedInUser{UserID: 1, OrgID: 1, Permissions: map[int64]map[string][]string{1: {datasources.ActionQuery: []string{datasources.ScopeAll}}}})
|
2022-09-15 00:19:57 +08:00
|
|
|
resp, err := srv.SendJSON(req)
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
|
|
|
require.Equal(t, tc.expectedStatus, resp.StatusCode)
|
2024-08-17 06:08:19 +08:00
|
|
|
|
|
|
|
bodyBytes, err := io.ReadAll(resp.Body)
|
2022-09-15 00:19:57 +08:00
|
|
|
require.NoError(t, err)
|
2024-08-17 06:08:19 +08:00
|
|
|
|
|
|
|
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)
|
|
|
|
|
2022-09-15 00:19:57 +08:00
|
|
|
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
|
|
|
|
}
|