diff --git a/aws-ssm-association/pom.xml b/aws-ssm-association/pom.xml index ce7300a7..ad368b64 100644 --- a/aws-ssm-association/pom.xml +++ b/aws-ssm-association/pom.xml @@ -30,7 +30,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0, 2.1.0) diff --git a/aws-ssm-document/.rpdk-config b/aws-ssm-document/.rpdk-config index 3a3d145e..b42c9e60 100644 --- a/aws-ssm-document/.rpdk-config +++ b/aws-ssm-document/.rpdk-config @@ -1,7 +1,7 @@ { "typeName": "AWS::SSM::Document", "language": "java", - "runtime": "java8", + "runtime": "java11", "entrypoint": "com.amazonaws.ssm.document.HandlerWrapper::handleRequest", "testEntrypoint": "com.amazonaws.ssm.document.HandlerWrapper::testEntrypoint", "settings": { diff --git a/aws-ssm-document/aws-ssm-document.json b/aws-ssm-document/aws-ssm-document.json index 091798c2..51e25299 100644 --- a/aws-ssm-document/aws-ssm-document.json +++ b/aws-ssm-document/aws-ssm-document.json @@ -190,19 +190,33 @@ "/properties/Name" ], "tagging": { - "taggable": true + "taggable": true, + "tagOnCreate": true, + "tagUpdatable": true, + "cloudFormationSystemTags": true, + "tagProperty": "/properties/Tags", + "permissions": [ + "ssm:AddTagsToResource", + "ssm:ListTagsForResource", + "ssm:RemoveTagsFromResource" + ] }, "handlers": { "create": { "permissions": [ "ssm:CreateDocument", + "ssm:GetDocument", + "ssm:AddTagsToResource", + "ssm:ListTagsForResource", "s3:GetObject", "iam:PassRole" ] }, "read": { "permissions": [ - "ssm:GetDocument" + "ssm:DescribeDocument", + "ssm:GetDocument", + "ssm:ListTagsForResource" ] }, "update": { @@ -219,7 +233,8 @@ }, "delete": { "permissions": [ - "ssm:DeleteDocument" + "ssm:DeleteDocument", + "ssm:GetDocument" ] }, "list": { diff --git a/aws-ssm-document/contract-tests-artifacts/inputs_1.json b/aws-ssm-document/contract-tests-artifacts/inputs_1.json index b284126a..5b27afef 100644 --- a/aws-ssm-document/contract-tests-artifacts/inputs_1.json +++ b/aws-ssm-document/contract-tests-artifacts/inputs_1.json @@ -1,6 +1,6 @@ { "CreateInputs": { - "Name": "commandTest1", + "Name": "commandTest1-{{ uuid }}", "Content": "{\"schemaVersion\":\"2.2\",\"description\":\"Command Document Example Create JSON Template\",\"mainSteps\":[{\"action\":\"aws:runPowerShellScript\",\"name\":\"example\",\"inputs\":{\"runCommand\":[\"Write-Output\"]}}]}", "DocumentType": "Command", "DocumentFormat": "JSON", diff --git a/aws-ssm-document/contract-tests-artifacts/inputs_2.json b/aws-ssm-document/contract-tests-artifacts/inputs_2.json index a998da95..4a92d628 100644 --- a/aws-ssm-document/contract-tests-artifacts/inputs_2.json +++ b/aws-ssm-document/contract-tests-artifacts/inputs_2.json @@ -1,6 +1,6 @@ { "CreateInputs": { - "Name": "automationTest1", + "Name": "automationTest1-{{ uuid }}", "Content": "{\"description\": \"Automation Document Example JSON Template\",\"schemaVersion\": \"0.3\",\"assumeRole\":\"{{AutomationAssumeRole }}\",\"parameters\":{\"AutomationAssumeRole\":{\"type\":\"String\",\"description\":\"(Optional) The ARN of the role that allows Automation to perform the actions on your behalf.\",\"default\": \"\"}},\"mainSteps\":[{\"name\": \"slp\",\"action\":\"aws:sleep\",\"inputs\":{\"Duration\":\"PT3S\"}}]}", "DocumentType": "Automation", "DocumentFormat": "JSON", diff --git a/aws-ssm-document/contract-tests-artifacts/inputs_3.json b/aws-ssm-document/contract-tests-artifacts/inputs_3.json index cbba65ba..3d460365 100644 --- a/aws-ssm-document/contract-tests-artifacts/inputs_3.json +++ b/aws-ssm-document/contract-tests-artifacts/inputs_3.json @@ -1,6 +1,6 @@ { "CreateInputs": { - "Name": "appConfigTest1", + "Name": "appConfigTest1-{{ uuid }}", "Content": "{\"whitelist\":[{\"name\":\"user1\",\"email\":\"user1@email.com\",\"location\":\"virginia\"},{\"name\":\"user2\",\"email\":\"{VersionName}@email.com\",\"location\":\"washington\"}]}", "DocumentType": "ApplicationConfiguration", "DocumentFormat": "JSON", diff --git a/aws-ssm-document/pom.xml b/aws-ssm-document/pom.xml index f79eb72c..a921ee06 100644 --- a/aws-ssm-document/pom.xml +++ b/aws-ssm-document/pom.xml @@ -37,7 +37,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0, 2.1.0) @@ -99,7 +99,6 @@ -Xlint:all,-options,-processing - -Werror diff --git a/aws-ssm-document/resource-role.yaml b/aws-ssm-document/resource-role.yaml index 1661b3bc..5511c036 100644 --- a/aws-ssm-document/resource-role.yaml +++ b/aws-ssm-document/resource-role.yaml @@ -15,6 +15,13 @@ Resources: Principal: Service: resources.cloudformation.amazonaws.com Action: sts:AssumeRole + Condition: + StringEquals: + aws:SourceAccount: + Ref: AWS::AccountId + StringLike: + aws:SourceArn: + Fn::Sub: arn:${AWS::Partition}:cloudformation:${AWS::Region}:${AWS::AccountId}:type/resource/AWS-SSM-Document/* Path: "/" Policies: - PolicyName: ResourceTypePolicy diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/CallbackContext.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/CallbackContext.java index 5aab4c18..ce879554 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/CallbackContext.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/CallbackContext.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonIgnore; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; import com.fasterxml.jackson.databind.annotation.JsonPOJOBuilder; +import software.amazon.cloudformation.proxy.StdCallbackContext; import lombok.Builder; import lombok.Data; @@ -11,8 +12,8 @@ @Data @Builder @JsonDeserialize(builder = CallbackContext.CallbackContextBuilder.class) -@EqualsAndHashCode -public class CallbackContext { +@EqualsAndHashCode(callSuper = true) +public class CallbackContext extends StdCallbackContext { /** * Boolean that tells whether the create operation on resource started. diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/CreateHandler.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/CreateHandler.java index ced28d78..4246c4cb 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/CreateHandler.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/CreateHandler.java @@ -9,7 +9,6 @@ import software.amazon.awssdk.services.ssm.model.CreateDocumentResponse; import software.amazon.awssdk.services.ssm.model.SsmException; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; -import software.amazon.cloudformation.exceptions.CfnUnauthorizedTaggingOperationException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.HandlerErrorCode; import software.amazon.cloudformation.proxy.Logger; diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentExceptionTranslator.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentExceptionTranslator.java index 5dc9a7bb..3add0329 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentExceptionTranslator.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentExceptionTranslator.java @@ -18,6 +18,7 @@ import software.amazon.awssdk.services.ssm.model.InvalidResourceIdException; import software.amazon.awssdk.services.ssm.model.MaxDocumentSizeExceededException; import software.amazon.awssdk.services.ssm.model.SsmException; +import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; @@ -32,6 +33,8 @@ import lombok.NonNull; import java.io.IOException; +import static com.amazonaws.ssm.document.tags.TagUtil.LIST_TAGS_OPERATION_NAME; + class DocumentExceptionTranslator { private static final int GENERIC_USER_ERROR_STATUS_CODE = 400; @@ -82,6 +85,13 @@ RuntimeException getCfnException(@NonNull final SsmException e, String documentN return new CfnServiceInternalErrorException(operationName, e); } else if (e.getCause() instanceof IOException) { return new CfnNetworkFailureException(operationName, e); + } else if (e.awsErrorDetails() != null + && e.awsErrorDetails().errorCode() != null + && e.awsErrorDetails().errorCode().equalsIgnoreCase("AccessDeniedException")) { + if (operationName.equalsIgnoreCase(LIST_TAGS_OPERATION_NAME)) { + return new CfnUnauthorizedTaggingOperationException(e); + } + return new CfnAccessDeniedException(operationName, e); } else if (e.statusCode() == GENERIC_USER_ERROR_STATUS_CODE) { return new CfnInvalidRequestException(e.getMessage(), e); } diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentModelTranslator.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentModelTranslator.java index aa68cbdc..a51f3809 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentModelTranslator.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentModelTranslator.java @@ -132,12 +132,35 @@ DeleteDocumentRequest generateDeleteDocumentRequest(@NonNull final ResourceModel .build(); } - ListDocumentsRequest generateListDocumentsRequest() { - List keyValuesFilters = - ImmutableList.of(DocumentKeyValuesFilter.builder().key("owner").values("self").build()); + static ListDocumentsRequest generateListDocumentsRequest(@Nullable final String nextToken, @Nullable final ResourceModel model) { + final ImmutableList.Builder filterListBuilder = + ImmutableList.builder(); + + if (model == null) { + return ListDocumentsRequest.builder() + .maxResults(50) + .nextToken(nextToken) + .build(); + } + + if (model.getName() != null) { + filterListBuilder.add(DocumentKeyValuesFilter.builder().key("Name").values(model.getName()).build()); + } + + if (model.getDocumentType() != null) { + filterListBuilder.add(DocumentKeyValuesFilter.builder().key("DocumentType").values(model.getDocumentType()).build()); + } + + if (model.getTargetType() != null) { + filterListBuilder.add(DocumentKeyValuesFilter.builder().key("TargetType").values(model.getTargetType()).build()); + } + + final List filters = filterListBuilder.build(); return ListDocumentsRequest.builder() - .filters(keyValuesFilters) + .filters(filters.isEmpty() ? null : filters) + .maxResults(50) + .nextToken(nextToken) .build(); } diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentResponseModelTranslator.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentResponseModelTranslator.java index 09bbb61c..28da3bc8 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentResponseModelTranslator.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/DocumentResponseModelTranslator.java @@ -48,7 +48,7 @@ ResourceInformation generateResourceInformation(@NonNull final DescribeDocumentR .versionName(response.document().versionName()) .documentFormat(response.document().documentFormatAsString()) .documentType(response.document().documentTypeAsString()) - .tags(translateDocumentTagsToResourceModelTags(response.document().tags())) + .tags(translateTags(response.document().tags())) .requires(translateRequires(response)) .targetType(response.document().targetType()) .build(); @@ -90,15 +90,6 @@ private List translateToResourceModelTags(final Map tagMap) .collect(Collectors.toList()); } - private List translateDocumentTagsToResourceModelTags(final List tags) { - return tags.stream().map( - tag -> Tag.builder() - .key(tag.key()) - .value(tag.value()) - .build()) - .collect(Collectors.toList()); - } - private List translateRequires(final GetDocumentResponse response) { if (!response.hasRequires()) { return null; @@ -128,4 +119,13 @@ private List translateRequires(final DescribeDocumentResponse .build()) .collect(Collectors.toList()); } + + private List translateTags(final List tags) { + return tags.stream().map( + tag -> Tag.builder() + .key(tag.key()) + .value(tag.value()) + .build()) + .collect(Collectors.toList()); + } } diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/ListHandler.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/ListHandler.java index c66bf3a8..10a74815 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/ListHandler.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/ListHandler.java @@ -5,14 +5,16 @@ import lombok.RequiredArgsConstructor; import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.ListDocumentsRequest; +import software.amazon.awssdk.services.ssm.model.ListDocumentsResponse; +import software.amazon.awssdk.services.ssm.model.SsmException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; -import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; @RequiredArgsConstructor public class ListHandler extends BaseHandler { @@ -26,25 +28,47 @@ public class ListHandler extends BaseHandler { @NonNull private final SsmClient ssmClient; + @NonNull + private final DocumentExceptionTranslator exceptionTranslator; + + @NonNull + private final SafeLogger safeLogger; + @VisibleForTesting public ListHandler() { - this(new DocumentModelTranslator(), new DocumentResponseModelTranslator(), SsmClient.create()); + this(DocumentModelTranslator.getInstance(), DocumentResponseModelTranslator.getInstance(), ClientBuilder.getClient(), + DocumentExceptionTranslator.getInstance(), SafeLogger.getInstance()); } + @Override public ProgressEvent handleRequest( - final AmazonWebServicesClientProxy proxy, - final ResourceHandlerRequest request, - final CallbackContext callbackContext, - final Logger logger) { + final AmazonWebServicesClientProxy proxy, + final ResourceHandlerRequest request, + final CallbackContext callbackContext, + final Logger logger) { + final ResourceModel model = request.getDesiredResourceState(); + + safeLogger.safeLogDocumentInformation(model, callbackContext, request.getAwsAccountId(), request.getSystemTags(), logger); - final List models = new ArrayList<>(); + try { + final ListDocumentsRequest listDocumentsRequest = DocumentModelTranslator.generateListDocumentsRequest(request.getNextToken(), model); + final ListDocumentsResponse listDocumentsResponse = proxy.injectCredentialsAndInvokeV2( + listDocumentsRequest, ClientBuilder.getClient()::listDocuments); - final ListDocumentsRequest listDocumentsRequest = documentModelTranslator.generateListDocumentsRequest(); + final List models = listDocumentsResponse + .documentIdentifiers() + .stream().map(documentIdentifier -> ResourceModel.builder().name(documentIdentifier.name()).build()).collect(Collectors.toList()); - return ProgressEvent.builder() - .resourceModels(models) - .status(OperationStatus.SUCCESS) - .build(); + return ProgressEvent.builder() + .resourceModels(models) + .status(OperationStatus.SUCCESS) + .nextToken(listDocumentsResponse.nextToken()) + .build(); + } catch (final SsmException e) { + throw exceptionTranslator.getCfnException(e, null, "AWS::SSM::ListDocuments", logger); + } } + + } diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/ReadHandler.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/ReadHandler.java index 69dde01d..4df19af1 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/ReadHandler.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/ReadHandler.java @@ -17,6 +17,8 @@ import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import static com.amazonaws.ssm.document.tags.TagUtil.LIST_TAGS_OPERATION_NAME; + /** * Get AWS::SSM::Document resource. */ @@ -25,6 +27,7 @@ public class ReadHandler extends BaseHandler { private static final String OPERATION_NAME = "AWS::SSM::GetDocument"; private static final String ACCESS_DENIED_ERROR_CODE = "AccessDeniedException"; + private static final String INVALID_DOCUMENT_FORMAT_JSON = "Invalid document format JSON"; @NonNull private final DocumentModelTranslator documentModelTranslator; @@ -61,21 +64,13 @@ public ProgressEvent handleRequest( safeLogger.safeLogDocumentInformation(model, callbackContext, request.getAwsAccountId(), request.getSystemTags(), logger); - final GetDocumentRequest getDocumentRequest = documentModelTranslator.generateGetDocumentRequest(model); - final DescribeDocumentRequest describeDocumentRequest = documentModelTranslator.generateDescribeDocumentRequest(model); + DescribeDocumentResponse describeDocumentResponse = null; + GetDocumentResponse getDocumentResponse; try { - final GetDocumentResponse getDocumentResponse = proxy.injectCredentialsAndInvokeV2(getDocumentRequest, ssmClient::getDocument); - - final Map documentTags = tagReader.getDocumentTags(model.getName(), ssmClient, proxy); - final ResourceInformation resourceInformation = - documentResponseModelTranslator.generateResourceInformation(getDocumentResponse, documentTags); - try { - final DescribeDocumentResponse describeDocumentResponse = proxy.injectCredentialsAndInvokeV2(describeDocumentRequest, ssmClient::describeDocument); - - resourceInformation.getResourceModel().setTargetType(describeDocumentResponse.document().targetType()); + describeDocumentResponse = proxy.injectCredentialsAndInvokeV2(describeDocumentRequest, ssmClient::describeDocument); } catch(SsmException e) { if (!ACCESS_DENIED_ERROR_CODE.equalsIgnoreCase(e.awsErrorDetails().errorCode())) { throw e; @@ -84,12 +79,44 @@ public ProgressEvent handleRequest( describeDocumentRequest.name())); } - return ProgressEvent.builder() - .resourceModel(resourceInformation.getResourceModel()) - .status(OperationStatus.SUCCESS) - .build(); + GetDocumentRequest getDocumentRequest = documentModelTranslator.generateGetDocumentRequest(model); + + if (describeDocumentResponse != null && + describeDocumentResponse.document().documentTypeAsString().equalsIgnoreCase("ChangeCalendar")) { + getDocumentRequest = getDocumentRequest.toBuilder().documentFormat("TEXT").build(); + } + + try { + getDocumentResponse = proxy.injectCredentialsAndInvokeV2(getDocumentRequest, ssmClient::getDocument); + } catch (SsmException e) { + if (INVALID_DOCUMENT_FORMAT_JSON.equalsIgnoreCase(e.awsErrorDetails().errorMessage())) { + getDocumentRequest = getDocumentRequest.toBuilder().documentFormat("TEXT").build(); + getDocumentResponse = proxy.injectCredentialsAndInvokeV2(getDocumentRequest, ssmClient::getDocument); + } else { + throw e; + } + } } catch (final SsmException e) { throw exceptionTranslator.getCfnException(e, model.getName(), OPERATION_NAME, logger); } + + final Map documentTags; + + try { + documentTags = tagReader.getDocumentTags(model.getName(), ssmClient, proxy); + } catch (final SsmException e) { + throw exceptionTranslator.getCfnException(e, model.getName(), LIST_TAGS_OPERATION_NAME, logger); + } + + final ResourceInformation resourceInformation = + documentResponseModelTranslator.generateResourceInformation(getDocumentResponse, documentTags); + + if (describeDocumentResponse != null) { + resourceInformation.getResourceModel().setTargetType(describeDocumentResponse.document().targetType()); + } + return ProgressEvent.builder() + .resourceModel(resourceInformation.getResourceModel()) + .status(OperationStatus.SUCCESS) + .build(); } } diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/SafeLogger.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/SafeLogger.java index d1244602..5453945e 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/SafeLogger.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/SafeLogger.java @@ -23,13 +23,13 @@ public static SafeLogger getInstance() { return INSTANCE; } - public void safeLogDocumentInformation(@NonNull final ResourceModel resourceModel, + public void safeLogDocumentInformation(@Nullable final ResourceModel resourceModel, @Nullable final CallbackContext callbackContext, @NonNull final String customerAccountId, @Nullable final Map systemTags, @NonNull final Logger logger) { - final String documentDetails = String.format(DOCUMENT_INFO_SAFE_LOG_FORMAT, resourceModel.getName(), - resourceModel.getDocumentType(), resourceModel.getDocumentFormat()); + final String documentDetails = resourceModel != null ? String.format(DOCUMENT_INFO_SAFE_LOG_FORMAT, resourceModel.getName(), + resourceModel.getDocumentType(), resourceModel.getDocumentFormat()) : null; final String stackId = systemTags != null ? systemTags.get(STACK_ID_KEY) : null; diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/tags/TagUpdater.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/tags/TagUpdater.java index d4d92815..5a20e570 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/tags/TagUpdater.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/tags/TagUpdater.java @@ -8,7 +8,6 @@ import software.amazon.awssdk.services.ssm.SsmClient; import software.amazon.awssdk.services.ssm.model.SsmException; import software.amazon.awssdk.services.ssm.model.Tag; -import software.amazon.cloudformation.exceptions.CfnUnauthorizedTaggingOperationException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; diff --git a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/tags/TagUtil.java b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/tags/TagUtil.java index 496ce5b1..09a340e7 100644 --- a/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/tags/TagUtil.java +++ b/aws-ssm-document/src/main/java/com/amazonaws/ssm/document/tags/TagUtil.java @@ -20,6 +20,8 @@ public class TagUtil { public static final String TAGGING_PERMISSION_MESSAGE_FORMAT = "Did not have IAM permissions to process tags on" + " AWS::SSM::Document resource."; + public static final String LIST_TAGS_OPERATION_NAME = "ListTagsForResource"; + private static final String ACCESS_DENIED_ERROR_CODE = "AccessDeniedException"; private static final String ADD_TAGS_ACTION = "ssm:AddTagsToResource"; diff --git a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/CallbackContextTest.java b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/CallbackContextTest.java index 923a3b1a..366ccac5 100644 --- a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/CallbackContextTest.java +++ b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/CallbackContextTest.java @@ -1,6 +1,7 @@ package com.amazonaws.ssm.document; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; import com.fasterxml.jackson.databind.ObjectMapper; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -16,6 +17,7 @@ public void testSerializeAndDeserialize() throws JsonProcessingException { .eventStarted(true) .build(); + OBJECT_MAPPER.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); final String serializedText = OBJECT_MAPPER.writeValueAsString(context); Assertions.assertEquals(context, OBJECT_MAPPER.readValue(serializedText, CallbackContext.class)); diff --git a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/CreateHandlerTest.java b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/CreateHandlerTest.java index d1c66ee1..0c76e2ed 100644 --- a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/CreateHandlerTest.java +++ b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/CreateHandlerTest.java @@ -381,6 +381,7 @@ public void handleRequest_NewDocumentCreation_ssmServiceThrowsException_doesNotS when(documentModelTranslator.generateCreateDocumentRequest(resourceModel, SAMPLE_LOGICAL_RESOURCE_ID, SAMPLE_SYSTEM_TAGS, SAMPLE_RESOURCE_TAGS, SAMPLE_REQUEST_TOKEN)).thenReturn(SAMPLE_CREATE_DOCUMENT_REQUEST); when(ssmException.awsErrorDetails()).thenReturn(ACCESS_DENIED_ERROR_DETAILS); when(proxy.injectCredentialsAndInvokeV2(eq(SAMPLE_CREATE_DOCUMENT_REQUEST), any())).thenThrow(ssmException); + when(exceptionTranslator.getCfnException(ssmException, SAMPLE_DOCUMENT_NAME, OPERATION_NAME, logger)).thenReturn(cfnException); Assertions.assertThrows(CfnGeneralServiceException.class, () -> unitUnderTest.handleRequest(proxy, resourceModelHandlerRequest, null, logger)); diff --git a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/DocumentExceptionTranslatorTest.java b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/DocumentExceptionTranslatorTest.java index 91d4726f..78b74957 100644 --- a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/DocumentExceptionTranslatorTest.java +++ b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/DocumentExceptionTranslatorTest.java @@ -7,6 +7,7 @@ import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import software.amazon.awssdk.awscore.exception.AwsErrorDetails; import software.amazon.awssdk.services.ssm.model.AutomationDefinitionNotFoundException; import software.amazon.awssdk.services.ssm.model.DocumentAlreadyExistsException; import software.amazon.awssdk.services.ssm.model.DocumentLimitExceededException; @@ -18,6 +19,7 @@ import software.amazon.awssdk.services.ssm.model.InvalidResourceIdException; import software.amazon.awssdk.services.ssm.model.MaxDocumentSizeExceededException; import software.amazon.awssdk.services.ssm.model.SsmException; +import software.amazon.cloudformation.exceptions.CfnAccessDeniedException; import software.amazon.cloudformation.exceptions.CfnAlreadyExistsException; import software.amazon.cloudformation.exceptions.CfnGeneralServiceException; import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; @@ -29,6 +31,8 @@ import software.amazon.cloudformation.exceptions.CfnUnauthorizedTaggingOperationException; import software.amazon.cloudformation.proxy.Logger; +import static com.amazonaws.ssm.document.tags.TagUtil.LIST_TAGS_OPERATION_NAME; + @ExtendWith(MockitoExtension.class) public class DocumentExceptionTranslatorTest { @@ -103,4 +107,18 @@ public void testGetCfnException_400StatusCode_verifyExceptionsReturned() { Assertions.assertTrue(unitUnderTest.getCfnException(ssmException, SAMPLE_DOCUMENT_NAME, SAMPLE_OPERATION_NAME, logger) instanceof CfnInvalidRequestException); } + + @Test + public void testGetCfnException_AccessDeniedException_verifyExceptionsReturned() { + Mockito.when(ssmException.awsErrorDetails()).thenReturn(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()); + + Assertions.assertTrue(unitUnderTest.getCfnException(ssmException, SAMPLE_DOCUMENT_NAME, SAMPLE_OPERATION_NAME, logger) instanceof CfnAccessDeniedException); + } + + @Test + public void testGetCfnException_ListTagsForResourceAccessDeniedException_verifyExceptionsReturned() { + Mockito.when(ssmException.awsErrorDetails()).thenReturn(AwsErrorDetails.builder().errorCode("AccessDeniedException").build()); + + Assertions.assertTrue(unitUnderTest.getCfnException(ssmException, SAMPLE_DOCUMENT_NAME, LIST_TAGS_OPERATION_NAME, logger) instanceof CfnUnauthorizedTaggingOperationException); + } } diff --git a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/DocumentModelTranslatorTest.java b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/DocumentModelTranslatorTest.java index de376e84..2786f84b 100644 --- a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/DocumentModelTranslatorTest.java +++ b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/DocumentModelTranslatorTest.java @@ -7,9 +7,10 @@ import software.amazon.awssdk.services.ssm.model.CreateDocumentRequest; import software.amazon.awssdk.services.ssm.model.DeleteDocumentRequest; import software.amazon.awssdk.services.ssm.model.DescribeDocumentRequest; +import software.amazon.awssdk.services.ssm.model.DocumentKeyValuesFilter; import software.amazon.awssdk.services.ssm.model.DocumentFormat; import software.amazon.awssdk.services.ssm.model.GetDocumentRequest; -import software.amazon.awssdk.services.ssm.model.GetDocumentResponse; +import software.amazon.awssdk.services.ssm.model.ListDocumentsRequest; import software.amazon.awssdk.services.ssm.model.UpdateDocumentRequest; import java.util.List; @@ -48,6 +49,7 @@ public class DocumentModelTranslatorTest { private static final String SAMPLE_DOCUMENT_TYPE = "type"; private static final String LATEST_DOCUMENT_VERSION = "$LATEST"; private static final String SAMPLE_TARGET_TYPE = "targetType"; + private static final Integer MAX_RESULTS = 50; private static final List SAMPLE_RESOURCE_MODEL_TAGS = ImmutableList.of( Tag.builder().key("tagKey1").value("tagValue1").build(), Tag.builder().key("tagKey2").value("tagValue2").build() @@ -488,6 +490,97 @@ public void testGenerateDescribeDocumentRequest_verifyResult() { Assertions.assertEquals(expectedRequest, request); } + //ListDocumentRequest tests + @Test + public void testGenerateListDocumentsRequest_NoFilters_verifyResult() { + final ResourceModel model = ResourceModel.builder().build(); + + final ListDocumentsRequest expectedRequest = ListDocumentsRequest.builder() + .maxResults(MAX_RESULTS) + .nextToken(SAMPLE_REQUEST_TOKEN) + .build(); + + final ListDocumentsRequest request = unitUnderTest.generateListDocumentsRequest(SAMPLE_REQUEST_TOKEN,model); + + Assertions.assertEquals(expectedRequest, request); + } + + @Test + public void testGenerateListDocumentsRequest_NameFilter_verifyResult() { + final ResourceModel model = ResourceModel.builder() + .name(SAMPLE_DOCUMENT_NAME) + .build(); + + List keyValuesFilters = + ImmutableList.of(DocumentKeyValuesFilter.builder().key("Name").values(SAMPLE_DOCUMENT_NAME).build()); + final ListDocumentsRequest expectedRequest = ListDocumentsRequest.builder() + .filters(keyValuesFilters) + .maxResults(MAX_RESULTS) + .nextToken(SAMPLE_REQUEST_TOKEN) + .build(); + + final ListDocumentsRequest request = unitUnderTest.generateListDocumentsRequest(SAMPLE_REQUEST_TOKEN,model); + + Assertions.assertEquals(expectedRequest, request); + } + @Test + public void testGenerateListDocumentsRequest_DocumentTypeFilter_verifyResult() { + final ResourceModel model = ResourceModel.builder() + .documentType(SAMPLE_DOCUMENT_TYPE) + .build(); + + List keyValuesFilters = + ImmutableList.of(DocumentKeyValuesFilter.builder().key("DocumentType").values(SAMPLE_DOCUMENT_TYPE).build()); + final ListDocumentsRequest expectedRequest = ListDocumentsRequest.builder() + .filters(keyValuesFilters) + .maxResults(MAX_RESULTS) + .nextToken(SAMPLE_REQUEST_TOKEN) + .build(); + + final ListDocumentsRequest request = unitUnderTest.generateListDocumentsRequest(SAMPLE_REQUEST_TOKEN,model); + + Assertions.assertEquals(expectedRequest, request); + } + @Test + public void testGenerateListDocumentsRequest_TargetTypeFilter_verifyResult() { + final ResourceModel model = ResourceModel.builder() + .targetType(SAMPLE_TARGET_TYPE) + .build(); + + List keyValuesFilters = + ImmutableList.of(DocumentKeyValuesFilter.builder().key("TargetType").values(SAMPLE_TARGET_TYPE).build()); + final ListDocumentsRequest expectedRequest = ListDocumentsRequest.builder() + .filters(keyValuesFilters) + .maxResults(MAX_RESULTS) + .nextToken(SAMPLE_REQUEST_TOKEN) + .build(); + + final ListDocumentsRequest request = unitUnderTest.generateListDocumentsRequest(SAMPLE_REQUEST_TOKEN,model); + + Assertions.assertEquals(expectedRequest, request); + } + @Test + public void testGenerateListDocumentsRequest_AllThreeFilter_verifyResult() { + final ResourceModel model = ResourceModel.builder() + .name(SAMPLE_DOCUMENT_NAME) + .documentType(SAMPLE_DOCUMENT_TYPE) + .targetType(SAMPLE_TARGET_TYPE) + .build(); + + List keyValuesFilters = + ImmutableList.of(DocumentKeyValuesFilter.builder().key("Name").values(SAMPLE_DOCUMENT_NAME).build(),DocumentKeyValuesFilter.builder().key("DocumentType").values(SAMPLE_DOCUMENT_TYPE).build(),DocumentKeyValuesFilter.builder().key("TargetType").values(SAMPLE_TARGET_TYPE).build()); + final ListDocumentsRequest expectedRequest = ListDocumentsRequest.builder() + .filters(keyValuesFilters) + .maxResults(MAX_RESULTS) + .nextToken(SAMPLE_REQUEST_TOKEN) + .build(); + + final ListDocumentsRequest request = unitUnderTest.generateListDocumentsRequest(SAMPLE_REQUEST_TOKEN,model); + + Assertions.assertEquals(expectedRequest, request); + } + + private ResourceModel createResourceModel() { return ResourceModel.builder() .name(SAMPLE_DOCUMENT_NAME) diff --git a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/ListHandlerTest.java b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/ListHandlerTest.java index 84eee957..0a7194c2 100644 --- a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/ListHandlerTest.java +++ b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/ListHandlerTest.java @@ -1,42 +1,93 @@ package com.amazonaws.ssm.document; +import com.google.common.collect.ImmutableList; +import software.amazon.awssdk.services.ssm.SsmClient; +import software.amazon.awssdk.services.ssm.model.DocumentIdentifier; +import software.amazon.awssdk.services.ssm.model.ListDocumentsRequest; +import software.amazon.awssdk.services.ssm.model.ListDocumentsResponse; +import software.amazon.awssdk.services.ssm.model.SsmException; +import software.amazon.cloudformation.exceptions.CfnInvalidRequestException; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.Logger; import software.amazon.cloudformation.proxy.OperationStatus; import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.ResourceHandlerRequest; +import org.junit.jupiter.api.Assertions; 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.util.Arrays; +import java.util.Collections; + import static org.assertj.core.api.Assertions.assertThat; + +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; @ExtendWith(MockitoExtension.class) public class ListHandlerTest { - @Mock private AmazonWebServicesClientProxy proxy; @Mock private Logger logger; + @Mock + private DocumentModelTranslator documentModelTranslator; + + @Mock + private DocumentResponseModelTranslator documentResponseModelTranslator; + + @Mock + private DocumentExceptionTranslator exceptionTranslator; + + @Mock + private SsmException ssmException; + + @Mock + private SafeLogger safeLogger; + + @Mock + private SsmClient ssmClient; + + private ListHandler handler; + + private static final String Document_Identifier = "doc:aws::ssm:test"; + private static final String Document_Identifier2 = "doc:aws::ssm:test2"; + private static final String Document_Identifier3 = "doc:aws::ssm:test3"; + private static final String OPERATION_NAME = "AWS::SSM::ListDocuments"; + @BeforeEach public void setup() { - proxy = mock(AmazonWebServicesClientProxy.class); - logger = mock(Logger.class); + documentModelTranslator = DocumentModelTranslator.getInstance(); + handler = new ListHandler(documentModelTranslator, documentResponseModelTranslator, ssmClient, + exceptionTranslator, safeLogger); } - //@Test + @Test public void handleRequest_SimpleSuccess() { - final ListHandler handler = new ListHandler(); + final ListDocumentsResponse listDocumentsResponse = ListDocumentsResponse.builder() + .documentIdentifiers(Arrays.asList( + DocumentIdentifier.builder().name(Document_Identifier).build(), + DocumentIdentifier.builder().name(Document_Identifier2).build())) + .nextToken("token2") + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(ListDocumentsRequest.class), any())).thenReturn(listDocumentsResponse); - final ResourceModel model = ResourceModel.builder().build(); + final ResourceModel model1 = ResourceModel.builder() + .name(Document_Identifier) + .build(); + final ResourceModel model2 = ResourceModel.builder() + .name(Document_Identifier2) + .build(); final ResourceHandlerRequest request = ResourceHandlerRequest.builder() - .desiredResourceState(model) + .nextToken("token") .build(); final ProgressEvent response = @@ -47,8 +98,57 @@ public void handleRequest_SimpleSuccess() { assertThat(response.getCallbackContext()).isNull(); assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); assertThat(response.getResourceModel()).isNull(); - assertThat(response.getResourceModels()).isNotNull(); + assertThat(response.getResourceModels()).containsAll(Arrays.asList(model1, model2)); + assertThat(response.getMessage()).isNull(); + assertThat(response.getErrorCode()).isNull(); + assertThat(response.getNextToken()).isEqualTo("token2"); + } + + @Test + public void handleRequest_EmptyToken_throwsException() { + final ResourceModel model3 = ResourceModel.builder() + .name(Document_Identifier3) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + .nextToken("") //empty string as nextToken should mean empty page + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(ListDocumentsRequest.class), any())).thenThrow(ssmException); + when(exceptionTranslator.getCfnException(ssmException, null, OPERATION_NAME, logger)).thenThrow(CfnInvalidRequestException.class); + + Assertions.assertThrows(CfnInvalidRequestException.class, () -> handler.handleRequest(proxy, request, null, logger)); + } + + @Test + public void handleRequest_NullToken() { + final ListDocumentsResponse listDocumentsResponse = ListDocumentsResponse.builder() + .documentIdentifiers(Arrays.asList( + DocumentIdentifier.builder().name(Document_Identifier3).build())) + .nextToken("token_next") + .build(); + + when(proxy.injectCredentialsAndInvokeV2(any(ListDocumentsRequest.class), any())).thenReturn(listDocumentsResponse); + + final ResourceModel model3 = ResourceModel.builder() + .name(Document_Identifier3) + .build(); + + final ResourceHandlerRequest request = ResourceHandlerRequest.builder() + //No nextToken in Request should mean first page + .build(); + + final ProgressEvent response = + handler.handleRequest(proxy, request, null, logger); + + assertThat(response).isNotNull(); + assertThat(response.getStatus()).isEqualTo(OperationStatus.SUCCESS); + assertThat(response.getCallbackContext()).isNull(); + assertThat(response.getCallbackDelaySeconds()).isEqualTo(0); + assertThat(response.getResourceModel()).isNull(); + assertThat(response.getResourceModels()).contains(model3); assertThat(response.getMessage()).isNull(); assertThat(response.getErrorCode()).isNull(); + assertThat(response.getNextToken()).isEqualTo("token_next"); } } diff --git a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/ReadHandlerTest.java b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/ReadHandlerTest.java index 91519d74..d5402d85 100644 --- a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/ReadHandlerTest.java +++ b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/ReadHandlerTest.java @@ -38,6 +38,8 @@ public class ReadHandlerTest { private static final String SAMPLE_ACCOUNT_ID = "123456"; private static final String SAMPLE_DOCUMENT_NAME = "sampleDocument"; private static final String SAMPLE_TARGET_TYPE = "/AWS::EC2::Volume"; + private static final String SAMPLE_DOCUMENT_TYPE = "Command"; + private static final String CHANGE_CALENDAR_DOCUMENT_TYPE = "ChangeCalendar"; private static final Map SAMPLE_DOCUMENT_CONTENT = ImmutableMap.of( "schemaVersion", "1.2", "description", "Join instances to an AWS Directory Service domain." @@ -55,15 +57,28 @@ public class ReadHandlerTest { private static final GetDocumentRequest SAMPLE_GET_DOCUMENT_REQUEST = GetDocumentRequest.builder() .name(SAMPLE_DOCUMENT_NAME) .build(); - final GetDocumentResponse SAMPLE_GET_DOCUMENT_RESPONSE = GetDocumentResponse.builder() + private static final GetDocumentRequest TEXT_GET_DOCUMENT_REQUEST = GetDocumentRequest.builder() + .name(SAMPLE_DOCUMENT_NAME) + .documentFormat("TEXT") + .build(); + private final GetDocumentResponse SAMPLE_GET_DOCUMENT_RESPONSE = GetDocumentResponse.builder() .name(SAMPLE_DOCUMENT_NAME).status(DocumentStatus.ACTIVE) .build(); private static final DescribeDocumentRequest SAMPLE_DESCRIBE_DOCUMENT_REQUEST = DescribeDocumentRequest.builder() .name(SAMPLE_DOCUMENT_NAME) .build(); - final DescribeDocumentResponse SAMPLE_DESCRIBE_DOCUMENT_RESPONSE = DescribeDocumentResponse.builder() + private final DescribeDocumentResponse SAMPLE_DESCRIBE_DOCUMENT_RESPONSE = DescribeDocumentResponse.builder() .document(DocumentDescription.builder() .name(SAMPLE_DOCUMENT_NAME) + .documentType(SAMPLE_DOCUMENT_TYPE) + .targetType(SAMPLE_TARGET_TYPE) + .status(DocumentStatus.ACTIVE) + .build()) + .build(); + final DescribeDocumentResponse CHANGE_CALENDAR_DESCRIBE_DOCUMENT_RESPONSE = DescribeDocumentResponse.builder() + .document(DocumentDescription.builder() + .name(SAMPLE_DOCUMENT_NAME) + .documentType(CHANGE_CALENDAR_DOCUMENT_TYPE) .targetType(SAMPLE_TARGET_TYPE) .status(DocumentStatus.ACTIVE) .build()) @@ -74,6 +89,13 @@ public class ReadHandlerTest { private static final SsmException ACCESS_DENIED_EXCEPTION = (SsmException) SsmException.builder() .awsErrorDetails(AwsErrorDetails.builder() .errorCode("AccessDeniedException") + .errorMessage("AccessDeniedMessage") + .build()) + .build(); + private static final SsmException INVALID_DOCUMENT_FORMAT_JSON_EXCEPTION = (SsmException) SsmException.builder() + .awsErrorDetails(AwsErrorDetails.builder() + .errorCode("ValidationException") + .errorMessage("Invalid document format JSON") .build()) .build(); @@ -182,7 +204,12 @@ public void testHandleRequest_DescribeDocumentAccessDeniedSoftFail_verifyResult( @Test public void testHandleRequest_GetDocumentThrowsSsmException_verifyExceptionReturned() { when(documentModelTranslator.generateGetDocumentRequest(SAMPLE_RESOURCE_MODEL)).thenReturn(SAMPLE_GET_DOCUMENT_REQUEST); + when(documentModelTranslator.generateDescribeDocumentRequest(SAMPLE_RESOURCE_MODEL)).thenReturn(SAMPLE_DESCRIBE_DOCUMENT_REQUEST); + when(proxy.injectCredentialsAndInvokeV2(eq(SAMPLE_DESCRIBE_DOCUMENT_REQUEST), any())).thenReturn(SAMPLE_DESCRIBE_DOCUMENT_RESPONSE); when(proxy.injectCredentialsAndInvokeV2(eq(SAMPLE_GET_DOCUMENT_REQUEST), any())).thenThrow(ssmException); + when(ssmException.awsErrorDetails()).thenReturn(awsErrorDetails); + when(awsErrorDetails.errorMessage()).thenReturn("Some SsmException"); + when(exceptionTranslator.getCfnException(ssmException, SAMPLE_DOCUMENT_NAME, OPERATION_NAME, logger)).thenReturn(cfnException); Assertions.assertThrows(CfnGeneralServiceException.class, () -> unitUnderTest.handleRequest(proxy, SAMPLE_RESOURCE_HANDLER_REQUEST, null, logger)); @@ -191,9 +218,7 @@ public void testHandleRequest_GetDocumentThrowsSsmException_verifyExceptionRetur @Test public void testHandleRequest_DescribeThrowsSsmException_verifyExceptionReturned() { - when(documentModelTranslator.generateGetDocumentRequest(SAMPLE_RESOURCE_MODEL)).thenReturn(SAMPLE_GET_DOCUMENT_REQUEST); when(documentModelTranslator.generateDescribeDocumentRequest(SAMPLE_RESOURCE_MODEL)).thenReturn(SAMPLE_DESCRIBE_DOCUMENT_REQUEST); - when(proxy.injectCredentialsAndInvokeV2(eq(SAMPLE_GET_DOCUMENT_REQUEST), any())).thenReturn(SAMPLE_GET_DOCUMENT_RESPONSE); when(proxy.injectCredentialsAndInvokeV2(eq(SAMPLE_DESCRIBE_DOCUMENT_REQUEST), any())).thenThrow(ssmException); when(ssmException.awsErrorDetails()).thenReturn(awsErrorDetails); when(exceptionTranslator.getCfnException(ssmException, SAMPLE_DOCUMENT_NAME, OPERATION_NAME, logger)).thenReturn(cfnException); @@ -201,4 +226,66 @@ public void testHandleRequest_DescribeThrowsSsmException_verifyExceptionReturned Assertions.assertThrows(CfnGeneralServiceException.class, () -> unitUnderTest.handleRequest(proxy, SAMPLE_RESOURCE_HANDLER_REQUEST, null, logger)); verify(safeLogger).safeLogDocumentInformation(SAMPLE_RESOURCE_MODEL, null, SAMPLE_ACCOUNT_ID, SAMPLE_SYSTEM_TAGS, logger); } + + @Test + public void testHandleRequest_DescribeReturnsChangeCalenarDocument_verifyResult() { + final ResourceModel expectedModel = ResourceModel.builder() + .name(SAMPLE_DOCUMENT_NAME) + .content(SAMPLE_DOCUMENT_CONTENT) + .targetType(SAMPLE_TARGET_TYPE).build(); + final ResourceInformation expectedResourceInformation = ResourceInformation.builder().resourceModel(expectedModel) + .status(SAMPLE_RESOURCE_STATE) + .statusInformation(SAMPLE_STATUS_INFO) + .build(); + final ProgressEvent expectedResponse = ProgressEvent.builder() + .resourceModel(expectedModel) + .status(OperationStatus.SUCCESS) + .build(); + + when(documentModelTranslator.generateGetDocumentRequest(SAMPLE_RESOURCE_MODEL)).thenReturn(SAMPLE_GET_DOCUMENT_REQUEST); + when(documentModelTranslator.generateDescribeDocumentRequest(SAMPLE_RESOURCE_MODEL)).thenReturn(SAMPLE_DESCRIBE_DOCUMENT_REQUEST); + when(proxy.injectCredentialsAndInvokeV2(eq(TEXT_GET_DOCUMENT_REQUEST), any())).thenReturn(SAMPLE_GET_DOCUMENT_RESPONSE); + when(proxy.injectCredentialsAndInvokeV2(eq(SAMPLE_DESCRIBE_DOCUMENT_REQUEST), any())).thenReturn(CHANGE_CALENDAR_DESCRIBE_DOCUMENT_RESPONSE); + when(tagReader.getDocumentTags(SAMPLE_DOCUMENT_NAME, ssmClient, proxy)).thenReturn(SAMPLE_TAG_MAP); + when(documentResponseModelTranslator.generateResourceInformation(SAMPLE_GET_DOCUMENT_RESPONSE, SAMPLE_TAG_MAP)) + .thenReturn(expectedResourceInformation); + + final ProgressEvent response + = unitUnderTest.handleRequest(proxy, SAMPLE_RESOURCE_HANDLER_REQUEST, null, logger); + + Assertions.assertEquals(expectedResponse, response); + verify(safeLogger).safeLogDocumentInformation(SAMPLE_RESOURCE_MODEL, null, SAMPLE_ACCOUNT_ID, SAMPLE_SYSTEM_TAGS, logger); + } + + @Test + public void testHandleRequest_DescribeDocumentAccessDeniedForChangeCalendarDocumentType_verifyResult() { + final ResourceModel expectedModel = ResourceModel.builder() + .name(SAMPLE_DOCUMENT_NAME) + .content(SAMPLE_DOCUMENT_CONTENT) + .targetType(SAMPLE_TARGET_TYPE).build(); + final ResourceInformation expectedResourceInformation = ResourceInformation.builder().resourceModel(expectedModel) + .status(SAMPLE_RESOURCE_STATE) + .statusInformation(SAMPLE_STATUS_INFO) + .build(); + final ProgressEvent expectedResponse = ProgressEvent.builder() + .resourceModel(expectedModel) + .status(OperationStatus.SUCCESS) + .build(); + + when(documentModelTranslator.generateGetDocumentRequest(SAMPLE_RESOURCE_MODEL)).thenReturn(SAMPLE_GET_DOCUMENT_REQUEST); + when(documentModelTranslator.generateDescribeDocumentRequest(SAMPLE_RESOURCE_MODEL)).thenReturn(SAMPLE_DESCRIBE_DOCUMENT_REQUEST); + when(proxy.injectCredentialsAndInvokeV2(eq(SAMPLE_DESCRIBE_DOCUMENT_REQUEST), any())).thenThrow(ACCESS_DENIED_EXCEPTION); + when(proxy.injectCredentialsAndInvokeV2(eq(SAMPLE_GET_DOCUMENT_REQUEST), any())).thenThrow(INVALID_DOCUMENT_FORMAT_JSON_EXCEPTION); + when(proxy.injectCredentialsAndInvokeV2(eq(TEXT_GET_DOCUMENT_REQUEST), any())).thenReturn(SAMPLE_GET_DOCUMENT_RESPONSE); + + when(tagReader.getDocumentTags(SAMPLE_DOCUMENT_NAME, ssmClient, proxy)).thenReturn(SAMPLE_TAG_MAP); + when(documentResponseModelTranslator.generateResourceInformation(SAMPLE_GET_DOCUMENT_RESPONSE, SAMPLE_TAG_MAP)) + .thenReturn(expectedResourceInformation); + + final ProgressEvent response + = unitUnderTest.handleRequest(proxy, SAMPLE_RESOURCE_HANDLER_REQUEST, null, logger); + + Assertions.assertEquals(expectedResponse, response); + verify(safeLogger).safeLogDocumentInformation(SAMPLE_RESOURCE_MODEL, null, SAMPLE_ACCOUNT_ID, SAMPLE_SYSTEM_TAGS, logger); + } } diff --git a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/UpdateHandlerTest.java b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/UpdateHandlerTest.java index 78bdd617..7aad690e 100644 --- a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/UpdateHandlerTest.java +++ b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/UpdateHandlerTest.java @@ -686,8 +686,8 @@ public void testHandleRequest_withTrueUpdate_StabilizationRetrieverThrowsExcepti .build(); when(progressUpdater.getEventProgress(expectedModel, inProgressCallbackContext, ssmClient, proxy, logger)) - .thenThrow(ssmException); - when(exceptionTranslator.getCfnException(ssmException, SAMPLE_DOCUMENT_NAME, OPERATION_NAME, logger)).thenThrow(CfnGeneralServiceException.class); + .thenThrow(SsmException.class); + when(exceptionTranslator.getCfnException(any(SsmException.class), eq(SAMPLE_DOCUMENT_NAME), eq(OPERATION_NAME), eq(logger))).thenThrow(CfnGeneralServiceException.class); final ResourceHandlerRequest request = buildRequest(previousModel, expectedModel); Assertions.assertThrows(CfnGeneralServiceException.class, () -> unitUnderTest.handleRequest(proxy, request, inProgressCallbackContext, logger)); diff --git a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/tags/TagUpdaterTest.java b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/tags/TagUpdaterTest.java index ed6500e0..3fc0982a 100644 --- a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/tags/TagUpdaterTest.java +++ b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/tags/TagUpdaterTest.java @@ -20,6 +20,8 @@ import java.util.List; import java.util.Map; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; import static com.amazonaws.ssm.document.tags.TagUtil.TAGGING_PERMISSION_MESSAGE_FORMAT; @ExtendWith(MockitoExtension.class) @@ -186,8 +188,9 @@ public void testUpdateTags_addTagsAPIAccessDenied_shouldHardFail_VerifyException Mockito.doThrow(ssmException).when(tagClient).addTags(expectedTagsToAdd, SAMPLE_DOCUMENT_NAME, ssmClient, proxy); Mockito.when(tagUtil.isResourceTagModified(SAMPLE_PREVIOUS_MODEL_TAGS, SAMPLE_MODEL_TAGS)).thenReturn(true); + Mockito.doNothing().when(logger).log(any()); - Assertions.assertThrows(CfnUnauthorizedTaggingOperationException.class, () -> unitUnderTest.updateTags(SAMPLE_DOCUMENT_NAME, SAMPLE_EXISTING_RESOURCE_REQUEST_TAGS, SAMPLE_RESOURCE_REQUEST_TAGS, + Assertions.assertThrows(SsmException.class, () -> unitUnderTest.updateTags(SAMPLE_DOCUMENT_NAME, SAMPLE_EXISTING_RESOURCE_REQUEST_TAGS, SAMPLE_RESOURCE_REQUEST_TAGS, SAMPLE_PREVIOUS_MODEL_TAGS, SAMPLE_MODEL_TAGS, ssmClient, proxy, logger)); verifyTagClientCalls(expectedTagsToAdd, expectedTagsToRemove); @@ -211,8 +214,9 @@ public void testUpdateTags_removeTagsAPIAccessDenied_shouldHardFail_VerifyExcept Mockito.doThrow(ssmException).when(tagClient).removeTags(expectedTagsToRemove, SAMPLE_DOCUMENT_NAME, ssmClient, proxy); Mockito.when(tagUtil.isResourceTagModified(SAMPLE_PREVIOUS_MODEL_TAGS, SAMPLE_MODEL_TAGS)).thenReturn(true); + Mockito.doNothing().when(logger).log(any()); - Assertions.assertThrows(CfnUnauthorizedTaggingOperationException.class, () -> unitUnderTest.updateTags(SAMPLE_DOCUMENT_NAME, SAMPLE_EXISTING_RESOURCE_REQUEST_TAGS, SAMPLE_RESOURCE_REQUEST_TAGS, + Assertions.assertThrows(SsmException.class, () -> unitUnderTest.updateTags(SAMPLE_DOCUMENT_NAME, SAMPLE_EXISTING_RESOURCE_REQUEST_TAGS, SAMPLE_RESOURCE_REQUEST_TAGS, SAMPLE_PREVIOUS_MODEL_TAGS, SAMPLE_MODEL_TAGS, ssmClient, proxy, logger)); Mockito.verify(tagClient, Mockito.times(1)).removeTags(expectedTagsToRemove, SAMPLE_DOCUMENT_NAME, ssmClient, proxy); diff --git a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/tags/TagUtilTest.java b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/tags/TagUtilTest.java index 87138d1e..3bf2422e 100644 --- a/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/tags/TagUtilTest.java +++ b/aws-ssm-document/src/test/java/com/amazonaws/ssm/document/tags/TagUtilTest.java @@ -111,7 +111,6 @@ public void testIsTaggingPermissionFailure_AccessDeniedException_CauseNotFromTag .build()); Assertions.assertFalse(unitUnderTest.isTaggingPermissionFailure(ssmException)); - } @Test diff --git a/aws-ssm-document/template.yml b/aws-ssm-document/template.yml index 9777e4a5..79575735 100644 --- a/aws-ssm-document/template.yml +++ b/aws-ssm-document/template.yml @@ -11,12 +11,14 @@ Resources: Type: AWS::Serverless::Function Properties: Handler: com.amazonaws.ssm.document.HandlerWrapper::handleRequest - Runtime: java8 - CodeUri: ./target/aws-ssm-document-handler-1.0-SNAPSHOT.jar + Runtime: java11 + CodeUri: ./target/aws-ssm-document-1.0.jar + MemorySize: 256 # to prevent OutOfMemory exception when testing TestEntrypoint: Type: AWS::Serverless::Function Properties: Handler: com.amazonaws.ssm.document.HandlerWrapper::testEntrypoint - Runtime: java8 - CodeUri: ./target/aws-ssm-document-handler-1.0-SNAPSHOT.jar + Runtime: java11 + CodeUri: ./target/aws-ssm-document-1.0.jar + MemorySize: 256 # to prevent OutOfMemory exception when testing diff --git a/aws-ssm-maintenancewindow/pom.xml b/aws-ssm-maintenancewindow/pom.xml index 80ae2d63..eb81df03 100644 --- a/aws-ssm-maintenancewindow/pom.xml +++ b/aws-ssm-maintenancewindow/pom.xml @@ -28,7 +28,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0, 2.1.0) diff --git a/aws-ssm-maintenancewindowtarget/pom.xml b/aws-ssm-maintenancewindowtarget/pom.xml index da2ebc23..5dbcf9d7 100644 --- a/aws-ssm-maintenancewindowtarget/pom.xml +++ b/aws-ssm-maintenancewindowtarget/pom.xml @@ -29,7 +29,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0, 2.1.0) diff --git a/aws-ssm-maintenancewindowtask/pom.xml b/aws-ssm-maintenancewindowtask/pom.xml index 32bc2cdd..1085385a 100644 --- a/aws-ssm-maintenancewindowtask/pom.xml +++ b/aws-ssm-maintenancewindowtask/pom.xml @@ -29,7 +29,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0, 2.1.0) diff --git a/aws-ssm-opsmetadata/pom.xml b/aws-ssm-opsmetadata/pom.xml index 84db1d7a..240008c3 100644 --- a/aws-ssm-opsmetadata/pom.xml +++ b/aws-ssm-opsmetadata/pom.xml @@ -41,7 +41,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0,3.0.0) + [2.0.0, 2.1.0) diff --git a/aws-ssm-parameter/pom.xml b/aws-ssm-parameter/pom.xml index d3f072e4..8ec3adc6 100644 --- a/aws-ssm-parameter/pom.xml +++ b/aws-ssm-parameter/pom.xml @@ -41,7 +41,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0, 2.1.0) diff --git a/aws-ssm-patchbaseline/pom.xml b/aws-ssm-patchbaseline/pom.xml index 267e3d17..33e066a4 100644 --- a/aws-ssm-patchbaseline/pom.xml +++ b/aws-ssm-patchbaseline/pom.xml @@ -29,7 +29,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0, 2.1.0) diff --git a/aws-ssm-resourcedatasync/pom.xml b/aws-ssm-resourcedatasync/pom.xml index 58875cf0..237904e0 100644 --- a/aws-ssm-resourcedatasync/pom.xml +++ b/aws-ssm-resourcedatasync/pom.xml @@ -29,7 +29,7 @@ software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - [2.0.0, 3.0.0) + [2.0.0, 2.1.0)