mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
CloudMonitoring: use CallResourceHandler instead of PluginProxy (#41064)
This commit is contained in:
parent
69fe2def89
commit
96f37b3f30
@ -21,6 +21,7 @@ import (
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/datasource"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
@ -81,8 +82,11 @@ func ProvideService(cfg *setting.Cfg, httpClientProvider httpclient.Provider, re
|
||||
dsService: dsService,
|
||||
}
|
||||
|
||||
mux := http.NewServeMux()
|
||||
s.registerRoutes(mux)
|
||||
factory := coreplugin.New(backend.ServeOpts{
|
||||
QueryDataHandler: s,
|
||||
QueryDataHandler: s,
|
||||
CallResourceHandler: httpadapter.New(mux),
|
||||
})
|
||||
|
||||
if err := registrar.LoadAndRegister(pluginID, factory); err != nil {
|
||||
@ -110,11 +114,16 @@ type datasourceInfo struct {
|
||||
defaultProject string
|
||||
clientEmail string
|
||||
tokenUri string
|
||||
client *http.Client
|
||||
services map[string]datasourceService
|
||||
|
||||
decryptedSecureJSONData map[string]string
|
||||
}
|
||||
|
||||
type datasourceService struct {
|
||||
url string
|
||||
client *http.Client
|
||||
}
|
||||
|
||||
func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc {
|
||||
return func(settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
|
||||
var jsonData map[string]interface{}
|
||||
@ -152,6 +161,7 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
clientEmail: clientEmail,
|
||||
tokenUri: tokenUri,
|
||||
decryptedSecureJSONData: settings.DecryptedSecureJSONData,
|
||||
services: map[string]datasourceService{},
|
||||
}
|
||||
|
||||
opts, err := settings.HTTPClientOptions()
|
||||
@ -159,9 +169,15 @@ func newInstanceSettings(httpClientProvider httpclient.Provider) datasource.Inst
|
||||
return nil, err
|
||||
}
|
||||
|
||||
dsInfo.client, err = newHTTPClient(dsInfo, opts, httpClientProvider)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
for name, info := range routes {
|
||||
client, err := newHTTPClient(dsInfo, opts, httpClientProvider, name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dsInfo.services[name] = datasourceService{
|
||||
url: info.url,
|
||||
client: client,
|
||||
}
|
||||
}
|
||||
|
||||
return dsInfo, nil
|
||||
@ -576,7 +592,7 @@ func (s *Service) createRequest(ctx context.Context, dsInfo *datasourceInfo, pro
|
||||
if body != nil {
|
||||
method = http.MethodPost
|
||||
}
|
||||
req, err := http.NewRequest(method, cloudMonitoringRoute.url, body)
|
||||
req, err := http.NewRequest(method, dsInfo.services[cloudMonitor].url, body)
|
||||
if err != nil {
|
||||
slog.Error("Failed to create request", "error", err)
|
||||
return nil, fmt.Errorf("failed to create request: %w", err)
|
||||
|
@ -8,25 +8,37 @@ import (
|
||||
infrahttp "github.com/grafana/grafana/pkg/infra/httpclient"
|
||||
)
|
||||
|
||||
var cloudMonitoringRoute = struct {
|
||||
path string
|
||||
const (
|
||||
cloudMonitor = "cloudmonitoring"
|
||||
resourceManager = "cloudresourcemanager"
|
||||
)
|
||||
|
||||
type routeInfo struct {
|
||||
method string
|
||||
url string
|
||||
scopes []string
|
||||
}{
|
||||
path: "cloudmonitoring",
|
||||
method: "GET",
|
||||
url: "https://monitoring.googleapis.com",
|
||||
scopes: []string{"https://www.googleapis.com/auth/monitoring.read"},
|
||||
}
|
||||
|
||||
func getMiddleware(model *datasourceInfo) (httpclient.Middleware, error) {
|
||||
var routes = map[string]routeInfo{
|
||||
cloudMonitor: {
|
||||
method: "GET",
|
||||
url: "https://monitoring.googleapis.com",
|
||||
scopes: []string{"https://www.googleapis.com/auth/monitoring.read"},
|
||||
},
|
||||
resourceManager: {
|
||||
method: "GET",
|
||||
url: "https://cloudresourcemanager.googleapis.com",
|
||||
scopes: []string{"https://www.googleapis.com/auth/cloudplatformprojects.readonly"},
|
||||
},
|
||||
}
|
||||
|
||||
func getMiddleware(model *datasourceInfo, routePath string) (httpclient.Middleware, error) {
|
||||
providerConfig := tokenprovider.Config{
|
||||
RoutePath: cloudMonitoringRoute.path,
|
||||
RouteMethod: cloudMonitoringRoute.method,
|
||||
RoutePath: routePath,
|
||||
RouteMethod: routes[routePath].method,
|
||||
DataSourceID: model.id,
|
||||
DataSourceUpdated: model.updated,
|
||||
Scopes: cloudMonitoringRoute.scopes,
|
||||
Scopes: routes[routePath].scopes,
|
||||
}
|
||||
|
||||
var provider tokenprovider.TokenProvider
|
||||
@ -45,8 +57,8 @@ func getMiddleware(model *datasourceInfo) (httpclient.Middleware, error) {
|
||||
return tokenprovider.AuthMiddleware(provider), nil
|
||||
}
|
||||
|
||||
func newHTTPClient(model *datasourceInfo, opts httpclient.Options, clientProvider infrahttp.Provider) (*http.Client, error) {
|
||||
m, err := getMiddleware(model)
|
||||
func newHTTPClient(model *datasourceInfo, opts httpclient.Options, clientProvider infrahttp.Provider, route string) (*http.Client, error) {
|
||||
m, err := getMiddleware(model, route)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
124
pkg/tsdb/cloudmonitoring/resource_handler.go
Normal file
124
pkg/tsdb/cloudmonitoring/resource_handler.go
Normal file
@ -0,0 +1,124 @@
|
||||
package cloudmonitoring
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
|
||||
)
|
||||
|
||||
func (s *Service) registerRoutes(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/cloudmonitoring/", s.resourceHandler(cloudMonitor))
|
||||
mux.HandleFunc("/cloudresourcemanager/", s.resourceHandler(resourceManager))
|
||||
}
|
||||
|
||||
func (s *Service) resourceHandler(subDataSource string) func(rw http.ResponseWriter, req *http.Request) {
|
||||
return func(rw http.ResponseWriter, req *http.Request) {
|
||||
client, code, err := s.setRequestVariables(req, subDataSource)
|
||||
if err != nil {
|
||||
writeResponse(rw, code, fmt.Sprintf("unexpected error %v", err))
|
||||
return
|
||||
}
|
||||
doRequest(rw, req, client)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) setRequestVariables(req *http.Request, subDataSource string) (*http.Client, int, error) {
|
||||
slog.Debug("Received resource call", "url", req.URL.String(), "method", req.Method)
|
||||
|
||||
newPath, err := getTarget(req.URL.Path)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
dsInfo, err := s.getDataSourceFromHTTPReq(req)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
|
||||
serviceURL, err := url.Parse(dsInfo.services[subDataSource].url)
|
||||
if err != nil {
|
||||
return nil, http.StatusBadRequest, err
|
||||
}
|
||||
req.URL.Path = newPath
|
||||
req.URL.Host = serviceURL.Host
|
||||
req.URL.Scheme = serviceURL.Scheme
|
||||
|
||||
return dsInfo.services[subDataSource].client, 0, nil
|
||||
}
|
||||
|
||||
func doRequest(rw http.ResponseWriter, req *http.Request, cli *http.Client) http.ResponseWriter {
|
||||
res, err := cli.Do(req)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusBadRequest)
|
||||
_, err = rw.Write([]byte(fmt.Sprintf("unexpected error %v", err)))
|
||||
if err != nil {
|
||||
slog.Error("Unable to write HTTP response", "error", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
defer func() {
|
||||
if err := res.Body.Close(); err != nil {
|
||||
slog.Warn("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
|
||||
body, err := ioutil.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
rw.WriteHeader(http.StatusInternalServerError)
|
||||
_, err = rw.Write([]byte(fmt.Sprintf("unexpected error %v", err)))
|
||||
if err != nil {
|
||||
slog.Error("Unable to write HTTP response", "error", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
rw.WriteHeader(res.StatusCode)
|
||||
_, err = rw.Write(body)
|
||||
if err != nil {
|
||||
slog.Error("Unable to write HTTP response", "error", err)
|
||||
}
|
||||
|
||||
for k, v := range res.Header {
|
||||
rw.Header().Set(k, v[0])
|
||||
for _, v := range v[1:] {
|
||||
rw.Header().Add(k, v)
|
||||
}
|
||||
}
|
||||
// Returning the response write for testing purposes
|
||||
return rw
|
||||
}
|
||||
|
||||
func getTarget(original string) (target string, err error) {
|
||||
splittedPath := strings.SplitN(original, "/", 3)
|
||||
if len(splittedPath) < 3 {
|
||||
err = fmt.Errorf("the request should contain the service on its path")
|
||||
return
|
||||
}
|
||||
target = fmt.Sprintf("/%s", splittedPath[2])
|
||||
return
|
||||
}
|
||||
|
||||
func writeResponse(rw http.ResponseWriter, code int, msg string) {
|
||||
rw.WriteHeader(code)
|
||||
_, err := rw.Write([]byte(msg))
|
||||
if err != nil {
|
||||
slog.Error("Unable to write HTTP response", "error", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) getDataSourceFromHTTPReq(req *http.Request) (*datasourceInfo, error) {
|
||||
ctx := req.Context()
|
||||
pluginContext := httpadapter.PluginConfigFromContext(ctx)
|
||||
i, err := s.im.Get(pluginContext)
|
||||
if err != nil {
|
||||
return nil, nil
|
||||
}
|
||||
ds, ok := i.(*datasourceInfo)
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("unable to convert datasource from service instance")
|
||||
}
|
||||
return ds, nil
|
||||
}
|
114
pkg/tsdb/cloudmonitoring/resource_handler_test.go
Normal file
114
pkg/tsdb/cloudmonitoring/resource_handler_test.go
Normal file
@ -0,0 +1,114 @@
|
||||
package cloudmonitoring
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func Test_parseResourcePath(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
original string
|
||||
expectedTarget string
|
||||
Err require.ErrorAssertionFunc
|
||||
}{
|
||||
{
|
||||
"Path with a subscription",
|
||||
"/cloudmonitoring/v3/projects/foo",
|
||||
"/v3/projects/foo",
|
||||
require.NoError,
|
||||
},
|
||||
{
|
||||
"Malformed path",
|
||||
"/projects?foo",
|
||||
"",
|
||||
require.Error,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
target, err := getTarget(tt.original)
|
||||
if target != tt.expectedTarget {
|
||||
t.Errorf("Unexpected target %s expecting %s", target, tt.expectedTarget)
|
||||
}
|
||||
tt.Err(t, err)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Test_doRequest(t *testing.T) {
|
||||
// test that it forwards the header and body
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Add("foo", "bar")
|
||||
_, err := w.Write([]byte("result"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}))
|
||||
req, err := http.NewRequest(http.MethodGet, srv.URL, nil)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
rw := httptest.NewRecorder()
|
||||
res := doRequest(rw, req, srv.Client())
|
||||
if res.Header().Get("foo") != "bar" {
|
||||
t.Errorf("Unexpected headers: %v", res.Header())
|
||||
}
|
||||
result := rw.Result()
|
||||
body, err := ioutil.ReadAll(result.Body)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
err = result.Body.Close()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if string(body) != "result" {
|
||||
t.Errorf("Unexpected body: %v", string(body))
|
||||
}
|
||||
}
|
||||
|
||||
type fakeInstance struct {
|
||||
services map[string]datasourceService
|
||||
}
|
||||
|
||||
func (f *fakeInstance) Get(pluginContext backend.PluginContext) (instancemgmt.Instance, error) {
|
||||
return &datasourceInfo{
|
||||
services: f.services,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (f *fakeInstance) Do(pluginContext backend.PluginContext, fn instancemgmt.InstanceCallbackFunc) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func Test_setRequestVariables(t *testing.T) {
|
||||
s := Service{
|
||||
im: &fakeInstance{
|
||||
services: map[string]datasourceService{
|
||||
cloudMonitor: {
|
||||
url: routes[cloudMonitor].url,
|
||||
client: &http.Client{},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
req, err := http.NewRequest(http.MethodGet, "http://foo/cloudmonitoring/v3/projects/bar/metricDescriptors", nil)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
_, _, err = s.setRequestVariables(req, cloudMonitor)
|
||||
if err != nil {
|
||||
t.Fatalf("Unexpected error %v", err)
|
||||
}
|
||||
expectedURL := "https://monitoring.googleapis.com/v3/projects/bar/metricDescriptors"
|
||||
if req.URL.String() != expectedURL {
|
||||
t.Errorf("Unexpected result URL. Got %s, expecting %s", req.URL.String(), expectedURL)
|
||||
}
|
||||
}
|
@ -73,7 +73,7 @@ func (timeSeriesFilter *cloudMonitoringTimeSeriesFilter) run(ctx context.Context
|
||||
}
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
res, err := dsInfo.client.Do(r)
|
||||
res, err := dsInfo.services[cloudMonitor].client.Do(r)
|
||||
if err != nil {
|
||||
dr.Error = err
|
||||
return dr, cloudMonitoringResponse{}, "", nil
|
||||
|
@ -73,7 +73,7 @@ func (timeSeriesQuery cloudMonitoringTimeSeriesQuery) run(ctx context.Context, r
|
||||
}
|
||||
|
||||
r = r.WithContext(ctx)
|
||||
res, err := dsInfo.client.Do(r)
|
||||
res, err := dsInfo.services[cloudMonitor].client.Do(r)
|
||||
if err != nil {
|
||||
dr.Error = err
|
||||
return dr, cloudMonitoringResponse{}, "", nil
|
||||
|
@ -31,7 +31,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
) {
|
||||
super(instanceSettings);
|
||||
this.authenticationType = instanceSettings.jsonData.authenticationType || 'jwt';
|
||||
this.api = new API(`${instanceSettings.url!}/cloudmonitoring/v3/projects/`);
|
||||
this.api = new API(`/api/datasources/${this.id}/resources/cloudmonitoring/v3/projects/`);
|
||||
this.variables = new CloudMonitoringVariableSupport(this);
|
||||
this.intervalMs = 0;
|
||||
}
|
||||
@ -293,7 +293,7 @@ export default class CloudMonitoringDatasource extends DataSourceWithBackend<
|
||||
value: projectId,
|
||||
label: name,
|
||||
}),
|
||||
baseUrl: `${this.instanceSettings.url!}/cloudresourcemanager/v1/`,
|
||||
baseUrl: `/api/datasources/${this.id}/resources/cloudresourcemanager/v1/`,
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -90,33 +90,5 @@
|
||||
"name": "Grafana Labs",
|
||||
"url": "https://grafana.com"
|
||||
}
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"path": "cloudmonitoring",
|
||||
"method": "GET",
|
||||
"url": "https://monitoring.googleapis.com",
|
||||
"jwtTokenAuth": {
|
||||
"scopes": ["https://www.googleapis.com/auth/monitoring.read"],
|
||||
"params": {
|
||||
"token_uri": "{{.JsonData.tokenUri}}",
|
||||
"client_email": "{{.JsonData.clientEmail}}",
|
||||
"private_key": "{{.SecureJsonData.privateKey}}"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"path": "cloudresourcemanager",
|
||||
"method": "GET",
|
||||
"url": "https://cloudresourcemanager.googleapis.com",
|
||||
"jwtTokenAuth": {
|
||||
"scopes": ["https://www.googleapis.com/auth/cloudplatformprojects.readonly"],
|
||||
"params": {
|
||||
"token_uri": "{{.JsonData.tokenUri}}",
|
||||
"client_email": "{{.JsonData.clientEmail}}",
|
||||
"private_key": "{{.SecureJsonData.privateKey}}"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user