From 17ab056b567d4669cd35159c209dc6899c26a42d Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Tue, 6 Feb 2024 14:48:12 -0500 Subject: [PATCH 01/10] Add StackHookTargetModel for support of stack-level hooks Add new type that represents the payload sent to hook handlers that will contain information related to stack hooks. --- .../hook/targetmodel/ChangedResource.java | 42 +++++++++ .../hook/targetmodel/HookTargetType.java | 14 ++- .../targetmodel/StackHookTargetModel.java | 61 +++++++++++++ .../HookLambdaWrapperOverride.java | 13 ++- .../cloudformation/HookLambdaWrapperTest.java | 54 ++++++++++- ...eCreate.request.with-stack-level-hook.json | 39 ++++++++ .../hook/targetmodel/HookTargetModelTest.java | 91 +++++++++++++++++++ 7 files changed, 308 insertions(+), 6 deletions(-) create mode 100644 src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/ChangedResource.java create mode 100644 src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/StackHookTargetModel.java create mode 100644 src/test/java/software/amazon/cloudformation/data/hook/preCreate.request.with-stack-level-hook.json 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..9cd80e41 --- /dev/null +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/ChangedResource.java @@ -0,0 +1,42 @@ +/* +* 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.*; + +@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..a501f0b4 --- /dev/null +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/StackHookTargetModel.java @@ -0,0 +1,61 @@ +/* +* 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(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 String template; + + @JsonProperty("PreviousTemplate") + private String previousTemplate; + + @JsonProperty("ResolvedTemplate") + private String 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..360fd89d 100644 --- a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java +++ b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java @@ -32,8 +32,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; @@ -112,7 +114,16 @@ 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; diff --git a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java index 729f7587..099d5d24 100644 --- a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java +++ b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java @@ -49,6 +49,8 @@ import software.amazon.cloudformation.proxy.hook.HookHandlerRequest; import software.amazon.cloudformation.proxy.hook.HookProgressEvent; 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 +170,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 +206,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 +242,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 +278,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 +321,54 @@ 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")); + + 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); + } + } + 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..811fb819 --- /dev/null +++ b/src/test/java/software/amazon/cloudformation/data/hook/preCreate.request.with-stack-level-hook.json @@ -0,0 +1,39 @@ +{ + "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": { + "template": "template string here", + "previousTemplate": "previous template string here", + "resolvedTemplate": "resolved template string here", + "changedResources": [ + { + "LogicalResourceId": "SomeLogicalResourceId", + "ResourceType": "AWS::S3::Bucket", + "LineNumber": 3, + "Action": "CREATE", + "ResourceProperties": "", + "PreviousResourceProperties": "" + } + ] + }, + "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..85257337 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,95 @@ 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); From dd20afbebf18fc9e787b8ed82f5d00069e30a9ab Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Thu, 29 Feb 2024 09:52:47 -0500 Subject: [PATCH 02/10] Add support to download hook target data for stack-level hooks Stack level hooks will not be provided with invocation payload information, unlike resource level hooks. Instead, Hooks Service will pass in an S3 presigned URL that points to a file that contains the stack-level invocation payload. The base hook handler (before it reaches the customer's handler code), will use that URL to download the data and set it on the target model that is passed to the customer's handler. --- .../cloudformation/HookAbstractWrapper.java | 71 +++++++++++++++++-- .../proxy/hook/HookRequestData.java | 1 + .../HookLambdaWrapperOverride.java | 12 ++++ .../cloudformation/HookLambdaWrapperTest.java | 50 +++++++++++++ ...eCreate.request.with-stack-level-hook.json | 17 +---- .../hook/targetmodel/HookTargetModelTest.java | 34 +++++---- 6 files changed, 147 insertions(+), 38 deletions(-) diff --git a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java index cbe7b244..980fab43 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 java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.OutputStream; +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 com.google.common.annotations.VisibleForTesting; 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 (Exception exp) { + loggerProxy.log("Failed to retrieve hook invocation payload" + exp.toString()); + } + } + return Collections.emptyMap(); + } + + @VisibleForTesting + protected boolean isHookInvocationPayloadRemote(HookRequestData hookRequestData) { + if (hookRequestData == null || hookRequestData.getTargetModel() == null) { + throw new TerminalException("Invalid request object received. Target Model can not be null."); + } + + if (hookRequestData.getTargetModel().isEmpty() && hookRequestData.getPayload() == null) { + throw new TerminalException("No payload data set."); + } + + return 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/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/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperOverride.java index 360fd89d..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; @@ -46,6 +47,8 @@ @EqualsAndHashCode(callSuper = true) public class HookLambdaWrapperOverride extends HookLambdaWrapper { + private Map hookInvocationPayloadFromS3; + /** * This .ctor provided for testing */ @@ -128,6 +131,15 @@ protected HookHandlerRequest transform(final HookInvocationRequest 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 099d5d24..a78430aa 100644 --- a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java +++ b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java @@ -30,7 +30,15 @@ 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 com.google.common.collect.ImmutableList; +import com.google.common.collect.ImmutableMap; +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 +46,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,6 +57,7 @@ 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; @@ -333,6 +343,22 @@ public void invokeHandler_WithStackLevelHook_returnsSuccess(final String request 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(); @@ -369,6 +395,30 @@ public void invokeHandler_WithStackLevelHook_returnsSuccess(final String request } } + @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(), + 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(); + + Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated)); + Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated)); + Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated)); + } + 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 index 811fb819..6206fcef 100644 --- 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 @@ -14,21 +14,8 @@ "targetName": "STACK", "targetType": "STACK", "targetLogicalId": "myStack", - "targetModel": { - "template": "template string here", - "previousTemplate": "previous template string here", - "resolvedTemplate": "resolved template string here", - "changedResources": [ - { - "LogicalResourceId": "SomeLogicalResourceId", - "ResourceType": "AWS::S3::Bucket", - "LineNumber": 3, - "Action": "CREATE", - "ResourceProperties": "", - "PreviousResourceProperties": "" - } - ] - }, + "targetModel": {}, + "payload": "http://someS3PresignedUrl", "callerCredentials": "callerCredentials", "providerCredentials": "providerCredentials", "providerLogGroupName": "providerLoggingGroupName", 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 85257337..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 @@ -222,11 +222,11 @@ public void testStackHookTargetModel() throws Exception { 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\\\"\"}]}", + "{\"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)); } @@ -252,14 +252,12 @@ public void testStackHookTargetModelWithAdditionalPropertiesInInput() throws Exc 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)); + 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 @@ -282,11 +280,11 @@ public void testStackHookTargetModelWithMissingPropertiesInInput() throws Except 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\\\"\"}]}", + "{\"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)); } From 2e2605629e21b7b49fbbc11dc046719cce0ffcc4 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Thu, 29 Feb 2024 10:25:55 -0500 Subject: [PATCH 03/10] Add support to download hook target data for stack-level hooks Stack level hooks will not be provided with invocation payload information, unlike resource level hooks. Instead, Hooks Service will pass in an S3 presigned URL that points to a file that contains the stack-level invocation payload. The base hook handler (before it reaches the customer's handler code), will use that URL to download the data and set it on the target model that is passed to the customer's handler. --- .../cloudformation/HookAbstractWrapper.java | 6 ++--- .../hook/targetmodel/ChangedResource.java | 5 +++- .../targetmodel/StackHookTargetModel.java | 12 ++++++---- .../cloudformation/HookLambdaWrapperTest.java | 24 ++++++++++--------- 4 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java index 980fab43..7b72465c 100644 --- a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java +++ b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java @@ -17,6 +17,7 @@ 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; @@ -27,8 +28,6 @@ import java.util.Collections; import java.util.Date; import java.util.Map; - -import com.google.common.annotations.VisibleForTesting; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.exception.ExceptionUtils; @@ -389,8 +388,7 @@ public Map retrieveHookInvocationPayloadFromS3(final String s3Pr try { URL presignedUrl = new URL(s3PresignedUrl); - SdkHttpRequest httpRequest = SdkHttpRequest.builder().method(SdkHttpMethod.GET).uri(presignedUrl.toURI()) - .build(); + SdkHttpRequest httpRequest = SdkHttpRequest.builder().method(SdkHttpMethod.GET).uri(presignedUrl.toURI()).build(); HttpExecuteRequest executeRequest = HttpExecuteRequest.builder().request(httpRequest).build(); 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 index 9cd80e41..e26dca05 100644 --- a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/ChangedResource.java +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/ChangedResource.java @@ -15,7 +15,10 @@ package software.amazon.cloudformation.proxy.hook.targetmodel; import com.fasterxml.jackson.annotation.JsonProperty; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; @Data @Builder 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 index a501f0b4..0c572960 100644 --- a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/StackHookTargetModel.java +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/StackHookTargetModel.java @@ -19,8 +19,12 @@ import com.fasterxml.jackson.annotation.JsonProperty; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + import java.util.List; -import lombok.*; @EqualsAndHashCode(callSuper = false) @Getter @@ -33,13 +37,13 @@ public class StackHookTargetModel extends HookTargetModel { }; @JsonProperty("Template") - private String template; + private Object template; @JsonProperty("PreviousTemplate") - private String previousTemplate; + private Object previousTemplate; @JsonProperty("ResolvedTemplate") - private String resolvedTemplate; + private Object resolvedTemplate; @JsonProperty("ChangedResources") private List changedResources; diff --git a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java index a78430aa..824ed8d9 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; @@ -33,9 +35,6 @@ import java.util.Collections; import java.util.List; import java.util.Map; - -import com.google.common.collect.ImmutableList; -import com.google.common.collect.ImmutableMap; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -408,16 +407,19 @@ public void testIsHookInvocationPayloadRemote() { Assertions.assertThrows(TerminalException.class, () -> wrapper.isHookInvocationPayloadRemote(requestData)); }); - Assertions.assertThrows(TerminalException.class, () -> wrapper.isHookInvocationPayloadRemote(null)); + 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 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(); - Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated)); - Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated)); - Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated)); - } + Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated)); + Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated)); + Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated)); + } 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" From 13b51376e5f8c241a5db3449d86a70c777281ada Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Thu, 29 Feb 2024 10:45:34 -0500 Subject: [PATCH 04/10] Add support to download hook target data for stack-level hooks Stack level hooks will not be provided with invocation payload information, unlike resource level hooks. Instead, Hooks Service will pass in an S3 presigned URL that points to a file that contains the stack-level invocation payload. The base hook handler (before it reaches the customer's handler code), will use that URL to download the data and set it on the target model that is passed to the customer's handler. --- .../software/amazon/cloudformation/HookAbstractWrapper.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java index 7b72465c..d47e1227 100644 --- a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java +++ b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java @@ -22,6 +22,7 @@ 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; @@ -405,7 +406,7 @@ public Map retrieveHookInvocationPayloadFromS3(final String s3Pr String str = byteArrayOutputStream.toString(StandardCharsets.UTF_8); return this.serializer.deserialize(str, hookStackPayloadS3TypeReference); - } catch (Exception exp) { + } catch (RuntimeException | IOException | URISyntaxException exp) { loggerProxy.log("Failed to retrieve hook invocation payload" + exp.toString()); } } From 73822f514cf7334d7ec954b36642907ae460f30d Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Thu, 4 Apr 2024 12:50:46 -0400 Subject: [PATCH 05/10] Skip stack level hook for stack if prior stack level change set hook succeeded For stack level hooks, customers are able to return a new status that allow stack level hooks that execute against a stack to skip with a successful status. The idea is that if a stack hook invoked against a change set succeeds, there is no need to invoke against the stack once the change set is processed. --- .../software/amazon/cloudformation/proxy/OperationStatus.java | 1 + .../software/amazon/cloudformation/proxy/hook/HookStatus.java | 1 + 2 files changed, 2 insertions(+) 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/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 } From e341c994bd0a18a3693bc2dbf031738387d82aa1 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Thu, 4 Apr 2024 12:53:21 -0400 Subject: [PATCH 06/10] Skip stack level hook for stack if prior stack level change set hook succeeded For stack level hooks, customers are able to return a new status that allow stack level hooks that execute against a stack to skip with a successful status. The idea is that if a stack hook invoked against a change set succeeds, there is no need to invoke against the stack once the change set is processed. --- pom.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pom.xml b/pom.xml index 7b20ea8b..87be9f8c 100644 --- a/pom.xml +++ b/pom.xml @@ -491,7 +491,7 @@ org.sonatype.plugins nexus-staging-maven-plugin - 1.6.13 + 1.6.8 true sonatype-nexus-staging From f88e5defde52bfebbfba565611852d9643cf0488 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Fri, 28 Jun 2024 09:51:23 -0400 Subject: [PATCH 07/10] fix method --- .../cloudformation/HookAbstractWrapper.java | 8 ++- .../targetmodel/StackHookTargetModel.java | 3 +- .../cloudformation/HookLambdaWrapperTest.java | 64 +++++++++---------- 3 files changed, 37 insertions(+), 38 deletions(-) diff --git a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java index d47e1227..9a10a774 100644 --- a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java +++ b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java @@ -415,15 +415,17 @@ public Map retrieveHookInvocationPayloadFromS3(final String s3Pr @VisibleForTesting protected boolean isHookInvocationPayloadRemote(HookRequestData hookRequestData) { - if (hookRequestData == null || hookRequestData.getTargetModel() == null) { + if (hookRequestData == null) { throw new TerminalException("Invalid request object received. Target Model can not be null."); } - if (hookRequestData.getTargetModel().isEmpty() && hookRequestData.getPayload() == null) { + if ((hookRequestData.getTargetModel() == null || hookRequestData.getTargetModel().isEmpty()) + && hookRequestData.getPayload() == null) { throw new TerminalException("No payload data set."); } - return hookRequestData.getTargetModel().isEmpty(); + return true; + // return hookRequestData.getTargetModel().isEmpty(); } /** 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 index 0c572960..08cb9e83 100644 --- a/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/StackHookTargetModel.java +++ b/src/main/java/software/amazon/cloudformation/proxy/hook/targetmodel/StackHookTargetModel.java @@ -19,13 +19,12 @@ 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; -import java.util.List; - @EqualsAndHashCode(callSuper = false) @Getter @NoArgsConstructor diff --git a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java index 824ed8d9..99cb3cb7 100644 --- a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java +++ b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java @@ -23,8 +23,6 @@ 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; @@ -32,12 +30,9 @@ 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; @@ -45,7 +40,6 @@ 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; @@ -56,7 +50,6 @@ 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; @@ -394,32 +387,37 @@ public void invokeHandler_WithStackLevelHook_returnsSuccess(final String request } } - @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(), - 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(); - - Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated)); - Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated)); - Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated)); - } + // @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(), + // 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(); + // + // Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated)); + // Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated)); + // Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated)); + // } 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" From c73d8d313617c58bad7b841e49bd6736c4b5a03e Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Tue, 17 Sep 2024 14:54:29 -0400 Subject: [PATCH 08/10] Fix resource targetting for a stack level hook --- .../cloudformation/HookAbstractWrapper.java | 3 +- .../cloudformation/HookLambdaWrapperTest.java | 67 ++++++++++--------- 2 files changed, 37 insertions(+), 33 deletions(-) diff --git a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java index 9a10a774..a2e0f675 100644 --- a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java +++ b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java @@ -424,8 +424,7 @@ protected boolean isHookInvocationPayloadRemote(HookRequestData hookRequestData) throw new TerminalException("No payload data set."); } - return true; - // return hookRequestData.getTargetModel().isEmpty(); + return hookRequestData.getTargetModel().isEmpty(); } /** diff --git a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java index 99cb3cb7..b799df50 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,9 +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; @@ -40,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; @@ -50,6 +56,7 @@ 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; @@ -387,37 +394,35 @@ public void invokeHandler_WithStackLevelHook_returnsSuccess(final String request } } - // @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(), - // 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(); - // - // Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated)); - // Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated)); - // Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated)); - // } + @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(); + + Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(bothFieldsPopulated)); + Assertions.assertFalse(wrapper.isHookInvocationPayloadRemote(onlyTargetModelPopulated)); + Assertions.assertTrue(wrapper.isHookInvocationPayloadRemote(onlyPayloadPopulated)); + } 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" From e74adc2288e2b92f9ef2409af1bd5cc25174c236 Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Wed, 18 Sep 2024 14:01:17 -0400 Subject: [PATCH 09/10] Fix resource targetting for a stack level hook --- .../software/amazon/cloudformation/HookAbstractWrapper.java | 2 +- .../software/amazon/cloudformation/HookLambdaWrapperTest.java | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java index a2e0f675..070cbb2e 100644 --- a/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java +++ b/src/main/java/software/amazon/cloudformation/HookAbstractWrapper.java @@ -424,7 +424,7 @@ protected boolean isHookInvocationPayloadRemote(HookRequestData hookRequestData) throw new TerminalException("No payload data set."); } - return hookRequestData.getTargetModel().isEmpty(); + return (hookRequestData.getTargetModel() == null || hookRequestData.getTargetModel().isEmpty()); } /** diff --git a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java index b799df50..f7315e5e 100644 --- a/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java +++ b/src/test/java/software/amazon/cloudformation/HookLambdaWrapperTest.java @@ -418,10 +418,13 @@ public void testIsHookInvocationPayloadRemote() { 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" From fccc46d9adf0556f0e08ab94d1b462230b33d3ba Mon Sep 17 00:00:00 2001 From: Brian Lao Date: Mon, 25 Nov 2024 13:00:21 -0500 Subject: [PATCH 10/10] bump version --- pom.xml | 2 +- python/rpdk/java/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pom.xml b/pom.xml index 87be9f8c..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. 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())