diff --git a/aws-ssm-parameter/.gitignore b/aws-ssm-parameter/.gitignore index 9b36fbcc..f95f7b19 100644 --- a/aws-ssm-parameter/.gitignore +++ b/aws-ssm-parameter/.gitignore @@ -18,3 +18,5 @@ target/ # our logs rpdk.log + +sam-tests diff --git a/aws-ssm-parameter/.rpdk-config b/aws-ssm-parameter/.rpdk-config index 136a7167..0767480e 100644 --- a/aws-ssm-parameter/.rpdk-config +++ b/aws-ssm-parameter/.rpdk-config @@ -1,4 +1,5 @@ { + "artifact_type": "RESOURCE", "typeName": "AWS::SSM::Parameter", "language": "java", "runtime": "java8", @@ -12,5 +13,6 @@ "parameter" ], "protocolVersion": "2.0.0" - } + }, + "executableEntrypoint": "com.amazonaws.ssm.parameter.HandlerWrapperExecutable" } diff --git a/aws-ssm-parameter/aws-ssm-parameter.json b/aws-ssm-parameter/aws-ssm-parameter.json index 61d9b4a2..1ea638b4 100644 --- a/aws-ssm-parameter/aws-ssm-parameter.json +++ b/aws-ssm-parameter/aws-ssm-parameter.json @@ -5,72 +5,45 @@ "properties": { "Type": { "type": "string", - "description": "The type of the parameter.", - "enum": [ - "String", - "StringList", - "SecureString" - ] - }, - "Value": { - "type": "string", - "description": "The value associated with the parameter.", - "minLength": 1, - "maxLength": 32768 + "description": "The type of parameter." }, "Description": { "type": "string", - "description": "The information about the parameter.", - "minLength": 0, - "maxLength": 1024 + "description": "Information about the parameter." }, "Policies": { "type": "string", - "description": "The policies attached to the parameter." + "description": "Information about the policies assigned to a parameter." }, "AllowedPattern": { "type": "string", - "description": "The regular expression used to validate the parameter value.", - "minLength": 0, - "maxLength": 1024 + "description": "A regular expression used to validate the parameter value." }, "Tier": { "type": "string", - "description": "The corresponding tier of the parameter.", - "enum": [ - "Standard", - "Advanced", - "Intelligent-Tiering" - ] + "description": "The parameter tier." }, - "Tags": { - "type": "object", - "description": "A key-value pair to associate with a resource.", - "patternProperties": { - "^([\\p{L}\\p{Z}\\p{N}_.:/=+\\-@]*)$": { - "type": "string" - } - }, - "additionalProperties": false + "Value": { + "type": "string", + "description": "The parameter value." }, "DataType": { "type": "string", - "description": "The corresponding DataType of the parameter.", - "enum": [ - "text", - "aws:ec2:image" - ] + "description": "The data type of the parameter, such as text or aws:ec2:image. The default is text." + }, + "Tags": { + "type": "object", + "description": "Optional metadata that you assign to a resource in the form of an arbitrary set of tags (key-value pairs)" }, "Name": { "type": "string", - "description": "The name of the parameter.", - "minLength": 1, - "maxLength": 2048 + "description": "The name of the parameter." } }, + "taggable": true, "required": [ - "Value", - "Type" + "Type", + "Value" ], "createOnlyProperties": [ "/properties/Name" @@ -78,32 +51,29 @@ "primaryIdentifier": [ "/properties/Name" ], - "readOnlyProperties": [ - "/properties/Name", - "/properties/Type", - "/properties/Value" - ], "handlers": { "create": { "permissions": [ + "ssm:GetParameters", "ssm:PutParameter", - "ssm:GetParameters" - ], - "timeoutInMinutes": 5 + "ssm:DescribeParameters", + "ssm:ListTagsForResource" + ] }, "read": { "permissions": [ - "ssm:GetParameters" + "ssm:GetParameters", + "ssm:DescribeParameters", + "ssm:ListTagsForResource" ] }, "update": { "permissions": [ "ssm:PutParameter", + "ssm:GetParameters", "ssm:AddTagsToResource", - "ssm:RemoveTagsFromResource", - "ssm:GetParameters" - ], - "timeoutInMinutes": 5 + "ssm:RemoveTagsFromResource" + ] }, "delete": { "permissions": [ diff --git a/aws-ssm-parameter/docs/README.md b/aws-ssm-parameter/docs/README.md new file mode 100644 index 00000000..5150e7e7 --- /dev/null +++ b/aws-ssm-parameter/docs/README.md @@ -0,0 +1,140 @@ +# AWS::SSM::Parameter + +Resource Type definition for AWS::SSM::Parameter + +## Syntax + +To declare this entity in your AWS CloudFormation template, use the following syntax: + +### JSON + +
+{
+    "Type" : "AWS::SSM::Parameter",
+    "Properties" : {
+        "Type" : String,
+        "Description" : String,
+        "Policies" : String,
+        "AllowedPattern" : String,
+        "Tier" : String,
+        "Value" : String,
+        "DataType" : String,
+        "Tags" : Map,
+        "Name" : String
+    }
+}
+
+ +### YAML + +
+Type: AWS::SSM::Parameter
+Properties:
+    Type: String
+    Description: String
+    Policies: String
+    AllowedPattern: String
+    Tier: String
+    Value: String
+    DataType: String
+    Tags: Map
+    Name: String
+
+ +## Properties + +#### Type + +The type of parameter. + +_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) + +#### Description + +Information about the parameter. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Policies + +Information about the policies assigned to a parameter. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### AllowedPattern + +A regular expression used to validate the parameter value. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Tier + +The parameter tier. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Value + +The parameter value. + +_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) + +#### DataType + +The data type of the parameter, such as text or aws:ec2:image. The default is text. + +_Required_: No + +_Type_: String + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Tags + +Optional metadata that you assign to a resource in the form of an arbitrary set of tags (key-value pairs) + +_Required_: No + +_Type_: Map + +_Update requires_: [No interruption](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-no-interrupt) + +#### Name + +The name of the parameter. + +_Required_: No + +_Type_: String + +_Update requires_: [Replacement](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-updating-stacks-update-behaviors.html#update-replacement) + +## Return Values + +### Ref + +When you pass the logical ID of this resource to the intrinsic `Ref` function, Ref returns the Name. diff --git a/aws-ssm-parameter/pom.xml b/aws-ssm-parameter/pom.xml index 7dbb2295..4cb64da3 100644 --- a/aws-ssm-parameter/pom.xml +++ b/aws-ssm-parameter/pom.xml @@ -12,8 +12,9 @@ jar - 1.8 - 1.8 + 1.8 + ${java.version} + ${java.version} UTF-8 UTF-8 @@ -35,48 +36,67 @@ software.amazon.awssdk ssm - 2.15.28 software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.6, 3.0.0) org.projectlombok lombok - 1.18.4 + 1.18.12 provided + + + org.apache.logging.log4j + log4j-api + 2.17.0 + + + + + org.apache.logging.log4j + log4j-core + 2.17.0 + org.assertj assertj-core - 3.12.2 + 3.16.1 test + + + org.apache.logging.log4j + log4j-slf4j-impl + 2.17.0 + + org.junit.jupiter junit-jupiter - 5.5.0-M1 + 5.6.2 test org.mockito mockito-core - 2.26.0 + 3.6.0 test org.mockito mockito-junit-jupiter - 2.26.0 + 3.6.0 test @@ -97,7 +117,7 @@ org.apache.maven.plugins maven-shade-plugin - 2.3 + 3.2.4 false @@ -113,7 +133,7 @@ org.codehaus.mojo exec-maven-plugin - 1.6.0 + 3.0.0 generate @@ -132,7 +152,7 @@ org.codehaus.mojo build-helper-maven-plugin - 3.0.0 + 3.2.0 add-source @@ -151,23 +171,21 @@ org.apache.maven.plugins maven-resources-plugin - 2.4 + 3.1.0 maven-surefire-plugin - 3.0.0-M3 + 3.0.0-M5 org.jacoco jacoco-maven-plugin - 0.8.4 + 0.8.6 **/BaseConfiguration* - **/BaseHandler* **/HandlerWrapper* **/ResourceModel* - **/Configuration* @@ -210,6 +228,31 @@ + + + org.apache.maven.plugins + maven-antrun-plugin + 1.8 + + + + + shade-package-post + + + + + + + + + run + + + + diff --git a/aws-ssm-parameter/resource-role.yaml b/aws-ssm-parameter/resource-role.yaml index 3a1a4ef8..cf68dd6a 100644 --- a/aws-ssm-parameter/resource-role.yaml +++ b/aws-ssm-parameter/resource-role.yaml @@ -27,6 +27,7 @@ Resources: - "ssm:DeleteParameter" - "ssm:DescribeParameters" - "ssm:GetParameters" + - "ssm:ListTagsForResource" - "ssm:PutParameter" - "ssm:RemoveTagsFromResource" Resource: "*" diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/BaseHandlerStd.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/BaseHandlerStd.java index 58653a80..12373c6e 100644 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/BaseHandlerStd.java +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/BaseHandlerStd.java @@ -1,94 +1,226 @@ package com.amazonaws.ssm.parameter; +import com.google.common.base.Strings; import com.google.common.collect.ImmutableSet; +import software.amazon.awssdk.awscore.exception.AwsServiceException; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.GetParametersResponse; +import software.amazon.awssdk.services.ssm.model.HierarchyLevelLimitExceededException; +import software.amazon.awssdk.services.ssm.model.HierarchyTypeMismatchException; +import software.amazon.awssdk.services.ssm.model.IncompatiblePolicyException; import software.amazon.awssdk.services.ssm.model.InternalServerErrorException; +import software.amazon.awssdk.services.ssm.model.InvalidAllowedPatternException; +import software.amazon.awssdk.services.ssm.model.InvalidFilterKeyException; +import software.amazon.awssdk.services.ssm.model.InvalidFilterOptionException; +import software.amazon.awssdk.services.ssm.model.InvalidFilterValueException; +import software.amazon.awssdk.services.ssm.model.InvalidKeyIdException; +import software.amazon.awssdk.services.ssm.model.InvalidNextTokenException; +import software.amazon.awssdk.services.ssm.model.InvalidPolicyAttributeException; +import software.amazon.awssdk.services.ssm.model.InvalidPolicyTypeException; +import software.amazon.awssdk.services.ssm.model.ParameterAlreadyExistsException; +import software.amazon.awssdk.services.ssm.model.ParameterLimitExceededException; +import software.amazon.awssdk.services.ssm.model.ParameterMaxVersionLimitExceededException; +import software.amazon.awssdk.services.ssm.model.ParameterNotFoundException; +import software.amazon.awssdk.services.ssm.model.ParameterPatternMismatchException; +import software.amazon.awssdk.services.ssm.model.PoliciesLimitExceededException; import software.amazon.awssdk.services.ssm.model.PutParameterRequest; import software.amazon.awssdk.services.ssm.model.PutParameterResponse; +import software.amazon.awssdk.services.ssm.model.SsmRequest; +import software.amazon.awssdk.services.ssm.model.TooManyTagsErrorException; +import software.amazon.awssdk.services.ssm.model.TooManyUpdatesException; +import software.amazon.awssdk.services.ssm.model.UnsupportedParameterTypeException; +import software.amazon.cloudformation.exceptions.BaseHandlerException; +import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; +import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; +import software.amazon.cloudformation.exceptions.CfnInternalFailureException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; +import software.amazon.cloudformation.exceptions.CfnNotFoundException; +import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; +import software.amazon.cloudformation.exceptions.CfnServiceLimitExceededException; +import software.amazon.cloudformation.exceptions.CfnThrottlingException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import software.amazon.cloudformation.proxy.delay.Constant; import java.time.Duration; +import java.util.Optional; import java.util.Set; +import static java.util.Objects.requireNonNull; + public abstract class BaseHandlerStd extends BaseHandler { - protected static final Set THROTTLING_ERROR_CODES = ImmutableSet.of( - "ThrottlingException", - "TooManyUpdates"); - - @Override - public ProgressEvent handleRequest(final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final Logger logger) { - return handleRequest( - proxy, - request, - callbackContext != null ? callbackContext : new CallbackContext(), - proxy.newProxy(SSMClientBuilder::getClient), - logger); - } - - protected abstract ProgressEvent handleRequest( - AmazonWebServicesClientProxy proxy, - ResourceHandlerRequest request, - CallbackContext callbackContext, - ProxyClient client, - Logger logger); - - protected Constant getBackOffDelay(final ResourceModel model) { - if(model.getDataType() != null && model.getDataType() == Constants.AWS_EC2_IMAGE_DATATYPE) { - return Constant.of() - .timeout(Duration.ofMinutes(5)) - .delay(Duration.ofSeconds(30)) - .build(); - } else { - return Constant.of() - .timeout(Duration.ofMinutes(5)) - .delay(Duration.ofSeconds(5)) - .build(); - } - } - - /** - * If your resource requires some form of stabilization (e.g. service does not provide strong - * consistency), you will need to ensure that your code accounts for any potential issues, so that - * a subsequent read/update requests will not cause any conflicts (e.g. - * NotFoundException/InvalidRequestException) for more information -> - * https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html - * - * @param putParameterRequest the aws service request to create a resource - * @param putParameterResponse the aws service response to create a resource - * @param proxyClient the aws service client to make the call - * @param resourceModel resource model - * @param callbackContext callback context - * @return boolean state of stabilized or not - */ - protected static boolean stabilize( - final PutParameterRequest putParameterRequest, - final PutParameterResponse putParameterResponse, - final ProxyClient proxyClient, - final ResourceModel resourceModel, - final CallbackContext callbackContext - ) { - final GetParametersResponse response; - try { - response = proxyClient.injectCredentialsAndInvokeV2(Translator.getParametersRequest(resourceModel), proxyClient.client()::getParameters); - } catch (final InternalServerErrorException exception) { - return false; - } - - // if invalid parameters list is not empty return false as the validation for - // DataType has not been completed and the parameter has not been created yet. - if(response == null || response.invalidParameters().size() != 0) { - return false; - } - return (response.parameters() != null && - response.parameters().get(0).version() == putParameterResponse.version()); - } + + private final SsmClient ssmClient; + + protected BaseHandlerStd() { + this(ClientBuilder.getClient()); + } + + protected BaseHandlerStd(SsmClient ssmClient) { + this.ssmClient = requireNonNull(ssmClient); + } + + private SsmClient getSsmClient() { + return ssmClient; + } + + protected static final Set THROTTLING_ERROR_CODES = ImmutableSet.of( + "ThrottlingException", + "TooManyUpdates"); + + protected static final Set ACCESS_DENIED_ERROR_CODES = ImmutableSet.of( + "AccessDenied" + ); + + @Override + public ProgressEvent handleRequest(final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + return handleRequest( + proxy, + request, + Optional.ofNullable(callbackContext).orElse(new CallbackContext()), + proxy.newProxy(ClientBuilder::getClient), + logger); + } + + protected abstract ProgressEvent handleRequest( + AmazonWebServicesClientProxy proxy, + ResourceHandlerRequest request, + CallbackContext callbackContext, + ProxyClient client, + Logger logger); + + protected Constant getBackOffDelay(final ResourceModel model) { + if (model.getDataType() != null && model.getDataType() == Constants.AWS_EC2_IMAGE_DATATYPE) { + return Constant.of() + .timeout(Duration.ofMinutes(5)) + .delay(Duration.ofSeconds(30)) + .build(); + } else { + return Constant.of() + .timeout(Duration.ofMinutes(5)) + .delay(Duration.ofSeconds(5)) + .build(); + } + } + + public static boolean isStabilizationNeeded(final String datatype) { + return (!Strings.isNullOrEmpty(datatype) && datatype.startsWith(Constants.AWS_EC2_IMAGE_DATATYPE)); + } + + /** + * If your resource requires some form of stabilization (e.g. service does not provide strong + * consistency), you will need to ensure that your code accounts for any potential issues, so that + * a subsequent read/update requests will not cause any conflicts (e.g. + * NotFoundException/InvalidRequestException) for more information -> + * https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html + * + * @param putParameterRequest the aws service request to create a resource + * @param putParameterResponse the aws service response to create a resource + * @param proxyClient the aws service client to make the call + * @param resourceModel resource model + * @param callbackContext callback context + * @return boolean state of stabilized or not + */ + protected Boolean stabilize( + final PutParameterRequest putParameterRequest, + final PutParameterResponse putParameterResponse, + final ProxyClient proxyClient, + final ResourceModel resourceModel, + final CallbackContext callbackContext, + final Logger logger + ) { + try { + logger.log(String.format("Trying to stabilize %s [%s]", ResourceModel.TYPE_NAME, putParameterRequest.name())); + GetParametersResponse response = proxyClient.injectCredentialsAndInvokeV2(Translator.getParametersRequest(resourceModel), proxyClient.client()::getParameters); + + // if invalid parameters list is not empty return false as the validation for + // DataType has not been completed and the parameter has not been created yet. + if (response == null || !response.invalidParameters().isEmpty()) { + return false; + } + return (response.parameters() != null && response.parameters().get(0).version() == putParameterResponse.version()); + } catch (Exception e) { + if (hasThrottled(e)) { + logger.log(String.format("Throttling during stabilization of SsmParameter [%s] with error: [%s] ... Retrying..", putParameterRequest.name(), e.getMessage())); + return false; + } else { + logger.log(String.format("Failed during stabilization of SsmParameter [%s] with error: [%s]", putParameterRequest.name(), e.getMessage())); + throw e; + } + } + } + + protected ProgressEvent handleError(SsmRequest ssmRequest, Exception e, ProxyClient proxyClient, + ResourceModel model, CallbackContext context, Logger logger) { + + BaseHandlerException ex; + + if (e instanceof AwsServiceException) { + if (e instanceof ParameterAlreadyExistsException) { + ex = new CfnAlreadyExistsException(e); + } else if (e instanceof ParameterNotFoundException) { + ex = new CfnNotFoundException(e); + } else if (e instanceof ParameterPatternMismatchException + || e instanceof InvalidKeyIdException + || e instanceof HierarchyTypeMismatchException + || e instanceof InvalidAllowedPatternException + || e instanceof UnsupportedParameterTypeException + || e instanceof InvalidPolicyTypeException + || e instanceof InvalidPolicyAttributeException + || e instanceof IncompatiblePolicyException + || e instanceof InvalidFilterKeyException + || e instanceof InvalidFilterOptionException + || e instanceof InvalidFilterValueException + || e instanceof InvalidNextTokenException + ) + { + ex = new CfnInvalidRequestException(e); + } else if (e instanceof ParameterLimitExceededException + || e instanceof HierarchyLevelLimitExceededException + || e instanceof ParameterMaxVersionLimitExceededException + || e instanceof PoliciesLimitExceededException + ) + { + ex = new CfnServiceLimitExceededException(e); + } else if (e instanceof InternalServerErrorException) { + ex = new CfnServiceInternalErrorException(e); + } else if (hasThrottled(e) + || e instanceof TooManyUpdatesException + || e instanceof TooManyTagsErrorException + ) + { + ex = new CfnThrottlingException(e); + } else { + ex = new CfnGeneralServiceException(e); + } + } else { + // InternalFailure: An unexpected error occurred within the handler. + ex = new CfnInternalFailureException(e); + } + logger.log(String.format("Handled Exception: error code [%s], message [%s]", ex.getErrorCode(), ex.getMessage())); + return ProgressEvent.failed(model, context, ex.getErrorCode(), ex.getMessage()); + } + + private boolean hasThrottled(Exception e) { + String errorCode = getErrorCode(e); + return (THROTTLING_ERROR_CODES.contains(errorCode)); + } + + protected boolean isAccessDenied(Exception e) { + String errorCode = getErrorCode(e); + return (ACCESS_DENIED_ERROR_CODES.contains(errorCode)); + } + + protected String getErrorCode(Exception e) { + if (e instanceof AwsServiceException && ((AwsServiceException) e).awsErrorDetails() != null) { + return ((AwsServiceException) e).awsErrorDetails().errorCode(); + } + return e.getMessage(); + } } diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ClientBuilder.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ClientBuilder.java new file mode 100644 index 00000000..2cf3a3ef --- /dev/null +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ClientBuilder.java @@ -0,0 +1,37 @@ +package com.amazonaws.ssm.parameter; + +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.core.internal.retry.SdkDefaultRetrySetting; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.core.retry.backoff.BackoffStrategy; +import software.amazon.awssdk.core.retry.backoff.EqualJitterBackoffStrategy; +import software.amazon.awssdk.core.retry.conditions.RetryCondition; +import software.amazon.awssdk.services.ssm.SsmClient; +import software.amazon.cloudformation.LambdaWrapper; + +import java.time.Duration; + +public class ClientBuilder { + + private static final BackoffStrategy BACKOFF_THROTTLING_STRATEGY = + EqualJitterBackoffStrategy.builder() + .baseDelay(Duration.ofMillis(2000)) //1st retry is ~2 sec + .maxBackoffTime(SdkDefaultRetrySetting.MAX_BACKOFF) //default is 20s + .build(); + + private static final RetryPolicy RETRY_POLICY = + RetryPolicy.builder() + .numRetries(4) + .retryCondition(RetryCondition.defaultRetryCondition()) + .throttlingBackoffStrategy(BACKOFF_THROTTLING_STRATEGY) + .build(); + + public static SsmClient getClient() { + return SsmClient.builder() + .httpClient(LambdaWrapper.HTTP_CLIENT) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .retryPolicy(RETRY_POLICY) + .build()) + .build(); + } +} diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Configuration.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Configuration.java index 919beb3c..2a23cc45 100644 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Configuration.java +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Configuration.java @@ -1,22 +1,25 @@ package com.amazonaws.ssm.parameter; +import com.google.common.collect.Maps; + import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; class Configuration extends BaseConfiguration { - public Configuration() { - super("aws-ssm-parameter.json"); - } + public Configuration() { + super("aws-ssm-parameter.json"); + } - /** - * Providers should implement this method if their resource has a 'Tags' property to define resource-level tags - * @return - */ - public Map resourceDefinedTags(final ResourceModel resourceModel) { - if (resourceModel.getTags() == null) { - return null; - } else { - return resourceModel.getTags(); - } - } + /** + * Providers should implement this method if their resource has a 'Tags' property to define resource-level tags + * + * @return + */ + public Map resourceDefinedTags(final ResourceModel resourceModel) { + return Optional.ofNullable(resourceModel.getTags()).orElse(Maps.newHashMap()) + .entrySet().stream() + .collect(Collectors.toMap(tag -> tag.getKey(), tag -> tag.getValue().toString())); + } } diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Constants.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Constants.java index 948bee85..5202b0bf 100644 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Constants.java +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Constants.java @@ -1,17 +1,10 @@ package com.amazonaws.ssm.parameter; public class Constants { - // ParameterName limit is 1024 chars. To make sure that we never hit that limit with auto name generation, - // lets have a number less than the allowed limit i.e 1000. - // The total of following three variables should be less than 1000. - // This can be increased in the future. - public static final int ALLOWED_LOGICAL_RESOURCE_ID_LENGTH = 500; - public static final String CF_PARAMETER_NAME_PREFIX = "CFN"; - public static final int GUID_LENGTH = 12; + public static final Integer MAX_RESULTS = 50; + public static final String AWS_EC2_IMAGE_DATATYPE = "aws:ec2:image"; - public static final int ERROR_STATUS_CODE_400 = 400; - public static final int ERROR_STATUS_CODE_500 = 500; - - public static final Integer MAX_RESULTS = 50; - public static final String AWS_EC2_IMAGE_DATATYPE = "aws:ec2:image"; + public static final int ALLOWED_LOGICAL_RESOURCE_ID_LENGTH = 500; + public static final String CF_PARAMETER_NAME_PREFIX = "CFN"; + public static final int GUID_LENGTH = 12; } diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/CreateHandler.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/CreateHandler.java index a9005699..f9d9f0c0 100644 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/CreateHandler.java +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/CreateHandler.java @@ -1,117 +1,99 @@ package com.amazonaws.ssm.parameter; -import com.amazonaws.AmazonServiceException; +import com.google.common.annotations.VisibleForTesting; import org.apache.commons.lang3.RandomStringUtils; - import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.model.ParameterAlreadyExistsException; -import software.amazon.awssdk.services.ssm.model.InternalServerErrorException; -import software.amazon.awssdk.services.ssm.model.PutParameterRequest; -import software.amazon.awssdk.services.ssm.model.PutParameterResponse; import software.amazon.awssdk.services.ssm.model.ParameterType; -import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; -import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; -import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; -import software.amazon.cloudformation.exceptions.CfnThrottlingException; -import software.amazon.cloudformation.exceptions.TerminalException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import java.util.HashMap; +import java.util.Collections; import java.util.Map; +import java.util.Optional; import java.util.Random; public class CreateHandler extends BaseHandlerStd { - private static final String OPERATION = "PutParameter"; - private static final String RETRY_MESSAGE = "Detected retryable error, retrying. Exception message: %s"; - private Logger logger; + private Logger logger; + + private final ReadHandler readHandler; + + public CreateHandler() { + super(); + readHandler = new ReadHandler(); + } + + @VisibleForTesting + protected CreateHandler(SsmClient ssmClient, ReadHandler readHandler) { + super(ssmClient); + this.readHandler = readHandler; + } - @Override - protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { - this.logger = logger; - final ResourceModel model = request.getDesiredResourceState(); + @Override + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + this.logger = logger; + final ResourceModel model = request.getDesiredResourceState(); - // Set model primary ID if absent - if(model.getName() == null) { - model.setName(generateParameterName( - request.getLogicalResourceIdentifier(), - request.getClientRequestToken() - )); - } + logger.log("Invoking Create Handler"); + logger.log("CREATE ResourceModel: " + request.getDesiredResourceState().toString()); - if(model.getType().equalsIgnoreCase(ParameterType.SECURE_STRING.toString())) { - String message = String.format("SSM Parameters of type %s cannot be created using CloudFormation", ParameterType.SECURE_STRING); - return ProgressEvent.defaultFailureHandler(new TerminalException(message), - HandlerErrorCode.InvalidRequest); - } + if (ParameterType.SECURE_STRING.toString().equalsIgnoreCase(model.getType())) { + String message = String.format("SSM Parameters of type %s cannot be created using CloudFormation", ParameterType.SECURE_STRING); + return ProgressEvent.failed(null, null, HandlerErrorCode.InvalidRequest, message); + } - Map consolidatedTagList = new HashMap<>(); - if (request.getDesiredResourceTags() != null) { - consolidatedTagList.putAll(request.getDesiredResourceTags()); - } - if (request.getSystemTags() != null) { - consolidatedTagList.putAll(request.getSystemTags()); - } + // Set model primary ID if absent + if (model.getName() == null) { + model.setName(generateParameterName(request.getLogicalResourceIdentifier(), request.getClientRequestToken())); + } - return proxy.initiate("aws-ssm-parameter::resource-create", proxyClient, model, callbackContext) - .translateToServiceRequest((resourceModel) -> Translator.createPutParameterRequest(resourceModel, consolidatedTagList)) - .backoffDelay(getBackOffDelay(model)) - .makeServiceCall(this::createResource) - .stabilize(BaseHandlerStd::stabilize) - .progress() - .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); - } + Map consolidatedTagsMap = Optional.ofNullable(request.getDesiredResourceTags()).orElse(Collections.emptyMap()); + consolidatedTagsMap.putAll(Optional.ofNullable(request.getSystemTags()).orElse(Collections.emptyMap())); - private PutParameterResponse createResource(final PutParameterRequest putParameterRequest, - final ProxyClient proxyClient) { - try { - return proxyClient.injectCredentialsAndInvokeV2(putParameterRequest, proxyClient.client()::putParameter); - } catch (final ParameterAlreadyExistsException exception) { - throw new CfnAlreadyExistsException(ResourceModel.TYPE_NAME, putParameterRequest.name()); - } catch (final InternalServerErrorException exception) { - throw new CfnServiceInternalErrorException(OPERATION, exception); - } catch (final AmazonServiceException exception) { - final Integer errorStatus = exception.getStatusCode(); - final String errorCode = exception.getErrorCode(); - if (errorStatus >= Constants.ERROR_STATUS_CODE_400 && errorStatus < Constants.ERROR_STATUS_CODE_500) { - if (THROTTLING_ERROR_CODES.contains(errorCode)) { - logger.log(String.format(RETRY_MESSAGE, exception.getMessage())); - throw new CfnThrottlingException(OPERATION, exception); - } - } - throw new CfnGeneralServiceException(OPERATION, exception); - } - } + return ProgressEvent.progress(model, callbackContext) + .then(progress -> proxy.initiate("aws-ssm-parameter::resource-create", proxyClient, model, callbackContext) + .translateToServiceRequest(resourceModel -> Translator.createPutParameterRequest(resourceModel, consolidatedTagsMap)) + .backoffDelay(getBackOffDelay(model)) + .makeServiceCall((putParameterRequest, ssmProxyClient) -> + ssmProxyClient.injectCredentialsAndInvokeV2(putParameterRequest, ssmProxyClient.client()::putParameter)) + .stabilize((req, response, client, model1, cbContext) -> { + if (isStabilizationNeeded(model1.getDataType())) + return stabilize(req, response, client, model1, cbContext, logger); + else + return true; + }) + .handleError((req, e, proxy1, model1, context1) -> handleError(req, e, proxy1, model1, context1, logger)) + .progress()) + .then(progress -> readHandler.handleRequest(proxy, request, callbackContext, proxyClient, logger)); + } - // We support this special use case of auto-generating names only for CloudFormation. - // Name format: Prefix - logical resource id - randomString - private String generateParameterName(final String logicalResourceId, final String clientRequestToken) { - StringBuilder sb = new StringBuilder(); - int endIndex = logicalResourceId.length() > Constants.ALLOWED_LOGICAL_RESOURCE_ID_LENGTH - ? Constants.ALLOWED_LOGICAL_RESOURCE_ID_LENGTH : logicalResourceId.length(); + // We support this special use case of auto-generating names only for CloudFormation. + // Name format: Prefix - logical resource id - randomString + private String generateParameterName(final String logicalResourceId, final String clientRequestToken) { + StringBuilder sb = new StringBuilder(); + int endIndex = Math.min(logicalResourceId.length(), Constants.ALLOWED_LOGICAL_RESOURCE_ID_LENGTH); - sb.append(Constants.CF_PARAMETER_NAME_PREFIX); - sb.append("-"); - sb.append(logicalResourceId.substring(0, endIndex)); - sb.append("-"); + sb.append(Constants.CF_PARAMETER_NAME_PREFIX); + sb.append("-"); + sb.append(logicalResourceId.substring(0, endIndex)); + sb.append("-"); - sb.append(RandomStringUtils.random( - Constants.GUID_LENGTH, - 0, - 0, - true, - true, - null, - new Random(clientRequestToken.hashCode()))); - return sb.toString(); - } + sb.append(RandomStringUtils.random( + Constants.GUID_LENGTH, + 0, + 0, + true, + true, + null, + new Random(clientRequestToken.hashCode()))); + return sb.toString(); + } } diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/DeleteHandler.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/DeleteHandler.java index 36f895fd..a38cd87b 100644 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/DeleteHandler.java +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/DeleteHandler.java @@ -1,60 +1,43 @@ package com.amazonaws.ssm.parameter; -import com.amazonaws.AmazonServiceException; +import com.google.common.annotations.VisibleForTesting; import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.model.DeleteParameterRequest; -import software.amazon.awssdk.services.ssm.model.DeleteParameterResponse; -import software.amazon.awssdk.services.ssm.model.ParameterNotFoundException; -import software.amazon.awssdk.services.ssm.model.InternalServerErrorException; -import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; -import software.amazon.cloudformation.exceptions.CfnNotFoundException; -import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; -import software.amazon.cloudformation.exceptions.CfnThrottlingException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; -import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; public class DeleteHandler extends BaseHandlerStd { - private static final String OPERATION = "DeleteParameter"; - private static final String RETRY_MESSAGE = "Detected retryable error, retrying. Exception message: %s"; - private Logger logger; + private Logger logger; + + public DeleteHandler() { + super(); + } + + @VisibleForTesting + protected DeleteHandler(SsmClient ssmClient) { + super(ssmClient); + } - @Override - protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { - this.logger = logger; - final ResourceModel model = request.getDesiredResourceState(); + @Override + protected ProgressEvent handleRequest( + 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-ssm-parameter::resource-delete", proxyClient, model, callbackContext) - .translateToServiceRequest(Translator::deleteParameterRequest) - .makeServiceCall(this::deleteResource) - .done((deleteParameterRequest, deleteParameterResponse, _client, _model, _callbackContext) -> ProgressEvent.defaultSuccessHandler(null)); - } + logger.log("Invoking Delete Handler"); + logger.log("DELETE ResourceModel: " + model.toString()); - private DeleteParameterResponse deleteResource(final DeleteParameterRequest deleteParameterRequest, - final ProxyClient proxyClient) { - try { - return proxyClient.injectCredentialsAndInvokeV2(deleteParameterRequest, proxyClient.client()::deleteParameter); - } catch (final ParameterNotFoundException exception) { - throw new CfnNotFoundException(ResourceModel.TYPE_NAME, deleteParameterRequest.name()); - } catch (final InternalServerErrorException exception) { - throw new CfnServiceInternalErrorException(OPERATION, exception); - } catch (final AmazonServiceException exception) { - final Integer errorStatus = exception.getStatusCode(); - final String errorCode = exception.getErrorCode(); - if (errorStatus >= Constants.ERROR_STATUS_CODE_400 && errorStatus < Constants.ERROR_STATUS_CODE_500) { - if (THROTTLING_ERROR_CODES.contains(errorCode)) { - logger.log(String.format(RETRY_MESSAGE, exception.getMessage())); - throw new CfnThrottlingException(OPERATION, exception); - } - } - throw new CfnGeneralServiceException(OPERATION, exception); - } - } + return proxy.initiate("aws-ssm-parameter::resource-delete", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::deleteParameterRequest) + .makeServiceCall(((deleteParameterRequest, ssmClientProxyClient) -> + ssmClientProxyClient.injectCredentialsAndInvokeV2(deleteParameterRequest, ssmClientProxyClient.client()::deleteParameter))) + .handleError((req, e, proxy1, model1, context1) -> handleError(req, e, proxy1, model1, context1, logger)) + .done((deleteParameterRequest, deleteParameterResponse, client1, model1, callbackContext1) -> ProgressEvent.defaultSuccessHandler(null)); + } } diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ListHandler.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ListHandler.java index 9677c60b..1a269523 100644 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ListHandler.java +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ListHandler.java @@ -1,34 +1,66 @@ package com.amazonaws.ssm.parameter; +import com.google.common.annotations.VisibleForTesting; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.DescribeParametersResponse; +import software.amazon.awssdk.services.ssm.model.ParameterMetadata; +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.OperationStatus; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; import java.util.List; import java.util.stream.Collectors; public class ListHandler extends BaseHandlerStd { - protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { - final DescribeParametersResponse describeParametersResponse = proxy.injectCredentialsAndInvokeV2(Translator.describeParametersRequest(request.getNextToken()), proxyClient.client()::describeParameters); - - final List models = describeParametersResponse - .parameters() - .stream().map(parameterMetadata -> ResourceModel.builder().name(parameterMetadata.name()).build()).collect(Collectors.toList()); - - return ProgressEvent.builder() - .resourceModels(models) - .nextToken(describeParametersResponse.nextToken()) - .status(OperationStatus.SUCCESS) - .build(); - } + + public ListHandler() { + super(); + } + + @VisibleForTesting + protected ListHandler(SsmClient ssmClient) { + super(ssmClient); + } + + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + + final ResourceModel resourceModel = request.getDesiredResourceState(); + + return ProgressEvent.progress(resourceModel, callbackContext) + .then(progress -> describeParameters(proxy, request, progress, proxyClient, progress.getResourceModel(), progress.getCallbackContext(), logger)) + .then(progress -> ProgressEvent.defaultSuccessHandler(progress.getResourceModel())); + } + + private ProgressEvent describeParameters( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final ProgressEvent progress, + final ProxyClient proxyClient, + final ResourceModel resourceModel, final CallbackContext callbackContext, final Logger logger) { + + return proxy.initiate("aws-ssm-parameter::resource-list-describeParameters", proxyClient, resourceModel, callbackContext) + .translateToServiceRequest(model1 -> Translator.describeParametersRequest(request.getNextToken())) + .makeServiceCall(((describeParametersRequest, ssmClientProxyClient) -> + ssmClientProxyClient.injectCredentialsAndInvokeV2(describeParametersRequest, ssmClientProxyClient.client()::describeParameters))) + .handleError((req, e, proxy1, model1, context1) -> handleError(req, e, proxy1, model1, context1, logger)) + .done(describeParametersResponse -> { + String nextToken = describeParametersResponse.nextToken(); + final List models = Translator.translateListOfParameters(describeParametersResponse.parameters()); + + return ProgressEvent.builder() + .resourceModels(models) + .nextToken(nextToken) + .status(OperationStatus.SUCCESS) + .build(); + }); + } } diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ReadHandler.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ReadHandler.java index b662d2a1..8552407a 100644 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ReadHandler.java +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/ReadHandler.java @@ -1,50 +1,124 @@ package com.amazonaws.ssm.parameter; +import com.google.common.annotations.VisibleForTesting; import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.model.GetParametersRequest; -import software.amazon.awssdk.services.ssm.model.GetParametersResponse; -import software.amazon.awssdk.services.ssm.model.InternalServerErrorException; import software.amazon.awssdk.services.ssm.model.Parameter; +import software.amazon.awssdk.services.ssm.model.ParameterMetadata; import software.amazon.cloudformation.exceptions.CfnNotFoundException; -import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; public class ReadHandler extends BaseHandlerStd { - private static final String OPERATION = "ReadParameter"; - - @Override - protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { - return proxy.initiate("aws-ssm-parameter::resource-read", proxyClient, request.getDesiredResourceState(), callbackContext) - .translateToServiceRequest(Translator::getParametersRequest) - .makeServiceCall(this::ReadResource) - .done((getParametersRequest, getParametersResponse, proxyInvocation, resourceModel, context) -> { - if(getParametersResponse.parameters().size() == 0) { - throw new CfnNotFoundException(ResourceModel.TYPE_NAME, request.getDesiredResourceState().getName()); - } - final Parameter parameter = getParametersResponse.parameters().stream().findFirst().get(); - - return ProgressEvent.defaultSuccessHandler(ResourceModel.builder() - .name(parameter.name()) - .type(parameter.typeAsString()) - .value(parameter.value()).build()); - }); - } - - private GetParametersResponse ReadResource(final GetParametersRequest getParametersRequest, - final ProxyClient proxyClient) { - try{ - return proxyClient.injectCredentialsAndInvokeV2(getParametersRequest, proxyClient.client()::getParameters); - } catch (final InternalServerErrorException exception) { - throw new CfnServiceInternalErrorException(OPERATION, exception); - } - } + + public ReadHandler() { + super(); + } + + @VisibleForTesting + protected ReadHandler(SsmClient ssmClient) { + super(ssmClient); + } + + @Override + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + final ResourceModel model = request.getDesiredResourceState(); + + logger.log("Invoking Read Handler"); + logger.log("READ ResourceModel: " + model.toString()); + + return ProgressEvent.progress(model, callbackContext) + .then(progress -> getParameters(proxy, progress, proxyClient, model, callbackContext, logger)) + .then(progress -> describeParameters(proxy, progress, proxyClient, progress.getResourceModel(), progress.getCallbackContext(), logger)) + .then(progress -> listTagsForResourceRequestForParameters(proxy, progress, proxyClient, progress.getResourceModel(), progress.getCallbackContext(), logger)) + .then(progress -> ProgressEvent.defaultSuccessHandler(progress.getResourceModel())); + } + + private ProgressEvent getParameters( + final AmazonWebServicesClientProxy proxy, + final ProgressEvent progress, + final ProxyClient proxyClient, + final ResourceModel model, final CallbackContext callbackContext, final Logger logger) { + + return proxy.initiate("aws-ssm-parameter::resource-read-getParameters", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::getParametersRequest) + .makeServiceCall(((getParametersRequest, ssmClientProxyClient) -> + ssmClientProxyClient.injectCredentialsAndInvokeV2(getParametersRequest, ssmClientProxyClient.client()::getParameters))) + .handleError((req, e, proxy1, model1, context1) -> handleError(req, e, proxy1, model1, context1, logger)) + .done((getParametersRequest, getParametersResponse, proxyClient1, resourceModel, context) -> { + if (getParametersResponse.parameters().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getName()); + } + final Parameter parameter = getParametersResponse.parameters().get(0); + + return ProgressEvent.progress( + ResourceModel.builder() + .name(parameter.name()) + .type(parameter.typeAsString()) + .value(parameter.value()) + .dataType(parameter.dataType()) + .build(), + context); + }); + } + + private ProgressEvent describeParameters( + final AmazonWebServicesClientProxy proxy, + final ProgressEvent progress, + final ProxyClient proxyClient, + final ResourceModel model, final CallbackContext callbackContext, final Logger logger) { + + return proxy.initiate("aws-ssm-parameter::resource-read-describeParameters", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::describeParametersRequestWithFilter) + .makeServiceCall(((describeParametersRequest, ssmClientProxyClient) -> + ssmClientProxyClient.injectCredentialsAndInvokeV2(describeParametersRequest, ssmClientProxyClient.client()::describeParameters))) + .handleError((req, e, proxy1, model1, context1) -> { + // soft-fail + if (isAccessDenied(e)) { + return progress; + } + return handleError(req, e, proxy1, model1, context1, logger); + }) + .done((describeParametersRequest, describeParametersResponse, proxyClient1, resourceModel, context) -> { + if (describeParametersResponse.parameters().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, model.getName()); + } + final ParameterMetadata parameterMetadata = describeParametersResponse.parameters().get(0); + resourceModel.setAllowedPattern(parameterMetadata.allowedPattern()); + resourceModel.setDescription(parameterMetadata.description()); + resourceModel.setPolicies(parameterMetadata.policies().toString()); + resourceModel.setTier(parameterMetadata.tierAsString()); + return ProgressEvent.progress(resourceModel, context); + }); + } + + private ProgressEvent listTagsForResourceRequestForParameters( + final AmazonWebServicesClientProxy proxy, + final ProgressEvent progress, + final ProxyClient proxyClient, + final ResourceModel model, final CallbackContext callbackContext, final Logger logger) { + + return proxy.initiate("aws-ssm-parameter::resource-read-listTagsForResourceRequestForParameters", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::listTagsForResourceRequest) + .makeServiceCall(((listTagsForResourceRequest, ssmClientProxyClient) -> + ssmClientProxyClient.injectCredentialsAndInvokeV2(listTagsForResourceRequest, ssmClientProxyClient.client()::listTagsForResource))) + .handleError((req, e, proxy1, model1, context1) -> { + // soft-fail + if (isAccessDenied(e)) { + return progress; + } + return handleError(req, e, proxy1, model1, context1, logger); + }) + .done((listTagsForResourceRequest, listTagsForResourceResponse, proxyClient1, resourceModel, context) -> { + resourceModel.setTags(Translator.translateTagsFromSdk(listTagsForResourceResponse.tagList())); + return ProgressEvent.progress(resourceModel, context); + }); + } } diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/SSMClientBuilder.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/SSMClientBuilder.java deleted file mode 100644 index cb46ec9b..00000000 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/SSMClientBuilder.java +++ /dev/null @@ -1,26 +0,0 @@ -package com.amazonaws.ssm.parameter; - -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.retry.RetryPolicy; -import software.amazon.awssdk.core.retry.conditions.RetryCondition; -import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.cloudformation.LambdaWrapper; - -public class SSMClientBuilder { - private static final RetryPolicy RETRY_POLICY = - RetryPolicy.builder() - .numRetries(16) - .retryCondition(RetryCondition.defaultRetryCondition()) - .build(); - /** - * Builds and returns SsmClient with configuration overrides. - * - * @return Configured SsmClient. - */ - public static SsmClient getClient() { - return SsmClient.builder() - .httpClient(LambdaWrapper.HTTP_CLIENT) - .overrideConfiguration(ClientOverrideConfiguration.builder().retryPolicy(RETRY_POLICY).build()) - .build(); - } -} diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Translator.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Translator.java index b1b64dac..5f62ff22 100644 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Translator.java +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/Translator.java @@ -4,89 +4,131 @@ import software.amazon.awssdk.services.ssm.model.DeleteParameterRequest; import software.amazon.awssdk.services.ssm.model.DescribeParametersRequest; import software.amazon.awssdk.services.ssm.model.GetParametersRequest; +import software.amazon.awssdk.services.ssm.model.ListTagsForResourceRequest; +import software.amazon.awssdk.services.ssm.model.ParameterMetadata; +import software.amazon.awssdk.services.ssm.model.ParameterStringFilter; +import software.amazon.awssdk.services.ssm.model.ParametersFilterKey; import software.amazon.awssdk.services.ssm.model.PutParameterRequest; import software.amazon.awssdk.services.ssm.model.RemoveTagsFromResourceRequest; import software.amazon.awssdk.services.ssm.model.ResourceTypeForTagging; import software.amazon.awssdk.services.ssm.model.Tag; import java.util.Collections; -import java.util.Map; import java.util.List; +import java.util.Map; import java.util.Optional; import java.util.stream.Collectors; public class Translator { - static PutParameterRequest createPutParameterRequest(final ResourceModel model, - final Map tags) { - return PutParameterRequest.builder() - .description(model.getDescription()) - .name(model.getName()) - .value(model.getValue()) - .type(model.getType()) - .overwrite(Boolean.FALSE) - .allowedPattern(model.getAllowedPattern()) - .policies(model.getPolicies()) - .tier(model.getTier()) - .tags(translateTagsToSdk(tags)) - .dataType(model.getDataType()) - .build(); - } + static PutParameterRequest createPutParameterRequest(final ResourceModel model, + final Map tags) { + return PutParameterRequest.builder() + .description(model.getDescription()) + .name(model.getName()) + .value(model.getValue()) + .type(model.getType()) + .overwrite(Boolean.FALSE) + .allowedPattern(model.getAllowedPattern()) + .policies(model.getPolicies()) + .tier(model.getTier()) + .tags(translateTagsToSdk(tags)) + .dataType(model.getDataType()) + .build(); + } + + static PutParameterRequest updatePutParameterRequest(final ResourceModel model) { + return PutParameterRequest.builder() + .description(model.getDescription()) + .name(model.getName()) + .value(model.getValue()) + .type(model.getType()) + .overwrite(Boolean.TRUE) + .allowedPattern(model.getAllowedPattern()) + .policies(model.getPolicies()) + .tier(model.getTier()) + .dataType(model.getDataType()) + .build(); + } + + static GetParametersRequest getParametersRequest(final ResourceModel model) { + return GetParametersRequest.builder() + .names(model.getName()) + .withDecryption(Boolean.FALSE) + .build(); + } + + static DescribeParametersRequest describeParametersRequestWithFilter(final ResourceModel model) { + return DescribeParametersRequest.builder() + .parameterFilters( + ParameterStringFilter.builder() + .key(ParametersFilterKey.NAME.toString()) + .option("Equals") + .values(model.getName()) + .build()) + .build(); + } + + static DescribeParametersRequest describeParametersRequest(final String nextToken) { + return DescribeParametersRequest.builder() + .nextToken(nextToken) + .maxResults(Constants.MAX_RESULTS) + .build(); + } - static PutParameterRequest updatePutParameterRequest(final ResourceModel model) { - return PutParameterRequest.builder() - .description(model.getDescription()) - .name(model.getName()) - .value(model.getValue()) - .type(model.getType()) - .overwrite(Boolean.TRUE) - .allowedPattern(model.getAllowedPattern()) - .policies(model.getPolicies()) - .tier(model.getTier()) - .dataType(model.getDataType()) - .build(); - } + static DeleteParameterRequest deleteParameterRequest(final ResourceModel model) { + return DeleteParameterRequest.builder() + .name(model.getName()) + .build(); + } - static GetParametersRequest getParametersRequest(final ResourceModel model) { - return GetParametersRequest.builder() - .names(model.getName()) - .withDecryption(Boolean.FALSE) - .build(); - } + static RemoveTagsFromResourceRequest removeTagsFromResourceRequest(final String parameterName, List tagsToRemove) { + return RemoveTagsFromResourceRequest.builder() + .resourceId(parameterName) + .resourceType(ResourceTypeForTagging.PARAMETER) + .tagKeys(tagsToRemove.stream().map(tag -> tag.key()).collect(Collectors.toList())) + .build(); + } - static DescribeParametersRequest describeParametersRequest(final String nextToken) { - return DescribeParametersRequest.builder() - .nextToken(nextToken) - .maxResults(Constants.MAX_RESULTS) - .build(); - } + static AddTagsToResourceRequest addTagsToResourceRequest(final String parameterName, List tagsToAdd) { + return AddTagsToResourceRequest.builder() + .resourceId(parameterName) + .resourceType(ResourceTypeForTagging.PARAMETER) + .tags(tagsToAdd) + .build(); + } - static DeleteParameterRequest deleteParameterRequest(final ResourceModel model) { - return DeleteParameterRequest.builder() - .name(model.getName()) - .build(); - } + static ListTagsForResourceRequest listTagsForResourceRequest(final ResourceModel model) { + return ListTagsForResourceRequest.builder() + .resourceType(ResourceTypeForTagging.PARAMETER) + .resourceId(model.getName()) + .build(); + } - static RemoveTagsFromResourceRequest removeTagsFromResourceRequest(final String parameterName, List tagsToRemove) { - return RemoveTagsFromResourceRequest.builder() - .resourceId(parameterName) - .resourceType(ResourceTypeForTagging.PARAMETER) - .tagKeys(tagsToRemove.stream().map(tag -> tag.key()).collect(Collectors.toList())) - .build(); - } + static List translateListOfParameters(final List parameterMetadataList) { + return parameterMetadataList.stream().map(parameterMetadata -> + ResourceModel.builder() + .name(parameterMetadata.name()) + .allowedPattern(parameterMetadata.allowedPattern()) + .description(parameterMetadata.description()) + .policies(parameterMetadata.policies().toString()) + .tier(parameterMetadata.tierAsString()) + .dataType(parameterMetadata.dataType()) + .type(parameterMetadata.typeAsString()) + .build()) + .collect(Collectors.toList()); + } - static AddTagsToResourceRequest addTagsToResourceRequest(final String parameterName, List tagsToAdd) { - return AddTagsToResourceRequest.builder() - .resourceId(parameterName) - .resourceType(ResourceTypeForTagging.PARAMETER) - .tags(tagsToAdd) - .build(); - } + // Translate tags + static List translateTagsToSdk(final Map tags) { + return Optional.of(tags.entrySet()).orElse(Collections.emptySet()) + .stream() + .map(tag -> Tag.builder().key(tag.getKey()).value(tag.getValue()).build()) + .collect(Collectors.toList()); + } - // Translate tags - static List translateTagsToSdk(final Map tags) { - return Optional.of(tags.entrySet()).orElse(Collections.emptySet()) - .stream() - .map(tag -> Tag.builder().key(tag.getKey()).value(tag.getValue()).build()) - .collect(Collectors.toList()); - } + static Map translateTagsFromSdk(final List tags) { + return Optional.of(tags).orElse(Collections.emptyList()) + .stream() + .collect(Collectors.toMap(tag -> tag.key(), tag -> tag.value())); + } } diff --git a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/UpdateHandler.java b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/UpdateHandler.java index 0db9bcdd..3f385b40 100644 --- a/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/UpdateHandler.java +++ b/aws-ssm-parameter/src/main/java/com/amazonaws/ssm/parameter/UpdateHandler.java @@ -1,136 +1,145 @@ package com.amazonaws.ssm.parameter; -import com.amazonaws.AmazonServiceException; import com.amazonaws.util.CollectionUtils; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Sets; import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.model.GetParametersRequest; -import software.amazon.awssdk.services.ssm.model.GetParametersResponse; -import software.amazon.awssdk.services.ssm.model.InternalServerErrorException; -import software.amazon.awssdk.services.ssm.model.ParameterAlreadyExistsException; import software.amazon.awssdk.services.ssm.model.ParameterType; -import software.amazon.awssdk.services.ssm.model.PutParameterResponse; -import software.amazon.awssdk.services.ssm.model.PutParameterRequest; import software.amazon.awssdk.services.ssm.model.Tag; -import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; -import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; -import software.amazon.cloudformation.exceptions.CfnThrottlingException; -import software.amazon.cloudformation.exceptions.TerminalException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import software.amazon.cloudformation.proxy.ProxyClient; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.ProxyClient; +import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import java.util.ArrayList; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.stream.Collectors; public class UpdateHandler extends BaseHandlerStd { - private static final String OPERATION = "PutParameter"; - private static final String RETRY_MESSAGE = "Detected retryable error, retrying. Exception message: %s"; - private Logger logger; - - @Override - protected ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final ProxyClient proxyClient, - final Logger logger) { - this.logger = logger; - final ResourceModel model = request.getDesiredResourceState(); - - if(model.getType().equalsIgnoreCase(ParameterType.SECURE_STRING.toString())) { - String message = String.format("SSM Parameters of type %s cannot be updated using CloudFormation", ParameterType.SECURE_STRING); - return ProgressEvent.defaultFailureHandler(new TerminalException(message), - HandlerErrorCode.InvalidRequest); - } - - return ProgressEvent.progress(model, callbackContext) - // First validate the resource actually exists per the contract requirements - // https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html - .then(progress -> - proxy.initiate("aws-ssm-parameter::validate-resource-exists", proxyClient, model, callbackContext) - .translateToServiceRequest(Translator::getParametersRequest) - .makeServiceCall(this::validateResourceExists) - .progress()) - - .then(progress -> - proxy.initiate("aws-ssm-parameter::resource-update", proxyClient, model, callbackContext) - .translateToServiceRequest(Translator::updatePutParameterRequest) - .backoffDelay(getBackOffDelay(model)) - .makeServiceCall(this::updateResource) - .stabilize(BaseHandlerStd::stabilize) - .progress()) - .then(progress -> handleTagging(proxy, proxyClient, progress, model, request.getDesiredResourceTags(), request.getPreviousResourceTags())) - .then(progress -> new ReadHandler().handleRequest(proxy, request, callbackContext, proxyClient, logger)); - } - - private GetParametersResponse validateResourceExists(GetParametersRequest getParametersRequest, ProxyClient proxyClient) { - GetParametersResponse getParametersResponse; - - getParametersResponse = proxyClient.injectCredentialsAndInvokeV2(getParametersRequest,proxyClient.client()::getParameters); - if (getParametersResponse.invalidParameters().size() != 0) { - throw new CfnNotFoundException(ResourceModel.TYPE_NAME, getParametersRequest.names().get(0)); - } - - return getParametersResponse; - } - - private PutParameterResponse updateResource(final PutParameterRequest putParameterRequest, - final ProxyClient proxyClient) { - try { - return proxyClient.injectCredentialsAndInvokeV2(putParameterRequest, proxyClient.client()::putParameter); - } catch (final ParameterAlreadyExistsException exception) { - throw new CfnAlreadyExistsException(ResourceModel.TYPE_NAME, putParameterRequest.name()); - } catch (final InternalServerErrorException exception) { - throw new CfnServiceInternalErrorException(OPERATION, exception); - } catch (final AmazonServiceException exception) { - final Integer errorStatus = exception.getStatusCode(); - final String errorCode = exception.getErrorCode(); - if (errorStatus >= Constants.ERROR_STATUS_CODE_400 && errorStatus < Constants.ERROR_STATUS_CODE_500) { - if (THROTTLING_ERROR_CODES.contains(errorCode)) { - logger.log(String.format(RETRY_MESSAGE, exception.getMessage())); - throw new CfnThrottlingException(OPERATION, exception); - } - } - throw new CfnGeneralServiceException(OPERATION, exception); - } - } - - private ProgressEvent handleTagging( - final AmazonWebServicesClientProxy proxy, - final ProxyClient proxyClient, - final ProgressEvent progress, - final ResourceModel resourceModel, - final Map desiredResourceTags, - final Map previousResourceTags) { - - final Set currentTags = new HashSet<>(Translator.translateTagsToSdk(desiredResourceTags)); - final Set existingTags = new HashSet<>(Translator.translateTagsToSdk(previousResourceTags)); - // Remove tags with aws prefix as they should not be modified once attached - existingTags.removeIf(tag -> tag.key().startsWith("aws")); - - final Set setTagsToRemove = Sets.difference(existingTags, currentTags); - final Set setTagsToAdd = Sets.difference(currentTags, existingTags); - - final List tagsToRemove = setTagsToRemove.stream().collect(Collectors.toList()); - final List tagsToAdd = setTagsToAdd.stream().collect(Collectors.toList()); - - // Deletes tags only if tagsToRemove is not empty. - if (!CollectionUtils.isNullOrEmpty(tagsToRemove)) proxy.injectCredentialsAndInvokeV2( - Translator.removeTagsFromResourceRequest(resourceModel.getName(), tagsToRemove), proxyClient.client()::removeTagsFromResource); - - // Adds tags only if tagsToAdd is not empty. - if (!CollectionUtils.isNullOrEmpty(tagsToAdd)) proxy.injectCredentialsAndInvokeV2( - Translator.addTagsToResourceRequest(resourceModel.getName(), tagsToAdd), proxyClient.client()::addTagsToResource); - - return ProgressEvent.progress(progress.getResourceModel(), progress.getCallbackContext()); - } + private Logger logger; + + private final ReadHandler readHandler; + + public UpdateHandler() { + super(); + readHandler = new ReadHandler(); + } + + @VisibleForTesting + protected UpdateHandler(SsmClient ssmClient, ReadHandler readHandler) { + super(ssmClient); + this.readHandler = readHandler; + } + + @Override + protected ProgressEvent handleRequest( + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final ProxyClient proxyClient, + final Logger logger) { + this.logger = logger; + final ResourceModel model = request.getDesiredResourceState(); + + if (ParameterType.SECURE_STRING.toString().equalsIgnoreCase(model.getType())) { + String message = String.format("SSM Parameters of type %s cannot be updated using CloudFormation", ParameterType.SECURE_STRING); + return ProgressEvent.defaultFailureHandler(new CfnServiceInternalErrorException(message), HandlerErrorCode.InvalidRequest); + } + + return ProgressEvent.progress(model, callbackContext) + // First validate the resource actually exists per the contract requirements + // https://docs.aws.amazon.com/cloudformation-cli/latest/userguide/resource-type-test-contract.html + .then(progress -> validateResourceExists(proxy, progress, proxyClient, progress.getResourceModel(), progress.getCallbackContext(), logger)) + .then(progress -> updateResourceExceptTagging(proxy, progress, proxyClient, progress.getResourceModel(), progress.getCallbackContext(), logger)) + .then(progress -> handleTagging(proxy, proxyClient, progress, progress.getResourceModel(), request.getDesiredResourceTags(), request.getPreviousResourceTags())) + .then(progress -> readHandler.handleRequest(proxy, request, progress.getCallbackContext(), proxyClient, logger)); + } + + private ProgressEvent validateResourceExists(final AmazonWebServicesClientProxy proxy, + final ProgressEvent progress, final ProxyClient proxyClient, + final ResourceModel model, final CallbackContext callbackContext, final Logger logger) { + return proxy.initiate("aws-ssm-parameter::Update:validate-resource-exists", proxyClient, model, callbackContext) + .translateToServiceRequest(Translator::getParametersRequest) + .makeServiceCall((getParametersRequest, ssmClientProxyClient) -> + ssmClientProxyClient.injectCredentialsAndInvokeV2(getParametersRequest, ssmClientProxyClient.client()::getParameters)) + .handleError((req, e, proxy1, model1, context1) -> handleError(req, e, proxy1, model1, context1, this.logger)) + .done((getParametersRequest, getParametersResponse, proxyClient1, resourceModel, context) -> { + if (!getParametersResponse.invalidParameters().isEmpty()) { + throw new CfnNotFoundException(ResourceModel.TYPE_NAME, resourceModel.getName()); + } + return progress; + }); + } + + private ProgressEvent updateResourceExceptTagging(AmazonWebServicesClientProxy proxy, ProgressEvent progress, + ProxyClient proxyClient, ResourceModel resourceModel, CallbackContext callbackContext, Logger logger) { + return proxy.initiate("aws-ssm-parameter::Update:resource-update", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest(Translator::updatePutParameterRequest) + .backoffDelay(getBackOffDelay(progress.getResourceModel())) + .makeServiceCall((putParameterRequest, ssmClientProxyClient) -> + ssmClientProxyClient.injectCredentialsAndInvokeV2(putParameterRequest, ssmClientProxyClient.client()::putParameter)) + .stabilize((req, response, client, model1, cbContext) -> { + if (isStabilizationNeeded(model1.getDataType())) + return stabilize(req, response, client, model1, cbContext, logger); + else + return true; + }) + .progress(); + } + + private ProgressEvent handleTagging( + final AmazonWebServicesClientProxy proxy, + final ProxyClient proxyClient, + final ProgressEvent progress, + final ResourceModel resourceModel, + final Map desiredResourceTags, + final Map previousResourceTags) { + + final Set currentTags = new HashSet<>(Translator.translateTagsToSdk(desiredResourceTags)); + final Set existingTags = new HashSet<>(Translator.translateTagsToSdk(previousResourceTags)); + // Remove tags with aws prefix as they should not be modified (or removed) once attached + existingTags.removeIf(tag -> tag.key().startsWith("aws:")); + + final Set setTagsToRemove = Sets.difference(existingTags, currentTags); + final Set setTagsToAdd = Sets.difference(currentTags, existingTags); + + final List tagsToRemove = new ArrayList<>(setTagsToRemove); + final List tagsToAdd = new ArrayList<>(setTagsToAdd); + + return ProgressEvent.progress(resourceModel, progress.getCallbackContext()) + // First validate the resource actually exists per the contract requirements + .then(progress1 -> removeTags(proxy, progress1, proxyClient, logger, tagsToRemove)) + .then(progress1 -> addTags(proxy, progress1, proxyClient, logger, tagsToAdd)) + .then(progress1 -> ProgressEvent.progress(progress1.getResourceModel(), progress1.getCallbackContext())); + } + + private ProgressEvent removeTags(AmazonWebServicesClientProxy proxy, ProgressEvent progress, + ProxyClient proxyClient, Logger logger, List tagsToRemove) { + if (CollectionUtils.isNullOrEmpty(tagsToRemove)) + return progress; + return proxy.initiate("aws-ssm-parameter::Update:handle-tagging-remove", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest(model -> Translator.removeTagsFromResourceRequest(model.getName(), tagsToRemove)) + .backoffDelay(getBackOffDelay(progress.getResourceModel())) + .makeServiceCall((removeTagsFromResourceRequest, ssmClientProxyClient) -> + ssmClientProxyClient.injectCredentialsAndInvokeV2(removeTagsFromResourceRequest, ssmClientProxyClient.client()::removeTagsFromResource)) + .progress(); + } + + private ProgressEvent addTags(AmazonWebServicesClientProxy proxy, ProgressEvent progress, + ProxyClient proxyClient, Logger logger, List tagsToAdd) { + if (CollectionUtils.isNullOrEmpty(tagsToAdd)) + return progress; + return proxy.initiate("aws-ssm-parameter::Update:handle-tagging-add", proxyClient, progress.getResourceModel(), progress.getCallbackContext()) + .translateToServiceRequest(model -> Translator.addTagsToResourceRequest(model.getName(), tagsToAdd)) + .backoffDelay(getBackOffDelay(progress.getResourceModel())) + .makeServiceCall((addTagsToResourceRequest, ssmClientProxyClient) -> + ssmClientProxyClient.injectCredentialsAndInvokeV2(addTagsToResourceRequest, ssmClientProxyClient.client()::addTagsToResource)) + .progress(); + } } diff --git a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/AbstractTestBase.java b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/AbstractTestBase.java index 765dcd39..dee9a556 100644 --- a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/AbstractTestBase.java +++ b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/AbstractTestBase.java @@ -1,10 +1,5 @@ package com.amazonaws.ssm.parameter; -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.CompletableFuture; -import java.util.function.Function; import org.slf4j.LoggerFactory; import software.amazon.awssdk.awscore.AwsRequest; import software.amazon.awssdk.awscore.AwsResponse; @@ -14,94 +9,125 @@ import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Credentials; +import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.LoggerProxy; +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.HashMap; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; + +import static org.assertj.core.api.Assertions.assertThat; public class AbstractTestBase { - protected static final Credentials MOCK_CREDENTIALS; - protected static final org.slf4j.Logger delegate; - protected static final LoggerProxy logger; - - protected static final String DESCRIPTION; - protected static final String NAME; - protected static final String TYPE_STRING; - protected static final String TYPE_SECURE_STRING; - protected static final String VALUE; - protected static final Long VERSION; - protected static final Map TAG_SET; - protected static final Map SYSTEM_TAGS_SET; - protected static final Map PREVIOUS_TAG_SET; - - static { - System.setProperty("org.slf4j.simpleLogger.showDateTime", "true"); - System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "HH:mm:ss:SSS Z"); - MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token"); - - delegate = LoggerFactory.getLogger("testing"); - logger = new LoggerProxy(); - - DESCRIPTION = "sample description"; - NAME = "ParameterName"; - TYPE_STRING = "String"; - TYPE_SECURE_STRING = "SecureString"; - VALUE = "dummy value"; - VERSION = 1L; - TAG_SET = new HashMap() { - { - put("key1", "value1"); - put("key2", "value2"); - } - }; - SYSTEM_TAGS_SET = new HashMap() { - { - put("aws:cloudformation:stack-name", "DummyStackName"); - put("aws:cloudformation:logical-id", "DummyLogicalId"); - put("aws:cloudformation:stack-id", "DummyStackArn"); - } - }; - PREVIOUS_TAG_SET = new HashMap() { - { - put("key3", "value3"); - } - }; - PREVIOUS_TAG_SET.putAll(TAG_SET); - PREVIOUS_TAG_SET.putAll(SYSTEM_TAGS_SET); - } - - static ProxyClient MOCK_PROXY( - final AmazonWebServicesClientProxy proxy, - final SsmClient ssmClient - ) { - return new ProxyClient() { - @Override - public ResponseT injectCredentialsAndInvokeV2(RequestT requestT, Function function) { - return proxy.injectCredentialsAndInvokeV2(requestT, function); - } - - @Override - public CompletableFuture injectCredentialsAndInvokeV2Async(RequestT requestT, Function> function) { - throw new UnsupportedOperationException(); - } - - @Override - public > IterableT injectCredentialsAndInvokeIterableV2(RequestT requestT, Function function) { - return proxy.injectCredentialsAndInvokeIterableV2(requestT, function); - } - - @Override - public ResponseInputStream injectCredentialsAndInvokeV2InputStream(RequestT requestT, Function> function) { - throw new UnsupportedOperationException(); - } - - @Override - public ResponseBytes injectCredentialsAndInvokeV2Bytes(RequestT requestT, Function> function) { - throw new UnsupportedOperationException(); - } - - @Override - public SsmClient client() { - return ssmClient; - } - }; - } + protected static final Credentials MOCK_CREDENTIALS; + protected static final org.slf4j.Logger delegate; + protected static final LoggerProxy logger; + + protected static final String DESCRIPTION; + protected static final String NAME; + protected static final String TYPE_STRING; + protected static final String TYPE_SECURE_STRING; + protected static final String VALUE; + protected static final Long VERSION; + protected static final Map TAG_SET; + protected static final Map SYSTEM_TAGS_SET; + protected static final Map PREVIOUS_TAG_SET; + + static { + System.setProperty("org.slf4j.simpleLogger.showDateTime", "true"); + System.setProperty("org.slf4j.simpleLogger.dateTimeFormat", "HH:mm:ss:SSS Z"); + MOCK_CREDENTIALS = new Credentials("accessKey", "secretKey", "token"); + + delegate = LoggerFactory.getLogger("testing"); + logger = new LoggerProxy(); + + DESCRIPTION = "sample description"; + NAME = "ParameterName"; + TYPE_STRING = "String"; + TYPE_SECURE_STRING = "SecureString"; + VALUE = "dummy value"; + VERSION = 1L; + TAG_SET = new HashMap() { + { + put("key1", "value1"); + put("key2", "value2"); + } + }; + SYSTEM_TAGS_SET = new HashMap() { + { + put("aws:cloudformation:stack-name", "DummyStackName"); + put("aws:cloudformation:logical-id", "DummyLogicalId"); + put("aws:cloudformation:stack-id", "DummyStackArn"); + } + }; + PREVIOUS_TAG_SET = new HashMap() { + { + put("key3", "value3"); + } + }; + PREVIOUS_TAG_SET.putAll(TAG_SET); + PREVIOUS_TAG_SET.putAll(SYSTEM_TAGS_SET); + } + + static Map toResourceModelTags(Map stringTags) { + return stringTags.entrySet().stream().collect(Collectors.toMap(tag -> tag.getKey(), tag -> tag)); + } + + static ProxyClient MOCK_PROXY( + final AmazonWebServicesClientProxy proxy, + final SsmClient ssmClient + ) { + return new ProxyClient() { + @Override + public ResponseT injectCredentialsAndInvokeV2(RequestT requestT, Function function) { + return proxy.injectCredentialsAndInvokeV2(requestT, function); + } + + @Override + public CompletableFuture injectCredentialsAndInvokeV2Async(RequestT requestT, + Function> function) { + throw new UnsupportedOperationException(); + } + + @Override + public > IterableT injectCredentialsAndInvokeIterableV2( + RequestT requestT, Function function) { + return proxy.injectCredentialsAndInvokeIterableV2(requestT, function); + } + + @Override + public ResponseInputStream injectCredentialsAndInvokeV2InputStream(RequestT requestT, + Function> function) { + throw new UnsupportedOperationException(); + } + + @Override + public ResponseBytes injectCredentialsAndInvokeV2Bytes(RequestT requestT, + Function> function) { + throw new UnsupportedOperationException(); + } + + @Override + public SsmClient client() { + return ssmClient; + } + }; + } + + protected void assertFailureErrorCode(final ResourceHandlerRequest request, + final ProgressEvent response, final HandlerErrorCode errorCode) { + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isEqualTo(request.getDesiredResourceState()); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getErrorCode()).isEqualTo(errorCode); + } } diff --git a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/CreateHandlerTest.java b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/CreateHandlerTest.java index 054c4cce..b8e29f08 100644 --- a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/CreateHandlerTest.java +++ b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/CreateHandlerTest.java @@ -1,400 +1,304 @@ package com.amazonaws.ssm.parameter; -import com.amazonaws.AmazonServiceException; import org.apache.commons.lang3.RandomStringUtils; import org.junit.jupiter.api.AfterEach; -import software.amazon.awssdk.services.ssm.SsmClient; -import software.amazon.awssdk.services.ssm.model.GetParametersResponse; -import software.amazon.awssdk.services.ssm.model.GetParametersRequest; -import software.amazon.awssdk.services.ssm.model.Parameter; -import software.amazon.awssdk.services.ssm.model.ParameterAlreadyExistsException; -import software.amazon.awssdk.services.ssm.model.PutParameterResponse; -import software.amazon.awssdk.services.ssm.model.PutParameterRequest; -import software.amazon.awssdk.services.ssm.model.ParameterTier; -import software.amazon.awssdk.services.ssm.model.InternalServerErrorException; -import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; -import software.amazon.cloudformation.exceptions.CfnThrottlingException; -import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; -import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; -import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.HandlerErrorCode; -import software.amazon.cloudformation.proxy.ProxyClient; -import software.amazon.cloudformation.proxy.ProgressEvent; -import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import software.amazon.cloudformation.proxy.OperationStatus; - import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; 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.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.exception.SdkException; +import software.amazon.awssdk.services.ssm.SsmClient; +import software.amazon.awssdk.services.ssm.model.*; +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 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.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class CreateHandlerTest extends AbstractTestBase { - @Mock - private AmazonWebServicesClientProxy proxy; - - @Mock - private ProxyClient proxySsmClient; - - @Mock - SsmClient ssmClient; - - private CreateHandler handler; - - private ResourceModel RESOURCE_MODEL; - - @BeforeEach - public void setup() { - handler = new CreateHandler(); - ssmClient = mock(SsmClient.class); - proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); - proxySsmClient = MOCK_PROXY(proxy, ssmClient); - - RESOURCE_MODEL = ResourceModel.builder() - .description(DESCRIPTION) - .name(NAME) - .value(VALUE) - .type(TYPE_STRING) - .tags(TAG_SET) - .build(); - } - - @AfterEach - public void post_execute() { - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_SimpleSuccess() { - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final PutParameterResponse putParameterResponse = PutParameterResponse.builder() - .version(VERSION) - .tier(ParameterTier.STANDARD) - .build(); - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logicalId").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_SecureStringFailure() { - RESOURCE_MODEL = ResourceModel.builder() - .description(DESCRIPTION) - .value(VALUE) - .type(TYPE_SECURE_STRING) - .tags(TAG_SET) - .build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logicalId").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isEqualTo("SSM Parameters of type SecureString cannot be created using CloudFormation"); - assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.InvalidRequest); - - verify(ssmClient, never()).serviceName(); - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_SimpleSuccess_With_No_ParameterName_Defined_Exceeding_LogicalResourceId() { - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final PutParameterResponse putParameterResponse = PutParameterResponse.builder() - .version(VERSION) - .tier(ParameterTier.STANDARD) - .build(); - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); - - RESOURCE_MODEL = ResourceModel.builder() - .description(DESCRIPTION) - .value(VALUE) - .type(TYPE_STRING) - .tags(TAG_SET) - .build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier(RandomStringUtils.random(600)).build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_SimpleSuccess_With_No_ParameterName_Defined_Not_Exceeding_LogicalResourceId() { - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final PutParameterResponse putParameterResponse = PutParameterResponse.builder() - .version(VERSION) - .tier(ParameterTier.STANDARD) - .build(); - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); - - RESOURCE_MODEL = ResourceModel.builder() - .description(DESCRIPTION) - .value(VALUE) - .type(TYPE_STRING) - .tags(TAG_SET) - .build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_SimpleSuccess_WithImageDataType() { - RESOURCE_MODEL = ResourceModel.builder() - .description(DESCRIPTION) - .name(NAME) - .value(VALUE) - .type(TYPE_STRING) - .tags(TAG_SET) - .dataType("aws:ec2:image") - .build(); - - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final PutParameterResponse putParameterResponse = PutParameterResponse.builder() - .version(VERSION) - .tier(ParameterTier.STANDARD) - .build(); - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logicalId").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_AmazonServiceException400ThrottlingException() { - AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); - amazonServiceException.setStatusCode(429); - amazonServiceException.setErrorCode("ThrottlingException"); - - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(amazonServiceException); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnThrottlingException ex) { - assertThat(ex).isInstanceOf(CfnThrottlingException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_ParameterAlreadyExistsException() { - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(ParameterAlreadyExistsException.builder().build()); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnAlreadyExistsException ex) { - assertThat(ex).isInstanceOf(CfnAlreadyExistsException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_AmazonServiceException500Exception() { - AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); - amazonServiceException.setStatusCode(500); - - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(amazonServiceException); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnGeneralServiceException ex) { - assertThat(ex).isInstanceOf(CfnGeneralServiceException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_AmazonServiceException400NonThrottlingException() { - AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); - amazonServiceException.setStatusCode(400); - amazonServiceException.setErrorCode("Invalid Input"); - - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(amazonServiceException); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnGeneralServiceException ex) { - assertThat(ex).isInstanceOf(CfnGeneralServiceException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_AmazonServiceExceptionInternalServerError() { - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(InternalServerErrorException.builder().build()); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnServiceInternalErrorException ex) { - assertThat(ex).isInstanceOf(CfnServiceInternalErrorException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } + @Mock + private AmazonWebServicesClientProxy proxy; + @Mock + private ProxyClient proxyClient; + @Mock + SsmClient ssmClient; + @Mock + private ReadHandler readHandler; + + private CreateHandler handler; + + private ResourceModel RESOURCE_MODEL; + + @BeforeEach + public void setup() { + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + ssmClient = mock(SsmClient.class); + proxyClient = MOCK_PROXY(proxy, ssmClient); + handler = new CreateHandler(ssmClient, readHandler); + + RESOURCE_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .name(NAME) + .value(VALUE) + .type(TYPE_STRING) + .tags(toResourceModelTags(TAG_SET)) + .build(); + } + + @AfterEach + public void tear_down(org.junit.jupiter.api.TestInfo testInfo) { + if (!testInfo.getTags().contains("skipSdkInteraction")) { + verify(ssmClient, atLeastOnce()).serviceName(); + } + verifyNoMoreInteractions(proxyClient.client()); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .logicalResourceIdentifier("logicalId") + .build(); + + final PutParameterResponse putParameterResponse = PutParameterResponse.builder() + .version(VERSION) + .tier(ParameterTier.STANDARD) + .build(); + when(proxyClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); + + when(readHandler.handleRequest(any(), any(), any(), any(), any())).thenReturn( + ProgressEvent.success(RESOURCE_MODEL, new CallbackContext())); + + 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.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Tag("skipSdkInteraction") + @Test + public void handleRequest_SecureStringFailure() { + RESOURCE_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .value(VALUE) + .type(TYPE_SECURE_STRING) + .tags(toResourceModelTags(TAG_SET)) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .logicalResourceIdentifier("logicalId").build(); + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.InvalidRequest); + } + + @Test + public void handleRequest_SimpleSuccess_With_No_ParameterName_Defined_Exceeding_LogicalResourceId() { + final PutParameterResponse putParameterResponse = PutParameterResponse.builder() + .version(VERSION) + .tier(ParameterTier.STANDARD) + .build(); + when(proxyClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); + + RESOURCE_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .value(VALUE) + .type(TYPE_STRING) + .tags(toResourceModelTags(TAG_SET)) + .build(); + + when(readHandler.handleRequest(any(), any(), any(), any(), any())).thenReturn( + ProgressEvent.success(RESOURCE_MODEL, new CallbackContext())); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .logicalResourceIdentifier(RandomStringUtils.random(600)).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.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_SimpleSuccess_With_No_ParameterName_Defined_Not_Exceeding_LogicalResourceId() { + final PutParameterResponse putParameterResponse = PutParameterResponse.builder() + .version(VERSION) + .tier(ParameterTier.STANDARD) + .build(); + when(proxyClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); + + RESOURCE_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .value(VALUE) + .type(TYPE_STRING) + .tags(toResourceModelTags(TAG_SET)) + .build(); + + when(readHandler.handleRequest(any(), any(), any(), any(), any())).thenReturn( + ProgressEvent.success(RESOURCE_MODEL, new CallbackContext())); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .logicalResourceIdentifier("logical_id").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.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_SimpleSuccess_WithImageDataType() { + RESOURCE_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .name(NAME) + .value(VALUE) + .type(TYPE_STRING) + .tags(toResourceModelTags(TAG_SET)) + .dataType("aws:ec2:image") + .build(); + + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final PutParameterResponse putParameterResponse = PutParameterResponse.builder() + .version(VERSION) + .tier(ParameterTier.STANDARD) + .build(); + when(proxyClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); + + when(readHandler.handleRequest(any(), any(), any(), any(), any())).thenReturn( + ProgressEvent.success(RESOURCE_MODEL, new CallbackContext())); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .logicalResourceIdentifier("logicalId").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.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_FailWithException() { + //Exceptions while calling the API + Exception[] exceptions = { + AwsServiceException.builder().awsErrorDetails(AwsErrorDetails.builder().errorCode("ThrottlingException").build()).build(), + ParameterAlreadyExistsException.builder().build(), + AwsServiceException.builder().build(), + SdkException.builder().build(), + ParameterPatternMismatchException.builder().build(), + InvalidKeyIdException.builder().build(), + HierarchyTypeMismatchException.builder().build(), + InvalidAllowedPatternException.builder().build(), + UnsupportedParameterTypeException.builder().build(), + InvalidPolicyTypeException.builder().build(), + InvalidPolicyAttributeException.builder().build(), + IncompatiblePolicyException.builder().build(), + InvalidFilterKeyException.builder().build(), + InvalidFilterOptionException.builder().build(), + InvalidFilterValueException.builder().build(), + InvalidNextTokenException.builder().build(), + ParameterLimitExceededException.builder().build(), + HierarchyLevelLimitExceededException.builder().build(), + ParameterMaxVersionLimitExceededException.builder().build(), + PoliciesLimitExceededException.builder().build(), + }; + + HandlerErrorCode[] handlerErrorCodes = { + HandlerErrorCode.Throttling, + HandlerErrorCode.AlreadyExists, + HandlerErrorCode.GeneralServiceException, + HandlerErrorCode.InternalFailure, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.InvalidRequest, + HandlerErrorCode.ServiceLimitExceeded, + HandlerErrorCode.ServiceLimitExceeded, + HandlerErrorCode.ServiceLimitExceeded, + HandlerErrorCode.ServiceLimitExceeded, + }; + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .logicalResourceIdentifier("logical_id").build(); + + for (int i = 0; i < exceptions.length; i++) { + //response is empty for pre check success + when(proxyClient.client().putParameter(any(PutParameterRequest.class))) + .thenThrow(exceptions[i]); + + final ProgressEvent response = handler + .handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + assertFailureErrorCode(request, response, handlerErrorCodes[i]); + } + } } diff --git a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/DeleteHandlerTest.java b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/DeleteHandlerTest.java index 0a8582e1..a7edbdf9 100644 --- a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/DeleteHandlerTest.java +++ b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/DeleteHandlerTest.java @@ -2,208 +2,153 @@ import com.amazonaws.AmazonServiceException; 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.awscore.exception.AwsErrorDetails; +import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.core.exception.SdkException; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.DeleteParameterRequest; -import software.amazon.awssdk.services.ssm.model.ParameterNotFoundException; import software.amazon.awssdk.services.ssm.model.InternalServerErrorException; +import software.amazon.awssdk.services.ssm.model.ParameterAlreadyExistsException; +import software.amazon.awssdk.services.ssm.model.ParameterNotFoundException; +import software.amazon.awssdk.services.ssm.model.PutParameterRequest; import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; import software.amazon.cloudformation.exceptions.CfnThrottlingException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.ProxyClient; +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.when; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class DeleteHandlerTest extends AbstractTestBase { - @Mock - private AmazonWebServicesClientProxy proxy; - - @Mock - private ProxyClient proxySsmClient; - - @Mock - SsmClient ssmClient; - - private DeleteHandler handler; - - private ResourceModel RESOURCE_MODEL; - - @BeforeEach - public void setup() { - handler = new DeleteHandler(); - ssmClient = mock(SsmClient.class); - proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); - proxySsmClient = MOCK_PROXY(proxy, ssmClient); - - RESOURCE_MODEL = ResourceModel.builder() - .description(DESCRIPTION) - .name(NAME) - .value(VALUE) - .type(TYPE_STRING) - .tags(TAG_SET) - .build(); - } - - @AfterEach - public void post_execute() { - verify(ssmClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_SimpleSuccess() { - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logicalId").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).deleteParameter(any(DeleteParameterRequest.class)); - } - - @Test - public void handleRequest_Failure_ParameterNotFound() { - when(proxySsmClient.client().deleteParameter(any(DeleteParameterRequest.class))) - .thenThrow(ParameterNotFoundException.builder().build()); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logicalId").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnNotFoundException ex) { - assertThat(ex).isInstanceOf(CfnNotFoundException.class); - } - - verify(proxySsmClient.client()).deleteParameter(any(DeleteParameterRequest.class)); - } - - @Test - public void handleRequest_ThrottlingException() { - AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); - amazonServiceException.setStatusCode(429); - amazonServiceException.setErrorCode("ThrottlingException"); - - when(proxySsmClient.client().deleteParameter(any(DeleteParameterRequest.class))) - .thenThrow(amazonServiceException); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnThrottlingException ex) { - assertThat(ex).isInstanceOf(CfnThrottlingException.class); - } - - verify(proxySsmClient.client()).deleteParameter(any(DeleteParameterRequest.class)); - } - - @Test - public void handleRequest_NonThrottlingAmazonServiceException() { - AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); - amazonServiceException.setStatusCode(500); - - when(proxySsmClient.client().deleteParameter(any(DeleteParameterRequest.class))) - .thenThrow(amazonServiceException); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnGeneralServiceException ex) { - assertThat(ex).isInstanceOf(CfnGeneralServiceException.class); - } - - verify(proxySsmClient.client()).deleteParameter(any(DeleteParameterRequest.class)); - } - - @Test - public void handleRequest_AmazonServiceExceptionInternalServerError() { - when(proxySsmClient.client().deleteParameter(any(DeleteParameterRequest.class))) - .thenThrow(InternalServerErrorException.builder().build()); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnServiceInternalErrorException ex) { - assertThat(ex).isInstanceOf(CfnServiceInternalErrorException.class); - } - - verify(proxySsmClient.client()).deleteParameter(any(DeleteParameterRequest.class)); - } - - @Test - public void handleRequest_AmazonServiceException400NonThrottlingException() { - AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); - amazonServiceException.setStatusCode(400); - amazonServiceException.setErrorCode("Invalid Input"); - - when(proxySsmClient.client().deleteParameter(any(DeleteParameterRequest.class))) - .thenThrow(amazonServiceException); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnGeneralServiceException ex) { - assertThat(ex).isInstanceOf(CfnGeneralServiceException.class); - } - - verify(proxySsmClient.client()).deleteParameter(any(DeleteParameterRequest.class)); - } + @Mock + private AmazonWebServicesClientProxy proxy; + @Mock + private ProxyClient proxyClient; + @Mock + SsmClient ssmClient; + + private DeleteHandler handler; + + private ResourceModel RESOURCE_MODEL; + + @BeforeEach + public void setup() { + handler = new DeleteHandler(ssmClient); + ssmClient = mock(SsmClient.class); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + proxyClient = MOCK_PROXY(proxy, ssmClient); + + RESOURCE_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .name(NAME) + .value(VALUE) + .type(TYPE_STRING) + .tags(toResourceModelTags(TAG_SET)) + .build(); + } + + @AfterEach + public void tear_down(org.junit.jupiter.api.TestInfo testInfo) { + if (!testInfo.getTags().contains("skipSdkInteraction")) { + verify(ssmClient, atLeastOnce()).serviceName(); + } + verifyNoMoreInteractions(proxyClient.client()); + } + + @Test + public void handleRequest_SimpleSuccess() { + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .logicalResourceIdentifier("logicalId").build(); + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + + verify(proxyClient.client()).deleteParameter(any(DeleteParameterRequest.class)); + } + + + @Test + public void handleRequest_FailWithException() { + //Exceptions while calling the API + Exception[] exceptions = { + ParameterNotFoundException.builder().build(), + // AwsServiceException.builder().awsErrorDetails(AwsErrorDetails.builder().errorCode("ThrottlingException").build()).build(), + // AwsServiceException.builder().build(), + // SdkException.builder().build() + }; + + HandlerErrorCode[] handlerErrorCodes = { + HandlerErrorCode.NotFound, + // HandlerErrorCode.Throttling, + // HandlerErrorCode.GeneralServiceException, + // HandlerErrorCode.InternalFailure, + }; + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .logicalResourceIdentifier("logical_id").build(); + + for (int i = 0; i < exceptions.length; i++) { + //response is empty for pre check success + when(proxyClient.client().deleteParameter(any(DeleteParameterRequest.class))) + .thenThrow(exceptions[i]); + + final ProgressEvent response = handler + .handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + assertFailureErrorCode(request, response, handlerErrorCodes[i]); + } + } + + @Test + public void handleRequest_NonThrottlingAmazonServiceException() { + AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); + amazonServiceException.setStatusCode(500); + + when(proxyClient.client().deleteParameter(any(DeleteParameterRequest.class))) + .thenThrow(amazonServiceException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .logicalResourceIdentifier("logical_id").build(); + + try { + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } catch (CfnGeneralServiceException ex) { + assertThat(ex).isInstanceOf(CfnGeneralServiceException.class); + } + + verify(proxyClient.client()).deleteParameter(any(DeleteParameterRequest.class)); + } } diff --git a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/ListHandlerTest.java b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/ListHandlerTest.java index 1bf3efe7..abaf622c 100644 --- a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/ListHandlerTest.java +++ b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/ListHandlerTest.java @@ -1,92 +1,82 @@ package com.amazonaws.ssm.parameter; 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.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.DescribeParametersRequest; import software.amazon.awssdk.services.ssm.model.DescribeParametersResponse; import software.amazon.awssdk.services.ssm.model.ParameterMetadata; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.ProxyClient; 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 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 org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class ListHandlerTest extends AbstractTestBase { - @Mock - private AmazonWebServicesClientProxy proxy; - - @Mock - private ProxyClient proxySsmClient; - - @Mock - SsmClient ssmClient; - - private ListHandler handler; - - private static final String PARAMETER_NAME = "parameterName"; - private static final String NEXT_TOKEN = "4b90a7e4-b790-456b"; - - @BeforeEach - public void setup() { - handler = new ListHandler(); - ssmClient = mock(SsmClient.class); - proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); - proxySsmClient = MOCK_PROXY(proxy, ssmClient); - } - - @AfterEach - public void post_execute() { - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_SimpleSuccess() { - final DescribeParametersResponse describeParametersResponse = DescribeParametersResponse.builder() - .parameters(Collections.singletonList(ParameterMetadata.builder().name(PARAMETER_NAME).build())) - .nextToken(NEXT_TOKEN).build(); - when(proxySsmClient.client().describeParameters(any(DescribeParametersRequest.class))) - .thenReturn(describeParametersResponse); - - final ResourceModel model = ResourceModel.builder().build(); - - final ResourceModel expectedModel = ResourceModel.builder().name(PARAMETER_NAME).build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) - .build(); - - final ProgressEvent response = - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, 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.getResourceModels()).containsExactly(expectedModel); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - assertThat(response.getNextToken()).isEqualTo(NEXT_TOKEN); - - verify(proxySsmClient.client()).describeParameters(any(DescribeParametersRequest.class)); - } + @Mock + private AmazonWebServicesClientProxy proxy; + @Mock + private ProxyClient proxyClient; + @Mock + SsmClient ssmClient; + + private ListHandler handler; + + private static final String PARAMETER_NAME = "parameterName"; + private static final String NEXT_TOKEN = "4b90a7e4-b790-456b"; + + @BeforeEach + public void setup() { + handler = new ListHandler(ssmClient); + ssmClient = mock(SsmClient.class); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + proxyClient = MOCK_PROXY(proxy, ssmClient); + } + + @AfterEach + public void tear_down() { + verify(ssmClient, atLeastOnce()).serviceName(); + verifyNoMoreInteractions(proxyClient.client()); + } + + @Test + public void handleRequest_SimpleSuccess() { + final DescribeParametersResponse describeParametersResponse = DescribeParametersResponse.builder() + .parameters(Collections.singletonList(ParameterMetadata.builder().name(PARAMETER_NAME).build())) + .nextToken(NEXT_TOKEN).build(); + when(proxyClient.client().describeParameters(any(DescribeParametersRequest.class))) + .thenReturn(describeParametersResponse); + + final ResourceModel model = ResourceModel.builder().build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(model) + .build(); + + final ProgressEvent response = + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, 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(); + assertThat(response.getNextToken()).isEqualTo(NEXT_TOKEN); + } } diff --git a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/ReadHandlerTest.java b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/ReadHandlerTest.java index 0607cb11..bbe90d96 100644 --- a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/ReadHandlerTest.java +++ b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/ReadHandlerTest.java @@ -1,126 +1,161 @@ package com.amazonaws.ssm.parameter; 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.ssm.SsmClient; +import software.amazon.awssdk.services.ssm.model.DescribeParametersRequest; +import software.amazon.awssdk.services.ssm.model.DescribeParametersResponse; import software.amazon.awssdk.services.ssm.model.GetParametersRequest; import software.amazon.awssdk.services.ssm.model.GetParametersResponse; import software.amazon.awssdk.services.ssm.model.InternalServerErrorException; +import software.amazon.awssdk.services.ssm.model.ListTagsForResourceRequest; +import software.amazon.awssdk.services.ssm.model.ListTagsForResourceResponse; import software.amazon.awssdk.services.ssm.model.Parameter; +import software.amazon.awssdk.services.ssm.model.ParameterMetadata; +import software.amazon.awssdk.services.ssm.model.ParameterTier; +import software.amazon.awssdk.services.ssm.model.Tag; import software.amazon.cloudformation.exceptions.CfnNotFoundException; import software.amazon.cloudformation.exceptions.CfnServiceInternalErrorException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; -import software.amazon.cloudformation.proxy.ProxyClient; +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 software.amazon.cloudformation.proxy.OperationStatus; - -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.when; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class ReadHandlerTest extends AbstractTestBase { - @Mock - private AmazonWebServicesClientProxy proxy; - - @Mock - private ProxyClient proxySsmClient; - - @Mock - SsmClient ssmClient; - - private ReadHandler handler; - - @BeforeEach - public void setup() { - handler = new ReadHandler(); - ssmClient = mock(SsmClient.class); - proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); - proxySsmClient = MOCK_PROXY(proxy, ssmClient); - } - - @AfterEach - public void post_execute() { - verify(ssmClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_SimpleSuccess() { - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(ResourceModel.builder() - .build()) - .build(); - final CallbackContext callbackContext = new CallbackContext(); - final ProgressEvent response = handler.handleRequest(proxy, request, callbackContext, proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).getParameters(any(GetParametersRequest.class)); - } - - @Test - public void handleRequest_EmptyGetParametersResponse() { - final GetParametersResponse getParametersResponse = GetParametersResponse.builder().build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(ResourceModel.builder().name("ParameterName") - .build()) - .build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnNotFoundException ex) { - assertThat(ex).isInstanceOf(CfnNotFoundException.class); - } - - verify(proxySsmClient.client()).getParameters(any(GetParametersRequest.class)); - } - - @Test - public void handleRequest_AmazonServiceExceptionInternalServerError() { - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))) - .thenThrow(InternalServerErrorException.builder().build()); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(ResourceModel.builder() - .build()) - .build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnServiceInternalErrorException ex) { - assertThat(ex).isInstanceOf(CfnServiceInternalErrorException.class); - } - - verify(proxySsmClient.client()).getParameters(any(GetParametersRequest.class)); - } + @Mock + private AmazonWebServicesClientProxy proxy; + @Mock + private ProxyClient proxyClient; + @Mock + SsmClient ssmClient; + + private ReadHandler handler; + + @BeforeEach + public void setup() { + handler = new ReadHandler(ssmClient); + ssmClient = mock(SsmClient.class); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + proxyClient = MOCK_PROXY(proxy, ssmClient); + } + + @AfterEach + public void tear_down(org.junit.jupiter.api.TestInfo testInfo) { + if (!testInfo.getTags().contains("skipSdkInteraction")) { + verify(ssmClient, atLeastOnce()).serviceName(); + } + verifyNoMoreInteractions(proxyClient.client()); + } + + @Test + public void handleRequest_SimpleSuccess() { + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final DescribeParametersResponse describeParametersResponse = DescribeParametersResponse.builder() + .parameters(ParameterMetadata.builder() + .description(DESCRIPTION) + .tier(ParameterTier.STANDARD) + .build()) + .build(); + when(proxyClient.client().describeParameters(any(DescribeParametersRequest.class))).thenReturn(describeParametersResponse); + + final ListTagsForResourceResponse listTagsForResourceResponse = ListTagsForResourceResponse.builder() + .tagList( + Tag.builder().key("k1").value("v1").build(), + Tag.builder().key("k2").value("v2").build() + ) + .build(); + when(proxyClient.client().listTagsForResource(any(ListTagsForResourceRequest.class))).thenReturn(listTagsForResourceResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(ResourceModel.builder().build()) + .build(); + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_EmptyGetParametersResponse() { + final GetParametersResponse getParametersResponse = GetParametersResponse.builder().build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(ResourceModel.builder().name("ParameterName") + .build()) + .build(); + + try { + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } catch (CfnNotFoundException ex) { + assertThat(ex).isInstanceOf(CfnNotFoundException.class); + } + } + + @Test + public void handleRequest_EmptyDescribeParametersResponse() { + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final DescribeParametersResponse describeParametersResponse = DescribeParametersResponse.builder().build(); + when(proxyClient.client().describeParameters(any(DescribeParametersRequest.class))).thenReturn(describeParametersResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(ResourceModel.builder().name("ParameterName") + .build()) + .build(); + + try { + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } catch (CfnNotFoundException ex) { + assertThat(ex).isInstanceOf(CfnNotFoundException.class); + } + } + + @Test + public void handleRequest_AmazonServiceExceptionInternalServerError() { + when(proxyClient.client().getParameters(any(GetParametersRequest.class))) + .thenThrow(InternalServerErrorException.builder().build()); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .desiredResourceState(ResourceModel.builder() + .build()) + .build(); + + try { + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } catch (CfnServiceInternalErrorException ex) { + assertThat(ex).isInstanceOf(CfnServiceInternalErrorException.class); + } + } } diff --git a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/UpdateHandlerTest.java b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/UpdateHandlerTest.java index c5fdcaf6..d786a9be 100644 --- a/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/UpdateHandlerTest.java +++ b/aws-ssm-parameter/src/test/java/com/amazonaws/ssm/parameter/UpdateHandlerTest.java @@ -1,17 +1,24 @@ package com.amazonaws.ssm.parameter; import com.amazonaws.AmazonServiceException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Tag; +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.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.AddTagsToResourceRequest; import software.amazon.awssdk.services.ssm.model.AddTagsToResourceResponse; -import software.amazon.awssdk.services.ssm.model.GetParametersResponse; import software.amazon.awssdk.services.ssm.model.GetParametersRequest; -import software.amazon.awssdk.services.ssm.model.Parameter; +import software.amazon.awssdk.services.ssm.model.GetParametersResponse; import software.amazon.awssdk.services.ssm.model.InternalServerErrorException; +import software.amazon.awssdk.services.ssm.model.Parameter; import software.amazon.awssdk.services.ssm.model.ParameterAlreadyExistsException; -import software.amazon.awssdk.services.ssm.model.PutParameterResponse; -import software.amazon.awssdk.services.ssm.model.PutParameterRequest; import software.amazon.awssdk.services.ssm.model.ParameterTier; +import software.amazon.awssdk.services.ssm.model.PutParameterRequest; +import software.amazon.awssdk.services.ssm.model.PutParameterResponse; import software.amazon.awssdk.services.ssm.model.RemoveTagsFromResourceRequest; import software.amazon.awssdk.services.ssm.model.RemoveTagsFromResourceResponse; import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; @@ -20,16 +27,10 @@ import software.amazon.cloudformation.exceptions.CfnThrottlingException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; -import software.amazon.cloudformation.proxy.ProxyClient; +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 software.amazon.cloudformation.proxy.OperationStatus; - -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 java.util.HashMap; @@ -37,433 +38,413 @@ 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.never; -import static org.mockito.Mockito.verify; -import static org.mockito.Mockito.when; -import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.*; @ExtendWith(MockitoExtension.class) public class UpdateHandlerTest extends AbstractTestBase { - @Mock - private AmazonWebServicesClientProxy proxy; - - @Mock - private ProxyClient proxySsmClient; - - @Mock - SsmClient ssmClient; - - private UpdateHandler handler; - - private ResourceModel RESOURCE_MODEL; - - private Map PREVIOUS_TAG_SET_NO_CHANGE; - - private Map TAG_SET_WITH_CHANGE; - - @BeforeEach - public void setup() { - handler = new UpdateHandler(); - ssmClient = mock(SsmClient.class); - proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); - proxySsmClient = MOCK_PROXY(proxy, ssmClient); - - RESOURCE_MODEL = ResourceModel.builder() - .description(DESCRIPTION) - .name(NAME) - .value(VALUE) - .type(TYPE_STRING) - .tags(TAG_SET) - .build(); - - PREVIOUS_TAG_SET_NO_CHANGE = new HashMap(); - TAG_SET_WITH_CHANGE = new HashMap(); - } - - @Test - public void handleRequest_SimpleSuccess() { - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final PutParameterResponse putParameterResponse = PutParameterResponse.builder() - .version(VERSION) - .tier(ParameterTier.STANDARD) - .build(); - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); - - final RemoveTagsFromResourceResponse removeTagsFromResourceResponse = RemoveTagsFromResourceResponse.builder().build(); - when(proxySsmClient.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class))).thenReturn(removeTagsFromResourceResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET) - .logicalResourceIdentifier("logicalId").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_SimpleSuccess_WithoutTagsChange() { - PREVIOUS_TAG_SET_NO_CHANGE.putAll(TAG_SET); - PREVIOUS_TAG_SET_NO_CHANGE.putAll(SYSTEM_TAGS_SET); - - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final PutParameterResponse putParameterResponse = PutParameterResponse.builder() - .version(VERSION) - .tier(ParameterTier.STANDARD) - .build(); - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET_NO_CHANGE) - .logicalResourceIdentifier("logicalId").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_SimpleSuccess_WithTagsChange() { - TAG_SET_WITH_CHANGE.put("AddTagKey", "AddTagValue"); - PREVIOUS_TAG_SET_NO_CHANGE.putAll(TAG_SET); - PREVIOUS_TAG_SET_NO_CHANGE.putAll(SYSTEM_TAGS_SET); - - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final PutParameterResponse putParameterResponse = PutParameterResponse.builder() - .version(VERSION) - .tier(ParameterTier.STANDARD) - .build(); - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); - - final RemoveTagsFromResourceResponse removeTagsFromResourceResponse = RemoveTagsFromResourceResponse.builder().build(); - when(proxySsmClient.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class))).thenReturn(removeTagsFromResourceResponse); - final AddTagsToResourceResponse addTagsToResourceResponse = AddTagsToResourceResponse.builder().build(); - when(proxySsmClient.client().addTagsToResource(any(AddTagsToResourceRequest.class))).thenReturn(addTagsToResourceResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET_WITH_CHANGE) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET_NO_CHANGE) - .logicalResourceIdentifier("logicalId").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_SecureStringFailure() { - RESOURCE_MODEL = ResourceModel.builder() - .description(DESCRIPTION) - .value(VALUE) - .type(TYPE_SECURE_STRING) - .tags(TAG_SET) - .build(); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET) - .logicalResourceIdentifier("logicalId").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isEqualTo("SSM Parameters of type SecureString cannot be updated using CloudFormation"); - assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.InvalidRequest); - - verify(ssmClient, never()).serviceName(); - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_SimpleSuccess_WithImageDataType() { - final PutParameterResponse putParameterResponse = PutParameterResponse.builder() - .version(VERSION) - .tier(ParameterTier.STANDARD) - .build(); - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); - - RESOURCE_MODEL = ResourceModel.builder() - .description(DESCRIPTION) - .name(NAME) - .value(VALUE) - .type(TYPE_STRING) - .tags(TAG_SET) - .dataType("aws:ec2:image") - .build(); - - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final RemoveTagsFromResourceResponse removeTagsFromResourceResponse = RemoveTagsFromResourceResponse.builder().build(); - when(proxySsmClient.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class))).thenReturn(removeTagsFromResourceResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET) - .logicalResourceIdentifier("logicalId").build(); - final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - - assertThat(response).isNotNull(); - assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); - assertThat(response.getCallbackContext()).isNull(); - assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); - assertThat(response.getResourceModels()).isNull(); - assertThat(response.getMessage()).isNull(); - assertThat(response.getErrorCode()).isNull(); - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - } - - @Test - public void handleRequest_AmazonServiceException400ThrottlingException() { - AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); - amazonServiceException.setStatusCode(429); - amazonServiceException.setErrorCode("ThrottlingException"); - - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(amazonServiceException); - - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnThrottlingException ex) { - assertThat(ex).isInstanceOf(CfnThrottlingException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_ParameterAlreadyExistsException() { - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(ParameterAlreadyExistsException.builder().build()); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnAlreadyExistsException ex) { - assertThat(ex).isInstanceOf(CfnAlreadyExistsException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_AmazonServiceException500Exception() { - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); - amazonServiceException.setStatusCode(500); - - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(amazonServiceException); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnGeneralServiceException ex) { - assertThat(ex).isInstanceOf(CfnGeneralServiceException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_AmazonServiceException400NonThrottlingException() { - AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); - amazonServiceException.setStatusCode(400); - amazonServiceException.setErrorCode("Invalid Input"); - - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(amazonServiceException); - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnGeneralServiceException ex) { - assertThat(ex).isInstanceOf(CfnGeneralServiceException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(proxySsmClient.client()); - } - - @Test - public void handleRequest_AmazonServiceExceptionInternalServerError() { - final GetParametersResponse getParametersResponse = GetParametersResponse.builder() - .parameters(Parameter.builder() - .name(NAME) - .type(TYPE_STRING) - .value(VALUE) - .version(VERSION).build()) - .build(); - when(proxySsmClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); - - when(proxySsmClient.client().putParameter(any(PutParameterRequest.class))) - .thenThrow(InternalServerErrorException.builder().build()); - - final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .clientRequestToken("token") - .desiredResourceTags(TAG_SET) - .systemTags(SYSTEM_TAGS_SET) - .desiredResourceState(RESOURCE_MODEL) - .previousResourceTags(PREVIOUS_TAG_SET) - .logicalResourceIdentifier("logical_id").build(); - - try { - handler.handleRequest(proxy, request, new CallbackContext(), proxySsmClient, logger); - } catch (CfnServiceInternalErrorException ex) { - assertThat(ex).isInstanceOf(CfnServiceInternalErrorException.class); - } - - verify(proxySsmClient.client()).putParameter(any(PutParameterRequest.class)); - verify(ssmClient, atLeastOnce()).serviceName(); - verifyNoMoreInteractions(proxySsmClient.client()); - } + @Mock + private AmazonWebServicesClientProxy proxy; + @Mock + private ProxyClient proxyClient; + @Mock + SsmClient ssmClient; + @Mock + private ReadHandler readHandler; + + private UpdateHandler handler; + + private ResourceModel RESOURCE_MODEL; + + private Map PREVIOUS_TAG_SET_NO_CHANGE; + + private Map TAG_SET_WITH_CHANGE; + + @BeforeEach + public void setup() { + handler = new UpdateHandler(ssmClient, readHandler); + ssmClient = mock(SsmClient.class); + proxy = new AmazonWebServicesClientProxy(logger, MOCK_CREDENTIALS, () -> Duration.ofSeconds(600).toMillis()); + proxyClient = MOCK_PROXY(proxy, ssmClient); + + RESOURCE_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .name(NAME) + .value(VALUE) + .type(TYPE_STRING) + .tags(toResourceModelTags(TAG_SET)) + .build(); + + PREVIOUS_TAG_SET_NO_CHANGE = new HashMap(); + TAG_SET_WITH_CHANGE = new HashMap(); + } + + @AfterEach + public void tear_down(org.junit.jupiter.api.TestInfo testInfo) { + if (!testInfo.getTags().contains("skipSdkInteraction")) { + verify(ssmClient, atLeastOnce()).serviceName(); + } + } + + @Test + public void handleRequest_SimpleSuccess() { + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final PutParameterResponse putParameterResponse = PutParameterResponse.builder() + .version(VERSION) + .tier(ParameterTier.STANDARD) + .build(); + when(proxyClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); + + final RemoveTagsFromResourceResponse removeTagsFromResourceResponse = RemoveTagsFromResourceResponse.builder().build(); + when(proxyClient.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class))).thenReturn(removeTagsFromResourceResponse); + + when(readHandler.handleRequest(any(), any(), any(), any(), any())).thenReturn( + ProgressEvent.success(RESOURCE_MODEL, null)); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET) + .logicalResourceIdentifier("logicalId").build(); + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_SimpleSuccess_WithoutTagsChange() { + PREVIOUS_TAG_SET_NO_CHANGE.putAll(TAG_SET); + PREVIOUS_TAG_SET_NO_CHANGE.putAll(SYSTEM_TAGS_SET); + + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final PutParameterResponse putParameterResponse = PutParameterResponse.builder() + .version(VERSION) + .tier(ParameterTier.STANDARD) + .build(); + when(proxyClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); + + when(readHandler.handleRequest(any(), any(), any(), any(), any())).thenReturn( + ProgressEvent.success(RESOURCE_MODEL, null)); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET_NO_CHANGE) + .logicalResourceIdentifier("logicalId").build(); + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_SimpleSuccess_WithTagsChange() { + TAG_SET_WITH_CHANGE.put("AddTagKey", "AddTagValue"); + PREVIOUS_TAG_SET_NO_CHANGE.putAll(TAG_SET); + PREVIOUS_TAG_SET_NO_CHANGE.putAll(SYSTEM_TAGS_SET); + + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final PutParameterResponse putParameterResponse = PutParameterResponse.builder() + .version(VERSION) + .tier(ParameterTier.STANDARD) + .build(); + when(proxyClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); + + final RemoveTagsFromResourceResponse removeTagsFromResourceResponse = RemoveTagsFromResourceResponse.builder().build(); + when(proxyClient.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class))).thenReturn(removeTagsFromResourceResponse); + final AddTagsToResourceResponse addTagsToResourceResponse = AddTagsToResourceResponse.builder().build(); + when(proxyClient.client().addTagsToResource(any(AddTagsToResourceRequest.class))).thenReturn(addTagsToResourceResponse); + + when(readHandler.handleRequest(any(), any(), any(), any(), any())).thenReturn( + ProgressEvent.success(RESOURCE_MODEL, null)); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET_WITH_CHANGE) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET_NO_CHANGE) + .logicalResourceIdentifier("logicalId").build(); + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Tag("skipSdkInteraction") + @Test + public void handleRequest_SecureStringFailure() { + RESOURCE_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .value(VALUE) + .type(TYPE_SECURE_STRING) + .tags(toResourceModelTags(TAG_SET)) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET) + .logicalResourceIdentifier("logicalId").build(); + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.FAILED); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNotNull(); + assertThat(response.getErrorCode()).isEqualTo(HandlerErrorCode.InvalidRequest); + } + + @Test + public void handleRequest_SimpleSuccess_WithImageDataType() { + final PutParameterResponse putParameterResponse = PutParameterResponse.builder() + .version(VERSION) + .tier(ParameterTier.STANDARD) + .build(); + when(proxyClient.client().putParameter(any(PutParameterRequest.class))).thenReturn(putParameterResponse); + + RESOURCE_MODEL = ResourceModel.builder() + .description(DESCRIPTION) + .name(NAME) + .value(VALUE) + .type(TYPE_STRING) + .tags(toResourceModelTags(TAG_SET)) + .dataType("aws:ec2:image") + .build(); + + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final RemoveTagsFromResourceResponse removeTagsFromResourceResponse = RemoveTagsFromResourceResponse.builder().build(); + when(proxyClient.client().removeTagsFromResource(any(RemoveTagsFromResourceRequest.class))).thenReturn(removeTagsFromResourceResponse); + + when(readHandler.handleRequest(any(), any(), any(), any(), any())).thenReturn( + ProgressEvent.success(RESOURCE_MODEL, null)); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET) + .logicalResourceIdentifier("logicalId").build(); + final ProgressEvent response = handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModels()).isNull(); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + } + + @Test + public void handleRequest_AmazonServiceException400ThrottlingException() { + AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); + amazonServiceException.setStatusCode(429); + amazonServiceException.setErrorCode("ThrottlingException"); + + when(proxyClient.client().putParameter(any(PutParameterRequest.class))) + .thenThrow(amazonServiceException); + + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET) + .logicalResourceIdentifier("logical_id").build(); + + try { + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } catch (CfnThrottlingException ex) { + assertThat(ex).isInstanceOf(CfnThrottlingException.class); + } + } + + @Test + public void handleRequest_ParameterAlreadyExistsException() { + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + when(proxyClient.client().putParameter(any(PutParameterRequest.class))) + .thenThrow(ParameterAlreadyExistsException.builder().build()); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET) + .logicalResourceIdentifier("logical_id").build(); + + try { + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } catch (CfnAlreadyExistsException ex) { + assertThat(ex).isInstanceOf(CfnAlreadyExistsException.class); + } + } + + @Test + public void handleRequest_AmazonServiceException500Exception() { + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); + amazonServiceException.setStatusCode(500); + + when(proxyClient.client().putParameter(any(PutParameterRequest.class))) + .thenThrow(amazonServiceException); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET) + .logicalResourceIdentifier("logical_id").build(); + + try { + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } catch (CfnGeneralServiceException ex) { + assertThat(ex).isInstanceOf(CfnGeneralServiceException.class); + } + } + + @Test + public void handleRequest_AmazonServiceException400NonThrottlingException() { + AmazonServiceException amazonServiceException = new AmazonServiceException("Client error"); + amazonServiceException.setStatusCode(400); + amazonServiceException.setErrorCode("Invalid Input"); + + when(proxyClient.client().putParameter(any(PutParameterRequest.class))) + .thenThrow(amazonServiceException); + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET) + .logicalResourceIdentifier("logical_id").build(); + + try { + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } catch (CfnGeneralServiceException ex) { + assertThat(ex).isInstanceOf(CfnGeneralServiceException.class); + } + } + + @Test + public void handleRequest_AmazonServiceExceptionInternalServerError() { + final GetParametersResponse getParametersResponse = GetParametersResponse.builder() + .parameters(Parameter.builder() + .name(NAME) + .type(TYPE_STRING) + .value(VALUE) + .version(VERSION).build()) + .build(); + when(proxyClient.client().getParameters(any(GetParametersRequest.class))).thenReturn(getParametersResponse); + + when(proxyClient.client().putParameter(any(PutParameterRequest.class))) + .thenThrow(InternalServerErrorException.builder().build()); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .clientRequestToken("token") + .desiredResourceTags(TAG_SET) + .systemTags(SYSTEM_TAGS_SET) + .desiredResourceState(RESOURCE_MODEL) + .previousResourceTags(PREVIOUS_TAG_SET) + .logicalResourceIdentifier("logical_id").build(); + + try { + handler.handleRequest(proxy, request, new CallbackContext(), proxyClient, logger); + } catch (CfnServiceInternalErrorException ex) { + assertThat(ex).isInstanceOf(CfnServiceInternalErrorException.class); + } + } } diff --git a/aws-ssm-parameter/template.yml b/aws-ssm-parameter/template.yml index 8d8490ab..32400121 100644 --- a/aws-ssm-parameter/template.yml +++ b/aws-ssm-parameter/template.yml @@ -4,7 +4,8 @@ Description: AWS SAM template for the AWS::SSM::Parameter resource type Globals: Function: - Timeout: 60 # docker start-up times can be long for SAM CLI + Timeout: 180 # docker start-up times can be long for SAM CLI + MemorySize: 1024 Resources: TypeFunction: