CloudMonitoring: use CallResourceHandler instead of PluginProxy (#41064)

pull/41147/head
Isabella Siu 4 years ago committed by GitHub
parent 69fe2def89
commit 96f37b3f30
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 28
      pkg/tsdb/cloudmonitoring/cloudmonitoring.go
  2. 38
      pkg/tsdb/cloudmonitoring/httpclient.go
  3. 124
      pkg/tsdb/cloudmonitoring/resource_handler.go
  4. 114
      pkg/tsdb/cloudmonitoring/resource_handler_test.go
  5. 2
      pkg/tsdb/cloudmonitoring/time_series_filter.go
  6. 2
      pkg/tsdb/cloudmonitoring/time_series_query.go
  7. 4
      public/app/plugins/datasource/cloud-monitoring/datasource.ts
  8. 30
      public/app/plugins/datasource/cloud-monitoring/plugin.json

@ -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
}

@ -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
}

@ -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…
Cancel
Save