diff --git a/go.mod b/go.mod index e40e2997aa..58ef7cee2d 100644 --- a/go.mod +++ b/go.mod @@ -140,6 +140,7 @@ require ( github.com/sagikazarmark/locafero v0.6.0 // indirect github.com/sagikazarmark/slog-shim v0.1.0 // indirect github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 // indirect + github.com/sethvargo/go-retry v0.3.0 // indirect github.com/skeema/knownhosts v1.3.0 // indirect github.com/sourcegraph/conc v0.3.0 // indirect github.com/tidwall/gjson v1.18.0 // indirect diff --git a/go.sum b/go.sum index 2380beb8c3..668c4e3b9f 100644 --- a/go.sum +++ b/go.sum @@ -1405,6 +1405,8 @@ github.com/sahilm/fuzzy v0.1.1 h1:ceu5RHF8DGgoi+/dR5PsECjCDH1BE3Fnmpo7aVXOdRA= github.com/sahilm/fuzzy v0.1.1/go.mod h1:VFvziUEIMCrT6A6tw2RFIXPXXmzXbOsSHF0DOI8ZK9Y= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sethvargo/go-retry v0.3.0 h1:EEt31A35QhrcRZtrYFDTBg91cqZVnFL2navjDrah2SE= +github.com/sethvargo/go-retry v0.3.0/go.mod h1:mNX17F0C/HguQMyMyJxcnU471gOZGxCLyYaFyAZraas= github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= diff --git a/pkg/retry/retry.go b/pkg/retry/retry.go new file mode 100644 index 0000000000..b20e652c60 --- /dev/null +++ b/pkg/retry/retry.go @@ -0,0 +1,82 @@ +package retry + +import ( + "context" + "time" + + "github.com/sethvargo/go-retry" +) + +const ( + defaultInterval = 1 * time.Second + defaultMaxRetries = 10 + defaultMaxDuration = 60 * time.Second +) + +// RetryConfig is the configuration for a retry operation. +type RetryConfig struct { + // BackoffStrategy is the backoff strategy to use. + BackoffStrategy retry.Backoff +} + +// Retryer is a utility for retrying functions. +type Retryer struct { + config *RetryConfig +} + +// NewNoOpRetryer creates a new Retryer that does not retry. +// This is useful for testing. +func NewNoOpRetryer() *Retryer { + b := retry.NewConstant(1 * time.Second) + b = retry.WithMaxRetries(0, b) + + return NewRetryer(&RetryConfig{ + BackoffStrategy: b, + }) +} + +// DefaultBackoffStrategy returns the default backoff strategy. +// The default backoff strategy is an exponential backoff with a maximum duration and maximum retries. +func DefaultBackoffStrategy() retry.Backoff { + b := retry.NewExponential(1 * time.Second) + b = retry.WithMaxDuration(defaultMaxDuration, b) + b = retry.WithMaxRetries(defaultMaxRetries, b) + + return b +} + +// NewDefaultRetryer creates a new Retryer with the default configuration. +// The default configuration is an exponential backoff with a maximum duration and maximum retries. +func NewDefaultRetryer() *Retryer { + return NewRetryer(&RetryConfig{ + BackoffStrategy: DefaultBackoffStrategy(), + }) +} + +// NewRetryer creates a new Retryer with the given configuration. +// If either the config or config.BackoffStrategy are nil, +// the default configuration is used. +// The default configuration is an exponential backoff with a maximum duration and maximum retries. +func NewRetryer(config *RetryConfig) *Retryer { + retryConfig := &RetryConfig{} + + if config != nil && config.BackoffStrategy != nil { + retryConfig.BackoffStrategy = config.BackoffStrategy + } else { + retryConfig.BackoffStrategy = DefaultBackoffStrategy() + } + + return &Retryer{ + config: retryConfig, + } +} + +// RetryFunc retries the given function with the backoff strategy. +func (r *Retryer) RetryFunc(ctx context.Context, f func(ctx context.Context) error) error { + return retry.Do(ctx, r.config.BackoffStrategy, f) +} + +// RetryableError marks an error as retryable. +func RetryableError(err error) error { + return retry.RetryableError(err) +} diff --git a/pkg/retry/retry_test.go b/pkg/retry/retry_test.go new file mode 100644 index 0000000000..8b628d942f --- /dev/null +++ b/pkg/retry/retry_test.go @@ -0,0 +1,89 @@ +package retry + +import ( + "context" + "errors" + "testing" + "time" + + goretry "github.com/sethvargo/go-retry" + "github.com/stretchr/testify/require" +) + +func TestNewNoOpRetryer(t *testing.T) { + retryer := NewNoOpRetryer() + require.NotNil(t, retryer) + require.NotNil(t, retryer.config) + require.NotNil(t, retryer.config.BackoffStrategy) + + expectedBackoffStrategy := goretry.NewConstant(time.Second * 1) + expectedBackoffStrategy = goretry.WithMaxRetries(0, expectedBackoffStrategy) + + require.IsType(t, expectedBackoffStrategy, retryer.config.BackoffStrategy) +} + +func TestDefaultBackoffStrategy(t *testing.T) { + backoff := DefaultBackoffStrategy() + require.NotNil(t, backoff) +} + +func TestNewDefaultRetryer(t *testing.T) { + retryer := NewDefaultRetryer() + require.NotNil(t, retryer) + require.NotNil(t, retryer.config) + require.NotNil(t, retryer.config.BackoffStrategy) + + expectedBackoffStrategy := goretry.NewConstant(time.Second * 1) + expectedBackoffStrategy = goretry.WithMaxRetries(0, expectedBackoffStrategy) + + require.IsType(t, expectedBackoffStrategy, retryer.config.BackoffStrategy) +} + +func TestNewRetryer(t *testing.T) { + config := &RetryConfig{ + BackoffStrategy: goretry.NewConstant(1 * time.Second), + } + retryer := NewRetryer(config) + require.NotNil(t, retryer) + require.NotNil(t, retryer.config) + + retryer = NewRetryer(nil) + require.NotNil(t, retryer) + require.NotNil(t, retryer.config) + require.NotNil(t, retryer.config.BackoffStrategy) + + config = &RetryConfig{} + retryer = NewRetryer(config) + require.NotNil(t, retryer) + require.NotNil(t, retryer.config) + require.NotNil(t, retryer.config.BackoffStrategy) +} + +func TestRetryer_RetryFunc(t *testing.T) { + retryer := NewDefaultRetryer() + ctx := context.Background() + + // Test successful function + err := retryer.RetryFunc(ctx, func(ctx context.Context) error { + return nil + }) + require.NoError(t, err) + + // Test retryable error + retryCount := 0 + err = retryer.RetryFunc(ctx, func(ctx context.Context) error { + retryCount++ + if retryCount < 3 { + return RetryableError(errors.New("retryable error")) + } + return nil + }) + require.NoError(t, err) + require.Equal(t, 3, retryCount) + + // Test non-retryable error + err = retryer.RetryFunc(ctx, func(ctx context.Context) error { + return errors.New("non-retryable error") + }) + require.Error(t, err) +} diff --git a/pkg/ucp/frontend/aws/routes.go b/pkg/ucp/frontend/aws/routes.go index 8bf2ba9327..d44fd9e934 100644 --- a/pkg/ucp/frontend/aws/routes.go +++ b/pkg/ucp/frontend/aws/routes.go @@ -29,6 +29,7 @@ import ( "github.com/radius-project/radius/pkg/armrpc/frontend/defaultoperation" "github.com/radius-project/radius/pkg/armrpc/frontend/server" aztoken "github.com/radius-project/radius/pkg/azure/tokencredentials" + "github.com/radius-project/radius/pkg/retry" "github.com/radius-project/radius/pkg/ucp" "github.com/radius-project/radius/pkg/ucp/api/v20231001preview" ucp_aws "github.com/radius-project/radius/pkg/ucp/aws" @@ -225,7 +226,7 @@ func (m *Module) Initialize(ctx context.Context) (http.Handler, error) { OperationType: &v1.OperationType{Type: OperationTypeAWSResource, Method: v1.OperationGetImperative}, ResourceType: OperationTypeAWSResource, ControllerFactory: func(opt controller.Options) (controller.Controller, error) { - return awsproxy_ctrl.NewGetAWSResourceWithPost(opt, m.AWSClients) + return awsproxy_ctrl.NewGetAWSResourceWithPost(opt, m.AWSClients, retry.NewDefaultRetryer()) }, }, { diff --git a/pkg/ucp/frontend/controller/awsproxy/createorupdateawsresource.go b/pkg/ucp/frontend/controller/awsproxy/createorupdateawsresource.go index aca9f6c88d..96727f7e2b 100644 --- a/pkg/ucp/frontend/controller/awsproxy/createorupdateawsresource.go +++ b/pkg/ucp/frontend/controller/awsproxy/createorupdateawsresource.go @@ -87,7 +87,7 @@ func (p *CreateOrUpdateAWSResource) Run(ctx context.Context, w http.ResponseWrit } cloudControlOpts := []func(*cloudcontrol.Options){CloudControlRegionOption(region)} - cloudFormationOpts := []func(*cloudformation.Options){CloudFormationWithRegionOption(region)} + cloudFormationOpts := []func(*cloudformation.Options){CloudFormationRegionOption(region)} // Create and update work differently for AWS - we need to know if the resource // we're working on exists already. @@ -125,7 +125,6 @@ func (p *CreateOrUpdateAWSResource) Run(ctx context.Context, w http.ResponseWrit if existing { // Get resource type schema - describeTypeOutput, err := p.awsClients.CloudFormation.DescribeType(ctx, &cloudformation.DescribeTypeInput{ Type: types.RegistryTypeResource, TypeName: to.Ptr(serviceCtx.ResourceTypeInAWSFormat()), diff --git a/pkg/ucp/frontend/controller/awsproxy/createorupdateawsresourcewithpost.go b/pkg/ucp/frontend/controller/awsproxy/createorupdateawsresourcewithpost.go index 3c0047e0b6..0d8d8b1f7a 100644 --- a/pkg/ucp/frontend/controller/awsproxy/createorupdateawsresourcewithpost.go +++ b/pkg/ucp/frontend/controller/awsproxy/createorupdateawsresourcewithpost.go @@ -76,7 +76,7 @@ func (p *CreateOrUpdateAWSResourceWithPost) Run(ctx context.Context, w http.Resp } cloudControlOpts := []func(*cloudcontrol.Options){CloudControlRegionOption(region)} - cloudFormationOpts := []func(*cloudformation.Options){CloudFormationWithRegionOption(region)} + cloudFormationOpts := []func(*cloudformation.Options){CloudFormationRegionOption(region)} describeTypeOutput, err := p.awsClients.CloudFormation.DescribeType(ctx, &cloudformation.DescribeTypeInput{ Type: types.RegistryTypeResource, diff --git a/pkg/ucp/frontend/controller/awsproxy/deleteawsresourcewithpost.go b/pkg/ucp/frontend/controller/awsproxy/deleteawsresourcewithpost.go index 876d322ec5..d7ded3e9fa 100644 --- a/pkg/ucp/frontend/controller/awsproxy/deleteawsresourcewithpost.go +++ b/pkg/ucp/frontend/controller/awsproxy/deleteawsresourcewithpost.go @@ -78,7 +78,7 @@ func (p *DeleteAWSResourceWithPost) Run(ctx context.Context, w http.ResponseWrit return armrpc_rest.NewBadRequestARMResponse(e), nil } - cloudFormationOpts := []func(*cloudformation.Options){CloudFormationWithRegionOption(region)} + cloudFormationOpts := []func(*cloudformation.Options){CloudFormationRegionOption(region)} describeTypeOutput, err := p.awsClients.CloudFormation.DescribeType(ctx, &cloudformation.DescribeTypeInput{ Type: types.RegistryTypeResource, TypeName: to.Ptr(serviceCtx.ResourceTypeInAWSFormat()), diff --git a/pkg/ucp/frontend/controller/awsproxy/getawsresourcewithpost.go b/pkg/ucp/frontend/controller/awsproxy/getawsresourcewithpost.go index 2947b38412..f6ac5dbcda 100644 --- a/pkg/ucp/frontend/controller/awsproxy/getawsresourcewithpost.go +++ b/pkg/ucp/frontend/controller/awsproxy/getawsresourcewithpost.go @@ -36,6 +36,8 @@ import ( "github.com/aws/aws-sdk-go-v2/service/cloudcontrol" "github.com/aws/aws-sdk-go-v2/service/cloudformation" "github.com/aws/aws-sdk-go-v2/service/cloudformation/types" + + "github.com/radius-project/radius/pkg/retry" ) var _ armrpc_controller.Controller = (*GetAWSResourceWithPost)(nil) @@ -44,13 +46,15 @@ var _ armrpc_controller.Controller = (*GetAWSResourceWithPost)(nil) type GetAWSResourceWithPost struct { armrpc_controller.Operation[*datamodel.AWSResource, datamodel.AWSResource] awsClients ucpaws.Clients + retryer *retry.Retryer } // NewGetAWSResourceWithPost creates a new GetAWSResourceWithPost controller with the given options and AWS clients. -func NewGetAWSResourceWithPost(opts armrpc_controller.Options, awsClients ucpaws.Clients) (armrpc_controller.Controller, error) { +func NewGetAWSResourceWithPost(opts armrpc_controller.Options, awsClients ucpaws.Clients, retryer *retry.Retryer) (armrpc_controller.Controller, error) { return &GetAWSResourceWithPost{ Operation: armrpc_controller.NewOperation(opts, armrpc_controller.ResourceOptions[datamodel.AWSResource]{}), awsClients: awsClients, + retryer: retryer, }, nil } @@ -77,7 +81,7 @@ func (p *GetAWSResourceWithPost) Run(ctx context.Context, w http.ResponseWriter, return armrpc_rest.NewBadRequestARMResponse(e), nil } - cloudFormationOpts := []func(*cloudformation.Options){CloudFormationWithRegionOption(region)} + cloudFormationOpts := []func(*cloudformation.Options){CloudFormationRegionOption(region)} describeTypeOutput, err := p.awsClients.CloudFormation.DescribeType(ctx, &cloudformation.DescribeTypeInput{ Type: types.RegistryTypeResource, TypeName: to.Ptr(serviceCtx.ResourceTypeInAWSFormat()), @@ -100,15 +104,27 @@ func (p *GetAWSResourceWithPost) Run(ctx context.Context, w http.ResponseWriter, cloudcontrolOpts := []func(*cloudcontrol.Options){CloudControlRegionOption(region)} logger.Info("Fetching resource", "resourceType", serviceCtx.ResourceTypeInAWSFormat(), "resourceID", awsResourceIdentifier) - response, err := p.awsClients.CloudControl.GetResource(ctx, &cloudcontrol.GetResourceInput{ - TypeName: to.Ptr(serviceCtx.ResourceTypeInAWSFormat()), - Identifier: aws.String(awsResourceIdentifier), - }, cloudcontrolOpts...) - - if ucpaws.IsAWSResourceNotFoundError(err) { - return armrpc_rest.NewNotFoundMessageResponse(constructNotFoundResponseMessage(middleware.GetRelativePath(p.Options().PathBase, req.URL.Path), awsResourceIdentifier)), nil - } else if err != nil { - return ucpaws.HandleAWSError(err) + + var response *cloudcontrol.GetResourceOutput + if err := p.retryer.RetryFunc(ctx, func(ctx context.Context) error { + response, err = p.awsClients.CloudControl.GetResource(ctx, &cloudcontrol.GetResourceInput{ + TypeName: to.Ptr(serviceCtx.ResourceTypeInAWSFormat()), + Identifier: aws.String(awsResourceIdentifier), + }, cloudcontrolOpts...) + + // If the resource is not found, retry. + if ucpaws.IsAWSResourceNotFoundError(err) { + return retry.RetryableError(err) + } + + // If any other error occurs, return the error. + return err + }); err != nil { + if ucpaws.IsAWSResourceNotFoundError(err) { + return armrpc_rest.NewNotFoundMessageResponse(constructNotFoundResponseMessage(middleware.GetRelativePath(p.Options().PathBase, req.URL.Path), awsResourceIdentifier)), nil + } else { + return ucpaws.HandleAWSError(err) + } } resourceProperties := map[string]any{} diff --git a/pkg/ucp/frontend/controller/awsproxy/getawsresourcewithpost_test.go b/pkg/ucp/frontend/controller/awsproxy/getawsresourcewithpost_test.go index 06063cadc8..645193ddbb 100644 --- a/pkg/ucp/frontend/controller/awsproxy/getawsresourcewithpost_test.go +++ b/pkg/ucp/frontend/controller/awsproxy/getawsresourcewithpost_test.go @@ -38,6 +38,7 @@ import ( armrpc_controller "github.com/radius-project/radius/pkg/armrpc/frontend/controller" armrpc_rest "github.com/radius-project/radius/pkg/armrpc/rest" "github.com/radius-project/radius/pkg/armrpc/rpctest" + "github.com/radius-project/radius/pkg/retry" ucp_aws "github.com/radius-project/radius/pkg/ucp/aws" "github.com/radius-project/radius/pkg/ucp/resources" "github.com/stretchr/testify/require" @@ -74,7 +75,7 @@ func Test_GetAWSResourceWithPost(t *testing.T) { CloudControl: testOptions.AWSCloudControlClient, CloudFormation: testOptions.AWSCloudFormationClient, } - awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients) + awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients, retry.NewNoOpRetryer()) require.NoError(t, err) requestBody := map[string]any{ @@ -126,7 +127,7 @@ func Test_GetAWSResourceWithPost_NotFound(t *testing.T) { CloudControl: testOptions.AWSCloudControlClient, CloudFormation: testOptions.AWSCloudFormationClient, } - awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients) + awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients, retry.NewNoOpRetryer()) require.NoError(t, err) requestBody := map[string]any{ @@ -165,7 +166,7 @@ func Test_GetAWSResourceWithPost_UnknownError(t *testing.T) { CloudControl: testOptions.AWSCloudControlClient, CloudFormation: testOptions.AWSCloudFormationClient, } - awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients) + awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients, retry.NewNoOpRetryer()) require.NoError(t, err) requestBody := map[string]any{ @@ -211,7 +212,7 @@ func Test_GetAWSResourceWithPost_SmithyError(t *testing.T) { CloudControl: testOptions.AWSCloudControlClient, CloudFormation: testOptions.AWSCloudFormationClient, } - awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients) + awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients, retry.NewNoOpRetryer()) require.NoError(t, err) requestBody := map[string]any{ @@ -287,7 +288,7 @@ func Test_GetAWSResourceWithPost_MultiIdentifier(t *testing.T) { CloudFormation: testOptions.AWSCloudFormationClient, } - awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients) + awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients, retry.NewNoOpRetryer()) require.NoError(t, err) actualResponse, err := awsController.Run(ctx, nil, request) @@ -323,3 +324,114 @@ func Test_GetAWSResourceWithPost_MultiIdentifier(t *testing.T) { require.Equal(t, expectedResponseObject, actualResponseObject) } + +func Test_GetAWSResourceWithPost_RetryableError(t *testing.T) { + testResource := CreateKinesisStreamTestResource(uuid.NewString()) + + output := cloudformation.DescribeTypeOutput{ + TypeName: aws.String(testResource.AWSResourceType), + Schema: aws.String(testResource.Schema), + } + + testOptions := setupTest(t) + testOptions.AWSCloudFormationClient.EXPECT().DescribeType(gomock.Any(), gomock.Any(), gomock.Any()).Return(&output, nil) + + getResponseBody := map[string]any{ + "Name": testResource.ResourceName, + "RetentionPeriodHours": 178, + "ShardCount": 3, + } + getResponseBodyBytes, err := json.Marshal(getResponseBody) + require.NoError(t, err) + + testOptions.AWSCloudControlClient.EXPECT().GetResource(gomock.Any(), gomock.Any(), gomock.Any()).Return( + nil, &types.ResourceNotFoundException{ + Message: aws.String("Resource not found"), + }).Times(1) + testOptions.AWSCloudControlClient.EXPECT().GetResource(gomock.Any(), gomock.Any(), gomock.Any()).Return( + &cloudcontrol.GetResourceOutput{ + ResourceDescription: &types.ResourceDescription{ + Identifier: aws.String(testResource.ResourceName), + Properties: aws.String(string(getResponseBodyBytes)), + }, + }, nil).Times(1) + + awsClients := ucp_aws.Clients{ + CloudControl: testOptions.AWSCloudControlClient, + CloudFormation: testOptions.AWSCloudFormationClient, + } + + retryer := retry.NewDefaultRetryer() + awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients, retryer) + require.NoError(t, err) + + requestBody := map[string]any{ + "properties": map[string]any{ + "Name": testResource.ResourceName, + }, + } + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + request, err := http.NewRequest(http.MethodPost, testResource.CollectionPath+"/:get", bytes.NewBuffer(body)) + require.NoError(t, err) + + ctx := rpctest.NewARMRequestContext(request) + actualResponse, err := awsController.Run(ctx, nil, request) + require.NoError(t, err) + + expectedResponse := armrpc_rest.NewOKResponse(map[string]any{ + "id": testResource.SingleResourcePath, + "type": testResource.ResourceType, + "name": aws.String(testResource.ResourceName), + "properties": map[string]any{ + "Name": testResource.ResourceName, + "RetentionPeriodHours": float64(178), + "ShardCount": float64(3), + }, + }) + + require.NoError(t, err) + require.Equal(t, expectedResponse, actualResponse) +} + +func Test_GetAWSResourceWithPost_NonRetryableError(t *testing.T) { + testResource := CreateKinesisStreamTestResource(uuid.NewString()) + + output := cloudformation.DescribeTypeOutput{ + TypeName: aws.String(testResource.AWSResourceType), + Schema: aws.String(testResource.Schema), + } + + testOptions := setupTest(t) + testOptions.AWSCloudFormationClient.EXPECT().DescribeType(gomock.Any(), gomock.Any(), gomock.Any()).Return(&output, nil) + + testOptions.AWSCloudControlClient.EXPECT().GetResource(gomock.Any(), gomock.Any(), gomock.Any()).Return(nil, errors.New("something bad happened")) + + awsClients := ucp_aws.Clients{ + CloudControl: testOptions.AWSCloudControlClient, + CloudFormation: testOptions.AWSCloudFormationClient, + } + + retryer := retry.NewDefaultRetryer() + awsController, err := NewGetAWSResourceWithPost(armrpc_controller.Options{DatabaseClient: testOptions.DatabaseClient}, awsClients, retryer) + require.NoError(t, err) + + requestBody := map[string]any{ + "properties": map[string]any{ + "Name": testResource.ResourceName, + }, + } + body, err := json.Marshal(requestBody) + require.NoError(t, err) + + request, err := http.NewRequest(http.MethodPost, testResource.CollectionPath+"/:get", bytes.NewBuffer(body)) + require.NoError(t, err) + + ctx := rpctest.NewARMRequestContext(request) + actualResponse, err := awsController.Run(ctx, nil, request) + require.Error(t, err) + + require.Nil(t, actualResponse) + require.Equal(t, "something bad happened", err.Error()) +} diff --git a/pkg/ucp/frontend/controller/awsproxy/options.go b/pkg/ucp/frontend/controller/awsproxy/options.go index c8184d9f55..52ba8f16fc 100644 --- a/pkg/ucp/frontend/controller/awsproxy/options.go +++ b/pkg/ucp/frontend/controller/awsproxy/options.go @@ -28,7 +28,7 @@ func CloudControlRegionOption(region string) func(*cloudcontrol.Options) { } // CloudFormationRegionOption sets the region for the CloudFormation client. -func CloudFormationWithRegionOption(region string) func(*cloudformation.Options) { +func CloudFormationRegionOption(region string) func(*cloudformation.Options) { return func(o *cloudformation.Options) { o.Region = region }