Cloudwatch: Refactor dimension keys resource request (#57148)

* use new layered architecture in get dimension keys request

* go lint fixes

* pr feedback

* more pr feedback

* remove not used code

* refactor route middleware

* change signature

* add integration tests for the dimension keys route

* use request suffix instead of query

* use typed args also in frontend

* remove unused import

* harmonize naming

* fix merge conflict
This commit is contained in:
Erik Sundell
2022-10-20 12:53:28 +02:00
committed by GitHub
parent 7f3536a6d2
commit b0c2ca6c1b
37 changed files with 1015 additions and 486 deletions
@@ -0,0 +1,55 @@
package routes
import (
"encoding/json"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func DimensionKeysHandler(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
dimensionKeysRequest, err := models.GetDimensionKeysRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusBadRequest, err)
}
service, err := newListMetricsService(pluginCtx, clientFactory, dimensionKeysRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
dimensionKeys := []string{}
switch dimensionKeysRequest.Type() {
case models.StandardDimensionKeysRequest:
dimensionKeys, err = service.GetHardCodedDimensionKeysByNamespace(dimensionKeysRequest.Namespace)
case models.FilterDimensionKeysRequest:
dimensionKeys, err = service.GetDimensionKeysByDimensionFilter(dimensionKeysRequest)
case models.CustomMetricDimensionKeysRequest:
dimensionKeys, err = service.GetDimensionKeysByNamespace(dimensionKeysRequest.Namespace)
}
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
dimensionKeysResponse, err := json.Marshal(dimensionKeys)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
return dimensionKeysResponse, nil
}
// newListMetricsService is an list metrics service factory.
//
// Stubbable by tests.
var newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
metricClient, err := clientFactory(pluginCtx, region)
if err != nil {
return nil, err
}
return services.NewListMetricsService(metricClient.MetricsClientProvider), nil
}
@@ -0,0 +1,81 @@
package routes
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
)
func Test_DimensionKeys_Route(t *testing.T) {
t.Run("rejects POST method", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/dimension-keys?region=us-east-1", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
})
t.Run("requires region query value", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/dimension-keys", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
})
tests := []struct {
url string
methodName string
requestType string
}{
{
url: "/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization",
methodName: "GetHardCodedDimensionKeysByNamespace",
requestType: "StandardDimensionKeysRequest"},
{
url: `/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`,
methodName: "GetDimensionKeysByDimensionFilter",
requestType: "FilterDimensionKeysRequest"},
{
url: `/dimension-keys?region=us-east-2&namespace=customNamespace&metricName=CPUUtilization`,
methodName: "GetDimensionKeysByNamespace",
requestType: "CustomMetricDimensionKeysRequest"},
}
for _, tc := range tests {
t.Run(fmt.Sprintf("calls %s when a StandardDimensionKeysRequest is passed", tc.requestType), func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On(tc.methodName).Return([]string{}, nil)
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", tc.url, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
mockListMetricsService.AssertNumberOfCalls(t, tc.methodName, 1)
})
}
for _, tc := range tests {
t.Run(fmt.Sprintf("return 500 if %s returns an error", tc.requestType), func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On(tc.methodName).Return([]string{}, fmt.Errorf("some error"))
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", tc.url, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Equal(t, `{"Message":"error in DimensionKeyHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())
})
}
}
@@ -0,0 +1,22 @@
package routes
import (
"encoding/json"
"net/http"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func respondWithError(rw http.ResponseWriter, httpError *models.HttpError) {
response, err := json.Marshal(httpError)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
return
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(httpError.StatusCode)
_, err = rw.Write(response)
if err != nil {
rw.WriteHeader(http.StatusInternalServerError)
}
}
+34
View File
@@ -0,0 +1,34 @@
package routes
import (
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/cwlog"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func ResourceRequestMiddleware(handleFunc models.RouteHandlerFunc, clientFactory models.ClientsFactoryFunc) func(rw http.ResponseWriter, req *http.Request) {
return func(rw http.ResponseWriter, req *http.Request) {
if req.Method != "GET" {
respondWithError(rw, models.NewHttpError("Invalid method", http.StatusMethodNotAllowed, nil))
return
}
ctx := req.Context()
pluginContext := httpadapter.PluginConfigFromContext(ctx)
json, httpError := handleFunc(pluginContext, clientFactory, req.URL.Query())
if httpError != nil {
cwlog.Error("error handling resource request", "error", httpError.Message)
respondWithError(rw, httpError)
return
}
rw.Header().Set("Content-Type", "application/json")
_, err := rw.Write(json)
if err != nil {
cwlog.Error("error handling resource request", "error", err)
respondWithError(rw, models.NewHttpError("error writing response in resource request middleware", http.StatusInternalServerError, err))
}
}
}