diff --git a/pom.xml b/pom.xml index 7b20ea8b..53155435 100644 --- a/pom.xml +++ b/pom.xml @@ -3,7 +3,7 @@ 4.0.0 software.amazon.cloudformation aws-cloudformation-rpdk-java-plugin - 2.1.1 + 2.2.1 AWS CloudFormation RPDK Java Plugin The CloudFormation Resource Provider Development Kit (RPDK) allows you to author your own resource providers that can be used by CloudFormation. This plugin library helps to provide runtime bindings for the execution of your providers by CloudFormation. @@ -491,7 +491,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + 1.6.8 true sonatype-nexus-staging diff --git a/python/rpdk/java/__init__.py b/python/rpdk/java/__init__.py index 38a89be0..4f2d8c09 100644 --- a/python/rpdk/java/__init__.py +++ b/python/rpdk/java/__init__.py @@ -1,5 +1,5 @@ import logging -__version__ = "2.1.1" +__version__ = "2.2.1" logging.getLogger(__name__).addHandler(logging.NullHandler()) diff --git a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java index cbe7b244..070cbb2e 100644 --- a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java +++ b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java @@ -17,12 +17,18 @@ import com.amazonaws.AmazonServiceException; import com.amazonaws.retry.RetryUtils; import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.annotations.VisibleForTesting; +import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.net.URISyntaxException; +import java.net.URL; import java.nio.charset.StandardCharsets; import java.time.Instant; +import java.util.Collections; import java.util.Date; +import java.util.Map; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -31,10 +37,15 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; import software.amazon.awssdk.awscore.exception.AwsServiceException; +import software.amazon.awssdk.http.HttpExecuteRequest; +import software.amazon.awssdk.http.HttpExecuteResponse; import software.amazon.awssdk.http.HttpStatusCode; import software.amazon.awssdk.http.HttpStatusFamily; import software.amazon.awssdk.http.SdkHttpClient; +import software.amazon.awssdk.http.SdkHttpMethod; +import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.apache.ApacheHttpClient; +import software.amazon.awssdk.utils.IoUtils; import software.amazon.cloudformation.encryption.Cipher; import software.amazon.cloudformation.encryption.KMSCipher; import software.amazon.cloudformation.exceptions.BaseHandlerException; @@ -63,6 +74,7 @@ import software.amazon.cloudformation.proxy.hook.HookInvocationRequest; import software.amazon.cloudformation.proxy.hook.HookProgressEvent; import software.amazon.cloudformation.proxy.hook.HookRequestContext; +import software.amazon.cloudformation.proxy.hook.HookRequestData; import software.amazon.cloudformation.proxy.hook.HookStatus; import software.amazon.cloudformation.resource.SchemaValidator; import software.amazon.cloudformation.resource.Serializer; @@ -89,6 +101,9 @@ public abstract class HookAbstractWrapper { final SchemaValidator validator; final TypeReference> typeReference; + final TypeReference> hookStackPayloadS3TypeReference = new TypeReference<>() { + }; + private MetricsPublisher providerMetricsPublisher; private CloudWatchLogHelper cloudWatchLogHelper; @@ -222,11 +237,7 @@ private ProgressEvent processInvocation(final JSONObject raw assert request != null : "Invalid request object received. Request object is null"; - if (request.getRequestData() == null || request.getRequestData().getTargetModel() == null) { - throw new TerminalException("Invalid request object received. Target Model can not be null."); - } - - // TODO: Include hook schema validation here after schema is finalized + boolean isPayloadRemote = isHookInvocationPayloadRemote(request.getRequestData()); try { // initialise dependencies with platform credentials @@ -234,6 +245,12 @@ private ProgressEvent processInvocation(final JSONObject raw request.getRequestData().getProviderLogGroupName(), request.getAwsAccountId(), request.getRequestData().getHookEncryptionKeyArn(), request.getRequestData().getHookEncryptionKeyRole()); + if (isPayloadRemote) { + Map targetModelData = retrieveHookInvocationPayloadFromS3(request.getRequestData().getPayload()); + + request.getRequestData().setTargetModel(targetModelData); + } + // transform the request object to pass to caller HookHandlerRequest hookHandlerRequest = transform(request); ConfigurationT typeConfiguration = request.getHookModel(); @@ -366,6 +383,50 @@ private void writeResponse(final OutputStream outputStream, final HookProgressEv outputStream.flush(); } + public Map retrieveHookInvocationPayloadFromS3(final String s3PresignedUrl) { + if (s3PresignedUrl != null) { + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + + try { + URL presignedUrl = new URL(s3PresignedUrl); + SdkHttpRequest httpRequest = SdkHttpRequest.builder().method(SdkHttpMethod.GET).uri(presignedUrl.toURI()).build(); + + HttpExecuteRequest executeRequest = HttpExecuteRequest.builder().request(httpRequest).build(); + + HttpExecuteResponse response = HTTP_CLIENT.prepareRequest(executeRequest).call(); + + response.responseBody().ifPresentOrElse(abortableInputStream -> { + try { + IoUtils.copy(abortableInputStream, byteArrayOutputStream); + } catch (IOException e) { + throw new RuntimeException(e); + } + }, () -> loggerProxy.log("Hook invocation payload is empty.")); + + String str = byteArrayOutputStream.toString(StandardCharsets.UTF_8); + + return this.serializer.deserialize(str, hookStackPayloadS3TypeReference); + } catch (RuntimeException | IOException | URISyntaxException exp) { + loggerProxy.log("Failed to retrieve hook invocation payload" + exp.toString()); + } + } + return Collections.emptyMap(); + } + + @VisibleForTesting + protected boolean isHookInvocationPayloadRemote(HookRequestData hookRequestData) { + if (hookRequestData == null) { + throw new TerminalException("Invalid request object received. Target Model can not be null."); + } + + if ((hookRequestData.getTargetModel() == null || hookRequestData.getTargetModel().isEmpty()) + && hookRequestData.getPayload() == null) { + throw new TerminalException("No payload data set."); + } + + return (hookRequestData.getTargetModel() == null || hookRequestData.getTargetModel().isEmpty()); + } + /** * Transforms the incoming request to the subset of typed models which the * handler implementor needs diff --git a/src/main/java/software/amazon/cloudformation/proxy/OperationStatus.java b/src/main/java/software/amazon/cloudformation/proxy/OperationStatus.java index 0031be3a..bf2943ed 100644 --- a/src/main/java/software/amazon/cloudformation/proxy/OperationStatus.java +++ b/src/main/java/software/amazon/cloudformation/proxy/OperationStatus.java @@ -18,5 +18,6 @@ public enum OperationStatus { PENDING, IN_PROGRESS, SUCCESS, + CHANGE_SET_SUCCESS_SKIP_STACK_HOOK, FAILED } diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestData.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestData.java index 53541480..952ab885 100644 --- a/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestData.java +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookRequestData.java @@ -29,6 +29,7 @@ public class HookRequestData { private String targetType; private String targetLogicalId; private Map targetModel; + private String payload; private String callerCredentials; private String providerCredentials; private String providerLogGroupName; diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/HookStatus.java b/src/main/java/software/amazon/cloudformation/proxy/hook/HookStatus.java index 1fd22521..01cc71d0 100644 --- a/src/main/java/software/amazon/cloudformation/proxy/hook/HookStatus.java +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/HookStatus.java @@ -18,5 +18,6 @@ public enum HookStatus { PENDING, IN_PROGRESS, SUCCESS, + CHANGE_SET_SUCCESS_SKIP_STACK_HOOK, FAILED } diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/ChangedResource.java b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/ChangedResource.java new file mode 100644 index 00000000..e26dca05 --- /dev/null +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/ChangedResource.java @@ -0,0 +1,45 @@ +/* +* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0 +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ +package software.amazon.cloudformation.proxy.hook.targetmodel; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@AllArgsConstructor +@NoArgsConstructor +public class ChangedResource { + @JsonProperty("LogicalResourceId") + private String logicalResourceId; + + @JsonProperty("ResourceType") + private String resourceType; + + @JsonProperty("LineNumber") + private Integer lineNumber; + + @JsonProperty("Action") + private String action; + + @JsonProperty("ResourceProperties") + private String resourceProperties; + + @JsonProperty("PreviousResourceProperties") + private String previousResourceProperties; +} diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetType.java b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetType.java index 73a96d25..ffbf84fe 100644 --- a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetType.java +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetType.java @@ -26,5 +26,17 @@ public enum HookTargetType { * A target model meant to represent a target for a Resource Hook. This model * type will have properties specific to the resource type. */ - RESOURCE; + RESOURCE, + + /** + * A target model meant to represent a target for a Stack Hook. This model type + * will have properties specific to the stack type. + */ + STACK, + + /** + * A target model meant to represent a target for a stack Change Set Hook. This + * model type will have properties specific to the change set type. + */ + CHANGE_SET; } diff --git a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/StackHookTargetModel.java b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/StackHookTargetModel.java new file mode 100644 index 00000000..08cb9e83 --- /dev/null +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/StackHookTargetModel.java @@ -0,0 +1,64 @@ +/* +* Copyright 2010-2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0 +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ +package software.amazon.cloudformation.proxy.hook.targetmodel; + +import com.fasterxml.jackson.annotation.JsonAutoDetect; +import com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import java.util.List; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@EqualsAndHashCode(callSuper = false) +@Getter +@NoArgsConstructor +@ToString +@JsonAutoDetect(fieldVisibility = Visibility.ANY, getterVisibility = Visibility.NONE, setterVisibility = Visibility.NONE) +@JsonDeserialize(as = StackHookTargetModel.class) +public class StackHookTargetModel extends HookTargetModel { + private static final TypeReference MODEL_REFERENCE = new TypeReference() { + }; + + @JsonProperty("Template") + private Object template; + + @JsonProperty("PreviousTemplate") + private Object previousTemplate; + + @JsonProperty("ResolvedTemplate") + private Object resolvedTemplate; + + @JsonProperty("ChangedResources") + private List changedResources; + + @Override + public TypeReference getHookTargetTypeReference() { + return null; + } + + @Override + public TypeReference getTargetModelTypeReference() { + return MODEL_REFERENCE; + } + + @Override + public final HookTargetType getHookTargetType() { + return HookTargetType.STACK; + } +} diff --git a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java index bdeff2be..a3bddb83 100644 --- a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java +++ b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java @@ -19,6 +19,7 @@ import java.nio.charset.StandardCharsets; import java.util.LinkedList; import java.util.List; +import java.util.Map; import java.util.Queue; import lombok.Data; import lombok.EqualsAndHashCode; @@ -32,8 +33,10 @@ import software.amazon.cloudformation.metrics.MetricsPublisher; import software.amazon.cloudformation.proxy.AmazonWebServicesClientProxy; import software.amazon.cloudformation.proxy.ProgressEvent; +import software.amazon.cloudformation.proxy.hook.HookContext; import software.amazon.cloudformation.proxy.hook.HookHandlerRequest; import software.amazon.cloudformation.proxy.hook.HookInvocationRequest; +import software.amazon.cloudformation.proxy.hook.targetmodel.HookTargetModel; import software.amazon.cloudformation.resource.SchemaValidator; import software.amazon.cloudformation.resource.Serializer; @@ -44,6 +47,8 @@ @EqualsAndHashCode(callSuper = true) public class HookLambdaWrapperOverride extends HookLambdaWrapper { + private Map hookInvocationPayloadFromS3; + /** * This .ctor provided for testing */ @@ -112,11 +117,29 @@ public void enqueueResponses(final List> r @Override protected HookHandlerRequest transform(final HookInvocationRequest request) { - return transformResponse; + this.request = HookHandlerRequest.builder().clientRequestToken(request.getClientRequestToken()) + .hookContext(HookContext.builder().awsAccountId(request.getAwsAccountId()).stackId(request.getStackId()) + .changeSetId(request.getChangeSetId()).hookTypeName(request.getHookTypeName()) + .hookTypeVersion(request.getHookTypeVersion()).invocationPoint(request.getActionInvocationPoint()) + .targetName(request.getRequestData().getTargetName()).targetType(request.getRequestData().getTargetType()) + .targetLogicalId(request.getRequestData().getTargetLogicalId()) + .targetModel(HookTargetModel.of(request.getRequestData().getTargetModel())).build()) + .build(); + + return this.request; } public HookHandlerRequest transformResponse; + @Override + public Map retrieveHookInvocationPayloadFromS3(final String s3PresignedUrl) { + return hookInvocationPayloadFromS3; + } + + public void setHookInvocationPayloadFromS3(Map input) { + hookInvocationPayloadFromS3 = input; + } + @Override protected TypeReference> getTypeReference() { return new TypeReference>() { diff --git a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java index 729f7587..f7315e5e 100644 --- a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java +++ b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java @@ -23,6 +23,8 @@ import com.amazonaws.services.lambda.runtime.Context; import com.amazonaws.services.lambda.runtime.LambdaLogger; import com.fasterxml.jackson.core.type.TypeReference; +import com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; @@ -30,7 +32,12 @@ import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +import java.util.Collections; +import java.util.List; +import java.util.Map; +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.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.CsvSource; @@ -38,6 +45,7 @@ import org.mockito.junit.jupiter.MockitoExtension; import software.amazon.awssdk.http.SdkHttpClient; import software.amazon.cloudformation.encryption.KMSCipher; +import software.amazon.cloudformation.exceptions.TerminalException; import software.amazon.cloudformation.injection.CredentialsProvider; import software.amazon.cloudformation.loggers.CloudWatchLogPublisher; import software.amazon.cloudformation.loggers.LogPublisher; @@ -48,7 +56,10 @@ import software.amazon.cloudformation.proxy.ProgressEvent; import software.amazon.cloudformation.proxy.hook.HookHandlerRequest; import software.amazon.cloudformation.proxy.hook.HookProgressEvent; +import software.amazon.cloudformation.proxy.hook.HookRequestData; import software.amazon.cloudformation.proxy.hook.HookStatus; +import software.amazon.cloudformation.proxy.hook.targetmodel.ChangedResource; +import software.amazon.cloudformation.proxy.hook.targetmodel.StackHookTargetModel; import software.amazon.cloudformation.resource.SchemaValidator; import software.amazon.cloudformation.resource.Serializer; @@ -168,7 +179,6 @@ public void invokeHandler_CompleteSynchronously_returnsSuccess(final String requ // assert handler receives correct injections assertThat(wrapper.awsClientProxy).isNotNull(); - assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest); assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint); assertThat(wrapper.callbackContext).isNull(); } @@ -205,7 +215,6 @@ public void invokeHandler_WithResourceProperties_returnsSuccess(final String req // assert handler receives correct injections assertThat(wrapper.awsClientProxy).isNotNull(); - assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest); assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint); assertThat(wrapper.callbackContext).isNull(); } @@ -242,7 +251,6 @@ public void invokeHandler_WithResourcePropertiesAndExtraneousFields_returnsSucce // assert handler receives correct injections assertThat(wrapper.awsClientProxy).isNotNull(); - assertThat(wrapper.getRequest()).isEqualTo(hookHandlerRequest); assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint); assertThat(wrapper.callbackContext).isNull(); } @@ -279,7 +287,6 @@ public void invokeHandler_StrictDeserializer_WithResourceProperties_returnsSucce // assert handler receives correct injections assertThat(wrapperStrictDeserialize.awsClientProxy).isNotNull(); - assertThat(wrapperStrictDeserialize.getRequest()).isEqualTo(hookHandlerRequest); assertThat(wrapperStrictDeserialize.invocationPoint).isEqualTo(invocationPoint); assertThat(wrapperStrictDeserialize.callbackContext).isNull(); } @@ -323,6 +330,103 @@ public void invokeHandler_StrictDeserializer_WithResourceProperties_returnsSucce } } + @ParameterizedTest + @CsvSource({ "preCreate.request.with-stack-level-hook.json,CREATE_PRE_PROVISION" }) + public void invokeHandler_WithStackLevelHook_returnsSuccess(final String requestDataPath, final String invocationPointString) + throws IOException { + final HookInvocationPoint invocationPoint = HookInvocationPoint.valueOf(invocationPointString); + + final ProgressEvent pe = ProgressEvent.builder().status(OperationStatus.SUCCESS).build(); + wrapper.setInvokeHandlerResponse(pe); + + lenient().when(cipher.decryptCredentials(any())).thenReturn(new Credentials("123", "123", "123")); + + wrapper.setHookInvocationPayloadFromS3(Map.of( + "Template", "template string here", + "PreviousTemplate", "previous template string here", + "ResolvedTemplate", "resolved template string here", + "ChangedResources", List.of( + Map.of( + "LogicalResourceId", "SomeLogicalResourceId", + "ResourceType", "AWS::S3::Bucket", + "Action", "CREATE", + "LineNumber", 3, + "ResourceProperties", "", + "PreviousResourceProperties", "" + ) + ) + )); + + try (final InputStream in = loadRequestStream(requestDataPath); final OutputStream out = new ByteArrayOutputStream()) { + final Context context = getLambdaContext(); + + wrapper.handleRequest(in, out, context); + + // verify initialiseRuntime was called and initialised dependencies + verifyInitialiseRuntime(); + + // verify output response + verifyHandlerResponse(out, + HookProgressEvent.builder().clientRequestToken("123456").hookStatus(HookStatus.SUCCESS).build()); + + // assert handler receives correct injections + assertThat(wrapper.awsClientProxy).isNotNull(); + assertThat(wrapper.invocationPoint).isEqualTo(invocationPoint); + assertThat(wrapper.callbackContext).isNull(); + + assertThat(wrapper.getRequest().getHookContext().getTargetType()).isEqualTo("STACK"); + assertThat(wrapper.getRequest().getHookContext().getTargetName()).isEqualTo("STACK"); + assertThat(wrapper.getRequest().getHookContext().getTargetLogicalId()).isEqualTo("myStack"); + + StackHookTargetModel stackHookTargetModel = wrapper.getRequest().getHookContext() + .getTargetModel(StackHookTargetModel.class); + assertThat(stackHookTargetModel.getTemplate()).isEqualTo("template string here"); + assertThat(stackHookTargetModel.getPreviousTemplate()).isEqualTo("previous template string here"); + assertThat(stackHookTargetModel.getResolvedTemplate()).isEqualTo("resolved template string here"); + assertThat(stackHookTargetModel.getChangedResources().size()).isEqualTo(1); + + ChangedResource expectedChangedResource = ChangedResource.builder().logicalResourceId("SomeLogicalResourceId") + .resourceType("AWS::S3::Bucket").lineNumber(3).action("CREATE") + .resourceProperties("") + .previousResourceProperties("").build(); + assertThat(stackHookTargetModel.getChangedResources().get(0)).isEqualTo(expectedChangedResource); + } + } + + @Test + public void testIsHookInvocationPayloadRemote() { + List invalidHookRequestDataObjects = ImmutableList.of( + HookRequestData.builder().targetModel(null).build(), + HookRequestData.builder().targetModel(null).payload(null).build(), + HookRequestData.builder().targetModel(Collections.emptyMap()).payload(null).build() + ); + + invalidHookRequestDataObjects.forEach(requestData -> { + Assertions.assertThrows(TerminalException.class, () -> wrapper.isHookInvocationPayloadRemote(requestData)); + }); + + Assertions.assertThrows(TerminalException.class, () -> wrapper.isHookInvocationPayloadRemote(null)); + + HookRequestData bothFieldsPopulated = HookRequestData.builder() + .targetModel(ImmutableMap.of("foo", "bar")) + .payload("http://s3PresignedUrl") + .build(); + HookRequestData onlyTargetModelPopulated = HookRequestData.builder() + .targetModel(ImmutableMap.of("foo", "bar")) + .payload(null).build(); + HookRequestData onlyPayloadPopulated = HookRequestData.builder() + .targetModel(Collections.emptyMap()) + .payload("http://s3PresignedUrl").build(); + HookRequestData onlyPayloadPopulatedWithNullTargetModel = HookRequestData.builder().targetModel(null) + .payload("http://s3PresignedUrl").build(); + + Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated)); + Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated)); + Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated)); + Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulatedWithNullTargetModel)); + } + private final String expectedStringWhenStrictDeserializingWithExtraneousFields = "Unrecognized field \"targetName\" (class software.amazon.cloudformation.proxy.hook.HookInvocationRequest), not marked as ignorable (10 known properties: \"requestContext\", \"stackId\", \"clientRequestToken\", \"hookModel\", \"hookTypeName\", \"requestData\", \"actionInvocationPoint\", \"awsAccountId\", \"changeSetId\", \"hookTypeVersion\"])\n" + " at [Source: (String)\"{\n" + " \"clientRequestToken\": \"123456\",\n" + " \"awsAccountId\": \"123456789012\",\n" + " \"stackId\": \"arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968\",\n" diff --git a/src/test/java/software/amazon/cloudformation/data/hook/preCreate.request.with-stack-level-hook.json b/src/test/java/software/amazon/cloudformation/data/hook/preCreate.request.with-stack-level-hook.json new file mode 100644 index 00000000..6206fcef --- /dev/null +++ b/src/test/java/software/amazon/cloudformation/data/hook/preCreate.request.with-stack-level-hook.json @@ -0,0 +1,26 @@ +{ + "clientRequestToken": "123456", + "awsAccountId": "123456789012", + "stackId": "arn:aws:cloudformation:us-east-1:123456789012:stack/SampleStack/e722ae60-fe62-11e8-9a0e-0ae8cc519968", + "changeSetId": "arn:aws:cloudformation:us-east-1:123456789012:changeSet/SampleChangeSet-conditional/1a2345b6-0000-00a0-a123-00abc0abc000", + "hookTypeName": "AWS::Test::TestModel", + "hookTypeVersion": "1.0", + "hookModel": { + "property1": "abc", + "property2": 123 + }, + "actionInvocationPoint": "CREATE_PRE_PROVISION", + "requestData": { + "targetName": "STACK", + "targetType": "STACK", + "targetLogicalId": "myStack", + "targetModel": {}, + "payload": "http://someS3PresignedUrl", + "callerCredentials": "callerCredentials", + "providerCredentials": "providerCredentials", + "providerLogGroupName": "providerLoggingGroupName", + "hookEncryptionKeyArn": "hookEncryptionKeyArn", + "hookEncryptionKeyRole": "hookEncryptionKeyRole" + }, + "requestContext": {} +} diff --git a/src/test/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetModelTest.java b/src/test/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetModelTest.java index b8e2bb8c..592bbf21 100644 --- a/src/test/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetModelTest.java +++ b/src/test/java/software/amazon/cloudformation/proxy/hook/targetmodel/HookTargetModelTest.java @@ -16,6 +16,7 @@ import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import com.google.common.collect.ImmutableList; import com.google.common.collect.ImmutableMap; import java.io.File; import java.io.FileInputStream; @@ -23,6 +24,7 @@ import java.nio.charset.StandardCharsets; import java.util.Collections; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.stream.Stream; @@ -199,6 +201,93 @@ public void testHookTargetModelWithAdditionalProperties() throws Exception { OBJECT_MAPPER.writeValueAsString(resourceProperties)); } + @Test + public void testStackHookTargetModel() throws Exception { + final String template = "{\"key1\":\"value1\"}"; + final String previousTemplate = "{\"previousKey1\":\"previousValue1\"}"; + final String resolvedTemplate = "{\"resolvedKey1\":\"resolvedValue1\"}"; + final List changedResources = ImmutableList + .of(ChangedResource.builder().logicalResourceId("SomeLogicalResourceId").resourceType("AWS::S3::Bucket") + .action("CREATE").lineNumber(11).previousResourceProperties("{\"BucketName\": \"some-prev-bucket-name\"") + .resourceProperties("{\"BucketName\": \"some-bucket-name\"").build()); + + final Map targetModelMap = ImmutableMap.of("Template", template, "PreviousTemplate", previousTemplate, + "ResolvedTemplate", resolvedTemplate, "ChangedResources", changedResources); + + final StackHookTargetModel targetModel = HookTargetModel.of(targetModelMap, StackHookTargetModel.class); + + Assertions.assertEquals(template, targetModel.getTemplate()); + Assertions.assertEquals(previousTemplate, targetModel.getPreviousTemplate()); + Assertions.assertEquals(resolvedTemplate, targetModel.getResolvedTemplate()); + Assertions.assertEquals(changedResources, targetModel.getChangedResources()); + Assertions.assertNull(targetModel.getHookTargetTypeReference()); + Assertions.assertEquals( + "{\"Template\":\"{\\\"key1\\\":\\\"value1\\\"}\",\"PreviousTemplate\":\"{\\\"previousKey1\\\":\\\"" + + "previousValue1\\\"}\",\"ResolvedTemplate\":\"{\\\"resolvedKey1\\\":\\\"resolvedValue1\\\"}\",\"ChangedResources\"" + + ":[{\"LogicalResourceId\":\"SomeLogicalResourceId\",\"ResourceType\":\"AWS::S3::Bucket\",\"LineNumber\":" + + "11,\"Action\":\"CREATE\",\"ResourceProperties\":\"{\\\"BucketName\\\": \\\"some-bucket-name\\\"\"," + + "\"PreviousResourceProperties\":\"{\\\"BucketName\\\": \\\"some-prev-bucket-name\\\"\"}]}", + OBJECT_MAPPER.writeValueAsString(targetModel)); + } + + @Test + public void testStackHookTargetModelWithAdditionalPropertiesInInput() throws Exception { + final String template = "{\"key1\":\"value1\"}"; + final String previousTemplate = "{\"previousKey1\":\"previousValue1\"}"; + final String resolvedTemplate = "{\"resolvedKey1\":\"resolvedValue1\"}"; + final String extraneousProperty = "{\"extraKey\":\"extraValue\"}"; + final List changedResources = ImmutableList + .of(ChangedResource.builder().logicalResourceId("SomeLogicalResourceId").resourceType("AWS::S3::Bucket") + .action("CREATE").lineNumber(11).previousResourceProperties("{\"BucketName\": \"some-prev-bucket-name\"") + .resourceProperties("{\"BucketName\": \"some-bucket-name\"").build()); + + final Map targetModelMap = ImmutableMap.of("Template", template, "PreviousTemplate", previousTemplate, + "ResolvedTemplate", resolvedTemplate, "ChangedResources", changedResources, "ExtraProperty", extraneousProperty); + + final StackHookTargetModel targetModel = HookTargetModel.of(targetModelMap, StackHookTargetModel.class); + + Assertions.assertEquals(template, targetModel.getTemplate()); + Assertions.assertEquals(previousTemplate, targetModel.getPreviousTemplate()); + Assertions.assertEquals(resolvedTemplate, targetModel.getResolvedTemplate()); + Assertions.assertEquals(changedResources, targetModel.getChangedResources()); + Assertions.assertNull(targetModel.getHookTargetTypeReference()); + + Assertions.assertEquals("{\"Template\":\"{\\\"key1\\\":\\\"value1\\\"}\",\"PreviousTemplate\":\"{\\\"previousKey1\\\":" + + "\\\"previousValue1\\\"}\",\"ResolvedTemplate\":\"{\\\"resolvedKey1\\\":\\\"resolvedValue1\\\"}\"," + + "\"ChangedResources\":[{\"LogicalResourceId\":\"SomeLogicalResourceId\",\"ResourceType\":" + + "\"AWS::S3::Bucket\",\"LineNumber\":11,\"Action\":\"CREATE\",\"ResourceProperties\":\"{" + + "\\\"BucketName\\\": \\\"some-bucket-name\\\"\",\"PreviousResourceProperties\":\"{\\\"BucketName\\\":" + + " \\\"some-prev-bucket-name\\\"\"}]}", OBJECT_MAPPER.writeValueAsString(targetModel)); + } + + @Test + public void testStackHookTargetModelWithMissingPropertiesInInput() throws Exception { + final String template = "{\"key1\":\"value1\"}"; + final String resolvedTemplate = "{\"resolvedKey1\":\"resolvedValue1\"}"; + final List changedResources = ImmutableList + .of(ChangedResource.builder().logicalResourceId("SomeLogicalResourceId").resourceType("AWS::S3::Bucket") + .action("CREATE").previousResourceProperties("{\"BucketName\": \"some-prev-bucket-name\"") + .resourceProperties("{\"BucketName\": \"some-bucket-name\"").build()); + + final Map targetModelMap = ImmutableMap.of("Template", template, "ResolvedTemplate", resolvedTemplate, + "ChangedResources", changedResources); + + final StackHookTargetModel targetModel = HookTargetModel.of(targetModelMap, StackHookTargetModel.class); + + Assertions.assertEquals(template, targetModel.getTemplate()); + Assertions.assertNull(targetModel.getPreviousTemplate()); + Assertions.assertEquals(resolvedTemplate, targetModel.getResolvedTemplate()); + Assertions.assertEquals(changedResources, targetModel.getChangedResources()); + Assertions.assertNull(targetModel.getHookTargetTypeReference()); + Assertions.assertEquals( + "{\"Template\":\"{\\\"key1\\\":\\\"value1\\\"}\",\"PreviousTemplate\":null,\"ResolvedTemplate\":" + + "\"{\\\"resolvedKey1\\\":\\\"resolvedValue1\\\"}\",\"ChangedResources\":[{\"LogicalResourceId\":" + + "\"SomeLogicalResourceId\",\"ResourceType\":\"AWS::S3::Bucket\",\"LineNumber\":null,\"Action\":" + + "\"CREATE\",\"ResourceProperties\":\"{\\\"BucketName\\\": \\\"some-bucket-name\\\"\"," + + "\"PreviousResourceProperties\":\"{\\\"BucketName\\\": \\\"some-prev-bucket-name\\\"\"}]}", + OBJECT_MAPPER.writeValueAsString(targetModel)); + } + @Test public void testHookTargetTypeWithNullValue() { final HookTargetModel targetModel = HookTargetModel.of(null);