mirror of https://github.com/grafana/grafana
CloudMonitoring: use CallResourceHandler instead of PluginProxy (#41064)
parent
69fe2def89
commit
96f37b3f30
@ -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) |
||||
} |
||||
} |
Loading…
Reference in new issue