Skip to content

Onboarding and fixing new contract tests for AWS::SSM::Document #215

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion aws-ssm-association/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
<dependency>
<groupId>software.amazon.cloudformation</groupId>
<artifactId>aws-cloudformation-rpdk-java-plugin</artifactId>
<version>[2.0.0, 3.0.0)</version>
<version>[2.0.0, 2.1.0)</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
Expand Down
2 changes: 1 addition & 1 deletion aws-ssm-document/.rpdk-config
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
21 changes: 18 additions & 3 deletions aws-ssm-document/aws-ssm-document.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand All @@ -219,7 +233,8 @@
},
"delete": {
"permissions": [
"ssm:DeleteDocument"
"ssm:DeleteDocument",
"ssm:GetDocument"
]
},
"list": {
Expand Down
2 changes: 1 addition & 1 deletion aws-ssm-document/contract-tests-artifacts/inputs_1.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion aws-ssm-document/contract-tests-artifacts/inputs_2.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion aws-ssm-document/contract-tests-artifacts/inputs_3.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"CreateInputs": {
"Name": "appConfigTest1",
"Name": "appConfigTest1-{{ uuid }}",
"Content": "{\"whitelist\":[{\"name\":\"user1\",\"email\":\"[email protected]\",\"location\":\"virginia\"},{\"name\":\"user2\",\"email\":\"{VersionName}@email.com\",\"location\":\"washington\"}]}",
"DocumentType": "ApplicationConfiguration",
"DocumentFormat": "JSON",
Expand Down
3 changes: 1 addition & 2 deletions aws-ssm-document/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@
<dependency>
<groupId>software.amazon.cloudformation</groupId>
<artifactId>aws-cloudformation-rpdk-java-plugin</artifactId>
<version>[2.0.0, 3.0.0)</version>
<version>[2.0.0, 2.1.0)</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.projectlombok/lombok -->
<dependency>
Expand Down Expand Up @@ -99,7 +99,6 @@
<configuration>
<compilerArgs>
<arg>-Xlint:all,-options,-processing</arg>
<arg>-Werror</arg>
</compilerArgs>
</configuration>
</plugin>
Expand Down
7 changes: 7 additions & 0 deletions aws-ssm-document/resource-role.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -132,12 +132,35 @@ DeleteDocumentRequest generateDeleteDocumentRequest(@NonNull final ResourceModel
.build();
}

ListDocumentsRequest generateListDocumentsRequest() {
List<DocumentKeyValuesFilter> keyValuesFilters =
ImmutableList.of(DocumentKeyValuesFilter.builder().key("owner").values("self").build());
static ListDocumentsRequest generateListDocumentsRequest(@Nullable final String nextToken, @Nullable final ResourceModel model) {
final ImmutableList.Builder<DocumentKeyValuesFilter> 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<DocumentKeyValuesFilter> filters = filterListBuilder.build();

return ListDocumentsRequest.builder()
.filters(keyValuesFilters)
.filters(filters.isEmpty() ? null : filters)
.maxResults(50)
.nextToken(nextToken)
.build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -90,15 +90,6 @@ private List<Tag> translateToResourceModelTags(final Map<String, String> tagMap)
.collect(Collectors.toList());
}

private List<Tag> translateDocumentTagsToResourceModelTags(final List<software.amazon.awssdk.services.ssm.model.Tag> tags) {
return tags.stream().map(
tag -> Tag.builder()
.key(tag.key())
.value(tag.value())
.build())
.collect(Collectors.toList());
}

private List<DocumentRequires> translateRequires(final GetDocumentResponse response) {
if (!response.hasRequires()) {
return null;
Expand Down Expand Up @@ -128,4 +119,13 @@ private List<DocumentRequires> translateRequires(final DescribeDocumentResponse
.build())
.collect(Collectors.toList());
}

private List<Tag> translateTags(final List<software.amazon.awssdk.services.ssm.model.Tag> tags) {
return tags.stream().map(
tag -> Tag.builder()
.key(tag.key())
.value(tag.value())
.build())
.collect(Collectors.toList());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<CallbackContext> {
Expand All @@ -26,25 +28,47 @@ public class ListHandler extends BaseHandler<CallbackContext> {
@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<ResourceModel, CallbackContext> handleRequest(
final AmazonWebServicesClientProxy proxy,
final ResourceHandlerRequest<ResourceModel> request,
final CallbackContext callbackContext,
final Logger logger) {
final AmazonWebServicesClientProxy proxy,
final ResourceHandlerRequest<ResourceModel> request,
final CallbackContext callbackContext,
final Logger logger) {
final ResourceModel model = request.getDesiredResourceState();

safeLogger.safeLogDocumentInformation(model, callbackContext, request.getAwsAccountId(), request.getSystemTags(), logger);

final List<ResourceModel> 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<ResourceModel> models = listDocumentsResponse
.documentIdentifiers()
.stream().map(documentIdentifier -> ResourceModel.builder().name(documentIdentifier.name()).build()).collect(Collectors.toList());

return ProgressEvent.<ResourceModel, CallbackContext>builder()
.resourceModels(models)
.status(OperationStatus.SUCCESS)
.build();
return ProgressEvent.<ResourceModel, CallbackContext>builder()
.resourceModels(models)
.status(OperationStatus.SUCCESS)
.nextToken(listDocumentsResponse.nextToken())
.build();
} catch (final SsmException e) {
throw exceptionTranslator.getCfnException(e, null, "AWS::SSM::ListDocuments", logger);
}
}


}
Loading