From 8960f1e5b3a2df9b7ea20ace7f3a25faabcaf45e Mon Sep 17 00:00:00 2001 From: Mugdha Patil Date: Tue, 13 May 2025 14:29:03 -0700 Subject: [PATCH 1/3] Working implementation of ClusterSubnetGroup with passing tests --- .../aws-redshift-clustersubnetgroup.json | 27 ++- .../docs/README.md | 20 +- aws-redshift-clustersubnetgroup/docs/tag.md | 1 + aws-redshift-clustersubnetgroup/pom.xml | 47 +++- .../sam-tests/create.json | 19 ++ .../sam-tests/delete.json | 8 + .../sam-tests/list.json | 7 + .../sam-tests/read.json | 8 + .../sam-tests/update.json | 31 +++ .../clustersubnetgroup/BaseHandlerStd.java | 87 +++++-- .../clustersubnetgroup/CreateHandler.java | 82 ++++--- .../clustersubnetgroup/DeleteHandler.java | 67 +++--- .../clustersubnetgroup/ListHandler.java | 70 ++++-- .../clustersubnetgroup/ModifyTagsRequest.java | 15 ++ .../clustersubnetgroup/ReadHandler.java | 77 +++--- .../clustersubnetgroup/Translator.java | 222 +++++++++++------- .../clustersubnetgroup/UpdateHandler.java | 159 +++++++------ .../clustersubnetgroup/CreateHandlerTest.java | 88 ++++--- .../clustersubnetgroup/DeleteHandlerTest.java | 90 ++++--- .../clustersubnetgroup/ListHandlerTest.java | 96 +++++--- .../clustersubnetgroup/ReadHandlerTest.java | 105 ++++++++- .../clustersubnetgroup/TestUtils.java | 150 +++++++++--- .../clustersubnetgroup/UpdateHandlerTest.java | 121 +++++----- 23 files changed, 1092 insertions(+), 505 deletions(-) create mode 100644 aws-redshift-clustersubnetgroup/sam-tests/create.json create mode 100644 aws-redshift-clustersubnetgroup/sam-tests/delete.json create mode 100644 aws-redshift-clustersubnetgroup/sam-tests/list.json create mode 100644 aws-redshift-clustersubnetgroup/sam-tests/read.json create mode 100644 aws-redshift-clustersubnetgroup/sam-tests/update.json create mode 100644 aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ModifyTagsRequest.java diff --git a/aws-redshift-clustersubnetgroup/aws-redshift-clustersubnetgroup.json b/aws-redshift-clustersubnetgroup/aws-redshift-clustersubnetgroup.json index 57265578..dd5f4510 100644 --- a/aws-redshift-clustersubnetgroup/aws-redshift-clustersubnetgroup.json +++ b/aws-redshift-clustersubnetgroup/aws-redshift-clustersubnetgroup.json @@ -36,13 +36,10 @@ "description": "The list of VPC subnet IDs", "type": "array", "insertionOrder": false, + "minItems": 1, "maxItems": 20, "items": { - "type": "string", - "relationshipRef": { - "typeName": "AWS::EC2::Subnet", - "propertyPath": "/properties/SubnetId" - } + "type": "string" } }, "Tags": { @@ -68,16 +65,20 @@ "primaryIdentifier": [ "/properties/ClusterSubnetGroupName" ], - "readOnlyProperties": [ + "createOnlyProperties": [ "/properties/ClusterSubnetGroupName" ], - "writeOnlyProperties": [ - "/properties/Tags", - "/properties/Tags/*/Key", - "/properties/Tags/*/Value" - ], "tagging": { - "taggable": true + "taggable": true, + "tagOnCreate": true, + "tagUpdatable": true, + "cloudFormationSystemTags": false, + "tagProperty": "/properties/Tags", + "permissions": [ + "redshift:DescribeTags", + "redshift:CreateTags", + "redshift:DeleteTags" + ] }, "handlers": { "create": { @@ -86,6 +87,7 @@ "redshift:CreateTags", "redshift:DescribeClusterSubnetGroups", "redshift:DescribeTags", + "redshift:CreateTags", "ec2:AllocateAddress", "ec2:AssociateAddress", "ec2:AttachNetworkInterface", @@ -138,6 +140,7 @@ "redshift:DeleteClusterSubnetGroup", "redshift:DescribeClusterSubnetGroups", "redshift:DescribeTags", + "redshift:DeleteTags", "ec2:AllocateAddress", "ec2:AssociateAddress", "ec2:AttachNetworkInterface", diff --git a/aws-redshift-clustersubnetgroup/docs/README.md b/aws-redshift-clustersubnetgroup/docs/README.md index 6bc0af6d..b5043860 100644 --- a/aws-redshift-clustersubnetgroup/docs/README.md +++ b/aws-redshift-clustersubnetgroup/docs/README.md @@ -15,6 +15,7 @@ To declare this entity in your AWS CloudFormation template, use the following sy "Description" : String, "SubnetIds" : [ String, ... ], "Tags" : [ Tag, ... ], + "ClusterSubnetGroupName" : String } } @@ -29,6 +30,7 @@ Properties: - String Tags: - Tag + ClusterSubnetGroupName: String ## Properties @@ -63,18 +65,20 @@ _Type_: List of Tag _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) -## Return Values +#### ClusterSubnetGroupName -### Ref +This name must be unique for all subnet groups that are created by your AWS account. If costumer do not provide it, cloudformation will generate it. Must not be "Default". -When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the ClusterSubnetGroupName. +_Required_: No -### Fn::GetAtt +_Type_: String -The `Fn::GetAtt` intrinsic function returns a value for a specified attribute of this type. The following are the available attributes and sample return values. +_Maximum Length_: 255 -For more information about using the `Fn::GetAtt` intrinsic function, see [Fn::GetAtt](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/intrinsic-function-reference-getatt.html). +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) -#### ClusterSubnetGroupName +## Return Values -This name must be unique for all subnet groups that are created by your AWS account. If costumer do not provide it, cloudformation will generate it. Must not be "Default". +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the ClusterSubnetGroupName. diff --git a/aws-redshift-clustersubnetgroup/docs/tag.md b/aws-redshift-clustersubnetgroup/docs/tag.md index bdb83ed8..49a4f2b6 100644 --- a/aws-redshift-clustersubnetgroup/docs/tag.md +++ b/aws-redshift-clustersubnetgroup/docs/tag.md @@ -51,3 +51,4 @@ _Minimum Length_: 1 _Maximum Length_: 255 _Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + diff --git a/aws-redshift-clustersubnetgroup/pom.xml b/aws-redshift-clustersubnetgroup/pom.xml index dc0e457b..635be119 100644 --- a/aws-redshift-clustersubnetgroup/pom.xml +++ b/aws-redshift-clustersubnetgroup/pom.xml @@ -16,6 +16,8 @@ 1.8 UTF-8 UTF-8 + 2.15.2 + 2.17.267 @@ -23,7 +25,22 @@ software.amazon.awssdk redshift - 2.15.51 + ${aws.sdk.version} + + + software.amazon.awssdk + sdk-core + ${aws.sdk.version} + + + software.amazon.awssdk + apache-client + ${aws.sdk.version} + + + software.amazon.awssdk + auth + ${aws.sdk.version} @@ -31,11 +48,31 @@ aws-cloudformation-rpdk-java-plugin [2.0.0, 3.0.0) + + software.amazon.awssdk + sdk-core + 2.17.267 + + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-core + ${jackson.version} + + + com.fasterxml.jackson.core + jackson-annotations + ${jackson.version} + org.projectlombok lombok - 1.18.4 + 1.18.30 provided @@ -67,6 +104,12 @@ 2.26.0 test + + + com.google.code.gson + gson + 2.8.9 + diff --git a/aws-redshift-clustersubnetgroup/sam-tests/create.json b/aws-redshift-clustersubnetgroup/sam-tests/create.json new file mode 100644 index 00000000..c254d737 --- /dev/null +++ b/aws-redshift-clustersubnetgroup/sam-tests/create.json @@ -0,0 +1,19 @@ +{ + "desiredResourceState": { + "ClusterSubnetGroupName": "cfn-subnet-group-4", + "Description": "Test Subnet Group Description", + "SubnetIds": [ + "subnet-0ddbac69e73569727", + "subnet-0d1b9f96842cb5777" + ], + "Tags": [ + { + "Key": "Environment", + "Value": "Test" + } + ] + }, + "previousResourceState": null, + "logicalResourceIdentifier": "MySubnetGroup", + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe" +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/sam-tests/delete.json b/aws-redshift-clustersubnetgroup/sam-tests/delete.json new file mode 100644 index 00000000..4ee884c9 --- /dev/null +++ b/aws-redshift-clustersubnetgroup/sam-tests/delete.json @@ -0,0 +1,8 @@ +{ + "desiredResourceState": { + "ClusterSubnetGroupName": "cfn-subnet-group-1" + }, + "previousResourceState": null, + "logicalResourceIdentifier": "cfn-subnet-group-1", + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe" +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/sam-tests/list.json b/aws-redshift-clustersubnetgroup/sam-tests/list.json new file mode 100644 index 00000000..7d0ad08b --- /dev/null +++ b/aws-redshift-clustersubnetgroup/sam-tests/list.json @@ -0,0 +1,7 @@ +{ + "desiredResourceState": {}, + "previousResourceState": null, + "nextToken": null, + "logicalResourceIdentifier": "cfn-subnet-group-4", + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe" +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/sam-tests/read.json b/aws-redshift-clustersubnetgroup/sam-tests/read.json new file mode 100644 index 00000000..956c07c5 --- /dev/null +++ b/aws-redshift-clustersubnetgroup/sam-tests/read.json @@ -0,0 +1,8 @@ +{ + "desiredResourceState": { + "ClusterSubnetGroupName": "cfn-subnet-group-4" + }, + "previousResourceState": null, + "logicalResourceIdentifier": "cfn-subnet-group-4", + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe" +} diff --git a/aws-redshift-clustersubnetgroup/sam-tests/update.json b/aws-redshift-clustersubnetgroup/sam-tests/update.json new file mode 100644 index 00000000..486e33b8 --- /dev/null +++ b/aws-redshift-clustersubnetgroup/sam-tests/update.json @@ -0,0 +1,31 @@ +{ + "desiredResourceState": { + "ClusterSubnetGroupName": "cfn-subnet-group-4", + "Description": "Updated Subnet Group Description", + "SubnetIds": [ + "subnet-0d1b9f96842cb5777" + ], + "Tags": [ + { + "Key": "Environment", + "Value": "Production" + } + ] + }, + "previousResourceState": { + "ClusterSubnetGroupName": "cfn-subnet-group-4", + "Description": "Test Subnet Group Description", + "SubnetIds": [ + "subnet-0ddbac69e73569727", + "subnet-0d1b9f96842cb5777" + ], + "Tags": [ + { + "Key": "Environment", + "Value": "Test" + } + ] + }, + "logicalResourceIdentifier": "MySubnetGroup", + "clientRequestToken": "4b90a7e4-b790-456b-a937-0cfdfa211dfe" +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/BaseHandlerStd.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/BaseHandlerStd.java index 75b43a75..e7b5ff77 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/BaseHandlerStd.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/BaseHandlerStd.java @@ -1,33 +1,88 @@ package software.amazon.redshift.clustersubnetgroup; +import com.amazonaws.util.StringUtils; import software.amazon.awssdk.services.redshift.RedshiftClient; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; - +import software.amazon.awssdk.services.redshift.model.DescribeTagsRequest; +import software.amazon.awssdk.services.redshift.model.DescribeTagsResponse; +import software.amazon.awssdk.services.redshift.model.CreateTagsResponse; +import software.amazon.awssdk.services.redshift.model.InvalidTagException; +import software.amazon.awssdk.services.redshift.model.InvalidClusterStateException; +import software.amazon.awssdk.services.redshift.model.ResourceNotFoundException; public abstract class BaseHandlerStd extends BaseHandler { + protected Logger logger; + protected static final String CALL_GRAPH_TYPE_NAME = StringUtils.replace(ResourceModel.TYPE_NAME, "::", "-"); + @Override public final ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final Logger logger) { + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { return handleRequest( - proxy, - request, - callbackContext != null ? callbackContext : new CallbackContext(), - proxy.newProxy(ClientBuilder::getClient), - logger + proxy, + request, + callbackContext != null ? callbackContext : new CallbackContext(), + proxy.newProxy(ClientBuilder::getClient), + logger ); } protected abstract ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger); -} + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger); + + protected DescribeTagsResponse readTags(final DescribeTagsRequest awsRequest, + final ProxyClient proxyClient) { + DescribeTagsResponse awsResponse; + awsResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::describeTags); + logger.log(awsResponse.toString()); + logger.log(String.format("%s's tags have successfully been read.", ResourceModel.TYPE_NAME)); + return awsResponse; + } + + protected CreateTagsResponse updateTags(final ModifyTagsRequest awsRequest, + final ProxyClient proxyClient) { + CreateTagsResponse awsResponse = null; + + if (awsRequest.getDeleteOldTagsRequest().tagKeys().isEmpty()) { + logger.log(String.format("No tags would be deleted for the resource: %s.", ResourceModel.TYPE_NAME)); + } else { + proxyClient.injectCredentialsAndInvokeV2(awsRequest.getDeleteOldTagsRequest(), proxyClient.client()::deleteTags); + logger.log(String.format("Delete tags for the resource: %s.", ResourceModel.TYPE_NAME)); + } + + if (awsRequest.getCreateNewTagsRequest().tags().isEmpty()) { + logger.log(String.format("No tags would be created for the resource: %s.", ResourceModel.TYPE_NAME)); + } else { + awsResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest.getCreateNewTagsRequest(), proxyClient.client()::createTags); + logger.log(String.format("Create tags for the resource: %s.", ResourceModel.TYPE_NAME)); + } + + return awsResponse; + } + + protected ProgressEvent operateTagsErrorHandler(final Object awsRequest, + final Exception exception, + final ProxyClient client, + final ResourceModel model, + final CallbackContext context) { + if (exception instanceof ResourceNotFoundException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.NotFound); + } else if (exception instanceof InvalidTagException || + exception instanceof InvalidClusterStateException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.InvalidRequest); + } else { + return ProgressEvent.failed(model, context, HandlerErrorCode.UnauthorizedTaggingOperation, exception.getMessage()); + } + } +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/CreateHandler.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/CreateHandler.java index 1217c545..39f0f7bc 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/CreateHandler.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/CreateHandler.java @@ -1,10 +1,14 @@ package software.amazon.redshift.clustersubnetgroup; -import com.amazonaws.util.StringUtils; +import org.apache.commons.lang3.ObjectUtils; +import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.RandomStringUtils; import software.amazon.awssdk.services.redshift.RedshiftClient; import software.amazon.awssdk.services.redshift.model.ClusterSubnetGroupAlreadyExistsException; import software.amazon.awssdk.services.redshift.model.CreateClusterSubnetGroupRequest; import software.amazon.awssdk.services.redshift.model.CreateClusterSubnetGroupResponse; +import software.amazon.awssdk.services.redshift.model.InvalidTagException; +import software.amazon.awssdk.services.redshift.model.TagLimitExceededException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; @@ -13,11 +17,13 @@ import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import software.amazon.cloudformation.resource.IdentifierUtils; +import java.util.Map; +import java.util.Optional; import java.util.UUID; +import java.util.Collections; +import com.google.common.collect.Maps; public class CreateHandler extends BaseHandlerStd { - - private Logger logger; private static final int MAX_SUBNET_GROUP_NAME_LENGTH = 255; protected ProgressEvent handleRequest( @@ -28,41 +34,61 @@ protected ProgressEvent handleRequest( final Logger logger) { this.logger = logger; + final ResourceModel model = request.getDesiredResourceState(); + // Generate name if not provided + if (StringUtils.isBlank(model.getClusterSubnetGroupName())) { + logger.log(String.format("%s Updating cluster subnet group identifier", ResourceModel.TYPE_NAME)); + model.setClusterSubnetGroupName(IdentifierUtils.generateResourceIdentifier( + ObjectUtils.defaultIfNull(request.getStackId(), RandomStringUtils.randomAlphabetic(1)), + ObjectUtils.defaultIfNull(request.getLogicalResourceIdentifier(), UUID.randomUUID().toString()), + ObjectUtils.defaultIfNull(request.getClientRequestToken(), UUID.randomUUID().toString()), + MAX_SUBNET_GROUP_NAME_LENGTH).toLowerCase()); + } + + // Resource level + stack level tags + Map convertedTags = Translator.translateFromResourceModelToSdkTags(model.getTags()); + Map mergedTags = Maps.newHashMap(); + + mergedTags.putAll(Optional.ofNullable(request.getDesiredResourceTags()).orElse(Collections.emptyMap())); + mergedTags.putAll(Optional.ofNullable(convertedTags).orElse(Collections.emptyMap())); + return ProgressEvent.progress(model, callbackContext) - .then(progress -> proxy.initiate("AWS-Redshift-ClusterSubnetGroup::Create", proxyClient, model, callbackContext) - .translateToServiceRequest((m) -> Translator.translateToCreateRequest(generateSubnetGroupName(request), - m, request.getDesiredResourceTags())) - .makeServiceCall(this::createResource) - .handleError((createDbSubnetGroupRequest, exception, client, resourceModel, cxt) -> { - if (exception instanceof ClusterSubnetGroupAlreadyExistsException) { - return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.AlreadyExists); - } - throw exception; - }) - .progress()) + .then(progress -> proxy.initiate(String.format("%s::Create", CALL_GRAPH_TYPE_NAME), proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest(rm -> Translator.translateToCreateRequest(rm.getClusterSubnetGroupName(), rm, mergedTags)) + .makeServiceCall(this::createClusterSubnetGroup) + .handleError(this::createClusterSubnetGroupErrorHandler) + .progress() + ) .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } - private CreateClusterSubnetGroupResponse createResource( - final CreateClusterSubnetGroupRequest createRequest, + private CreateClusterSubnetGroupResponse createClusterSubnetGroup( + final CreateClusterSubnetGroupRequest awsRequest, final ProxyClient proxyClient) { - CreateClusterSubnetGroupResponse createResponse = null; + CreateClusterSubnetGroupResponse awsResponse; + awsResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, proxyClient.client()::createClusterSubnetGroup); - createResponse = proxyClient.injectCredentialsAndInvokeV2(createRequest, proxyClient.client()::createClusterSubnetGroup); logger.log(String.format("%s successfully created.", ResourceModel.TYPE_NAME)); - return createResponse; + return awsResponse; } - private String generateSubnetGroupName(ResourceHandlerRequest request) { - final String logicalResourceIdentifier = StringUtils.isNullOrEmpty(request.getLogicalResourceIdentifier()) - ? UUID.randomUUID().toString() : request.getLogicalResourceIdentifier(); + private ProgressEvent createClusterSubnetGroupErrorHandler( + final CreateClusterSubnetGroupRequest awsRequest, + final Exception exception, + final ProxyClient client, + final ResourceModel model, + final CallbackContext context) { - return IdentifierUtils.generateResourceIdentifier( - logicalResourceIdentifier, - request.getClientRequestToken(), - MAX_SUBNET_GROUP_NAME_LENGTH - ).toLowerCase(); + if (exception instanceof ClusterSubnetGroupAlreadyExistsException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.AlreadyExists); + } else if (exception instanceof TagLimitExceededException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.ServiceLimitExceeded); + } else if (exception instanceof InvalidTagException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.InvalidRequest); + } else { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.GeneralServiceException); + } } -} +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/DeleteHandler.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/DeleteHandler.java index b72405c5..5bcf491d 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/DeleteHandler.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/DeleteHandler.java @@ -6,8 +6,6 @@ import software.amazon.awssdk.services.redshift.model.DeleteClusterSubnetGroupResponse; import software.amazon.awssdk.services.redshift.model.InvalidClusterSubnetGroupStateException; import software.amazon.awssdk.services.redshift.model.InvalidClusterSubnetStateException; -import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; @@ -16,40 +14,53 @@ import software.amazon.cloudformation.proxy.ResourceHandlerRequest; public class DeleteHandler extends BaseHandlerStd { - private Logger logger; protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { this.logger = logger; - final ResourceModel model = request.getDesiredResourceState(); - return ProgressEvent.progress(model, callbackContext) - .then(progress -> - proxy.initiate("AWS-Redshift-ClusterSubnetGroup::Delete", proxyClient, model, callbackContext) - .translateToServiceRequest(Translator::translateToDeleteRequest) - .makeServiceCall(this::deleteResource) - .handleError((deleteDbSubnetGroupRequest, exception, client, resourceModel, cxt) -> { - if (exception instanceof ClusterSubnetGroupNotFoundException) { - return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.NotFound); - } - throw exception; - }) - .done((deleteClusterSubnetGroupRequest, deleteClusterSubnetGroupResponse, client, resourceModel, cxt) - -> ProgressEvent.defaultSuccessHandler(null))); + return ProgressEvent.progress(request.getDesiredResourceState(), callbackContext) + .then(progress -> + proxy.initiate(String.format("%s::Delete", CALL_GRAPH_TYPE_NAME), proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest(Translator::translateToDeleteRequest) + .makeServiceCall(this::deleteClusterSubnetGroup) + .handleError(this::deleteClusterSubnetGroupErrorHandler) + .progress() + ) + .then(progress -> ProgressEvent.defaultSuccessHandler(null)); } - private DeleteClusterSubnetGroupResponse deleteResource( - final DeleteClusterSubnetGroupRequest deleteRequest, - final ProxyClient proxyClient) { - DeleteClusterSubnetGroupResponse awsResponse = proxyClient.injectCredentialsAndInvokeV2(deleteRequest, + private DeleteClusterSubnetGroupResponse deleteClusterSubnetGroup( + final DeleteClusterSubnetGroupRequest awsRequest, + final ProxyClient proxyClient) { + + DeleteClusterSubnetGroupResponse awsResponse = proxyClient.injectCredentialsAndInvokeV2( + awsRequest, proxyClient.client()::deleteClusterSubnetGroup); - logger.log(String.format("%s [%s] Deleted Successfully", ResourceModel.TYPE_NAME, deleteRequest.clusterSubnetGroupName())); + logger.log(String.format("%s successfully deleted.", ResourceModel.TYPE_NAME)); return awsResponse; } -} + + private ProgressEvent deleteClusterSubnetGroupErrorHandler( + final DeleteClusterSubnetGroupRequest awsRequest, + final Exception exception, + final ProxyClient client, + final ResourceModel model, + final CallbackContext context) { + + if (exception instanceof ClusterSubnetGroupNotFoundException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.NotFound); + } else if (exception instanceof InvalidClusterSubnetGroupStateException || + exception instanceof InvalidClusterSubnetStateException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.ResourceConflict); + } else { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.GeneralServiceException); + } + } +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ListHandler.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ListHandler.java index c61fa1e1..a9e3f2d0 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ListHandler.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ListHandler.java @@ -1,45 +1,65 @@ package software.amazon.redshift.clustersubnetgroup; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.redshift.RedshiftClient; import software.amazon.awssdk.services.redshift.model.ClusterSubnetGroupNotFoundException; +import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsRequest; import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsResponse; import software.amazon.awssdk.services.redshift.model.InvalidTagException; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; -import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.OperationStatus; -import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import java.util.ArrayList; -import java.util.List; - -public class ListHandler extends BaseHandlerStd { +public class ListHandler extends BaseHandler { + private Logger logger; @Override public ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { - - final List models = new ArrayList<>(); - DescribeClusterSubnetGroupsResponse describeClusterSubnetGroupsResponse = null; + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + + this.logger = logger; + + DescribeClusterSubnetGroupsRequest awsRequest = Translator.translateToListRequest(request.getNextToken()); + DescribeClusterSubnetGroupsResponse awsResponse = listClusterSubnetGroups(awsRequest, proxy); + + return ProgressEvent.builder() + .resourceModels(Translator.translateFromListRequest(awsResponse)) // Updated method name + .nextToken(awsResponse.marker()) + .status(OperationStatus.SUCCESS) + .build(); + } + + private DescribeClusterSubnetGroupsResponse listClusterSubnetGroups( + final DescribeClusterSubnetGroupsRequest awsRequest, + final AmazonWebServicesClientProxy proxy) { + + DescribeClusterSubnetGroupsResponse awsResponse; + try { - describeClusterSubnetGroupsResponse = - proxy.injectCredentialsAndInvokeV2(Translator.translateToListRequest(request.getNextToken()), - proxyClient.client()::describeClusterSubnetGroups); + awsResponse = proxy.injectCredentialsAndInvokeV2( + awsRequest, + ClientBuilder.getClient()::describeClusterSubnetGroups); - } catch (final ClusterSubnetGroupNotFoundException | InvalidTagException e) { - throw new CfnNotFoundException(ResourceModel.TYPE_NAME, request.getDesiredResourceState().getClusterSubnetGroupName()); + } catch (final ClusterSubnetGroupNotFoundException e) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, + awsRequest.clusterSubnetGroupName(), e); + + } catch (final InvalidTagException e) { + throw new CfnInvalidRequestException(e); + + } catch (final AwsServiceException e) { + throw new CfnGeneralServiceException(e); } - return ProgressEvent.builder() - .resourceModels(Translator.translateFromListResponse(describeClusterSubnetGroupsResponse)) - .nextToken(describeClusterSubnetGroupsResponse.marker()) - .status(OperationStatus.SUCCESS) - .build(); + logger.log(String.format("%s has successfully been listed.", ResourceModel.TYPE_NAME)); + return awsResponse; } -} +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ModifyTagsRequest.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ModifyTagsRequest.java new file mode 100644 index 00000000..82e71990 --- /dev/null +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ModifyTagsRequest.java @@ -0,0 +1,15 @@ +package software.amazon.redshift.clustersubnetgroup; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import software.amazon.awssdk.services.redshift.model.CreateTagsRequest; +import software.amazon.awssdk.services.redshift.model.DeleteTagsRequest; + +@AllArgsConstructor +@Builder +@Data +public class ModifyTagsRequest { + private CreateTagsRequest createNewTagsRequest; + private DeleteTagsRequest deleteOldTagsRequest; +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ReadHandler.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ReadHandler.java index d4737f79..2f6eede1 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ReadHandler.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ReadHandler.java @@ -1,11 +1,9 @@ package software.amazon.redshift.clustersubnetgroup; -import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.redshift.RedshiftClient; import software.amazon.awssdk.services.redshift.model.ClusterSubnetGroupNotFoundException; import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsRequest; import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsResponse; -import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; @@ -14,56 +12,49 @@ import software.amazon.cloudformation.proxy.ResourceHandlerRequest; public class ReadHandler extends BaseHandlerStd { - private Logger logger; protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { this.logger = logger; - final ResourceModel model = request.getDesiredResourceState(); - - return proxy.initiate("AWS-Redshift-ClusterSubnetGroup::Read", proxyClient, model, callbackContext) - .translateToServiceRequest(Translator::translateToReadRequest) - .makeServiceCall((awsRequest, sdkProxyClient) -> readResource(awsRequest, sdkProxyClient)) - .handleError((awsRequest, exception, client, resourceModel, cxt) -> { - if (exception instanceof ClusterSubnetGroupNotFoundException) { - return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.NotFound); - } - throw exception; - }) - .done(this::constructResourceModelFromResponse); + final String resourceName = String.format("arn:%s:redshift:%s:%s:subnetgroup:%s", + request.getAwsPartition(), + request.getRegion(), + request.getAwsAccountId(), + model.getClusterSubnetGroupName()); + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> proxy.initiate(String.format("%s::Read::SubnetGroup", CALL_GRAPH_TYPE_NAME), proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::translateToReadRequest) + .makeServiceCall(this::readResource) + .handleError((awsRequest, exception, client, resourceModel, cxt) -> { + if (exception instanceof ClusterSubnetGroupNotFoundException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.NotFound); + } + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.GeneralServiceException); + }) + .done(awsResponse -> ProgressEvent.progress(Translator.translateFromReadResponse(awsResponse), callbackContext))) + .then(progress -> proxy.initiate(String.format("%s::Read::Tags", CALL_GRAPH_TYPE_NAME), proxyClient, progress.getResourceModel(), callbackContext) + .translateToServiceRequest(rm -> Translator.translateToReadTagsRequest(resourceName)) + .makeServiceCall(this::readTags) // Using inherited readTags method + .handleError(this::operateTagsErrorHandler) // Using inherited error handler + .done((tagsRequest, tagsResponse, client, resourceModel, context) -> + ProgressEvent.defaultSuccessHandler(Translator.translateFromReadTagsResponse(resourceModel, tagsResponse)))); } - /** - * Implement client invocation of the read request through the proxyClient, which is already initialised with - * caller credentials, correct region and retry settings - * @param awsRequest the aws service request to describe a resource - * @param proxyClient the aws service client to make the call - * @return describe resource response - */ private DescribeClusterSubnetGroupsResponse readResource( - final DescribeClusterSubnetGroupsRequest awsRequest, - final ProxyClient proxyClient) { - DescribeClusterSubnetGroupsResponse awsResponse = proxyClient.injectCredentialsAndInvokeV2(awsRequest, - proxyClient.client()::describeClusterSubnetGroups); + final DescribeClusterSubnetGroupsRequest awsRequest, + final ProxyClient proxyClient) { + DescribeClusterSubnetGroupsResponse awsResponse = proxyClient.injectCredentialsAndInvokeV2( + awsRequest, + proxyClient.client()::describeClusterSubnetGroups); logger.log(String.format("%s has successfully been read.", ResourceModel.TYPE_NAME)); return awsResponse; } - - /** - * Implement client invocation of the read request through the proxyClient, which is already initialised with - * caller credentials, correct region and retry settings - * @param awsResponse the aws service describe resource response - * @return progressEvent indicating success, in progress with delay callback or failed state - */ - private ProgressEvent constructResourceModelFromResponse( - final DescribeClusterSubnetGroupsResponse awsResponse) { - return ProgressEvent.defaultSuccessHandler(Translator.translateFromReadResponse(awsResponse)); - } -} +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/Translator.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/Translator.java index 4474c71c..25c5b907 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/Translator.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/Translator.java @@ -1,6 +1,8 @@ package software.amazon.redshift.clustersubnetgroup; -import com.amazonaws.util.StringUtils; +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.commons.lang3.StringUtils; import software.amazon.awssdk.services.redshift.RedshiftClient; import software.amazon.awssdk.services.redshift.model.CreateClusterSubnetGroupRequest; import software.amazon.awssdk.services.redshift.model.CreateTagsRequest; @@ -14,13 +16,7 @@ import software.amazon.awssdk.services.redshift.model.Subnet; import software.amazon.awssdk.services.redshift.model.Tag; import software.amazon.awssdk.services.redshift.model.TaggedResource; -import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.exceptions.CfnNotFoundException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.HandlerErrorCode; -import software.amazon.cloudformation.proxy.ProgressEvent; -import software.amazon.cloudformation.proxy.ProxyClient; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import java.util.Collection; import java.util.Collections; @@ -28,18 +24,11 @@ import java.util.Map; import java.util.Objects; import java.util.Optional; -import java.util.Set; import java.util.stream.Collectors; import java.util.stream.Stream; -/** - * This class is a centralized placeholder for - * - api request construction - * - object translation to/from aws sdk - * - resource model construction for read/list handlers - */ - public class Translator { + private static final Gson GSON = new GsonBuilder().create(); /** * Request to create a resource @@ -49,32 +38,22 @@ public class Translator { static CreateClusterSubnetGroupRequest translateToCreateRequest(final String generateSubnetGroupName, final ResourceModel model, final Map tags) { - //Based on contract_read_without_create test - model.setClusterSubnetGroupName(generateSubnetGroupName); return CreateClusterSubnetGroupRequest.builder() .clusterSubnetGroupName(model.getClusterSubnetGroupName()) .subnetIds(model.getSubnetIds()) .description(model.getDescription()) - .tags(translateTagsMapToTagCollection(tags)) + .tags(translateToSdkTags(translateTagsMapToTagCollection(tags))) .build(); } - static List translateTagsMapToTagCollection(final Map tags) { - if (tags == null) return null; - return tags.keySet().stream() - .map(key -> Tag.builder().key(key).value(tags.get(key)).build()) - .collect(Collectors.toList()); - } - /** * Request to read a resource * @param model resource model * @return awsRequest the aws service request to describe a resource */ static DescribeClusterSubnetGroupsRequest translateToReadRequest(final ResourceModel model) { - //Based on contract_read_without_create test - if (StringUtils.isNullOrEmpty(model.getClusterSubnetGroupName())) { + if (StringUtils.isBlank(model.getClusterSubnetGroupName())) { throw new CfnNotFoundException(ResourceModel.TYPE_NAME, null); } return DescribeClusterSubnetGroupsRequest.builder() @@ -82,15 +61,11 @@ static DescribeClusterSubnetGroupsRequest translateToReadRequest(final ResourceM .build(); } - static DescribeClusterSubnetGroupsRequest translateToListRequest(final String nextToken) { - return DescribeClusterSubnetGroupsRequest.builder().marker(nextToken).build(); - } - - /** - * Translates resource object from sdk into a resource model - * @param awsResponse the aws service describe resource response - * @return model resource model - */ + /** + * Translates resource object from sdk into a resource model + * @param awsResponse the aws service describe resource response + * @return model resource model + */ static ResourceModel translateFromReadResponse(final DescribeClusterSubnetGroupsResponse awsResponse) { final String subnetGroupName = streamOfOrEmpty(awsResponse.clusterSubnetGroups()) .map(software.amazon.awssdk.services.redshift.model.ClusterSubnetGroup::clusterSubnetGroupName) @@ -109,28 +84,25 @@ static ResourceModel translateFromReadResponse(final DescribeClusterSubnetGroups .findAny() .orElse(null); + final List tags = streamOfOrEmpty(awsResponse.clusterSubnetGroups()) + .map(software.amazon.awssdk.services.redshift.model.ClusterSubnetGroup::tags) + .filter(Objects::nonNull) + .findAny() + .orElse(null); + return ResourceModel.builder() .clusterSubnetGroupName(subnetGroupName) .description(description) .subnetIds(translateSubnetIdsFromSdk(subnetIds)) + .tags(translateTagsFromSdk(tags)) .build(); } - static List translateSubnetIdsFromSdk (final List subnets) { - return subnets.stream().map(subnet -> subnet.subnetIdentifier()).collect(Collectors.toList()); - - } - - static List translateTagsFromSdk (final List tags) { - return Optional.ofNullable(tags).orElse(Collections.emptyList()) - .stream() - .map(tag -> software.amazon.redshift.clustersubnetgroup.Tag.builder() - .key(tag.key()) - .value(tag.value()).build()) - .collect(Collectors.toList()); - } - - + /** + * Request to delete a resource + * @param model resource model + * @return awsRequest the aws service request to delete a resource + */ static DeleteClusterSubnetGroupRequest translateToDeleteRequest(final ResourceModel model) { return DeleteClusterSubnetGroupRequest.builder() .clusterSubnetGroupName(model.getClusterSubnetGroupName()) @@ -150,59 +122,147 @@ static ModifyClusterSubnetGroupRequest translateToUpdateRequest(final ResourceMo .build(); } + /** + * Request to list resources + * @param nextToken token passed to the aws service list resources request + * @return awsRequest the aws service request to list resources within aws account + */ + static DescribeClusterSubnetGroupsRequest translateToListRequest(final String nextToken) { + return DescribeClusterSubnetGroupsRequest.builder() + .marker(nextToken) + .build(); + } + /** * Translates resource objects from sdk into a resource model (primary identifier only) * @param awsResponse the aws service describe resource response * @return list of resource models */ - static List translateFromListResponse(final DescribeClusterSubnetGroupsResponse awsResponse) { + static List translateFromListRequest(final DescribeClusterSubnetGroupsResponse awsResponse) { return streamOfOrEmpty(awsResponse.clusterSubnetGroups()) - .map(clusterSubnetGroup -> ResourceModel.builder() - .clusterSubnetGroupName(clusterSubnetGroup.clusterSubnetGroupName()) - .build()) - .collect(Collectors.toList()); + .map(clusterSubnetGroup -> ResourceModel.builder() + .clusterSubnetGroupName(clusterSubnetGroup.clusterSubnetGroupName()) + .description(clusterSubnetGroup.description()) // You might want to include these additional fields + .subnetIds(translateSubnetIdsFromSdk(clusterSubnetGroup.subnets())) + .tags(translateTagsFromSdk(clusterSubnetGroup.tags())) + .build()) + .collect(Collectors.toList()); } - static DescribeTagsRequest describeTagsRequest(final String arn) { + // Tag-related methods + static DescribeTagsRequest translateToReadTagsRequest(final String resourceName) { return DescribeTagsRequest.builder() - .resourceName(arn) + .resourceName(resourceName) .build(); } - static Set getTagsKeySet(final Collection tags) { - return tags.stream().map(tag -> tag.key()).collect(Collectors.toSet()); + static ResourceModel translateFromReadTagsResponse(final ResourceModel model, + final DescribeTagsResponse awsResponse) { + model.setTags(translateToModelTags(awsResponse.taggedResources() + .stream() + .map(TaggedResource::tag) + .collect(Collectors.toList()))); + return model; } - static CreateTagsRequest createTagsRequest(final Collection tags, final String arn) { - return CreateTagsRequest.builder() - .resourceName(arn) - .tags(tags) + static ModifyTagsRequest translateToUpdateTagsRequest( + List desiredTags, + List currentTags, + final String resourceName) { + List toBeCreatedTags = subtract(desiredTags, currentTags); + List toBeDeletedTags = subtract(currentTags, desiredTags); + + return ModifyTagsRequest.builder() + .createNewTagsRequest(CreateTagsRequest.builder() + .tags(translateToSdkTags(toBeCreatedTags)) + .resourceName(resourceName) + .build()) + .deleteOldTagsRequest(DeleteTagsRequest.builder() + .tagKeys(toBeDeletedTags + .stream() + .map(software.amazon.redshift.clustersubnetgroup.Tag::getKey) + .collect(Collectors.toList())) + .resourceName(resourceName) + .build()) .build(); } - static DeleteTagsRequest deleteTagsRequest(final Collection tagsKey, final String arn) { - return DeleteTagsRequest.builder() - .resourceName(arn) - .tagKeys(tagsKey) - .build(); + // Helper methods + private static List translateSubnetIdsFromSdk(final List subnets) { + return subnets.stream() + .map(Subnet::subnetIdentifier) + .collect(Collectors.toList()); + } + + private static List translateTagsFromSdk(final List tags) { + return Optional.ofNullable(tags).orElse(Collections.emptyList()) + .stream() + .map(tag -> software.amazon.redshift.clustersubnetgroup.Tag.builder() + .key(tag.key()) + .value(tag.value()) + .build()) + .collect(Collectors.toList()); + } + + static List translateTagsMapToTagCollection(final Map tags) { + if (tags == null) return null; + return tags.keySet().stream() + .map(key -> software.amazon.redshift.clustersubnetgroup.Tag.builder().key(key).value(tags.get(key)).build()) + .collect(Collectors.toList()); + } + + private static software.amazon.awssdk.services.redshift.model.Tag translateToSdkTag(Tag tag) { + return GSON.fromJson(GSON.toJson(tag), software.amazon.awssdk.services.redshift.model.Tag.class); } - static String getArn(final ResourceHandlerRequest request) { - final String subnetGroupName = request.getDesiredResourceState().getClusterSubnetGroupName(); - String partition = "aws"; - if (request.getRegion().indexOf("us-gov-") == 0) partition = partition.concat("-us-gov"); - if (request.getRegion().indexOf("cn-") == 0) partition = partition.concat("-cn"); - return String.format("arn:%s:redshift:%s:%s:subnetgroup:%s", partition, request.getRegion(), request.getAwsAccountId(), subnetGroupName); + public static List translateToSdkTags( + List tags) { + return Optional.ofNullable(tags) + .map(ts -> ts + .stream() + .map(tag -> software.amazon.awssdk.services.redshift.model.Tag.builder() + .key(tag.getKey()) + .value(tag.getValue()) + .build()) + .collect(Collectors.toList())) + .orElse(null); + } + + private static software.amazon.redshift.clustersubnetgroup.Tag translateToModelTag( + software.amazon.awssdk.services.redshift.model.Tag tag) { + return GSON.fromJson(GSON.toJson(tag), software.amazon.redshift.clustersubnetgroup.Tag.class); } - static List getTags(final String arn, final AmazonWebServicesClientProxy proxy, final ProxyClient proxyClient) { - final DescribeTagsResponse response = proxy.injectCredentialsAndInvokeV2(Translator.describeTagsRequest(arn), proxyClient.client()::describeTags); - return response.taggedResources().stream().map(TaggedResource::tag).collect(Collectors.toList()); + private static List translateToModelTags( + List tags) { + return Optional.ofNullable(tags) + .map(ts -> ts + .stream() + .map(Translator::translateToModelTag) + .collect(Collectors.toList())) + .orElse(null); } private static Stream streamOfOrEmpty(final Collection collection) { return Optional.ofNullable(collection) - .map(Collection::stream) - .orElseGet(Stream::empty); + .map(Collection::stream) + .orElseGet(Stream::empty); + } + + static Map translateFromResourceModelToSdkTags(final List listOfTags) { + Map sdkTags = streamOfOrEmpty(listOfTags) + .collect(Collectors.toMap(software.amazon.redshift.clustersubnetgroup.Tag::getKey, software.amazon.redshift.clustersubnetgroup.Tag::getValue)); + return sdkTags.isEmpty() ? null : sdkTags; + } + + private static List subtract(List a, List b) { + return Optional.ofNullable(a) + .map(aIfNotNull -> aIfNotNull + .stream() + .filter(ao -> Optional.ofNullable(b) + .map(bIfNotNull -> !bIfNotNull.contains(ao)) + .orElse(true)) + .collect(Collectors.toList())) + .orElse(Collections.emptyList()); } -} +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/UpdateHandler.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/UpdateHandler.java index 02d2cc55..abe4df78 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/UpdateHandler.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/UpdateHandler.java @@ -1,108 +1,121 @@ package software.amazon.redshift.clustersubnetgroup; -import com.amazonaws.util.StringUtils; -import com.google.common.collect.Sets; -import org.apache.commons.collections.CollectionUtils; -import software.amazon.awssdk.awscore.exception.AwsServiceException; -import software.amazon.awssdk.core.SdkClient; import software.amazon.awssdk.services.redshift.RedshiftClient; import software.amazon.awssdk.services.redshift.model.ClusterSubnetGroupNotFoundException; import software.amazon.awssdk.services.redshift.model.ClusterSubnetQuotaExceededException; -import software.amazon.awssdk.services.redshift.model.DependentServiceRequestThrottlingException; -import software.amazon.awssdk.services.redshift.model.DescribeTagsResponse; import software.amazon.awssdk.services.redshift.model.InvalidSubnetException; import software.amazon.awssdk.services.redshift.model.InvalidTagException; -import software.amazon.awssdk.services.redshift.model.ModifyClusterSubnetGroupRequest; import software.amazon.awssdk.services.redshift.model.ModifyClusterSubnetGroupResponse; import software.amazon.awssdk.services.redshift.model.ResourceNotFoundException; -import software.amazon.awssdk.services.redshift.model.SubnetAlreadyInUseException; -import software.amazon.awssdk.services.redshift.model.Tag; -import software.amazon.awssdk.services.redshift.model.TagLimitExceededException; -import software.amazon.awssdk.services.redshift.model.TaggedResource; import software.amazon.awssdk.services.redshift.model.UnauthorizedOperationException; -import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; -import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import java.util.HashSet; +import java.util.HashMap; import java.util.List; -import java.util.Set; -import java.util.stream.Collectors; +import java.util.Map; +import java.util.Optional; +import java.util.Collections; public class UpdateHandler extends BaseHandlerStd { - private Logger logger; protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { this.logger = logger; + final ResourceModel desiredResourceState = request.getDesiredResourceState(); + final String resourceName = String.format("arn:%s:redshift:%s:%s:subnetgroup:%s", + request.getAwsPartition(), + request.getRegion(), + request.getAwsAccountId(), + desiredResourceState.getClusterSubnetGroupName()); + + // Handle tags + Map allDesiredTags = new HashMap<>(); + allDesiredTags.putAll(Optional.ofNullable(request.getDesiredResourceTags()).orElse(Collections.emptyMap())); + allDesiredTags.putAll(Optional.ofNullable( + Translator.translateFromResourceModelToSdkTags(desiredResourceState.getTags())) + .orElse(Collections.emptyMap())); + List desiredTags = + Translator.translateTagsMapToTagCollection(allDesiredTags); - final ResourceModel model = request.getDesiredResourceState(); + List previousTags = + request.getPreviousResourceState() == null ? + null : request.getPreviousResourceState().getTags(); + Map allPreviousTags = new HashMap<>(); + allPreviousTags.putAll(Optional.ofNullable(request.getPreviousResourceTags()).orElse(Collections.emptyMap())); + allPreviousTags.putAll(Optional.ofNullable(Translator.translateFromResourceModelToSdkTags(previousTags)) + .orElse(Collections.emptyMap())); + List currentTags = + Translator.translateTagsMapToTagCollection(allPreviousTags); - return ProgressEvent.progress(model, callbackContext) - .then(progress -> proxy.initiate("AWS-Redshift-ClusterSubnetGroup::Update", proxyClient, model, callbackContext) + return ProgressEvent.progress(desiredResourceState, callbackContext) + // Read current tags + .then(progress -> proxy.initiate(String.format("%s::Update::ReadTags", CALL_GRAPH_TYPE_NAME), proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest(rm -> Translator.translateToReadTagsRequest(resourceName)) + .makeServiceCall(this::readTags) + .handleError(this::operateTagsErrorHandler) + .done((tagsRequest, tagsResponse, client, model, context) -> ProgressEvent.builder() + .callbackContext(callbackContext) + .callbackDelaySeconds(0) + .resourceModel(Translator.translateFromReadTagsResponse(model, tagsResponse)) + .status(OperationStatus.IN_PROGRESS) + .build())) + // Update tags + .then(progress -> proxy.initiate(String.format("%s::Update::UpdateTags", CALL_GRAPH_TYPE_NAME), proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest(model -> Translator.translateToUpdateTagsRequest(desiredTags, currentTags, resourceName)) + .makeServiceCall(this::updateTags) + .handleError(this::operateTagsErrorHandler) + .done((tagsRequest, tagsResponse, client, model, context) -> ProgressEvent.builder() + .callbackContext(callbackContext) + .callbackDelaySeconds(0) + .resourceModel(desiredResourceState) + .status(OperationStatus.IN_PROGRESS) + .build())) + // Update subnet group + .then(progress -> proxy.initiate(String.format("%s::Update::SubnetGroup", CALL_GRAPH_TYPE_NAME), proxyClient, progress.getResourceModel(), progress.getCallbackContext()) .translateToServiceRequest(Translator::translateToUpdateRequest) - .makeServiceCall((modifyRequest, proxyInvocation) -> { - //Base on contract_update_non_existent_resource contract test - if (StringUtils.isNullOrEmpty(model.getClusterSubnetGroupName())){ - return ProgressEvent.defaultFailureHandler( - new Exception("SubnetGroupName in update handler cannot be null"), HandlerErrorCode.NotFound); - } - ModifyClusterSubnetGroupResponse response = proxyInvocation.injectCredentialsAndInvokeV2( - modifyRequest, proxyInvocation.client()::modifyClusterSubnetGroup); - logger.log(String.format("%s has successfully been updated.", ResourceModel.TYPE_NAME)); - return response; - }) - .handleError((modifyDbSubnetGroupRequest, exception, client, resourceModel, cxt) -> { - System.out.println(exception.toString()); - if (exception instanceof ClusterSubnetGroupNotFoundException) - return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.NotFound); - throw exception; - }) + .makeServiceCall(this::modifyClusterSubnetGroup) + .handleError(this::handleError) .progress()) - .then(progress -> handleTagging(request, proxyClient, proxy, progress)) + // Read the final state .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); } - private ProgressEvent handleTagging( - ResourceHandlerRequest request, - final ProxyClient proxyClient, - final AmazonWebServicesClientProxy proxy, - final ProgressEvent progress) { - try { - final String arn = Translator.getArn(request); - final List prevTags = Translator.getTags(arn, proxy, proxyClient); - final List currTags = Translator.translateTagsMapToTagCollection(request.getDesiredResourceTags()); - final Set prevTagSet = CollectionUtils.isEmpty(prevTags) ? new HashSet<>() : new HashSet<>(prevTags); - final Set currTagSet = CollectionUtils.isEmpty(currTags) ? new HashSet<>() : new HashSet<>(currTags); - - List tagsToCreate = Sets.difference(currTagSet, prevTagSet).immutableCopy().asList(); - List tagsKeyToDelete = Sets.difference(Translator.getTagsKeySet(prevTagSet), Translator.getTagsKeySet(currTagSet)).immutableCopy().asList(); - - if(CollectionUtils.isNotEmpty(tagsToCreate)) { - proxy.injectCredentialsAndInvokeV2(Translator.createTagsRequest(tagsToCreate, arn), proxyClient.client()::createTags); - } + private ModifyClusterSubnetGroupResponse modifyClusterSubnetGroup( + final software.amazon.awssdk.services.redshift.model.ModifyClusterSubnetGroupRequest awsRequest, + final ProxyClient proxyClient) { + ModifyClusterSubnetGroupResponse response = proxyClient.injectCredentialsAndInvokeV2( + awsRequest, proxyClient.client()::modifyClusterSubnetGroup); + logger.log(String.format("%s has successfully been updated.", ResourceModel.TYPE_NAME)); + return response; + } - if(CollectionUtils.isNotEmpty(tagsKeyToDelete)) { - proxy.injectCredentialsAndInvokeV2(Translator.deleteTagsRequest(tagsKeyToDelete, arn), proxyClient.client()::deleteTags); - } + private ProgressEvent handleError( + final software.amazon.awssdk.services.redshift.model.ModifyClusterSubnetGroupRequest awsRequest, + final Exception exception, + final ProxyClient client, + final ResourceModel model, + final CallbackContext context) { - } catch (final InvalidTagException | TagLimitExceededException e) { - throw new CfnGeneralServiceException("updateTagging", e); - } catch (final ResourceNotFoundException e) { - throw new CfnNotFoundException(ResourceModel.TYPE_NAME, request.getDesiredResourceState().getClusterSubnetGroupName()); + if (exception instanceof ClusterSubnetGroupNotFoundException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.NotFound); + } else if (exception instanceof ClusterSubnetQuotaExceededException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.ServiceLimitExceeded); + } else if (exception instanceof InvalidSubnetException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.InvalidRequest); + } else if (exception instanceof UnauthorizedOperationException) { + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.AccessDenied); } - return progress; + return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.GeneralServiceException); } - -} +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/CreateHandlerTest.java b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/CreateHandlerTest.java index 60f06447..8c3c1f08 100644 --- a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/CreateHandlerTest.java +++ b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/CreateHandlerTest.java @@ -1,63 +1,55 @@ package software.amazon.redshift.clustersubnetgroup; -import java.time.Duration; - +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import software.amazon.awssdk.services.redshift.RedshiftClient; import software.amazon.awssdk.services.redshift.model.CreateClusterSubnetGroupRequest; import software.amazon.awssdk.services.redshift.model.CreateClusterSubnetGroupResponse; import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsRequest; import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsResponse; +import software.amazon.awssdk.services.redshift.model.DescribeTagsRequest; +import software.amazon.awssdk.services.redshift.model.DescribeTagsResponse; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.AWS_REGION; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_MODEL; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_MODEL_CREATE; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.DESIRED_RESOURCE_TAGS; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_CLUSTER_SUBNET_GROUP; +import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.*; @ExtendWith(MockitoExtension.class) public class CreateHandlerTest extends AbstractTestBase { + @Mock + RedshiftClient sdkClient; @Mock private AmazonWebServicesClientProxy proxy; - @Mock private ProxyClient proxyClient; - - @Mock - RedshiftClient sdkClient; - private CreateHandler handler; @BeforeEach public void setup() { - handler = new CreateHandler(); - sdkClient = mock(RedshiftClient.class); proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(RedshiftClient.class); proxyClient = MOCK_PROXY(proxy, sdkClient); + handler = new CreateHandler(); } @Test public void handleRequest_SimpleSuccess() { - final ResourceModel model = BASIC_MODEL_CREATE; - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) + .desiredResourceState(MODEL_WITH_TAGS) .region(AWS_REGION) .desiredResourceTags(DESIRED_RESOURCE_TAGS) .logicalResourceIdentifier("logicalId") @@ -74,9 +66,11 @@ public void handleRequest_SimpleSuccess() { .clusterSubnetGroups(BASIC_CLUSTER_SUBNET_GROUP) .build()); + when(proxyClient.client().describeTags(any(DescribeTagsRequest.class))) + .thenReturn(DESCRIBE_TAGS_RESPONSE); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); - System.out.println(response); + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); @@ -84,7 +78,45 @@ public void handleRequest_SimpleSuccess() { assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + verify(proxyClient.client()).createClusterSubnetGroup(any(CreateClusterSubnetGroupRequest.class)); verify(proxyClient.client()).describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class)); } -} + + @Test + public void handleRequest_GeneratedName() { + final ResourceModel model = ResourceModel.builder() + .description(DESCRIPTION) + .subnetIds(SUBNET_IDS) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .region(AWS_REGION) + .logicalResourceIdentifier("logicalId") + .clientRequestToken("token") + .build(); + + when(proxyClient.client().createClusterSubnetGroup(any(CreateClusterSubnetGroupRequest.class))) + .thenReturn(CreateClusterSubnetGroupResponse.builder() + .clusterSubnetGroup(BASIC_CLUSTER_SUBNET_GROUP) + .build()); + + when(proxyClient.client().describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class))) + .thenReturn(DescribeClusterSubnetGroupsResponse.builder() + .clusterSubnetGroups(BASIC_CLUSTER_SUBNET_GROUP) + .build()); + + when(proxyClient.client().describeTags(any(DescribeTagsRequest.class))) + .thenReturn(DescribeTagsResponse.builder().build()); + + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNotNull(); + assertThat(response.getResourceModel().getClusterSubnetGroupName()).isNotNull(); + } +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/DeleteHandlerTest.java b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/DeleteHandlerTest.java index 7894bade..97a061d7 100644 --- a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/DeleteHandlerTest.java +++ b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/DeleteHandlerTest.java @@ -1,28 +1,27 @@ package software.amazon.redshift.clustersubnetgroup; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; import software.amazon.awssdk.services.redshift.RedshiftClient; +import software.amazon.awssdk.services.redshift.model.ClusterSubnetGroupNotFoundException; import software.amazon.awssdk.services.redshift.model.DeleteClusterSubnetGroupRequest; import software.amazon.awssdk.services.redshift.model.DeleteClusterSubnetGroupResponse; +import software.amazon.awssdk.services.redshift.model.InvalidClusterSubnetGroupStateException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import static software.amazon.redshift.clustersubnetgroup.TestUtils.AWS_REGION; import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_MODEL; @@ -31,38 +30,26 @@ @ExtendWith(MockitoExtension.class) public class DeleteHandlerTest extends AbstractTestBase { + @Mock + RedshiftClient sdkClient; @Mock private AmazonWebServicesClientProxy proxy; - @Mock private ProxyClient proxyClient; - - @Mock - RedshiftClient sdkClient; - private DeleteHandler handler; @BeforeEach public void setup() { - handler = new DeleteHandler(); - sdkClient = mock(RedshiftClient.class); proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(RedshiftClient.class); proxyClient = MOCK_PROXY(proxy, sdkClient); - } - - @AfterEach - public void tear_down() { - verify(sdkClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(sdkClient); + handler = new DeleteHandler(); } @Test public void handleRequest_SimpleSuccess() { - - final ResourceModel model = BASIC_MODEL; - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) + .desiredResourceState(BASIC_MODEL) .region(AWS_REGION) .desiredResourceTags(DESIRED_RESOURCE_TAGS) .logicalResourceIdentifier("logicalId") @@ -72,7 +59,9 @@ public void handleRequest_SimpleSuccess() { when(proxyClient.client().deleteClusterSubnetGroup(any(DeleteClusterSubnetGroupRequest.class))) .thenReturn(DeleteClusterSubnetGroupResponse.builder().build()); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); @@ -81,4 +70,49 @@ public void handleRequest_SimpleSuccess() { assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } -} + + @Test + public void handleRequest_SubnetGroupNotFound() { + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(BASIC_MODEL) + .region(AWS_REGION) + .desiredResourceTags(DESIRED_RESOURCE_TAGS) + .logicalResourceIdentifier("logicalId") + .clientRequestToken("token") + .build(); + + when(proxyClient.client().deleteClusterSubnetGroup(any(DeleteClusterSubnetGroupRequest.class))) + .thenThrow(ClusterSubnetGroupNotFoundException.class); + + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.NotFound); + } + + @Test + public void handleRequest_InvalidState() { + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(BASIC_MODEL) + .region(AWS_REGION) + .desiredResourceTags(DESIRED_RESOURCE_TAGS) + .logicalResourceIdentifier("logicalId") + .clientRequestToken("token") + .build(); + + when(proxyClient.client().deleteClusterSubnetGroup(any(DeleteClusterSubnetGroupRequest.class))) + .thenThrow(InvalidClusterSubnetGroupStateException.class); + + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.ResourceConflict); + } +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/ListHandlerTest.java b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/ListHandlerTest.java index 40894891..176072fe 100644 --- a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/ListHandlerTest.java +++ b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/ListHandlerTest.java @@ -1,25 +1,21 @@ package software.amazon.redshift.clustersubnetgroup; -import software.amazon.awssdk.services.redshift.RedshiftClient; -import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsRequest; -import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsResponse; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.OperationStatus; -import software.amazon.cloudformation.proxy.ProgressEvent; -import software.amazon.cloudformation.proxy.ProxyClient; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; - -import java.time.Duration; +import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsResponse; +import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.Logger; +import software.amazon.cloudformation.proxy.OperationStatus; +import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; import static software.amazon.redshift.clustersubnetgroup.TestUtils.AWS_REGION; import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_CLUSTER_SUBNET_GROUP; import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_MODEL; @@ -31,43 +27,87 @@ public class ListHandlerTest extends AbstractTestBase { private AmazonWebServicesClientProxy proxy; @Mock - private ProxyClient proxyClient; - - @Mock - RedshiftClient sdkClient; - - private ListHandler handler; + private Logger logger; @BeforeEach public void setup() { - handler = new ListHandler(); - sdkClient = mock(RedshiftClient.class); - proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); - proxyClient = MOCK_PROXY(proxy, sdkClient); + proxy = mock(AmazonWebServicesClientProxy.class); + logger = mock(Logger.class); + System.setProperty("aws.region", AWS_REGION); } @Test public void handleRequest_SimpleSuccess() { - when(proxyClient.client().describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class))) - .thenReturn(DescribeClusterSubnetGroupsResponse.builder() - .clusterSubnetGroups(BASIC_CLUSTER_SUBNET_GROUP) - .build()); + final ListHandler handler = new ListHandler(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() .desiredResourceState(BASIC_MODEL) .region(AWS_REGION) .build(); + doReturn(DescribeClusterSubnetGroupsResponse.builder() + .clusterSubnetGroups(BASIC_CLUSTER_SUBNET_GROUP) + .marker("") + .build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + final ProgressEvent response = - handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + handler.handleRequest(proxy, request, null, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); assertThat(response.getResourceModel()).isNull(); assertThat(response.getResourceModels()).isNotNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); } -} + + @Test + public void handleRequest_WithPagination() { + final ListHandler handler = new ListHandler(); + final String nextToken = "next-token"; + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(BASIC_MODEL) + .region(AWS_REGION) + .nextToken("current-token") + .build(); + + doReturn(DescribeClusterSubnetGroupsResponse.builder() + .clusterSubnetGroups(BASIC_CLUSTER_SUBNET_GROUP) + .marker(nextToken) + .build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + final ProgressEvent response = + handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getNextToken()).isEqualTo(nextToken); + assertThat(response.getResourceModels()).isNotNull(); + } + + @Test + public void handleRequest_EmptyList() { + final ListHandler handler = new ListHandler(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .build(); + + doReturn(DescribeClusterSubnetGroupsResponse.builder() + .clusterSubnetGroups(java.util.Collections.emptyList()) + .build()) + .when(proxy) + .injectCredentialsAndInvokeV2(any(), any()); + + final ProgressEvent response = + handler.handleRequest(proxy, request, null, logger); + + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getResourceModels()).isEmpty(); + } +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/ReadHandlerTest.java b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/ReadHandlerTest.java index 42e48d44..8660365b 100644 --- a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/ReadHandlerTest.java +++ b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/ReadHandlerTest.java @@ -1,9 +1,14 @@ package software.amazon.redshift.clustersubnetgroup; import software.amazon.awssdk.services.redshift.RedshiftClient; +import software.amazon.awssdk.services.redshift.model.ClusterSubnetGroupNotFoundException; import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsRequest; import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsResponse; +import software.amazon.awssdk.services.redshift.model.DescribeTagsRequest; +import software.amazon.awssdk.services.redshift.model.DescribeTagsResponse; +import software.amazon.awssdk.services.redshift.model.InvalidTagException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; @@ -15,13 +20,21 @@ import org.mockito.junit.jupiter.MockitoExtension; import java.time.Duration; +import java.util.Arrays; +import java.util.Collections; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.AWS_ACCOUNT_ID; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.AWS_REGION; import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_CLUSTER_SUBNET_GROUP; import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_MODEL; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.DESCRIBE_TAGS_RESPONSE; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.MODEL_WITH_TAGS; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.TAGS; @ExtendWith(MockitoExtension.class) public class ReadHandlerTest extends AbstractTestBase { @@ -39,7 +52,6 @@ public class ReadHandlerTest extends AbstractTestBase { @BeforeEach public void setup() { - handler = new ReadHandler(); sdkClient = mock(RedshiftClient.class); proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); @@ -50,22 +62,97 @@ public void setup() { public void handleRequest_SimpleSuccess() { final ResourceModel model = BASIC_MODEL; + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .region(AWS_REGION) + .awsAccountId(AWS_ACCOUNT_ID) + .build(); + when(proxyClient.client().describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class))) .thenReturn(DescribeClusterSubnetGroupsResponse.builder() - .clusterSubnetGroups(BASIC_CLUSTER_SUBNET_GROUP) + .clusterSubnetGroups(Arrays.asList(BASIC_CLUSTER_SUBNET_GROUP)) + .build()); + + when(proxyClient.client().describeTags(any(DescribeTagsRequest.class))) + .thenReturn(DescribeTagsResponse.builder() + .taggedResources(Collections.emptyList()) .build()); + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getResourceModel()).isEqualTo(model); + assertThat(response.getResourceModel().getTags()).isEmpty(); + + verify(proxyClient.client()).describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class)); + verify(proxyClient.client()).describeTags(any(DescribeTagsRequest.class)); + } + + @Test + public void handleRequest_NotFound() { + final ResourceModel model = BASIC_MODEL; + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + when(proxyClient.client().describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class))) + .thenThrow(ClusterSubnetGroupNotFoundException.class); + + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.NotFound); + } + + @Test + public void handleRequest_WithTags() { + final ResourceModel model = MODEL_WITH_TAGS; + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() .desiredResourceState(model) .build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + when(proxyClient.client().describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class))) + .thenReturn(DescribeClusterSubnetGroupsResponse.builder() + .clusterSubnetGroups(Arrays.asList(BASIC_CLUSTER_SUBNET_GROUP)) + .build()); + + when(proxyClient.client().describeTags(any(DescribeTagsRequest.class))) + .thenReturn(DESCRIBE_TAGS_RESPONSE); + + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); + assertThat(response.getResourceModel().getTags()).isNotNull(); + assertThat(response.getResourceModel().getTags()).isEqualTo(TAGS); + } + + @Test + public void handleRequest_InvalidTagResponse() { + final ResourceModel model = BASIC_MODEL; + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + when(proxyClient.client().describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class))) + .thenReturn(DescribeClusterSubnetGroupsResponse.builder() + .clusterSubnetGroups(Arrays.asList(BASIC_CLUSTER_SUBNET_GROUP)) + .build()); + + when(proxyClient.client().describeTags(any(DescribeTagsRequest.class))) + .thenThrow(InvalidTagException.class); + + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.InvalidRequest); } -} +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/TestUtils.java b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/TestUtils.java index 661ca013..214c9c58 100644 --- a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/TestUtils.java +++ b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/TestUtils.java @@ -12,63 +12,114 @@ import software.amazon.awssdk.services.redshift.model.TaggedResource; import java.util.Arrays; +import java.util.Collections; import java.util.List; import java.util.Map; public class TestUtils { + // Basic constants final static String AWS_REGION = "us-east-1"; final static String DESCRIPTION = "description"; - final static String SUBNET_GROUP_NAME = "name"; - final static String AWS_ACCOUNT_ID ="1111"; - final static String ARN = String.format("arn:aws:redshift:%s:%s:subnetgroup:%s", AWS_REGION, AWS_ACCOUNT_ID, SUBNET_GROUP_NAME); + final static String SUBNET_GROUP_NAME = "logicalid-kvw2fztz3cvh"; + final static String AWS_ACCOUNT_ID = "1111"; + final static String ARN = String.format("arn:aws:redshift:%s:%s:subnetgroup:%s", + AWS_REGION, AWS_ACCOUNT_ID, SUBNET_GROUP_NAME); + // Subnet IDs final static List SUBNET_IDS = Arrays.asList("subnet-1", "subnet-2"); - final static List TAGGED_RESOURCES = Arrays.asList( - TaggedResource.builder().tag(Tag.builder().key("key1").value("val1").build()).build(), - TaggedResource.builder().tag(Tag.builder().key("key2").value("val2").build()).build(), - TaggedResource.builder().tag(Tag.builder().key("stackKey").value("stackValue").build()).build() + + // Tags + final static Map DESIRED_RESOURCE_TAGS = ImmutableMap.of( + "key1", "val1", + "key2", "val2", + "key3", "val3" ); - final static List TAGGED_RESOURCES_CREATING = Arrays.asList( - TaggedResource.builder().tag(Tag.builder().key("key1").value("val1_create").build()).build(), - TaggedResource.builder().tag(Tag.builder().key("key3").value("val3").build()).build(), - TaggedResource.builder().tag(Tag.builder().key("stackKey").value("stackValueCreated").build()).build() + final static Map PREVIOUS_RESOURCE_TAGS = ImmutableMap.of( + "key4", "val4", + "key2", "val2" ); final static List TAGS = Arrays.asList( new software.amazon.redshift.clustersubnetgroup.Tag("key1", "val1"), new software.amazon.redshift.clustersubnetgroup.Tag("key2", "val2"), - new software.amazon.redshift.clustersubnetgroup.Tag("stackKey", "stackValue")); - final static Map DESIRED_RESOURCE_TAGS = ImmutableMap.of("key1", "val1", "key2", "val2", "stackKey", "stackValue"); - final static List SDK_SUBNET_GROUP = Arrays.asList(ClusterSubnetGroup.builder().build()); - - - final static List SDK_TAGS = Arrays.asList( - Tag.builder().key("key1").value("val1").build(), - Tag.builder().key("key2").value("val2").build(), - Tag.builder().key("stackKey").value("stackValue").build() + new software.amazon.redshift.clustersubnetgroup.Tag("key3", "val3") ); - final static List SDK_TAGS_TO_CREATE = Arrays.asList( + final static List SDK_TAGS = Arrays.asList( Tag.builder().key("key1").value("val1").build(), Tag.builder().key("key2").value("val2").build(), Tag.builder().key("stackKey").value("stackValue").build() ); - final static List SDK_TAG_KEYS_TO_DELETE = ImmutableList.of("key3"); + // Resource Models + final static ResourceModel BASIC_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .clusterSubnetGroupName(SUBNET_GROUP_NAME) + .subnetIds(SUBNET_IDS) + .tags(Collections.emptyList()) + .build(); - final static ResourceModel BASIC_MODEL_CREATE = ResourceModel.builder() + final static ResourceModel MODEL_WITH_TAGS = ResourceModel.builder() .description(DESCRIPTION) + .clusterSubnetGroupName(SUBNET_GROUP_NAME) .subnetIds(SUBNET_IDS) .tags(TAGS) .build(); - final static ResourceModel BASIC_MODEL = ResourceModel.builder() + final static ResourceModel PREVIOUS_MODEL = ResourceModel.builder() + .description("old-description") + .clusterSubnetGroupName(SUBNET_GROUP_NAME) + .subnetIds(Arrays.asList("subnet-old")) + .tags(Arrays.asList( + new software.amazon.redshift.clustersubnetgroup.Tag("key4", "val4"), + new software.amazon.redshift.clustersubnetgroup.Tag("key2", "val2") + )) + .build(); + + // Subnet Groups + final static ClusterSubnetGroup BASIC_CLUSTER_SUBNET_GROUP = ClusterSubnetGroup.builder() + .clusterSubnetGroupName(SUBNET_GROUP_NAME) .description(DESCRIPTION) + .subnets(Arrays.asList( + Subnet.builder().subnetIdentifier(SUBNET_IDS.get(0)).build(), + Subnet.builder().subnetIdentifier(SUBNET_IDS.get(1)).build() + )) + .tags(SDK_TAGS) + .build(); + + final static ClusterSubnetGroup UPDATED_CLUSTER_SUBNET_GROUP = ClusterSubnetGroup.builder() .clusterSubnetGroupName(SUBNET_GROUP_NAME) - .subnetIds(SUBNET_IDS) + .description(DESCRIPTION) + .subnets(Arrays.asList( + Subnet.builder().subnetIdentifier(SUBNET_IDS.get(0)).build(), + Subnet.builder().subnetIdentifier(SUBNET_IDS.get(1)).build() + )) + .tags(SDK_TAGS) .build(); + // Tagged Resources + final static List TAGGED_RESOURCES = Arrays.asList( + TaggedResource.builder().tag(Tag.builder().key("key1").value("val1").build()).build(), + TaggedResource.builder().tag(Tag.builder().key("key2").value("val2").build()).build(), + TaggedResource.builder().tag(Tag.builder().key("key3").value("val3").build()).build() + ); + + final static List TAGGED_RESOURCES_CREATING = Arrays.asList( + TaggedResource.builder().tag(Tag.builder().key("key1").value("val1_create").build()).build(), + TaggedResource.builder().tag(Tag.builder().key("key3").value("val3").build()).build(), + TaggedResource.builder().tag(Tag.builder().key("stackKey").value("stackValueCreated").build()).build() + ); + + // Tag Operations + final static List SDK_TAGS_TO_CREATE = Arrays.asList( + Tag.builder().key("key1").value("val1").build(), + Tag.builder().key("key3").value("val3").build() + ); + + final static List SDK_TAGS_TO_DELETE = ImmutableList.of("key4"); + + // Responses final static DescribeTagsResponse DESCRIBE_TAGS_RESPONSE = DescribeTagsResponse.builder() .taggedResources(TAGGED_RESOURCES) .build(); @@ -77,14 +128,47 @@ public class TestUtils { .taggedResources(TAGGED_RESOURCES_CREATING) .build(); - final static ClusterSubnetGroup BASIC_CLUSTER_SUBNET_GROUP = ClusterSubnetGroup.builder() - .clusterSubnetGroupName(SUBNET_GROUP_NAME) + final static DescribeTagsResponse DESCRIBE_TAGS_RESPONSE_UPDATING = DescribeTagsResponse.builder() + .taggedResources(Arrays.asList( + TaggedResource.builder() + .tag(Tag.builder().key("key2").value("val2").build()) + .build(), + TaggedResource.builder() + .tag(Tag.builder().key("key4").value("val4").build()) + .build() + )) + .build(); + + // Requests + final static CreateTagsRequest CREATE_TAGS_REQUEST = CreateTagsRequest.builder() + .resourceName(ARN) + .tags(SDK_TAGS_TO_CREATE) + .build(); + + final static DeleteTagsRequest DELETE_TAGS_REQUEST = DeleteTagsRequest.builder() + .resourceName(ARN) + .tagKeys(SDK_TAGS_TO_DELETE) + .build(); + + final static DescribeTagsRequest DESCRIBE_TAGS_REQUEST = DescribeTagsRequest.builder() + .resourceName(ARN) + .build(); + + // Invalid Models + final static ResourceModel INVALID_SUBNET_MODEL = ResourceModel.builder() .description(DESCRIPTION) - .subnets(Subnet.builder().subnetIdentifier("subnet-1").build(), Subnet.builder().subnetIdentifier("subnet-2").build()) - .tags(SDK_TAGS) + .clusterSubnetGroupName(SUBNET_GROUP_NAME) + .subnetIds(Arrays.asList("invalid-subnet-1")) + .tags(TAGS) .build(); - final static CreateTagsRequest CREATE_TAGS_REQUEST = CreateTagsRequest.builder().resourceName(ARN).tags(SDK_TAGS_TO_CREATE).build(); - final static DeleteTagsRequest DELETE_TAGS_REQUEST = DeleteTagsRequest.builder().resourceName(ARN).tagKeys(SDK_TAG_KEYS_TO_DELETE).build(); - final static DescribeTagsRequest DESCRIBE_TAGS_REQUEST = DescribeTagsRequest.builder().resourceName(ARN).build(); -} + // Helper Methods + static ResourceModel buildModelWithTags(List tags) { + return ResourceModel.builder() + .description(DESCRIPTION) + .clusterSubnetGroupName(SUBNET_GROUP_NAME) + .subnetIds(SUBNET_IDS) + .tags(tags) + .build(); + } +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/UpdateHandlerTest.java b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/UpdateHandlerTest.java index 618bd0c3..3d66300a 100644 --- a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/UpdateHandlerTest.java +++ b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/UpdateHandlerTest.java @@ -1,91 +1,71 @@ package software.amazon.redshift.clustersubnetgroup; -import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.ArgumentCaptor; -import software.amazon.awssdk.awscore.AwsRequest; -import software.amazon.awssdk.awscore.AwsResponse; -import software.amazon.awssdk.core.SdkClient; -import software.amazon.awssdk.core.pagination.sync.SdkIterable; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import software.amazon.awssdk.services.redshift.RedshiftClient; -import software.amazon.awssdk.services.redshift.model.ClusterSubnetGroup; import software.amazon.awssdk.services.redshift.model.ClusterSubnetGroupNotFoundException; -import software.amazon.awssdk.services.redshift.model.ClusterSubnetQuotaExceededException; import software.amazon.awssdk.services.redshift.model.CreateTagsRequest; +import software.amazon.awssdk.services.redshift.model.CreateTagsResponse; import software.amazon.awssdk.services.redshift.model.DeleteTagsRequest; -import software.amazon.awssdk.services.redshift.model.DependentServiceRequestThrottlingException; import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsRequest; import software.amazon.awssdk.services.redshift.model.DescribeClusterSubnetGroupsResponse; import software.amazon.awssdk.services.redshift.model.DescribeTagsRequest; -import software.amazon.awssdk.services.redshift.model.InvalidSubnetException; +import software.amazon.awssdk.services.redshift.model.DescribeTagsResponse; import software.amazon.awssdk.services.redshift.model.ModifyClusterSubnetGroupRequest; import software.amazon.awssdk.services.redshift.model.ModifyClusterSubnetGroupResponse; -import software.amazon.awssdk.services.redshift.model.Subnet; -import software.amazon.awssdk.services.redshift.model.SubnetAlreadyInUseException; -import software.amazon.awssdk.services.redshift.model.Tag; -import software.amazon.awssdk.services.redshift.model.UnauthorizedOperationException; -import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; -import org.mockito.junit.jupiter.MockitoExtension; import java.time.Duration; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.AWS_ACCOUNT_ID; import static software.amazon.redshift.clustersubnetgroup.TestUtils.AWS_REGION; import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_CLUSTER_SUBNET_GROUP; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.BASIC_MODEL; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.CREATE_TAGS_REQUEST; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.DELETE_TAGS_REQUEST; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.DESCRIBE_TAGS_RESPONSE; -import static software.amazon.redshift.clustersubnetgroup.TestUtils.DESCRIBE_TAGS_RESPONSE_CREATING; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.MODEL_WITH_TAGS; import static software.amazon.redshift.clustersubnetgroup.TestUtils.DESIRED_RESOURCE_TAGS; -import static org.mockito.Mockito.*; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.PREVIOUS_RESOURCE_TAGS; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.DESCRIBE_TAGS_RESPONSE_CREATING; @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public class UpdateHandlerTest extends AbstractTestBase { + @Mock + RedshiftClient sdkClient; @Mock private AmazonWebServicesClientProxy proxy; - @Mock private ProxyClient proxyClient; - - @Mock - RedshiftClient sdkClient; - private UpdateHandler handler; @BeforeEach public void setup() { - handler = new UpdateHandler(); - sdkClient = mock(RedshiftClient.class); proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + sdkClient = mock(RedshiftClient.class); proxyClient = MOCK_PROXY(proxy, sdkClient); + handler = new UpdateHandler(); } @Test - public void handleRequest_SimpleSuccess() { - final ResourceModel model = BASIC_MODEL; - + public void handleRequest_UpdateTags() { final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) - .region(AWS_REGION) - .awsAccountId(AWS_ACCOUNT_ID) + .desiredResourceState(MODEL_WITH_TAGS) .desiredResourceTags(DESIRED_RESOURCE_TAGS) + .previousResourceTags(PREVIOUS_RESOURCE_TAGS) + .region(AWS_REGION) .logicalResourceIdentifier("logicalId") .clientRequestToken("token") .build(); @@ -96,67 +76,82 @@ public void handleRequest_SimpleSuccess() { .build()); when(proxyClient.client().describeTags(any(DescribeTagsRequest.class))) - .thenReturn(DESCRIBE_TAGS_RESPONSE); + .thenReturn(DESCRIBE_TAGS_RESPONSE_CREATING); when(proxyClient.client().describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class))) .thenReturn(DescribeClusterSubnetGroupsResponse.builder() .clusterSubnetGroups(BASIC_CLUSTER_SUBNET_GROUP) .build()); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + ArgumentCaptor createTagArgument = ArgumentCaptor.forClass(CreateTagsRequest.class); + verify(proxyClient.client()).createTags(createTagArgument.capture()); + ArgumentCaptor deleteTagArgument = ArgumentCaptor.forClass(DeleteTagsRequest.class); + verify(proxyClient.client()).deleteTags(deleteTagArgument.capture()); } @Test - public void handleRequest_UpdateTags() { - final ResourceModel model = BASIC_MODEL; - + public void handleRequest_SimpleSuccess() { final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) - .region(AWS_REGION) - .awsAccountId(AWS_ACCOUNT_ID) + .desiredResourceState(MODEL_WITH_TAGS) .desiredResourceTags(DESIRED_RESOURCE_TAGS) + .region(AWS_REGION) .logicalResourceIdentifier("logicalId") .clientRequestToken("token") .build(); + when(proxyClient.client().describeTags(any(DescribeTagsRequest.class))) + .thenReturn(DescribeTagsResponse.builder().build()); + + when(proxyClient.client().createTags(any(CreateTagsRequest.class))) + .thenReturn(CreateTagsResponse.builder().build()); + when(proxyClient.client().modifyClusterSubnetGroup(any(ModifyClusterSubnetGroupRequest.class))) .thenReturn(ModifyClusterSubnetGroupResponse.builder() .clusterSubnetGroup(BASIC_CLUSTER_SUBNET_GROUP) .build()); - when(proxyClient.client().describeTags(any(DescribeTagsRequest.class))) - .thenReturn(DESCRIBE_TAGS_RESPONSE_CREATING); - when(proxyClient.client().describeClusterSubnetGroups(any(DescribeClusterSubnetGroupsRequest.class))) .thenReturn(DescribeClusterSubnetGroupsResponse.builder() .clusterSubnetGroups(BASIC_CLUSTER_SUBNET_GROUP) .build()); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); assertThat(response).isNotNull(); assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); assertThat(response.getResourceModels()).isNull(); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + } - ArgumentCaptor createTagArgument = ArgumentCaptor.forClass(CreateTagsRequest.class); - verify(proxyClient.client()).createTags(createTagArgument.capture()); - assertEquals(CREATE_TAGS_REQUEST.tags(), createTagArgument.getValue().tags()); + @Test + public void handleRequest_NotFound() { + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(MODEL_WITH_TAGS) + .desiredResourceTags(DESIRED_RESOURCE_TAGS) + .region(AWS_REGION) + .build(); - ArgumentCaptor deleteTagArgument = ArgumentCaptor.forClass(DeleteTagsRequest.class); - verify(proxyClient.client()).deleteTags(deleteTagArgument.capture()); - assertEquals(DELETE_TAGS_REQUEST.tagKeys(), deleteTagArgument.getValue().tagKeys()); + when(proxyClient.client().modifyClusterSubnetGroup(any(ModifyClusterSubnetGroupRequest.class))) + .thenThrow(ClusterSubnetGroupNotFoundException.class); + + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getErrorCode()).isNotNull(); } -} +} \ No newline at end of file From 5280910e6f48cf9ca67891145238834670ab50c8 Mon Sep 17 00:00:00 2001 From: Mugdha Patil Date: Sat, 31 May 2025 21:52:32 -0700 Subject: [PATCH 2/3] Completed contract testing --- .../clustersubnetgroup/CreateHandler.java | 5 ++++- .../clustersubnetgroup/ReadHandler.java | 17 ++++++++++++++++- .../redshift/clustersubnetgroup/Translator.java | 2 +- .../clustersubnetgroup/CreateHandlerTest.java | 1 + .../clustersubnetgroup/UpdateHandlerTest.java | 2 +- 5 files changed, 23 insertions(+), 4 deletions(-) diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/CreateHandler.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/CreateHandler.java index 39f0f7bc..8d1d5542 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/CreateHandler.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/CreateHandler.java @@ -61,7 +61,10 @@ protected ProgressEvent handleRequest( .handleError(this::createClusterSubnetGroupErrorHandler) .progress() ) - .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); + .then(progress -> { + model.setClusterSubnetGroupName(progress.getResourceModel().getClusterSubnetGroupName()); + return new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger); + }); } private CreateClusterSubnetGroupResponse createClusterSubnetGroup( diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ReadHandler.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ReadHandler.java index 2f6eede1..a69c6710 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ReadHandler.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/ReadHandler.java @@ -22,6 +22,13 @@ protected ProgressEvent handleRequest( this.logger = logger; final ResourceModel model = request.getDesiredResourceState(); + + // Validate primary identifier + if (model == null || model.getClusterSubnetGroupName() == null) { + return ProgressEvent.failed(null, callbackContext, HandlerErrorCode.NotFound, + "ClusterSubnetGroupName is required"); + } + final String resourceName = String.format("arn:%s:redshift:%s:%s:subnetgroup:%s", request.getAwsPartition(), request.getRegion(), @@ -38,7 +45,15 @@ protected ProgressEvent handleRequest( } return ProgressEvent.defaultFailureHandler(exception, HandlerErrorCode.GeneralServiceException); }) - .done(awsResponse -> ProgressEvent.progress(Translator.translateFromReadResponse(awsResponse), callbackContext))) + .done(awsResponse -> { + ResourceModel updatedModel = Translator.translateFromReadResponse(awsResponse); + // Ensure primaryIdentifier is set + if (updatedModel.getClusterSubnetGroupName() == null) { + updatedModel.setClusterSubnetGroupName( + model.getClusterSubnetGroupName()); + } + return ProgressEvent.progress(updatedModel, callbackContext); + })) .then(progress -> proxy.initiate(String.format("%s::Read::Tags", CALL_GRAPH_TYPE_NAME), proxyClient, progress.getResourceModel(), callbackContext) .translateToServiceRequest(rm -> Translator.translateToReadTagsRequest(resourceName)) .makeServiceCall(this::readTags) // Using inherited readTags method diff --git a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/Translator.java b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/Translator.java index 25c5b907..b9358181 100644 --- a/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/Translator.java +++ b/aws-redshift-clustersubnetgroup/src/main/java/software/amazon/redshift/clustersubnetgroup/Translator.java @@ -265,4 +265,4 @@ private static List subtract(List a, List b) { .collect(Collectors.toList())) .orElse(Collections.emptyList()); } -} \ No newline at end of file +} diff --git a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/CreateHandlerTest.java b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/CreateHandlerTest.java index 8c3c1f08..f4796cd1 100644 --- a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/CreateHandlerTest.java +++ b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/CreateHandlerTest.java @@ -26,6 +26,7 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static software.amazon.redshift.clustersubnetgroup.TestUtils.*; +import static software.amazon.redshift.clustersubnetgroup.TestUtils.MODEL_WITH_TAGS; @ExtendWith(MockitoExtension.class) public class CreateHandlerTest extends AbstractTestBase { diff --git a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/UpdateHandlerTest.java b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/UpdateHandlerTest.java index 3d66300a..0f0ef61f 100644 --- a/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/UpdateHandlerTest.java +++ b/aws-redshift-clustersubnetgroup/src/test/java/software/amazon/redshift/clustersubnetgroup/UpdateHandlerTest.java @@ -154,4 +154,4 @@ public void handleRequest_NotFound() { assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); assertThat(response.getErrorCode()).isNotNull(); } -} \ No newline at end of file +} From 5d8bd5009dd857a2af5e2e66dd5a80f124e2a082 Mon Sep 17 00:00:00 2001 From: Mugdha Patil Date: Tue, 3 Jun 2025 12:48:17 -0700 Subject: [PATCH 3/3] SAM CLI tests and contract tests coverage --- .../aws-redshift-clustersubnetgroup.json | 19 +++++++++++-------- .../docs/README.md | 4 ++-- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/aws-redshift-clustersubnetgroup/aws-redshift-clustersubnetgroup.json b/aws-redshift-clustersubnetgroup/aws-redshift-clustersubnetgroup.json index dd5f4510..2bb94d02 100644 --- a/aws-redshift-clustersubnetgroup/aws-redshift-clustersubnetgroup.json +++ b/aws-redshift-clustersubnetgroup/aws-redshift-clustersubnetgroup.json @@ -1,6 +1,6 @@ { "typeName": "AWS::Redshift::ClusterSubnetGroup", - "description": "Specifies an Amazon Redshift subnet group.", + "description": "Resource Type definition for AWS::Redshift::ClusterSubnetGroup. Specifies an Amazon Redshift subnet group.", "sourceUrl": "https://github.com/aws-cloudformation/aws-cloudformation-resource-providers-redshift", "definitions": { "Tag": { @@ -36,10 +36,13 @@ "description": "The list of VPC subnet IDs", "type": "array", "insertionOrder": false, - "minItems": 1, "maxItems": 20, "items": { - "type": "string" + "type": "string", + "relationshipRef": { + "typeName": "AWS::EC2::Subnet", + "propertyPath": "/properties/SubnetId" + } } }, "Tags": { @@ -62,10 +65,11 @@ "Description", "SubnetIds" ], - "primaryIdentifier": [ - "/properties/ClusterSubnetGroupName" - ], "createOnlyProperties": [ + "/properties/ClusterSubnetGroupName", + "/properties/Description" + ], + "primaryIdentifier": [ "/properties/ClusterSubnetGroupName" ], "tagging": { @@ -87,7 +91,6 @@ "redshift:CreateTags", "redshift:DescribeClusterSubnetGroups", "redshift:DescribeTags", - "redshift:CreateTags", "ec2:AllocateAddress", "ec2:AssociateAddress", "ec2:AttachNetworkInterface", @@ -170,4 +173,4 @@ ] } } -} +} \ No newline at end of file diff --git a/aws-redshift-clustersubnetgroup/docs/README.md b/aws-redshift-clustersubnetgroup/docs/README.md index b5043860..11a05795 100644 --- a/aws-redshift-clustersubnetgroup/docs/README.md +++ b/aws-redshift-clustersubnetgroup/docs/README.md @@ -1,6 +1,6 @@ # AWS::Redshift::ClusterSubnetGroup -Specifies an Amazon Redshift subnet group. +Resource Type definition for AWS::Redshift::ClusterSubnetGroup. Specifies an Amazon Redshift subnet group. ## Syntax @@ -43,7 +43,7 @@ _Required_: Yes _Type_: String -_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) #### SubnetIds