From 96f37b3f301e339763f2014f282581356ba690c2 Mon Sep 17 00:00:00 2001 From: Isabella Siu Date: Tue, 2 Nov 2021 10:37:02 -0400 Subject: [PATCH] CloudMonitoring: use CallResourceHandler instead of PluginProxy (#41064) --- pkg/tsdb/cloudmonitoring/cloudmonitoring.go | 28 +++- pkg/tsdb/cloudmonitoring/httpclient.go | 38 ++++-- pkg/tsdb/cloudmonitoring/resource_handler.go | 124 ++++++++++++++++++ .../cloudmonitoring/resource_handler_test.go | 114 ++++++++++++++++ .../cloudmonitoring/time_series_filter.go | 2 +- pkg/tsdb/cloudmonitoring/time_series_query.go | 2 +- .../datasource/cloud-monitoring/datasource.ts | 4 +- .../datasource/cloud-monitoring/plugin.json | 30 +---- 8 files changed, 290 insertions(+), 52 deletions(-) create mode 100644 pkg/tsdb/cloudmonitoring/resource_handler.go create mode 100644 pkg/tsdb/cloudmonitoring/resource_handler_test.go diff --git a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go index 1545362ef2a..9e09755535f 100644 --- a/pkg/tsdb/cloudmonitoring/cloudmonitoring.go +++ b/pkg/tsdb/cloudmonitoring/cloudmonitoring.go @@ -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) diff --git a/pkg/tsdb/cloudmonitoring/httpclient.go b/pkg/tsdb/cloudmonitoring/httpclient.go index 711795be6dd..b104e53dd38 100644 --- a/pkg/tsdb/cloudmonitoring/httpclient.go +++ b/pkg/tsdb/cloudmonitoring/httpclient.go @@ -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 } diff --git a/pkg/tsdb/cloudmonitoring/resource_handler.go b/pkg/tsdb/cloudmonitoring/resource_handler.go new file mode 100644 index 00000000000..5fb302613dc --- /dev/null +++ b/pkg/tsdb/cloudmonitoring/resource_handler.go @@ -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 +} diff --git a/pkg/tsdb/cloudmonitoring/resource_handler_test.go b/pkg/tsdb/cloudmonitoring/resource_handler_test.go new file mode 100644 index 00000000000..755f7e1a18c --- /dev/null +++ b/pkg/tsdb/cloudmonitoring/resource_handler_test.go @@ -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) + } +} diff --git a/pkg/tsdb/cloudmonitoring/time_series_filter.go b/pkg/tsdb/cloudmonitoring/time_series_filter.go index 223f3bf4f09..adbe98b8bff 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_filter.go +++ b/pkg/tsdb/cloudmonitoring/time_series_filter.go @@ -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 diff --git a/pkg/tsdb/cloudmonitoring/time_series_query.go b/pkg/tsdb/cloudmonitoring/time_series_query.go index 3fa0d4dd414..9a515104c3a 100644 --- a/pkg/tsdb/cloudmonitoring/time_series_query.go +++ b/pkg/tsdb/cloudmonitoring/time_series_query.go @@ -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 diff --git a/public/app/plugins/datasource/cloud-monitoring/datasource.ts b/public/app/plugins/datasource/cloud-monitoring/datasource.ts index 874bde7f0cc..3a1e5169d77 100644 --- a/public/app/plugins/datasource/cloud-monitoring/datasource.ts +++ b/public/app/plugins/datasource/cloud-monitoring/datasource.ts @@ -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/`, }); } diff --git a/public/app/plugins/datasource/cloud-monitoring/plugin.json b/public/app/plugins/datasource/cloud-monitoring/plugin.json index cf43e37106a..5a96830945c 100644 --- a/public/app/plugins/datasource/cloud-monitoring/plugin.json +++ b/public/app/plugins/datasource/cloud-monitoring/plugin.json @@ -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}}" - } - } - } - ] + } }