Cloudwatch: Fix Unexpected error (#74420)

Fix unexpected error when creating a new cloudwatch datasource.

Involves a fair amount of refactoring, so if this causes unexpected issues related to region fetching we can turn this off with the cloudwatchNewRegionsHandler feature toggle, although we do not predict it will so we are enabling it to default to true and hope to remove it shortly.
This commit is contained in:
Sarah Zinger
2023-09-25 14:19:12 -04:00
committed by GitHub
parent 7e1b45ba31
commit ef441f02d0
25 changed files with 588 additions and 60 deletions

View File

@@ -27,6 +27,7 @@ Some features are enabled by default. You can disable these feature by setting t
| `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes | | `cloudWatchCrossAccountQuerying` | Enables cross-account querying in CloudWatch datasources | Yes |
| `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | Yes | | `redshiftAsyncQueryDataSupport` | Enable async query data support for Redshift | Yes |
| `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | Yes | | `athenaAsyncQueryDataSupport` | Enable async query data support for Athena | Yes |
| `cloudwatchNewRegionsHandler` | Refactor of /regions endpoint, no user-facing changes | Yes |
| `nestedFolderPicker` | Enables the new folder picker to work with nested folders. Requires the nestedFolders feature flag | Yes | | `nestedFolderPicker` | Enables the new folder picker to work with nested folders. Requires the nestedFolders feature flag | Yes |
| `accessTokenExpirationCheck` | Enable OAuth access_token expiration check and token refresh using the refresh_token | | | `accessTokenExpirationCheck` | Enable OAuth access_token expiration check and token refresh using the refresh_token | |
| `emptyDashboardPage` | Enable the redesigned user interface of a dashboard page that includes no panels | Yes | | `emptyDashboardPage` | Enable the redesigned user interface of a dashboard page that includes no panels | Yes |

View File

@@ -47,6 +47,7 @@ export interface FeatureToggles {
cloudWatchCrossAccountQuerying?: boolean; cloudWatchCrossAccountQuerying?: boolean;
redshiftAsyncQueryDataSupport?: boolean; redshiftAsyncQueryDataSupport?: boolean;
athenaAsyncQueryDataSupport?: boolean; athenaAsyncQueryDataSupport?: boolean;
cloudwatchNewRegionsHandler?: boolean;
showDashboardValidationWarnings?: boolean; showDashboardValidationWarnings?: boolean;
mysqlAnsiQuotes?: boolean; mysqlAnsiQuotes?: boolean;
accessControlOnCall?: boolean; accessControlOnCall?: boolean;

View File

@@ -203,6 +203,13 @@ var (
FrontendOnly: true, FrontendOnly: true,
Owner: awsDatasourcesSquad, Owner: awsDatasourcesSquad,
}, },
{
Name: "cloudwatchNewRegionsHandler",
Description: "Refactor of /regions endpoint, no user-facing changes",
Stage: FeatureStageGeneralAvailability,
Expression: "true", // enabled by default
Owner: awsDatasourcesSquad,
},
{ {
Name: "showDashboardValidationWarnings", Name: "showDashboardValidationWarnings",
Description: "Show warnings when dashboards do not validate against the schema", Description: "Show warnings when dashboards do not validate against the schema",

View File

@@ -28,6 +28,7 @@ entityStore,experimental,@grafana/grafana-app-platform-squad,true,false,false,fa
cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false,false cloudWatchCrossAccountQuerying,GA,@grafana/aws-datasources,false,false,false,false
redshiftAsyncQueryDataSupport,GA,@grafana/aws-datasources,false,false,false,false redshiftAsyncQueryDataSupport,GA,@grafana/aws-datasources,false,false,false,false
athenaAsyncQueryDataSupport,GA,@grafana/aws-datasources,false,false,false,true athenaAsyncQueryDataSupport,GA,@grafana/aws-datasources,false,false,false,true
cloudwatchNewRegionsHandler,GA,@grafana/aws-datasources,false,false,false,false
showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,false,false,false,false showDashboardValidationWarnings,experimental,@grafana/dashboards-squad,false,false,false,false
mysqlAnsiQuotes,experimental,@grafana/backend-platform,false,false,false,false mysqlAnsiQuotes,experimental,@grafana/backend-platform,false,false,false,false
accessControlOnCall,preview,@grafana/grafana-authnz-team,false,false,false,false accessControlOnCall,preview,@grafana/grafana-authnz-team,false,false,false,false
1 Name Stage Owner requiresDevMode RequiresLicense RequiresRestart FrontendOnly
28 cloudWatchCrossAccountQuerying GA @grafana/aws-datasources false false false false
29 redshiftAsyncQueryDataSupport GA @grafana/aws-datasources false false false false
30 athenaAsyncQueryDataSupport GA @grafana/aws-datasources false false false true
31 cloudwatchNewRegionsHandler GA @grafana/aws-datasources false false false false
32 showDashboardValidationWarnings experimental @grafana/dashboards-squad false false false false
33 mysqlAnsiQuotes experimental @grafana/backend-platform false false false false
34 accessControlOnCall preview @grafana/grafana-authnz-team false false false false

View File

@@ -123,6 +123,10 @@ const (
// Enable async query data support for Athena // Enable async query data support for Athena
FlagAthenaAsyncQueryDataSupport = "athenaAsyncQueryDataSupport" FlagAthenaAsyncQueryDataSupport = "athenaAsyncQueryDataSupport"
// FlagCloudwatchNewRegionsHandler
// Refactor of /regions endpoint, no user-facing changes
FlagCloudwatchNewRegionsHandler = "cloudwatchNewRegionsHandler"
// FlagShowDashboardValidationWarnings // FlagShowDashboardValidationWarnings
// Show warnings when dashboards do not validate against the schema // Show warnings when dashboards do not validate against the schema
FlagShowDashboardValidationWarnings = "showDashboardValidationWarnings" FlagShowDashboardValidationWarnings = "showDashboardValidationWarnings"

View File

@@ -49,10 +49,10 @@ var NewCWLogsClient = func(sess *session.Session) cloudwatchlogsiface.CloudWatch
return cloudwatchlogs.New(sess) return cloudwatchlogs.New(sess)
} }
// EC2 client factory. // NewEC2Client is a client factory.
// //
// Stubbable by tests. // Stubbable by tests.
var newEC2Client = func(provider client.ConfigProvider) models.EC2APIProvider { var NewEC2Client = func(provider client.ConfigProvider) models.EC2APIProvider {
return ec2.New(provider) return ec2.New(provider)
} }

View File

@@ -128,14 +128,21 @@ func (e *cloudWatchExecutor) getRequestContext(ctx context.Context, pluginCtx ba
r = instance.Settings.Region r = instance.Settings.Region
} }
ec2Client, err := e.getEC2Client(ctx, pluginCtx, defaultRegion)
if err != nil {
return models.RequestContext{}, err
}
sess, err := e.newSession(ctx, pluginCtx, r) sess, err := e.newSession(ctx, pluginCtx, r)
if err != nil { if err != nil {
return models.RequestContext{}, err return models.RequestContext{}, err
} }
return models.RequestContext{ return models.RequestContext{
OAMAPIProvider: NewOAMAPI(sess), OAMAPIProvider: NewOAMAPI(sess),
MetricsClientProvider: clients.NewMetricsClient(NewMetricsAPI(sess), e.cfg), MetricsClientProvider: clients.NewMetricsClient(NewMetricsAPI(sess), e.cfg),
LogsAPIProvider: NewLogsAPI(sess), LogsAPIProvider: NewLogsAPI(sess),
EC2APIProvider: ec2Client,
Settings: instance.Settings, Settings: instance.Settings,
Features: e.features, Features: e.features,
}, nil }, nil
@@ -233,6 +240,9 @@ func (e *cloudWatchExecutor) newSession(ctx context.Context, pluginCtx backend.P
} }
if region == defaultRegion { if region == defaultRegion {
if len(instance.Settings.Region) == 0 {
return nil, models.ErrMissingRegion
}
region = instance.Settings.Region region = instance.Settings.Region
} }
@@ -300,7 +310,7 @@ func (e *cloudWatchExecutor) getEC2Client(ctx context.Context, pluginCtx backend
return nil, err return nil, err
} }
return newEC2Client(sess), nil return NewEC2Client(sess), nil
} }
func (e *cloudWatchExecutor) getRGTAClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI, func (e *cloudWatchExecutor) getRGTAClient(ctx context.Context, pluginCtx backend.PluginContext, region string) (resourcegroupstaggingapiiface.ResourceGroupsTaggingAPIAPI,

View File

@@ -7,9 +7,12 @@ import (
"testing" "testing"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend" "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/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
@@ -28,17 +31,23 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
origNewMetricsAPI := NewMetricsAPI origNewMetricsAPI := NewMetricsAPI
origNewOAMAPI := NewOAMAPI origNewOAMAPI := NewOAMAPI
origNewLogsAPI := NewLogsAPI origNewLogsAPI := NewLogsAPI
origNewEC2Client := NewEC2Client
NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider { return nil } NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider { return nil }
var logApi mocks.LogsAPI var logApi mocks.LogsAPI
NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider { NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
return &logApi return &logApi
} }
ec2Mock := &mocks.EC2Mock{}
ec2Mock.On("DescribeRegions", mock.Anything, mock.Anything).Return(&ec2.DescribeRegionsOutput{}, nil)
NewEC2Client = func(provider client.ConfigProvider) models.EC2APIProvider {
return ec2Mock
}
t.Cleanup(func() { t.Cleanup(func() {
NewOAMAPI = origNewOAMAPI NewOAMAPI = origNewOAMAPI
NewMetricsAPI = origNewMetricsAPI NewMetricsAPI = origNewMetricsAPI
NewLogsAPI = origNewLogsAPI NewLogsAPI = origNewLogsAPI
NewEC2Client = origNewEC2Client
}) })
var api mocks.FakeMetricsAPI var api mocks.FakeMetricsAPI
@@ -46,9 +55,13 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
return &api return &api
} }
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { im := datasource.NewInstanceManager((func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil return DataSource{Settings: models.CloudWatchSettings{
}) AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Region: "us-east-1",
},
}}, nil
}))
t.Run("Should handle dimension value request and return values from the api", func(t *testing.T) { t.Run("Should handle dimension value request and return values from the api", func(t *testing.T) {
pageLimit := 100 pageLimit := 100
@@ -239,4 +252,67 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
require.Nil(t, err) require.Nil(t, err)
assert.JSONEq(t, `[{"value":{"name":"field1","percent":50}},{"value":{"name":"field2","percent":50}}]`, string(sent.Body)) assert.JSONEq(t, `[{"value":{"name":"field1","percent":50}},{"value":{"name":"field2","percent":50}}]`, string(sent.Body))
}) })
t.Run("Should handle region requests and return regions from the api", func(t *testing.T) {
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures(featuremgmt.FlagCloudwatchNewRegionsHandler, true))
req := &backend.CallResourceRequest{
Method: "GET",
Path: `/regions`,
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ID: 0},
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
require.NotNil(t, sent)
require.Equal(t, http.StatusOK, sent.Status)
require.Nil(t, err)
assert.Contains(t, string(sent.Body), `"name":"us-east-1"`)
})
t.Run("Should handle legacy region requests and feature toggle is turned off", func(t *testing.T) {
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures(featuremgmt.FlagCloudwatchNewRegionsHandler, false))
req := &backend.CallResourceRequest{
Method: "GET",
Path: `/regions`,
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ID: 0},
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
require.NotNil(t, sent)
require.Equal(t, http.StatusOK, sent.Status)
require.Nil(t, err)
assert.Contains(t, string(sent.Body), `"text":"us-east-1"`)
})
t.Run("Should error for any request when a default region is not selected", func(t *testing.T) {
imWithoutDefaultRegion := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{},
}}, nil
})
executor := newExecutor(imWithoutDefaultRegion, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures(featuremgmt.FlagCloudwatchNewRegionsHandler, false))
req := &backend.CallResourceRequest{
Method: "GET",
Path: `/regions`,
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ID: 0},
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
require.NotNil(t, sent)
require.Equal(t, http.StatusBadRequest, sent.Status)
require.Nil(t, err)
assert.Contains(t, string(sent.Body), "unexpected error missing default region")
})
} }

View File

@@ -6,6 +6,7 @@ import (
"testing" "testing"
"github.com/aws/aws-sdk-go/aws" "github.com/aws/aws-sdk-go/aws"
awsclient "github.com/aws/aws-sdk-go/aws/client"
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch" "github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
@@ -94,6 +95,7 @@ func Test_CheckHealth(t *testing.T) {
origNewMetricsAPI := NewMetricsAPI origNewMetricsAPI := NewMetricsAPI
origNewCWLogsClient := NewCWLogsClient origNewCWLogsClient := NewCWLogsClient
origNewLogsAPI := NewLogsAPI origNewLogsAPI := NewLogsAPI
t.Cleanup(func() { t.Cleanup(func() {
NewMetricsAPI = origNewMetricsAPI NewMetricsAPI = origNewMetricsAPI
NewCWLogsClient = origNewCWLogsClient NewCWLogsClient = origNewCWLogsClient
@@ -107,12 +109,16 @@ func Test_CheckHealth(t *testing.T) {
NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider { NewLogsAPI = func(sess *session.Session) models.CloudWatchLogsAPIProvider {
return client return client
} }
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Region: "us-east-1",
},
}}, nil
})
t.Run("successfully query metrics and logs", func(t *testing.T) { t.Run("successfully query metrics and logs", func(t *testing.T) {
client = fakeCheckHealthClient{} client = fakeCheckHealthClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
@@ -131,9 +137,7 @@ func Test_CheckHealth(t *testing.T) {
describeLogGroups: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) { describeLogGroups: func(input *cloudwatchlogs.DescribeLogGroupsInput) (*cloudwatchlogs.DescribeLogGroupsOutput, error) {
return nil, fmt.Errorf("some logs query error") return nil, fmt.Errorf("some logs query error")
}} }}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
@@ -152,9 +156,7 @@ func Test_CheckHealth(t *testing.T) {
listMetricsPages: func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error { listMetricsPages: func(input *cloudwatch.ListMetricsInput, fn func(*cloudwatch.ListMetricsOutput, bool) bool) error {
return fmt.Errorf("some list metrics error") return fmt.Errorf("some list metrics error")
}} }}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{ resp, err := executor.CheckHealth(context.Background(), &backend.CheckHealthRequest{
@@ -170,9 +172,7 @@ func Test_CheckHealth(t *testing.T) {
t.Run("fail to get clients", func(t *testing.T) { t.Run("fail to get clients", func(t *testing.T) {
client = fakeCheckHealthClient{} client = fakeCheckHealthClient{}
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil
})
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) { executor := newExecutor(im, newTestConfig(), &fakeSessionCache{getSession: func(c awsds.SessionConfig) (*session.Session, error) {
return nil, fmt.Errorf("some sessions error") return nil, fmt.Errorf("some sessions error")
}}, featuremgmt.WithFeatures()) }}, featuremgmt.WithFeatures())
@@ -194,12 +194,15 @@ func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *te
origNewMetricsAPI := NewMetricsAPI origNewMetricsAPI := NewMetricsAPI
origNewOAMAPI := NewOAMAPI origNewOAMAPI := NewOAMAPI
origNewLogsAPI := NewLogsAPI origNewLogsAPI := NewLogsAPI
origNewEC2Client := NewEC2Client
NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider { return nil } NewMetricsAPI = func(sess *session.Session) models.CloudWatchMetricsAPIProvider { return nil }
NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider { return nil } NewOAMAPI = func(sess *session.Session) models.OAMAPIProvider { return nil }
NewEC2Client = func(provider awsclient.ConfigProvider) models.EC2APIProvider { return nil }
t.Cleanup(func() { t.Cleanup(func() {
NewOAMAPI = origNewOAMAPI NewOAMAPI = origNewOAMAPI
NewMetricsAPI = origNewMetricsAPI NewMetricsAPI = origNewMetricsAPI
NewLogsAPI = origNewLogsAPI NewLogsAPI = origNewLogsAPI
NewEC2Client = origNewEC2Client
}) })
var logsApi mocks.LogsAPI var logsApi mocks.LogsAPI
@@ -208,7 +211,11 @@ func TestQuery_ResourceRequest_DescribeLogGroups_with_CrossAccountQuerying(t *te
} }
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil return DataSource{Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Region: "us-east-1",
},
}}, nil
}) })
t.Run("maps log group api response to resource response of log-groups", func(t *testing.T) { t.Run("maps log group api response to resource response of log-groups", func(t *testing.T) {

View File

@@ -10,6 +10,7 @@ import (
"github.com/aws/aws-sdk-go/aws/session" "github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs" "github.com/aws/aws-sdk-go/service/cloudwatchlogs"
"github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface" "github.com/aws/aws-sdk-go/service/cloudwatchlogs/cloudwatchlogsiface"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend" "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/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
@@ -18,9 +19,9 @@ import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
@@ -200,7 +201,11 @@ func TestQuery_StartQuery(t *testing.T) {
} }
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil return DataSource{Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Region: "us-east-2",
},
}}, nil
}) })
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
@@ -253,7 +258,11 @@ func TestQuery_StartQuery(t *testing.T) {
} }
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil return DataSource{Settings: models.CloudWatchSettings{
AWSDatasourceSettings: awsds.AWSDatasourceSettings{
Region: "us-east-2",
},
}}, nil
}) })
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())

View File

@@ -12,38 +12,43 @@ import (
"github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/ec2"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi"
"github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface" "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi/resourcegroupstaggingapiiface"
"github.com/grafana/grafana-aws-sdk/pkg/awsds"
"github.com/grafana/grafana-plugin-sdk-go/backend" "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/datasource"
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
"github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock" "github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
) )
func TestQuery_Regions(t *testing.T) { func TestQuery_Regions(t *testing.T) {
origNewEC2Client := newEC2Client origNewEC2Client := NewEC2Client
t.Cleanup(func() { t.Cleanup(func() {
newEC2Client = origNewEC2Client NewEC2Client = origNewEC2Client
}) })
var cli fakeEC2Client ec2Mock := &mocks.EC2Mock{}
NewEC2Client = func(provider client.ConfigProvider) models.EC2APIProvider {
newEC2Client = func(client.ConfigProvider) models.EC2APIProvider { return ec2Mock
return cli
} }
t.Run("An extra region", func(t *testing.T) { t.Run("An extra region", func(t *testing.T) {
const regionName = "xtra-region" const regionName = "xtra-region"
cli = fakeEC2Client{ ec2Mock.On("DescribeRegions", mock.Anything, mock.Anything).Return(&ec2.DescribeRegionsOutput{
regions: []string{regionName}, Regions: []*ec2.Region{
} {
RegionName: utils.Pointer(regionName),
},
},
}, nil)
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "us-east-2"}}}, nil
}) })
executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures()) executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}, featuremgmt.WithFeatures())
@@ -91,16 +96,16 @@ func buildSortedSliceOfDefaultAndExtraRegions(t *testing.T, regionName string) [
} }
func Test_handleGetRegions_regionCache(t *testing.T) { func Test_handleGetRegions_regionCache(t *testing.T) {
origNewEC2Client := newEC2Client origNewEC2Client := NewEC2Client
t.Cleanup(func() { t.Cleanup(func() {
newEC2Client = origNewEC2Client NewEC2Client = origNewEC2Client
}) })
cli := mockEC2Client{} cli := mockEC2Client{}
newEC2Client = func(client.ConfigProvider) models.EC2APIProvider { NewEC2Client = func(client.ConfigProvider) models.EC2APIProvider {
return &cli return &cli
} }
im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { im := datasource.NewInstanceManager(func(ctx context.Context, s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
return DataSource{Settings: models.CloudWatchSettings{}}, nil return DataSource{Settings: models.CloudWatchSettings{AWSDatasourceSettings: awsds.AWSDatasourceSettings{Region: "us-east-2"}}}, nil
}) })
t.Run("AWS only called once for multiple calls to handleGetRegions", func(t *testing.T) { t.Run("AWS only called once for multiple calls to handleGetRegions", func(t *testing.T) {
@@ -119,22 +124,21 @@ func Test_handleGetRegions_regionCache(t *testing.T) {
cli.AssertNumberOfCalls(t, "DescribeRegions", 1) cli.AssertNumberOfCalls(t, "DescribeRegions", 1)
}) })
} }
func TestQuery_InstanceAttributes(t *testing.T) { func TestQuery_InstanceAttributes(t *testing.T) {
origNewEC2Client := newEC2Client origNewEC2Client := NewEC2Client
t.Cleanup(func() { t.Cleanup(func() {
newEC2Client = origNewEC2Client NewEC2Client = origNewEC2Client
}) })
var cli fakeEC2Client var cli oldEC2Client
newEC2Client = func(client.ConfigProvider) models.EC2APIProvider { NewEC2Client = func(client.ConfigProvider) models.EC2APIProvider {
return cli return cli
} }
t.Run("Get instance ID", func(t *testing.T) { t.Run("Get instance ID", func(t *testing.T) {
const instanceID = "i-12345678" const instanceID = "i-12345678"
cli = fakeEC2Client{ cli = oldEC2Client{
reservations: []*ec2.Reservation{ reservations: []*ec2.Reservation{
{ {
Instances: []*ec2.Instance{ Instances: []*ec2.Instance{
@@ -183,19 +187,19 @@ func TestQuery_InstanceAttributes(t *testing.T) {
} }
func TestQuery_EBSVolumeIDs(t *testing.T) { func TestQuery_EBSVolumeIDs(t *testing.T) {
origNewEC2Client := newEC2Client origNewEC2Client := NewEC2Client
t.Cleanup(func() { t.Cleanup(func() {
newEC2Client = origNewEC2Client NewEC2Client = origNewEC2Client
}) })
var cli fakeEC2Client var cli oldEC2Client
newEC2Client = func(client.ConfigProvider) models.EC2APIProvider { NewEC2Client = func(client.ConfigProvider) models.EC2APIProvider {
return cli return cli
} }
t.Run("", func(t *testing.T) { t.Run("", func(t *testing.T) {
cli = fakeEC2Client{ cli = oldEC2Client{
reservations: []*ec2.Reservation{ reservations: []*ec2.Reservation{
{ {
Instances: []*ec2.Instance{ Instances: []*ec2.Instance{

View File

@@ -0,0 +1,30 @@
package mocks
import (
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/mock"
)
type RegionsService struct {
mock.Mock
}
func (r *RegionsService) GetRegions() ([]resources.ResourceResponse[resources.Region], error) {
args := r.Called()
return args.Get(0).(([]resources.ResourceResponse[resources.Region])), args.Error(1)
}
type EC2Mock struct {
mock.Mock
}
func (e *EC2Mock) DescribeRegions(in *ec2.DescribeRegionsInput) (*ec2.DescribeRegionsOutput, error) {
args := e.Called()
return args.Get(0).(*ec2.DescribeRegionsOutput), args.Error(1)
}
func (e *EC2Mock) DescribeInstancesPages(in *ec2.DescribeInstancesInput, fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
args := e.Called(in, fn)
return args.Error(0)
}

View File

@@ -21,6 +21,7 @@ type RequestContext struct {
MetricsClientProvider MetricsClientProvider MetricsClientProvider MetricsClientProvider
LogsAPIProvider CloudWatchLogsAPIProvider LogsAPIProvider CloudWatchLogsAPIProvider
OAMAPIProvider OAMAPIProvider OAMAPIProvider OAMAPIProvider
EC2APIProvider EC2APIProvider
Settings CloudWatchSettings Settings CloudWatchSettings
Features featuremgmt.FeatureToggles Features featuremgmt.FeatureToggles
} }
@@ -41,6 +42,10 @@ type AccountsProvider interface {
GetAccountsForCurrentUserOrRole() ([]resources.ResourceResponse[resources.Account], error) GetAccountsForCurrentUserOrRole() ([]resources.ResourceResponse[resources.Account], error)
} }
type RegionsAPIProvider interface {
GetRegions() ([]resources.ResourceResponse[resources.Region], error)
}
// Clients // Clients
type MetricsClientProvider interface { type MetricsClientProvider interface {
ListMetricsWithPageLimit(params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error) ListMetricsWithPageLimit(params *cloudwatch.ListMetricsInput) ([]resources.MetricResponse, error)

View File

@@ -0,0 +1,7 @@
package models
import "fmt"
// put misc expected user errors here
var ErrMissingRegion = fmt.Errorf("missing default region")

View File

@@ -24,6 +24,10 @@ type Account struct {
IsMonitoringAccount bool `json:"isMonitoringAccount"` IsMonitoringAccount bool `json:"isMonitoringAccount"`
} }
type Region struct {
Name string `json:"name"`
}
type Metric struct { type Metric struct {
Name string `json:"name"` Name string `json:"name"`
Namespace string `json:"namespace"` Namespace string `json:"namespace"`

View File

@@ -15,7 +15,6 @@ import (
func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux { func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux {
mux := http.NewServeMux() mux := http.NewServeMux()
mux.HandleFunc("/regions", handleResourceReq(e.handleGetRegions))
mux.HandleFunc("/ebs-volume-ids", handleResourceReq(e.handleGetEbsVolumeIds)) mux.HandleFunc("/ebs-volume-ids", handleResourceReq(e.handleGetEbsVolumeIds))
mux.HandleFunc("/ec2-instance-attribute", handleResourceReq(e.handleGetEc2InstanceAttribute)) mux.HandleFunc("/ec2-instance-attribute", handleResourceReq(e.handleGetEc2InstanceAttribute))
mux.HandleFunc("/resource-arns", handleResourceReq(e.handleGetResourceArns)) mux.HandleFunc("/resource-arns", handleResourceReq(e.handleGetResourceArns))
@@ -28,6 +27,13 @@ func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux {
mux.HandleFunc("/log-group-fields", routes.ResourceRequestMiddleware(routes.LogGroupFieldsHandler, logger, e.getRequestContext)) mux.HandleFunc("/log-group-fields", routes.ResourceRequestMiddleware(routes.LogGroupFieldsHandler, logger, e.getRequestContext))
mux.HandleFunc("/external-id", routes.ResourceRequestMiddleware(routes.ExternalIdHandler, logger, e.getRequestContext)) mux.HandleFunc("/external-id", routes.ResourceRequestMiddleware(routes.ExternalIdHandler, logger, e.getRequestContext))
// feature is enabled by default, just putting behind a feature flag in case of unexpected bugs
if e.features.IsEnabled("cloudwatchNewRegionsHandler") {
mux.HandleFunc("/regions", routes.ResourceRequestMiddleware(routes.RegionsHandler, logger, e.getRequestContext))
} else {
mux.HandleFunc("/regions", handleResourceReq(e.handleGetRegions))
}
// remove this once AWS's Cross Account Observability is supported in GovCloud // remove this once AWS's Cross Account Observability is supported in GovCloud
mux.HandleFunc("/legacy-log-groups", handleResourceReq(e.handleGetLogGroups)) mux.HandleFunc("/legacy-log-groups", handleResourceReq(e.handleGetLogGroups))

View File

@@ -0,0 +1,48 @@
package routes
import (
"context"
"encoding/json"
"errors"
"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"
)
const (
defaultRegion = "default"
)
func RegionsHandler(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
service, err := newRegionsService(ctx, pluginCtx, reqCtxFactory, defaultRegion)
if err != nil {
if errors.Is(err, models.ErrMissingRegion) {
return nil, models.NewHttpError("Error in Regions Handler when connecting to aws without a default region selection", http.StatusBadRequest, err)
}
return nil, models.NewHttpError("Error in Regions Handler when connecting to aws", http.StatusInternalServerError, err)
}
regions, err := service.GetRegions()
if err != nil {
return nil, models.NewHttpError("Error in Regions Handler while fetching regions", http.StatusInternalServerError, err)
}
regionsResponse, err := json.Marshal(regions)
if err != nil {
return nil, models.NewHttpError("Error in Regions Handler while parsing regions", http.StatusInternalServerError, err)
}
return regionsResponse, nil
}
var newRegionsService = func(ctx context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
reqCtx, err := reqCtxFactory(ctx, pluginCtx, region)
if err != nil {
return nil, err
}
return services.NewRegionsService(reqCtx.EC2APIProvider), nil
}

View File

@@ -0,0 +1,84 @@
package routes
import (
"context"
"errors"
"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/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
)
func TestRegionsRoute(t *testing.T) {
origNewRegionsService := newRegionsService
t.Cleanup(func() {
newRegionsService = origNewRegionsService
})
t.Run("returns 200 and regions", func(t *testing.T) {
mockRegionService := mocks.RegionsService{}
newRegionsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
return &mockRegionService, nil
}
mockRegionService.On("GetRegions", mock.Anything).Return([]resources.ResourceResponse[resources.Region]{{
Value: resources.Region{
Name: "us-east-1",
},
}}, nil).Once()
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ResourceRequestMiddleware(RegionsHandler, logger, nil))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusOK, rr.Code)
assert.Contains(t, rr.Body.String(), "us-east-1")
})
t.Run("returns 400 when the service returns a missing region error", func(t *testing.T) {
newRegionsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
return nil, models.ErrMissingRegion
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ResourceRequestMiddleware(RegionsHandler, logger, nil))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Contains(t, rr.Body.String(), "Error in Regions Handler when connecting to aws without a default region selection: missing default region")
})
t.Run("returns 500 when the service returns an unexpected error", func(t *testing.T) {
newRegionsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
return nil, errors.New("something unexpected happened")
}
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ResourceRequestMiddleware(RegionsHandler, logger, nil))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Contains(t, rr.Body.String(), "Error in Regions Handler when connecting to aws: something unexpected happened")
})
t.Run("returns 500 when get regions returns an error", func(t *testing.T) {
mockRegionService := mocks.RegionsService{}
newRegionsService = func(_ context.Context, pluginCtx backend.PluginContext, reqCtxFactory models.RequestContextFactoryFunc, region string) (models.RegionsAPIProvider, error) {
return &mockRegionService, nil
}
mockRegionService.On("GetRegions", mock.Anything).Return([]resources.ResourceResponse[resources.Region](nil), errors.New("aws is having some kind of outage")).Once()
rr := httptest.NewRecorder()
handler := http.HandlerFunc(ResourceRequestMiddleware(RegionsHandler, logger, nil))
req := httptest.NewRequest("GET", `/regions`, nil)
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Contains(t, rr.Body.String(), "Error in Regions Handler while fetching regions: aws is having some kind of outage")
})
}

View File

@@ -0,0 +1,55 @@
package services
import (
"sort"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
)
type RegionsService struct {
models.EC2APIProvider
}
func NewRegionsService(ec2client models.EC2APIProvider) models.RegionsAPIProvider {
return &RegionsService{
ec2client,
}
}
func mergeEC2RegionsAndConstantRegions(regions map[string]struct{}, ec2Regions []*ec2.Region) {
for _, region := range ec2Regions {
if _, ok := regions[*region.RegionName]; !ok {
regions[*region.RegionName] = struct{}{}
}
}
}
func (r *RegionsService) GetRegions() ([]resources.ResourceResponse[resources.Region], error) {
regions := constants.Regions()
result := make([]resources.ResourceResponse[resources.Region], 0)
ec2Regions, err := r.DescribeRegions(&ec2.DescribeRegionsInput{})
if err != nil {
return nil, err
}
mergeEC2RegionsAndConstantRegions(regions, ec2Regions.Regions)
for region := range regions {
result = append(result, resources.ResourceResponse[resources.Region]{
Value: resources.Region{
Name: region,
},
})
}
sort.Slice(result, func(i, j int) bool {
return result[i].Value.Name < result[j].Value.Name
})
return result, nil
}

View File

@@ -0,0 +1,44 @@
package services
import (
"testing"
"github.com/aws/aws-sdk-go/service/ec2"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/resources"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
"github.com/stretchr/testify/assert"
)
func TestRegions(t *testing.T) {
t.Run("returns regions from the api and merges them with default regions", func(t *testing.T) {
mockRegions := &ec2.DescribeRegionsOutput{
Regions: []*ec2.Region{
{
RegionName: utils.Pointer("earth-1"),
},
},
}
ec2Mock := &mocks.EC2Mock{}
ec2Mock.On("DescribeRegions").Return(mockRegions, nil)
regions, err := NewRegionsService(ec2Mock).GetRegions()
assert.NoError(t, err)
assert.Contains(t, regions, resources.ResourceResponse[resources.Region]{
Value: resources.Region{
Name: "us-east-2",
},
})
assert.Contains(t, regions, resources.ResourceResponse[resources.Region]{
Value: resources.Region{
Name: "earth-1",
},
})
})
t.Run("forwards error if DescribeRegions errors out", func(t *testing.T) {
ec2Mock := &mocks.EC2Mock{}
ec2Mock.On("DescribeRegions").Return((*ec2.DescribeRegionsOutput)(nil), assert.AnError)
_, err := NewRegionsService(ec2Mock).GetRegions()
assert.Error(t, err)
})
}

View File

@@ -135,14 +135,15 @@ func (c *mockEC2Client) DescribeInstancesPages(in *ec2.DescribeInstancesInput, f
return args.Error(0) return args.Error(0)
} }
type fakeEC2Client struct { // Please use mockEC2Client above, we are slowly migrating towards using testify's mocks only
type oldEC2Client struct {
ec2iface.EC2API ec2iface.EC2API
regions []string regions []string
reservations []*ec2.Reservation reservations []*ec2.Reservation
} }
func (c fakeEC2Client) DescribeRegions(*ec2.DescribeRegionsInput) (*ec2.DescribeRegionsOutput, error) { func (c oldEC2Client) DescribeRegions(*ec2.DescribeRegionsInput) (*ec2.DescribeRegionsOutput, error) {
regions := []*ec2.Region{} regions := []*ec2.Region{}
for _, region := range c.regions { for _, region := range c.regions {
regions = append(regions, &ec2.Region{ regions = append(regions, &ec2.Region{
@@ -154,7 +155,7 @@ func (c fakeEC2Client) DescribeRegions(*ec2.DescribeRegionsInput) (*ec2.Describe
}, nil }, nil
} }
func (c fakeEC2Client) DescribeInstancesPages(in *ec2.DescribeInstancesInput, func (c oldEC2Client) DescribeInstancesPages(in *ec2.DescribeInstancesInput,
fn func(*ec2.DescribeInstancesOutput, bool) bool) error { fn func(*ec2.DescribeInstancesOutput, bool) bool) error {
reservations := []*ec2.Reservation{} reservations := []*ec2.Reservation{}
for _, r := range c.reservations { for _, r := range c.reservations {

View File

@@ -13,7 +13,7 @@ export function setupMockedResourcesAPI({
getMock, getMock,
}: { }: {
getMock?: jest.Mock; getMock?: jest.Mock;
response?: Array<{ text: string; label: string; value: string }>; response?: unknown;
variables?: CustomVariableModel[]; variables?: CustomVariableModel[];
mockGetVariableName?: boolean; mockGetVariableName?: boolean;
} = {}) { } = {}) {

View File

@@ -1,3 +1,5 @@
import { config } from '@grafana/runtime';
import { setupMockedResourcesAPI } from '../__mocks__/ResourcesAPI'; import { setupMockedResourcesAPI } from '../__mocks__/ResourcesAPI';
describe('ResourcesAPI', () => { describe('ResourcesAPI', () => {
@@ -118,4 +120,109 @@ describe('ResourcesAPI', () => {
]); ]);
}); });
}); });
const originalFeatureToggleValue = config.featureToggles.cloudwatchNewRegionsHandler;
describe('getRegions', () => {
afterEach(() => {
config.featureToggles.cloudwatchNewRegionsHandler = originalFeatureToggleValue;
});
it('should return regions as an array of options when using legacy regions route', async () => {
config.featureToggles.cloudwatchNewRegionsHandler = false;
const response = Promise.resolve([
{
text: 'US East (Ohio)',
value: 'us-east-2',
label: 'US East (Ohio)',
},
{
text: 'US East (N. Virginia)',
value: 'us-east-1',
label: 'US East (N. Virginia)',
},
{
text: 'US West (N. California)',
value: 'us-west-1',
label: 'US West (N. California)',
},
]);
const { api } = setupMockedResourcesAPI({ response });
const expectedRegions = [
{
text: 'default',
value: 'default',
label: 'default',
},
{
text: 'US East (Ohio)',
value: 'us-east-2',
label: 'US East (Ohio)',
},
{
text: 'US East (N. Virginia)',
value: 'us-east-1',
label: 'US East (N. Virginia)',
},
{
text: 'US West (N. California)',
value: 'us-west-1',
label: 'US West (N. California)',
},
];
const regions = await api.getRegions();
expect(regions).toEqual(expectedRegions);
});
it('should return regions as an array of options when using new regions route', async () => {
config.featureToggles.cloudwatchNewRegionsHandler = true;
const response = Promise.resolve([
{
value: {
name: 'us-east-2',
},
},
{
value: {
name: 'us-east-1',
},
},
{
value: {
name: 'us-west-1',
},
},
]);
const { api } = setupMockedResourcesAPI({ response });
const expectedRegions = [
{
text: 'default',
value: 'default',
label: 'default',
},
{
text: 'us-east-2',
value: 'us-east-2',
label: 'us-east-2',
},
{
text: 'us-east-1',
value: 'us-east-1',
label: 'us-east-1',
},
{
text: 'us-west-1',
value: 'us-west-1',
label: 'us-west-1',
},
];
const regions = await api.getRegions();
expect(regions).toEqual(expectedRegions);
});
});
}); });

View File

@@ -1,7 +1,7 @@
import { memoize } from 'lodash'; import { memoize } from 'lodash';
import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data'; import { DataSourceInstanceSettings, SelectableValue } from '@grafana/data';
import { getBackendSrv } from '@grafana/runtime'; import { getBackendSrv, config } from '@grafana/runtime';
import { TemplateSrv } from 'app/features/templating/template_srv'; import { TemplateSrv } from 'app/features/templating/template_srv';
import { CloudWatchRequest } from '../query-runner/CloudWatchRequest'; import { CloudWatchRequest } from '../query-runner/CloudWatchRequest';
@@ -19,6 +19,7 @@ import {
GetDimensionValuesRequest, GetDimensionValuesRequest,
MetricResponse, MetricResponse,
SelectableResourceValue, SelectableResourceValue,
RegionResponse,
} from './types'; } from './types';
export class ResourcesAPI extends CloudWatchRequest { export class ResourcesAPI extends CloudWatchRequest {
@@ -51,11 +52,23 @@ export class ResourcesAPI extends CloudWatchRequest {
.catch(() => false); .catch(() => false);
} }
getRegions() { getRegions(): Promise<SelectableResourceValue[]> {
return this.memoizedGetRequest<SelectableResourceValue[]>('regions').then((regions) => [ if (!config.featureToggles.cloudwatchNewRegionsHandler) {
{ label: 'default', value: 'default', text: 'default' }, return this.memoizedGetRequest<SelectableResourceValue[]>('regions').then((regions) => [
...regions.filter((r) => r.value), { label: 'default', value: 'default', text: 'default' },
]); ...regions.filter((r) => r.value),
]);
}
return this.memoizedGetRequest<Array<ResourceResponse<RegionResponse>>>('regions').then((regions) => {
return [
{ label: 'default', value: 'default', text: 'default' },
...regions.map((r) => ({
label: r.value.name,
value: r.value.name,
text: r.value.name,
})),
].filter((r) => r.value);
});
} }
getNamespaces() { getNamespaces() {

View File

@@ -59,6 +59,10 @@ export interface MetricResponse {
namespace: string; namespace: string;
} }
export interface RegionResponse {
name: string;
}
export interface SelectableResourceValue extends SelectableValue<string> { export interface SelectableResourceValue extends SelectableValue<string> {
text: string; text: string;
} }