Cloudwatch: Refactor namespaces resource request (#57590)

* add route tests

* remove not used method

* pr feedback

* change variable name
This commit is contained in:
Erik Sundell 2022-10-26 15:54:23 +02:00 committed by GitHub
parent c8689fd591
commit 94aa090fb0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 152 additions and 58 deletions

View File

@ -105,22 +105,23 @@ func newExecutor(im instancemgmt.InstanceManager, cfg *setting.Cfg, sessions Ses
return e
}
func (e *cloudWatchExecutor) getClients(pluginCtx backend.PluginContext, region string) (models.Clients, error) {
func (e *cloudWatchExecutor) getRequestContext(pluginCtx backend.PluginContext, region string) (models.RequestContext, error) {
r := region
instance, err := e.getInstance(pluginCtx)
if region == defaultRegion {
instance, err := e.getInstance(pluginCtx)
if err != nil {
return models.Clients{}, err
return models.RequestContext{}, err
}
r = instance.Settings.Region
}
sess, err := e.newSession(pluginCtx, r)
if err != nil {
return models.Clients{}, err
return models.RequestContext{}, err
}
return models.Clients{
return models.RequestContext{
MetricsClientProvider: clients.NewMetricsClient(NewMetricsAPI(sess), e.cfg),
Settings: instance.Settings,
}, nil
}

View File

@ -94,31 +94,6 @@ func (e *cloudWatchExecutor) handleGetRegions(pluginCtx backend.PluginContext, p
return result, nil
}
func (e *cloudWatchExecutor) handleGetNamespaces(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
var keys []string
for key := range constants.NamespaceMetricsMap {
keys = append(keys, key)
}
instance, err := e.getInstance(pluginCtx)
if err != nil {
return nil, err
}
customNamespaces := instance.Settings.Namespace
if customNamespaces != "" {
keys = append(keys, strings.Split(customNamespaces, ",")...)
}
sort.Strings(keys)
result := make([]suggestData, 0)
for _, key := range keys {
result = append(result, suggestData{Text: key, Value: key, Label: key})
}
return result, nil
}
func (e *cloudWatchExecutor) handleGetEbsVolumeIds(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
region := parameters.Get("region")
instanceId := parameters.Get("instanceId")

View File

@ -6,13 +6,14 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
)
type Clients struct {
type RequestContext struct {
MetricsClientProvider MetricsClientProvider
Settings *CloudWatchSettings
}
type ClientsFactoryFunc func(pluginCtx backend.PluginContext, region string) (clients Clients, err error)
type RequestContextFactoryFunc func(pluginCtx backend.PluginContext, region string) (reqCtx RequestContext, err error)
type RouteHandlerFunc func(pluginCtx backend.PluginContext, clientFactory ClientsFactoryFunc, parameters url.Values) ([]byte, *HttpError)
type RouteHandlerFunc func(pluginCtx backend.PluginContext, reqContextFactory RequestContextFactoryFunc, parameters url.Values) ([]byte, *HttpError)
type cloudWatchLink struct {
View string `json:"view"`

View File

@ -15,15 +15,15 @@ import (
func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/regions", handleResourceReq(e.handleGetRegions))
mux.HandleFunc("/namespaces", handleResourceReq(e.handleGetNamespaces))
mux.HandleFunc("/ebs-volume-ids", handleResourceReq(e.handleGetEbsVolumeIds))
mux.HandleFunc("/ec2-instance-attribute", handleResourceReq(e.handleGetEc2InstanceAttribute))
mux.HandleFunc("/resource-arns", handleResourceReq(e.handleGetResourceArns))
mux.HandleFunc("/log-groups", handleResourceReq(e.handleGetLogGroups))
mux.HandleFunc("/all-log-groups", handleResourceReq(e.handleGetAllLogGroups))
mux.HandleFunc("/metrics", routes.ResourceRequestMiddleware(routes.MetricsHandler, e.getClients))
mux.HandleFunc("/dimension-values", routes.ResourceRequestMiddleware(routes.DimensionValuesHandler, e.getClients))
mux.HandleFunc("/dimension-keys", routes.ResourceRequestMiddleware(routes.DimensionKeysHandler, e.getClients))
mux.HandleFunc("/metrics", routes.ResourceRequestMiddleware(routes.MetricsHandler, e.getRequestContext))
mux.HandleFunc("/dimension-values", routes.ResourceRequestMiddleware(routes.DimensionValuesHandler, e.getRequestContext))
mux.HandleFunc("/dimension-keys", routes.ResourceRequestMiddleware(routes.DimensionKeysHandler, e.getRequestContext))
mux.HandleFunc("/namespaces", routes.ResourceRequestMiddleware(routes.NamespacesHandler, e.getRequestContext))
return mux
}

View File

@ -11,13 +11,13 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func DimensionKeysHandler(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
func DimensionKeysHandler(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
dimensionKeysRequest, err := request.GetDimensionKeysRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusBadRequest, err)
}
service, err := newListMetricsService(pluginCtx, clientFactory, dimensionKeysRequest.Region)
service, err := newListMetricsService(pluginCtx, reqCtxFactory, dimensionKeysRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusInternalServerError, err)
}
@ -46,8 +46,8 @@ func DimensionKeysHandler(pluginCtx backend.PluginContext, clientFactory models.
// 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)
var newListMetricsService = func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
metricClient, err := reqCtxFactory(pluginCtx, region)
if err != nil {
return nil, err
}

View File

@ -19,7 +19,7 @@ func Test_DimensionKeys_Route(t *testing.T) {
t.Run("calls FilterDimensionKeysRequest when a StandardDimensionKeysRequest is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionKeysByDimensionFilter").Return([]string{}, nil)
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
newListMetricsService = func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
@ -32,7 +32,7 @@ func Test_DimensionKeys_Route(t *testing.T) {
t.Run("calls GetDimensionKeysByNamespace when a CustomMetricDimensionKeysRequest is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionKeysByNamespace").Return([]string{}, nil)
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
newListMetricsService = func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
@ -68,7 +68,7 @@ func Test_DimensionKeys_Route(t *testing.T) {
t.Run("return 500 if GetDimensionKeysByDimensionFilter returns an error", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionKeysByDimensionFilter").Return([]string{}, fmt.Errorf("some error"))
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
newListMetricsService = func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()

View File

@ -10,13 +10,13 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/request"
)
func DimensionValuesHandler(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
func DimensionValuesHandler(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
dimensionValuesRequest, err := request.GetDimensionValuesRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusBadRequest, err)
}
service, err := newListMetricsService(pluginCtx, clientFactory, dimensionValuesRequest.Region)
service, err := newListMetricsService(pluginCtx, reqCtxFactory, dimensionValuesRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}

View File

@ -16,7 +16,7 @@ func Test_DimensionValues_Route(t *testing.T) {
t.Run("Calls GetDimensionValuesByDimensionFilter when a valid request is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionValuesByDimensionFilter").Return([]string{}, nil)
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
newListMetricsService = func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
@ -28,7 +28,7 @@ func Test_DimensionValues_Route(t *testing.T) {
t.Run("returns 500 if GetDimensionValuesByDimensionFilter returns an error", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionValuesByDimensionFilter").Return([]string{}, fmt.Errorf("some error"))
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
newListMetricsService = func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()

View File

@ -11,13 +11,13 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func MetricsHandler(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
func MetricsHandler(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
metricsRequest, err := request.GetMetricsRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusBadRequest, err)
}
service, err := newListMetricsService(pluginCtx, clientFactory, metricsRequest.Region)
service, err := newListMetricsService(pluginCtx, reqCtxFactory, metricsRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}

View File

@ -19,7 +19,7 @@ func Test_Metrics_Route(t *testing.T) {
t.Run("calls GetMetricsByNamespace when a CustomNamespaceRequestType is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetMetricsByNamespace").Return([]models.Metric{}, nil)
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
newListMetricsService = func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
@ -75,7 +75,7 @@ func Test_Metrics_Route(t *testing.T) {
t.Run("returns 500 if GetMetricsByNamespace returns an error", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetMetricsByNamespace").Return([]models.Metric{}, fmt.Errorf("some error"))
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
newListMetricsService = func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()

View File

@ -8,7 +8,7 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
func ResourceRequestMiddleware(handleFunc models.RouteHandlerFunc, clientFactory models.ClientsFactoryFunc) func(rw http.ResponseWriter, req *http.Request) {
func ResourceRequestMiddleware(handleFunc models.RouteHandlerFunc, reqCtxFactory models.RequestContextFactoryFunc) 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))
@ -17,7 +17,7 @@ func ResourceRequestMiddleware(handleFunc models.RouteHandlerFunc, clientFactory
ctx := req.Context()
pluginContext := httpadapter.PluginConfigFromContext(ctx)
json, httpError := handleFunc(pluginContext, clientFactory, req.URL.Query())
json, httpError := handleFunc(pluginContext, reqCtxFactory, req.URL.Query())
if httpError != nil {
cwlog.Error("error handling resource request", "error", httpError.Message)
respondWithError(rw, httpError)

View File

@ -16,7 +16,7 @@ func Test_Middleware(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(func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
handler := http.HandlerFunc(ResourceRequestMiddleware(func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
return []byte{}, nil
}, nil))
handler.ServeHTTP(rr, req)
@ -27,7 +27,7 @@ func Test_Middleware(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/some-path", nil)
var testPluginContext backend.PluginContext
handler := http.HandlerFunc(ResourceRequestMiddleware(func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
handler := http.HandlerFunc(ResourceRequestMiddleware(func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
testPluginContext = pluginCtx
return []byte{}, nil
}, nil))
@ -38,7 +38,7 @@ func Test_Middleware(t *testing.T) {
t.Run("should propagate handler error to response", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/some-path", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
handler := http.HandlerFunc(ResourceRequestMiddleware(func(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
return []byte{}, models.NewHttpError("error", http.StatusBadRequest, fmt.Errorf("error from handler"))
}, nil))
handler.ServeHTTP(rr, req)

View File

@ -0,0 +1,34 @@
package routes
import (
"encoding/json"
"net/http"
"net/url"
"sort"
"strings"
"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 NamespacesHandler(pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, _ url.Values) ([]byte, *models.HttpError) {
reqCtx, err := reqCtxFactory(pluginCtx, "default")
if err != nil {
return nil, models.NewHttpError("error in NamespacesHandler", http.StatusInternalServerError, err)
}
result := services.GetHardCodedNamespaces()
customNamespace := reqCtx.Settings.Namespace
if customNamespace != "" {
result = append(result, strings.Split(customNamespace, ",")...)
}
sort.Strings(result)
namespacesResponse, err := json.Marshal(result)
if err != nil {
return nil, models.NewHttpError("error in NamespacesHandler", http.StatusInternalServerError, err)
}
return namespacesResponse, nil
}

View File

@ -0,0 +1,72 @@
package routes
import (
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
"github.com/stretchr/testify/assert"
)
func Test_Namespaces_Route(t *testing.T) {
customNamespaces := ""
factoryFunc := func(pluginCtx backend.PluginContext, region string) (reqCtx models.RequestContext, err error) {
return models.RequestContext{
Settings: &models.CloudWatchSettings{
Namespace: customNamespaces,
},
}, nil
}
t.Run("calls GetHardCodedNamespaces", func(t *testing.T) {
origGetHardCodedNamespaces := services.GetHardCodedNamespaces
t.Cleanup(func() {
services.GetHardCodedNamespaces = origGetHardCodedNamespaces
})
haveBeenCalled := false
services.GetHardCodedNamespaces = func() []string {
haveBeenCalled = true
return []string{}
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/namespaces", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(NamespacesHandler, factoryFunc))
handler.ServeHTTP(rr, req)
assert.True(t, haveBeenCalled)
})
t.Run("returns merges hardcoded namespaces and custom namespaces", func(t *testing.T) {
origGetHardCodedNamespaces := services.GetHardCodedNamespaces
t.Cleanup(func() {
services.GetHardCodedNamespaces = origGetHardCodedNamespaces
})
services.GetHardCodedNamespaces = func() []string {
return []string{"AWS/EC2", "AWS/ELB"}
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/namespaces", nil)
customNamespaces = "customNamespace1,customNamespace2"
handler := http.HandlerFunc(ResourceRequestMiddleware(NamespacesHandler, factoryFunc))
handler.ServeHTTP(rr, req)
assert.JSONEq(t, `["AWS/EC2", "AWS/ELB", "customNamespace1", "customNamespace2"]`, rr.Body.String())
})
t.Run("sorts result", func(t *testing.T) {
origGetHardCodedNamespaces := services.GetHardCodedNamespaces
t.Cleanup(func() {
services.GetHardCodedNamespaces = origGetHardCodedNamespaces
})
services.GetHardCodedNamespaces = func() []string {
return []string{"AWS/XYZ", "AWS/ELB"}
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/namespaces", nil)
customNamespaces = "DCustomNamespace1,ACustomNamespace2"
handler := http.HandlerFunc(ResourceRequestMiddleware(NamespacesHandler, factoryFunc))
handler.ServeHTTP(rr, req)
assert.JSONEq(t, `["ACustomNamespace2", "AWS/ELB", "AWS/XYZ", "DCustomNamespace1"]`, rr.Body.String())
})
}

View File

@ -41,3 +41,12 @@ var GetAllHardCodedMetrics = func() []models.Metric {
return response
}
var GetHardCodedNamespaces = func() []string {
var namespaces []string
for key := range constants.NamespaceMetricsMap {
namespaces = append(namespaces, key)
}
return namespaces
}

View File

@ -40,7 +40,9 @@ export class CloudWatchAPI extends CloudWatchRequest {
}
getNamespaces() {
return this.memoizedGetRequest<SelectableResourceValue[]>('namespaces');
return this.memoizedGetRequest<string[]>('namespaces').then((namespaces) =>
namespaces.map((n) => ({ label: n, value: n }))
);
}
async describeLogGroups(params: DescribeLogGroupsRequest) {