mirror of
https://github.com/grafana/grafana.git
synced 2025-07-29 11:22:21 +08:00

Automatically forward core plugin request HTTP headers in outgoing HTTP requests. Core datasource plugin authors don't have to specifically handle forwarding of HTTP headers, e.g. do not have to "hardcode" the header-names in the datasource plugin, if not having custom needs. Fixes #57065
701 lines
22 KiB
Go
701 lines
22 KiB
Go
package backendplugin
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
|
"github.com/grafana/grafana/pkg/api/dtos"
|
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
|
"github.com/grafana/grafana/pkg/infra/log"
|
|
"github.com/grafana/grafana/pkg/plugins"
|
|
"github.com/grafana/grafana/pkg/plugins/backendplugin"
|
|
"github.com/grafana/grafana/pkg/server"
|
|
"github.com/grafana/grafana/pkg/services/datasources"
|
|
"github.com/grafana/grafana/pkg/services/org"
|
|
"github.com/grafana/grafana/pkg/services/user"
|
|
"github.com/grafana/grafana/pkg/tests/testinfra"
|
|
"github.com/stretchr/testify/require"
|
|
"golang.org/x/oauth2"
|
|
)
|
|
|
|
const loginCookieName = "grafana_session"
|
|
|
|
func TestIntegrationBackendPlugins(t *testing.T) {
|
|
if testing.Short() {
|
|
t.Skip("skipping integration test")
|
|
}
|
|
|
|
oauthToken := &oauth2.Token{
|
|
TokenType: "bearer",
|
|
AccessToken: "access-token",
|
|
RefreshToken: "refresh-token",
|
|
Expiry: time.Now().UTC().Add(24 * time.Hour),
|
|
}
|
|
oauthToken = oauthToken.WithExtra(map[string]interface{}{"id_token": "id-token"})
|
|
|
|
newTestScenario(t, "Datasource with no custom HTTP settings",
|
|
options(
|
|
withIncomingRequest(func(req *http.Request) {
|
|
req.Header.Set("X-Custom", "custom")
|
|
req.AddCookie(&http.Cookie{Name: "cookie1"})
|
|
req.AddCookie(&http.Cookie{Name: "cookie2"})
|
|
req.AddCookie(&http.Cookie{Name: "cookie3"})
|
|
req.AddCookie(&http.Cookie{Name: loginCookieName})
|
|
}),
|
|
),
|
|
func(t *testing.T, tsCtx *testScenarioContext) {
|
|
verify := func(h backend.ForwardHTTPHeaders) {
|
|
require.NotNil(t, h)
|
|
require.Empty(t, h.GetHTTPHeader(backend.CookiesHeaderName))
|
|
require.Empty(t, h.GetHTTPHeader("Authorization"))
|
|
|
|
require.NotNil(t, tsCtx.outgoingRequest)
|
|
require.NotEmpty(t, tsCtx.outgoingRequest.Header)
|
|
}
|
|
|
|
tsCtx.runCheckHealthTest(t, func(pReq *backend.CheckHealthRequest) {
|
|
verify(pReq)
|
|
})
|
|
|
|
tsCtx.runCallResourceTest(t, func(pReq *backend.CallResourceRequest) {
|
|
verify(pReq)
|
|
require.Equal(t, "custom", pReq.GetHTTPHeader("X-Custom"))
|
|
require.Equal(t, "custom", tsCtx.outgoingRequest.Header.Get("X-Custom"))
|
|
})
|
|
|
|
verifyQueryData := func(pReq *backend.QueryDataRequest) {
|
|
verify(pReq)
|
|
}
|
|
|
|
t.Run("regular query", func(t *testing.T) {
|
|
tsCtx.runQueryDataTest(t, createRegularQuery(t, tsCtx), verifyQueryData)
|
|
|
|
t.Run("expression query", func(t *testing.T) {
|
|
tsCtx.runQueryDataTest(t, createExpressionQuery(t, tsCtx), verifyQueryData)
|
|
})
|
|
})
|
|
})
|
|
|
|
newTestScenario(t, "Datasource with most HTTP settings set except oauthPassThru and oauth token available",
|
|
options(
|
|
withIncomingRequest(func(req *http.Request) {
|
|
req.AddCookie(&http.Cookie{Name: "cookie1"})
|
|
req.AddCookie(&http.Cookie{Name: "cookie2"})
|
|
req.AddCookie(&http.Cookie{Name: "cookie3"})
|
|
req.AddCookie(&http.Cookie{Name: loginCookieName})
|
|
}),
|
|
withOAuthToken(oauthToken),
|
|
withDsBasicAuth("basicAuthUser", "basicAuthPassword"),
|
|
withDsCustomHeader(map[string]string{"X-CUSTOM-HEADER": "custom-header-value"}),
|
|
withDsCookieForwarding([]string{"cookie1", "cookie3", loginCookieName}),
|
|
),
|
|
func(t *testing.T, tsCtx *testScenarioContext) {
|
|
verify := func(h backend.ForwardHTTPHeaders) {
|
|
require.NotNil(t, h)
|
|
require.Equal(t, "cookie1=; cookie3=", h.GetHTTPHeader(backend.CookiesHeaderName))
|
|
require.Empty(t, h.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName))
|
|
require.Empty(t, h.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName))
|
|
|
|
require.NotNil(t, tsCtx.outgoingRequest)
|
|
require.Equal(t, "cookie1=; cookie3=", tsCtx.outgoingRequest.Header.Get(backend.CookiesHeaderName))
|
|
require.Equal(t, "custom-header-value", tsCtx.outgoingRequest.Header.Get("X-CUSTOM-HEADER"))
|
|
|
|
username, pwd, ok := tsCtx.outgoingRequest.BasicAuth()
|
|
require.True(t, ok)
|
|
require.Equal(t, "basicAuthUser", username)
|
|
require.Equal(t, "basicAuthPassword", pwd)
|
|
}
|
|
|
|
tsCtx.runCheckHealthTest(t, func(pReq *backend.CheckHealthRequest) {
|
|
verify(pReq)
|
|
})
|
|
|
|
tsCtx.runCallResourceTest(t, func(pReq *backend.CallResourceRequest) {
|
|
verify(pReq)
|
|
})
|
|
|
|
verifyQueryData := func(pReq *backend.QueryDataRequest) {
|
|
verify(pReq)
|
|
}
|
|
|
|
t.Run("regular query", func(t *testing.T) {
|
|
tsCtx.runQueryDataTest(t, createRegularQuery(t, tsCtx), verifyQueryData)
|
|
|
|
t.Run("expression query", func(t *testing.T) {
|
|
tsCtx.runQueryDataTest(t, createExpressionQuery(t, tsCtx), verifyQueryData)
|
|
})
|
|
})
|
|
})
|
|
|
|
newTestScenario(t, "Datasource with oauthPassThru and basic auth configured and oauth token available",
|
|
options(
|
|
withOAuthToken(oauthToken),
|
|
withDsOAuthForwarding(),
|
|
withDsBasicAuth("basicAuthUser", "basicAuthPassword"),
|
|
),
|
|
func(t *testing.T, tsCtx *testScenarioContext) {
|
|
verify := func(h backend.ForwardHTTPHeaders) {
|
|
require.NotNil(t, h)
|
|
|
|
expectedAuthHeader := fmt.Sprintf("Bearer %s", oauthToken.AccessToken)
|
|
expectedTokenHeader := oauthToken.Extra("id_token").(string)
|
|
|
|
require.Equal(t, expectedAuthHeader, h.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName))
|
|
require.Equal(t, expectedTokenHeader, h.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName))
|
|
|
|
require.NotNil(t, tsCtx.outgoingRequest)
|
|
require.Equal(t, expectedAuthHeader, tsCtx.outgoingRequest.Header.Get(backend.OAuthIdentityTokenHeaderName))
|
|
require.Equal(t, expectedTokenHeader, tsCtx.outgoingRequest.Header.Get(backend.OAuthIdentityIDTokenHeaderName))
|
|
}
|
|
|
|
tsCtx.runCheckHealthTest(t, func(pReq *backend.CheckHealthRequest) {
|
|
verify(pReq)
|
|
})
|
|
|
|
tsCtx.runCallResourceTest(t, func(pReq *backend.CallResourceRequest) {
|
|
verify(pReq)
|
|
})
|
|
|
|
verifyQueryData := func(pReq *backend.QueryDataRequest) {
|
|
verify(pReq)
|
|
}
|
|
|
|
t.Run("regular query", func(t *testing.T) {
|
|
tsCtx.runQueryDataTest(t, createRegularQuery(t, tsCtx), verifyQueryData)
|
|
|
|
t.Run("expression query", func(t *testing.T) {
|
|
tsCtx.runQueryDataTest(t, createExpressionQuery(t, tsCtx), verifyQueryData)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
type testScenarioContext struct {
|
|
testPluginID string
|
|
uid string
|
|
grafanaListeningAddr string
|
|
testEnv *server.TestEnv
|
|
outgoingServer *httptest.Server
|
|
outgoingRequest *http.Request
|
|
backendTestPlugin *testPlugin
|
|
rt http.RoundTripper
|
|
modifyIncomingRequest func(req *http.Request)
|
|
}
|
|
|
|
type testScenarioInput struct {
|
|
ds *datasources.AddDataSourceCommand
|
|
token *oauth2.Token
|
|
modifyIncomingRequest func(req *http.Request)
|
|
}
|
|
|
|
type testScenarioOption func(*testScenarioInput)
|
|
|
|
func options(opts ...testScenarioOption) []testScenarioOption {
|
|
return opts
|
|
}
|
|
|
|
func withIncomingRequest(cb func(req *http.Request)) testScenarioOption {
|
|
return func(in *testScenarioInput) {
|
|
in.modifyIncomingRequest = cb
|
|
}
|
|
}
|
|
|
|
func withOAuthToken(token *oauth2.Token) testScenarioOption {
|
|
return func(in *testScenarioInput) {
|
|
in.token = token
|
|
}
|
|
}
|
|
|
|
func withDsBasicAuth(username, password string) testScenarioOption {
|
|
return func(in *testScenarioInput) {
|
|
in.ds.BasicAuth = true
|
|
in.ds.BasicAuthUser = username
|
|
in.ds.SecureJsonData["basicAuthPassword"] = password
|
|
}
|
|
}
|
|
|
|
func withDsCustomHeader(headers map[string]string) testScenarioOption {
|
|
return func(in *testScenarioInput) {
|
|
index := 1
|
|
for k, v := range headers {
|
|
in.ds.JsonData.Set(fmt.Sprintf("httpHeaderName%d", index), k)
|
|
in.ds.SecureJsonData[fmt.Sprintf("httpHeaderValue%d", index)] = v
|
|
index++
|
|
}
|
|
}
|
|
}
|
|
|
|
func withDsOAuthForwarding() testScenarioOption {
|
|
return func(in *testScenarioInput) {
|
|
in.ds.JsonData.Set("oauthPassThru", true)
|
|
}
|
|
}
|
|
|
|
func withDsCookieForwarding(names []string) testScenarioOption {
|
|
return func(in *testScenarioInput) {
|
|
in.ds.JsonData.Set("keepCookies", names)
|
|
}
|
|
}
|
|
|
|
func newTestScenario(t *testing.T, name string, opts []testScenarioOption, callback func(t *testing.T, ctx *testScenarioContext)) {
|
|
tsCtx := testScenarioContext{
|
|
testPluginID: "test-plugin",
|
|
}
|
|
|
|
dir, path := testinfra.CreateGrafDir(t, testinfra.GrafanaOpts{
|
|
DisableAnonymous: true,
|
|
// EnableLog: true,
|
|
})
|
|
|
|
grafanaListeningAddr, testEnv := testinfra.StartGrafanaEnv(t, dir, path)
|
|
tsCtx.grafanaListeningAddr = grafanaListeningAddr
|
|
testEnv.SQLStore.Cfg.LoginCookieName = loginCookieName
|
|
tsCtx.testEnv = testEnv
|
|
ctx := context.Background()
|
|
|
|
testinfra.CreateUser(t, testEnv.SQLStore, user.CreateUserCommand{
|
|
DefaultOrgRole: string(org.RoleAdmin),
|
|
Password: "admin",
|
|
Login: "admin",
|
|
})
|
|
|
|
tsCtx.outgoingServer = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
tsCtx.outgoingRequest = r
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
}))
|
|
t.Cleanup(tsCtx.outgoingServer.Close)
|
|
|
|
testPlugin, backendTestPlugin := createTestPlugin(tsCtx.testPluginID)
|
|
tsCtx.backendTestPlugin = backendTestPlugin
|
|
err := testEnv.PluginRegistry.Add(ctx, testPlugin)
|
|
require.NoError(t, err)
|
|
|
|
jsonData := simplejson.New()
|
|
secureJSONData := map[string]string{}
|
|
|
|
tsCtx.uid = "test-plugin"
|
|
cmd := &datasources.AddDataSourceCommand{
|
|
OrgId: 1,
|
|
Access: datasources.DS_ACCESS_PROXY,
|
|
Name: "TestPlugin",
|
|
Type: tsCtx.testPluginID,
|
|
Uid: tsCtx.uid,
|
|
Url: tsCtx.outgoingServer.URL,
|
|
JsonData: jsonData,
|
|
SecureJsonData: secureJSONData,
|
|
}
|
|
|
|
in := &testScenarioInput{ds: cmd}
|
|
for _, opt := range opts {
|
|
opt(in)
|
|
}
|
|
|
|
tsCtx.modifyIncomingRequest = in.modifyIncomingRequest
|
|
tsCtx.testEnv.OAuthTokenService.Token = in.token
|
|
|
|
err = testEnv.Server.HTTPServer.DataSourcesService.AddDataSource(ctx, cmd)
|
|
require.NoError(t, err)
|
|
|
|
getDataSourceQuery := &datasources.GetDataSourceQuery{
|
|
OrgId: 1,
|
|
Uid: tsCtx.uid,
|
|
}
|
|
err = testEnv.Server.HTTPServer.DataSourcesService.GetDataSource(ctx, getDataSourceQuery)
|
|
require.NoError(t, err)
|
|
|
|
rt, err := testEnv.Server.HTTPServer.DataSourcesService.GetHTTPTransport(ctx, getDataSourceQuery.Result, testEnv.HTTPClientProvider)
|
|
require.NoError(t, err)
|
|
|
|
tsCtx.rt = rt
|
|
|
|
t.Run(name, func(t *testing.T) {
|
|
callback(t, &tsCtx)
|
|
})
|
|
}
|
|
|
|
func createRegularQuery(t *testing.T, tsCtx *testScenarioContext) dtos.MetricRequest {
|
|
t.Helper()
|
|
|
|
return metricRequestWithQueries(t, fmt.Sprintf(`{
|
|
"datasource": {
|
|
"uid": "%s"
|
|
}
|
|
}`, tsCtx.uid))
|
|
}
|
|
|
|
func createExpressionQuery(t *testing.T, tsCtx *testScenarioContext) dtos.MetricRequest {
|
|
t.Helper()
|
|
|
|
return metricRequestWithQueries(t, fmt.Sprintf(`{
|
|
"refId": "A",
|
|
"datasource": {
|
|
"uid": "%s",
|
|
"type": "%s"
|
|
}
|
|
}`, tsCtx.uid, tsCtx.testPluginID), `{
|
|
"refId": "B",
|
|
"datasource": {
|
|
"type": "__expr__",
|
|
"uid": "__expr__",
|
|
"name": "Expression"
|
|
},
|
|
"type": "math",
|
|
"expression": "$A - 50"
|
|
}`)
|
|
}
|
|
|
|
func (tsCtx *testScenarioContext) runQueryDataTest(t *testing.T, mr dtos.MetricRequest, callback func(req *backend.QueryDataRequest)) {
|
|
t.Run("When calling /api/ds/query should set expected headers on outgoing QueryData and HTTP request", func(t *testing.T) {
|
|
var received *backend.QueryDataRequest
|
|
tsCtx.backendTestPlugin.QueryDataHandler = backend.QueryDataHandlerFunc(func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
received = req
|
|
|
|
c := http.Client{
|
|
Transport: tsCtx.rt,
|
|
}
|
|
outReq, err := http.NewRequestWithContext(ctx, http.MethodGet, tsCtx.outgoingServer.URL, nil)
|
|
require.NoError(t, err)
|
|
resp, err := c.Do(outReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
tsCtx.testEnv.Server.HTTPServer.Cfg.Logger.Error("Failed to close body", "error", err)
|
|
}
|
|
}()
|
|
|
|
_, err = io.Copy(io.Discard, resp.Body)
|
|
if err != nil {
|
|
tsCtx.testEnv.Server.HTTPServer.Cfg.Logger.Error("Failed to discard body", "error", err)
|
|
}
|
|
|
|
return &backend.QueryDataResponse{}, nil
|
|
})
|
|
|
|
buf1 := &bytes.Buffer{}
|
|
err := json.NewEncoder(buf1).Encode(mr)
|
|
require.NoError(t, err)
|
|
u := fmt.Sprintf("http://admin:admin@%s/api/ds/query", tsCtx.grafanaListeningAddr)
|
|
|
|
req, err := http.NewRequest(http.MethodPost, u, buf1)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36")
|
|
|
|
if tsCtx.modifyIncomingRequest != nil {
|
|
tsCtx.modifyIncomingRequest(req)
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(b))
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
_, err = io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
require.NotEmpty(t, tsCtx.outgoingRequest.Header.Get("Accept-Encoding"))
|
|
require.Equal(t, fmt.Sprintf("Grafana/%s", tsCtx.testEnv.SQLStore.Cfg.BuildVersion), tsCtx.outgoingRequest.Header.Get("User-Agent"))
|
|
|
|
callback(received)
|
|
})
|
|
}
|
|
|
|
func (tsCtx *testScenarioContext) runCheckHealthTest(t *testing.T, callback func(req *backend.CheckHealthRequest)) {
|
|
t.Run("When calling /api/datasources/uid/:uid/health should set expected headers on outgoing CheckHealth and HTTP request", func(t *testing.T) {
|
|
var received *backend.CheckHealthRequest
|
|
tsCtx.backendTestPlugin.CheckHealthHandler = backend.CheckHealthHandlerFunc(func(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
|
received = req
|
|
|
|
c := http.Client{
|
|
Transport: tsCtx.rt,
|
|
}
|
|
outReq, err := http.NewRequestWithContext(ctx, http.MethodGet, tsCtx.outgoingServer.URL, nil)
|
|
require.NoError(t, err)
|
|
resp, err := c.Do(outReq)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
tsCtx.testEnv.Server.HTTPServer.Cfg.Logger.Error("Failed to close body", "error", err)
|
|
}
|
|
}()
|
|
|
|
_, err = io.Copy(io.Discard, resp.Body)
|
|
if err != nil {
|
|
tsCtx.testEnv.Server.HTTPServer.Cfg.Logger.Error("Failed to discard body", "error", err)
|
|
}
|
|
|
|
return &backend.CheckHealthResult{
|
|
Status: backend.HealthStatusOk,
|
|
}, nil
|
|
})
|
|
|
|
u := fmt.Sprintf("http://admin:admin@%s/api/datasources/uid/%s/health", tsCtx.grafanaListeningAddr, tsCtx.uid)
|
|
|
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36")
|
|
|
|
if tsCtx.modifyIncomingRequest != nil {
|
|
tsCtx.modifyIncomingRequest(req)
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(b))
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
_, err = io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
require.NotEmpty(t, tsCtx.outgoingRequest.Header.Get("Accept-Encoding"))
|
|
require.Equal(t, fmt.Sprintf("Grafana/%s", tsCtx.testEnv.SQLStore.Cfg.BuildVersion), tsCtx.outgoingRequest.Header.Get("User-Agent"))
|
|
|
|
callback(received)
|
|
})
|
|
}
|
|
|
|
func (tsCtx *testScenarioContext) runCallResourceTest(t *testing.T, callback func(req *backend.CallResourceRequest)) {
|
|
t.Run("When calling /api/datasources/uid/:uid/resources should set expected headers on outgoing CallResource and HTTP request", func(t *testing.T) {
|
|
var received *backend.CallResourceRequest
|
|
tsCtx.backendTestPlugin.CallResourceHandler = backend.CallResourceHandlerFunc(func(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
|
received = req
|
|
|
|
c := http.Client{
|
|
Transport: tsCtx.rt,
|
|
}
|
|
outReq, err := http.NewRequestWithContext(ctx, http.MethodGet, tsCtx.outgoingServer.URL, nil)
|
|
require.NoError(t, err)
|
|
for k, vals := range req.Headers {
|
|
for _, v := range vals {
|
|
outReq.Header.Add(k, v)
|
|
}
|
|
}
|
|
|
|
resp, err := c.Do(outReq)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
if err := resp.Body.Close(); err != nil {
|
|
tsCtx.testEnv.Server.HTTPServer.Cfg.Logger.Error("Failed to close body", "error", err)
|
|
}
|
|
}()
|
|
|
|
_, err = io.Copy(io.Discard, resp.Body)
|
|
if err != nil {
|
|
tsCtx.testEnv.Server.HTTPServer.Cfg.Logger.Error("Failed to discard body", "error", err)
|
|
}
|
|
|
|
responseHeaders := map[string][]string{
|
|
"Connection": {"close, TE"},
|
|
"Te": {"foo", "bar, trailers"},
|
|
"Proxy-Connection": {"should be deleted"},
|
|
"Upgrade": {"foo"},
|
|
"Set-Cookie": {"should be deleted"},
|
|
"X-Custom": {"should not be deleted"},
|
|
}
|
|
|
|
err = sender.Send(&backend.CallResourceResponse{
|
|
Status: http.StatusOK,
|
|
Headers: responseHeaders,
|
|
})
|
|
|
|
return err
|
|
})
|
|
|
|
u := fmt.Sprintf("http://admin:admin@%s/api/datasources/uid/%s/resources", tsCtx.grafanaListeningAddr, tsCtx.uid)
|
|
|
|
req, err := http.NewRequest(http.MethodGet, u, nil)
|
|
req.Header.Set("Content-Type", "application/json")
|
|
req.Header.Set("Connection", "X-Some-Conn-Header")
|
|
req.Header.Set("X-Some-Conn-Header", "should be deleted")
|
|
req.Header.Set("Proxy-Connection", "should be deleted")
|
|
req.Header.Set("User-Agent", "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36")
|
|
|
|
if tsCtx.modifyIncomingRequest != nil {
|
|
tsCtx.modifyIncomingRequest(req)
|
|
}
|
|
|
|
require.NoError(t, err)
|
|
resp, err := http.DefaultClient.Do(req)
|
|
require.NoError(t, err)
|
|
b, err := io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
require.Equal(t, http.StatusOK, resp.StatusCode, string(b))
|
|
t.Cleanup(func() {
|
|
err := resp.Body.Close()
|
|
require.NoError(t, err)
|
|
})
|
|
_, err = io.ReadAll(resp.Body)
|
|
require.NoError(t, err)
|
|
|
|
require.Empty(t, resp.Header.Get("Connection"))
|
|
require.Empty(t, resp.Header.Get("Te"))
|
|
require.Empty(t, resp.Header.Get("Proxy-Connection"))
|
|
require.Empty(t, resp.Header.Get("Upgrade"))
|
|
require.Empty(t, resp.Header.Get("Set-Cookie"))
|
|
require.Equal(t, "should not be deleted", resp.Header.Get("X-Custom"))
|
|
|
|
require.NotNil(t, received)
|
|
require.Empty(t, received.Headers["Connection"])
|
|
require.Empty(t, received.Headers["X-Some-Conn-Header"])
|
|
require.Empty(t, received.Headers["Proxy-Connection"])
|
|
|
|
require.Empty(t, tsCtx.outgoingRequest.Header.Get("Connection"))
|
|
require.Empty(t, tsCtx.outgoingRequest.Header.Get("X-Some-Conn-Header"))
|
|
require.Empty(t, tsCtx.outgoingRequest.Header.Get("Proxy-Connection"))
|
|
require.NotEmpty(t, tsCtx.outgoingRequest.Header.Get("Accept-Encoding"))
|
|
require.Equal(t, fmt.Sprintf("Grafana/%s", tsCtx.testEnv.SQLStore.Cfg.BuildVersion), tsCtx.outgoingRequest.Header.Get("User-Agent"))
|
|
|
|
callback(received)
|
|
})
|
|
}
|
|
|
|
func createTestPlugin(id string) (*plugins.Plugin, *testPlugin) {
|
|
p := &plugins.Plugin{
|
|
JSONData: plugins.JSONData{
|
|
ID: id,
|
|
},
|
|
Class: plugins.Core,
|
|
}
|
|
|
|
p.SetLogger(log.New("test-plugin"))
|
|
tp := &testPlugin{
|
|
pluginID: id,
|
|
logger: p.Logger(),
|
|
QueryDataHandler: backend.QueryDataHandlerFunc(func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
return &backend.QueryDataResponse{}, nil
|
|
}),
|
|
}
|
|
p.RegisterClient(tp)
|
|
|
|
return p, tp
|
|
}
|
|
|
|
type testPlugin struct {
|
|
pluginID string
|
|
logger log.Logger
|
|
backend.CheckHealthHandler
|
|
backend.CallResourceHandler
|
|
backend.QueryDataHandler
|
|
backend.StreamHandler
|
|
}
|
|
|
|
func (tp *testPlugin) PluginID() string {
|
|
return tp.pluginID
|
|
}
|
|
|
|
func (tp *testPlugin) Logger() log.Logger {
|
|
return tp.logger
|
|
}
|
|
|
|
func (tp *testPlugin) Start(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (tp *testPlugin) Stop(ctx context.Context) error {
|
|
return nil
|
|
}
|
|
|
|
func (tp *testPlugin) IsManaged() bool {
|
|
return true
|
|
}
|
|
|
|
func (tp *testPlugin) Exited() bool {
|
|
return false
|
|
}
|
|
|
|
func (tp *testPlugin) Decommission() error {
|
|
return nil
|
|
}
|
|
|
|
func (tp *testPlugin) IsDecommissioned() bool {
|
|
return false
|
|
}
|
|
|
|
func (tp *testPlugin) CollectMetrics(_ context.Context, _ *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
|
|
return nil, backendplugin.ErrMethodNotImplemented
|
|
}
|
|
|
|
func (tp *testPlugin) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
|
|
if tp.CheckHealthHandler != nil {
|
|
return tp.CheckHealthHandler.CheckHealth(ctx, req)
|
|
}
|
|
|
|
return nil, backendplugin.ErrMethodNotImplemented
|
|
}
|
|
|
|
func (tp *testPlugin) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
|
if tp.QueryDataHandler != nil {
|
|
return tp.QueryDataHandler.QueryData(ctx, req)
|
|
}
|
|
|
|
return nil, backendplugin.ErrMethodNotImplemented
|
|
}
|
|
|
|
func (tp *testPlugin) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
|
|
if tp.CallResourceHandler != nil {
|
|
return tp.CallResourceHandler.CallResource(ctx, req, sender)
|
|
}
|
|
|
|
return backendplugin.ErrMethodNotImplemented
|
|
}
|
|
|
|
func (tp *testPlugin) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
|
|
if tp.StreamHandler != nil {
|
|
return tp.StreamHandler.SubscribeStream(ctx, req)
|
|
}
|
|
return nil, backendplugin.ErrMethodNotImplemented
|
|
}
|
|
|
|
func (tp *testPlugin) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
|
|
if tp.StreamHandler != nil {
|
|
return tp.StreamHandler.PublishStream(ctx, req)
|
|
}
|
|
return nil, backendplugin.ErrMethodNotImplemented
|
|
}
|
|
|
|
func (tp *testPlugin) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
|
|
if tp.StreamHandler != nil {
|
|
return tp.StreamHandler.RunStream(ctx, req, sender)
|
|
}
|
|
return backendplugin.ErrMethodNotImplemented
|
|
}
|
|
|
|
func metricRequestWithQueries(t *testing.T, rawQueries ...string) dtos.MetricRequest {
|
|
t.Helper()
|
|
queries := make([]*simplejson.Json, 0)
|
|
for _, q := range rawQueries {
|
|
json, err := simplejson.NewJson([]byte(q))
|
|
require.NoError(t, err)
|
|
queries = append(queries, json)
|
|
}
|
|
return dtos.MetricRequest{
|
|
From: "now-1h",
|
|
To: "now",
|
|
Queries: queries,
|
|
Debug: false,
|
|
}
|
|
}
|