From f7fde2a45c79f1517b640f7aad48bf8c948dcba0 Mon Sep 17 00:00:00 2001 From: Mark Sailes Date: Wed, 1 Mar 2023 10:03:02 +0000 Subject: [PATCH 01/14] cf fix --- pom.xml | 6 + powertools-cloudformation/pom.xml | 5 + .../AbstractCustomResourceHandler.java | 5 +- .../powertools/cloudformation/Response.java | 11 +- .../cloudformation/EndToEndTest.java | 107 ++++++++++++++++++ .../UpdateCausesRuntimeException.java | 24 ++++ 6 files changed, 156 insertions(+), 2 deletions(-) create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/UpdateCausesRuntimeException.java diff --git a/pom.xml b/pom.xml index 121b3459e..641640a94 100644 --- a/pom.xml +++ b/pom.xml @@ -283,6 +283,12 @@ 1.1.1 test + + com.github.tomakehurst + wiremock-jre8 + 2.35.0 + test + diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml index 4946d9424..42ef703e0 100644 --- a/powertools-cloudformation/pom.xml +++ b/powertools-cloudformation/pom.xml @@ -93,6 +93,11 @@ assertj-core test + + com.github.tomakehurst + wiremock-jre8 + test + \ No newline at end of file diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java index 05f1a0f27..e2d5a1206 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java @@ -65,7 +65,10 @@ public final Response handleRequest(CloudFormationCustomResourceEvent event, Con } catch (CustomResourceResponseException rse) { LOG.error("Unable to generate response. Sending empty failure to {}", responseUrl, rse); try { - client.send(event, context, Response.failed()); + // If the customers code throws an exception, Powertools should respond in a way that doesn't + // change the CloudFormation resources. A failure is sent with the existing PhysicalResourceId + // indicating no change. + client.send(event, context, Response.failed(event.getPhysicalResourceId())); } catch (Exception e) { // unable to generate response AND send the failure LOG.error("Unable to send failure response to {}.", responseUrl, e); diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java index 3ae6b9296..e658c2129 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java @@ -137,6 +137,15 @@ public static Builder builder() { return new Builder(); } + /** + * Creates failed Response with a given physicalResourceId. + * + * @return a failed Response with no value. + */ + public static Response failed(String physicalResourceId) { + return new Response(null, Status.FAILED, physicalResourceId, false); + } + /** * Creates an empty, failed Response. * @@ -149,7 +158,7 @@ public static Response failed() { /** * Creates an empty, successful Response. * - * @return a failed Response with no value. + * @return a success Response with no value. */ public static Response success() { return new Response(null, Status.SUCCESS, null, false); diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java new file mode 100644 index 000000000..7069f2182 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java @@ -0,0 +1,107 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT-0 + +package software.amazon.lambda.powertools.cloudformation; + +import com.amazonaws.services.lambda.runtime.ClientContext; +import com.amazonaws.services.lambda.runtime.CognitoIdentity; +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.LambdaLogger; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; +import com.github.tomakehurst.wiremock.junit5.WireMockTest; +import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.cloudformation.handlers.UpdateCausesRuntimeException; + +import java.util.UUID; + +import static com.github.tomakehurst.wiremock.client.WireMock.*; +import static org.assertj.core.api.Assertions.assertThat; + +@WireMockTest +public class EndToEndTest { + + public static final String PHYSICAL_RESOURCE_ID = UUID.randomUUID().toString(); + + @Test + void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdating(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor(put("/").willReturn(ok())); + + UpdateCausesRuntimeException handler = new UpdateCausesRuntimeException(); + CloudFormationCustomResourceEvent event = eventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); + Response response = handler.handleRequest(event, new FakeContext()); + + assertThat(response).isNull(); + verify(putRequestedFor(urlPathMatching("/")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) + ); + } + + private static CloudFormationCustomResourceEvent eventWithPhysicalResourceId(int httpPort, String physicalResourceId) { + CloudFormationCustomResourceEvent event = new CloudFormationCustomResourceEvent(); + event.setRequestType("Update"); + event.setResponseUrl("http://localhost:" + httpPort + "/"); + event.setStackId("123"); + event.setRequestId("234"); + event.setLogicalResourceId("345"); + event.setPhysicalResourceId(physicalResourceId); + return event; + } + + private static class FakeContext implements Context { + @Override + public String getAwsRequestId() { + return null; + } + + @Override + public String getLogGroupName() { + return null; + } + + @Override + public String getLogStreamName() { + return "LogStreamName"; + } + + @Override + public String getFunctionName() { + return null; + } + + @Override + public String getFunctionVersion() { + return null; + } + + @Override + public String getInvokedFunctionArn() { + return null; + } + + @Override + public CognitoIdentity getIdentity() { + return null; + } + + @Override + public ClientContext getClientContext() { + return null; + } + + @Override + public int getRemainingTimeInMillis() { + return 0; + } + + @Override + public int getMemoryLimitInMB() { + return 0; + } + + @Override + public LambdaLogger getLogger() { + return null; + } + } +} diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/UpdateCausesRuntimeException.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/UpdateCausesRuntimeException.java new file mode 100644 index 000000000..9ac589233 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/UpdateCausesRuntimeException.java @@ -0,0 +1,24 @@ +package software.amazon.lambda.powertools.cloudformation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; +import software.amazon.lambda.powertools.cloudformation.Response; + +public class UpdateCausesRuntimeException extends AbstractCustomResourceHandler { + + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + return null; + } + + @Override + protected Response update(CloudFormationCustomResourceEvent event, Context context) { + throw new RuntimeException("failure"); + } + + @Override + protected Response delete(CloudFormationCustomResourceEvent event, Context context) { + return null; + } +} From 9a657fcce15e444eb5d9a43a69cc0a68417a3fcd Mon Sep 17 00:00:00 2001 From: Mark Sailes Date: Wed, 1 Mar 2023 13:22:11 +0000 Subject: [PATCH 02/14] cf fix --- .../CloudFormationResponse.java | 20 ++-- .../powertools/cloudformation/Response.java | 53 +++++++++-- .../CloudFormationResponseTest.java | 26 ++---- .../cloudformation/EndToEndTest.java | 91 ++++++++++++++++--- .../NoPhysicalResourceIdSetHandler.java | 24 +++++ 5 files changed, 165 insertions(+), 49 deletions(-) create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index 33cc533d2..c300a48c0 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -53,14 +53,14 @@ static class ResponseBody { private final boolean noEcho; ResponseBody(CloudFormationCustomResourceEvent event, - Context context, Response.Status responseStatus, String physicalResourceId, - boolean noEcho) { + boolean noEcho, + String reason) { Objects.requireNonNull(event, "CloudFormationCustomResourceEvent cannot be null"); - Objects.requireNonNull(context, "Context cannot be null"); - this.physicalResourceId = physicalResourceId != null ? physicalResourceId : context.getLogStreamName(); - this.reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName(); + + this.physicalResourceId = physicalResourceId; + this.reason = reason; this.status = responseStatus == null ? Response.Status.SUCCESS.name() : responseStatus.name(); this.stackId = event.getStackId(); this.requestId = event.getRequestId(); @@ -146,7 +146,7 @@ SdkHttpClient getClient() { */ public HttpExecuteResponse send(CloudFormationCustomResourceEvent event, Context context) throws IOException, CustomResourceResponseException { - return send(event, context, null); + return send(event, context, Response.success(context.getLogGroupName())); } /** @@ -201,13 +201,17 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, Context context, Response resp) throws CustomResourceResponseException { try { + // Silent success + String reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName(); if (resp == null) { - ResponseBody body = new ResponseBody(event, context, Response.Status.SUCCESS, null, false); + ResponseBody body = new ResponseBody(event, Response.Status.SUCCESS, context.getLogStreamName(), false, reason); ObjectNode node = body.toObjectNode(null); return new StringInputStream(node.toString()); } else { + String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() : context.getLogStreamName(); + ResponseBody body = new ResponseBody( - event, context, resp.getStatus(), resp.getPhysicalResourceId(), resp.isNoEcho()); + event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), reason); ObjectNode node = body.toObjectNode(resp.getJsonNode()); return new StringInputStream(node.toString()); } diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java index e658c2129..9d67fc8a9 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java @@ -138,32 +138,67 @@ public static Builder builder() { } /** - * Creates failed Response with a given physicalResourceId. + * Creates a failed Response with no physicalResourceId set. Powertools will set the physicalResourceId to the + * Lambda LogStreamName + * + * The value returned for a PhysicalResourceId can change custom resource update operations. If the value returned + * is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes + * the update as a replacement and sends a delete request to the old resource. For more information, + * see AWS::CloudFormation::CustomResource. * * @return a failed Response with no value. */ - public static Response failed(String physicalResourceId) { - return new Response(null, Status.FAILED, physicalResourceId, false); + public static Response failed() { + return new Response(null, Status.FAILED, null, false); } /** - * Creates an empty, failed Response. + * Creates a failed Response with a given physicalResourceId. * - * @return a failed Response with no value. + * @param physicalResourceId The value must be a non-empty string and must be identical for all responses for the + * same resource. + * The value returned for a PhysicalResourceId can change custom resource update + * operations. If the value returned is the same, it is considered a normal update. If the + * value returned is different, AWS CloudFormation recognizes the update as a replacement + * and sends a delete request to the old resource. For more information, + * see AWS::CloudFormation::CustomResource. + * @return a failed Response with physicalResourceId */ - public static Response failed() { - return new Response(null, Status.FAILED, null, false); + public static Response failed(String physicalResourceId) { + return new Response(null, Status.FAILED, physicalResourceId, false); } /** - * Creates an empty, successful Response. + * Creates a successful Response with no physicalResourceId set. Powertools will set the physicalResourceId to the + * Lambda LogStreamName * - * @return a success Response with no value. + * The value returned for a PhysicalResourceId can change custom resource update operations. If the value returned + * is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes + * the update as a replacement and sends a delete request to the old resource. For more information, + * see AWS::CloudFormation::CustomResource. + * + * @return a success Response with no physicalResourceId value. */ public static Response success() { return new Response(null, Status.SUCCESS, null, false); } + /** + * Creates a successful Response with a given physicalResourceId. + * + * @param physicalResourceId The value must be a non-empty string and must be identical for all responses for the + * same resource. + * The value returned for a PhysicalResourceId can change custom resource update + * operations. If the value returned is the same, it is considered a normal update. If the + * value returned is different, AWS CloudFormation recognizes the update as a replacement + * and sends a delete request to the old resource. For more information, + * see AWS::CloudFormation::CustomResource. + * @return a success Response with physicalResourceId + */ + public static Response success(String physicalResourceId) { + return new Response(null, Status.SUCCESS, physicalResourceId, false); + } + private final JsonNode jsonNode; private final Status status; private final String physicalResourceId; diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java index 207eb9b7f..64c313695 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java @@ -103,20 +103,6 @@ void eventResponseUrlRequiredToSend() { .isInstanceOf(RuntimeException.class); } - @Test - void defaultPhysicalResponseIdIsLogStreamName() { - CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); - when(event.getPhysicalResourceId()).thenReturn("This-Is-Ignored"); - - String logStreamName = "My-Log-Stream-Name"; - Context context = mock(Context.class); - when(context.getLogStreamName()).thenReturn(logStreamName); - - ResponseBody body = new ResponseBody( - event, context, Response.Status.SUCCESS, null, false); - assertThat(body.getPhysicalResourceId()).isEqualTo(logStreamName); - } - @Test void customPhysicalResponseId() { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); @@ -127,7 +113,7 @@ void customPhysicalResponseId() { String customPhysicalResourceId = "Custom-Physical-Resource-ID"; ResponseBody body = new ResponseBody( - event, context, Response.Status.SUCCESS, customPhysicalResourceId, false); + event, Response.Status.SUCCESS, customPhysicalResourceId, false, "See the details in CloudWatch Log Stream: " + context.getLogStreamName()); assertThat(body.getPhysicalResourceId()).isEqualTo(customPhysicalResourceId); } @@ -136,7 +122,7 @@ void responseBodyWithNullDataNode() { CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); Context context = mock(Context.class); - ResponseBody responseBody = new ResponseBody(event, context, Response.Status.FAILED, null, true); + ResponseBody responseBody = new ResponseBody(event, Response.Status.FAILED, null, true, "See the details in CloudWatch Log Stream: " + context.getLogStreamName()); String actualJson = responseBody.toObjectNode(null).toString(); String expectedJson = "{" + @@ -160,7 +146,7 @@ void responseBodyWithNonNullDataNode() { dataNode.put("foo", "bar"); dataNode.put("baz", 10); - ResponseBody responseBody = new ResponseBody(event, context, Response.Status.FAILED, null, true); + ResponseBody responseBody = new ResponseBody(event, Response.Status.FAILED, null, true, "See the details in CloudWatch Log Stream: " + context.getLogStreamName()); String actualJson = responseBody.toObjectNode(dataNode).toString(); String expectedJson = "{" + @@ -182,7 +168,7 @@ void defaultStatusIsSuccess() { Context context = mock(Context.class); ResponseBody body = new ResponseBody( - event, context, null, null, false); + event, null, null, false, "See the details in CloudWatch Log Stream: " + context.getLogStreamName()); assertThat(body.getStatus()).isEqualTo("SUCCESS"); } @@ -192,7 +178,7 @@ void customStatus() { Context context = mock(Context.class); ResponseBody body = new ResponseBody( - event, context, Response.Status.FAILED, null, false); + event, Response.Status.FAILED, null, false, "See the details in CloudWatch Log Stream: " + context.getLogStreamName()); assertThat(body.getStatus()).isEqualTo("FAILED"); } @@ -205,7 +191,7 @@ void reasonIncludesLogStreamName() { when(context.getLogStreamName()).thenReturn(logStreamName); ResponseBody body = new ResponseBody( - event, context, Response.Status.SUCCESS, null, false); + event, Response.Status.SUCCESS, null, false, "See the details in CloudWatch Log Stream: " + context.getLogStreamName()); assertThat(body.getReason()).contains(logStreamName); } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java index 7069f2182..ee752327f 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java @@ -11,6 +11,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.junit.jupiter.api.Test; +import software.amazon.lambda.powertools.cloudformation.handlers.NoPhysicalResourceIdSetHandler; import software.amazon.lambda.powertools.cloudformation.handlers.UpdateCausesRuntimeException; import java.util.UUID; @@ -28,24 +29,90 @@ void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdating(WireM stubFor(put("/").willReturn(ok())); UpdateCausesRuntimeException handler = new UpdateCausesRuntimeException(); - CloudFormationCustomResourceEvent event = eventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); - Response response = handler.handleRequest(event, new FakeContext()); + CloudFormationCustomResourceEvent updateEvent = updateEventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); + Response response = handler.handleRequest(updateEvent, new FakeContext()); assertThat(response).isNull(); verify(putRequestedFor(urlPathMatching("/")) + .withRequestBody(matchingJsonPath("[?(@.Status == 'FAILED')]")) .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) ); } - private static CloudFormationCustomResourceEvent eventWithPhysicalResourceId(int httpPort, String physicalResourceId) { - CloudFormationCustomResourceEvent event = new CloudFormationCustomResourceEvent(); - event.setRequestType("Update"); - event.setResponseUrl("http://localhost:" + httpPort + "/"); - event.setStackId("123"); - event.setRequestId("234"); - event.setLogicalResourceId("345"); - event.setPhysicalResourceId(physicalResourceId); - return event; + // Existing functionality - could well be incorrect + @Test + void physicalResourceIdSetAsLogStreamOnUpdateWhenCustomerDoesntProvideAPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor(put("/").willReturn(ok())); + + NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); + CloudFormationCustomResourceEvent updateEvent = updateEventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); + Response response = handler.handleRequest(updateEvent, new FakeContext()); + + assertThat(response).isNotNull(); + verify(putRequestedFor(urlPathMatching("/")) + .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == 'FakeLogStreamName')]")) + ); + } + + @Test + void physicalResourceIdSetAsLogStreamOnDeleteWhenCustomerDoesntProvideAPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor(put("/").willReturn(ok())); + + NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); + CloudFormationCustomResourceEvent createEvent = deleteEventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); + Response response = handler.handleRequest(createEvent, new FakeContext()); + + assertThat(response).isNotNull(); + verify(putRequestedFor(urlPathMatching("/")) + .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == 'FakeLogStreamName')]")) + ); + } + + @Test + void createNewResourceBecausePhysicalResourceIdNoSetByCustomerOnCreate(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor(put("/").willReturn(ok())); + + NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); + CloudFormationCustomResourceEvent createEvent = baseEvent(wmRuntimeInfo.getHttpPort()) + .withRequestType("Create") + .build(); + Response response = handler.handleRequest(createEvent, new FakeContext()); + + assertThat(response).isNotNull(); + verify(putRequestedFor(urlPathMatching("/")) + .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == 'FakeLogStreamName')]")) + ); + } + + private static CloudFormationCustomResourceEvent updateEventWithPhysicalResourceId(int httpPort, String physicalResourceId) { + CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = baseEvent(httpPort); + + builder.withPhysicalResourceId(physicalResourceId); + builder.withRequestType("Update"); + + return builder.build(); + } + + private static CloudFormationCustomResourceEvent deleteEventWithPhysicalResourceId(int httpPort, String physicalResourceId) { + CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = baseEvent(httpPort); + + builder.withPhysicalResourceId(physicalResourceId); + builder.withRequestType("Delete"); + + return builder.build(); + } + + private static CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder baseEvent(int httpPort) { + CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = CloudFormationCustomResourceEvent.builder() + .withResponseUrl("http://localhost:" + httpPort + "/") + .withStackId("123") + .withRequestId("234") + .withLogicalResourceId("345"); + + return builder; } private static class FakeContext implements Context { @@ -61,7 +128,7 @@ public String getLogGroupName() { @Override public String getLogStreamName() { - return "LogStreamName"; + return "FakeLogStreamName"; } @Override diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java new file mode 100644 index 000000000..68d057b54 --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/NoPhysicalResourceIdSetHandler.java @@ -0,0 +1,24 @@ +package software.amazon.lambda.powertools.cloudformation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; +import software.amazon.lambda.powertools.cloudformation.Response; + +public class NoPhysicalResourceIdSetHandler extends AbstractCustomResourceHandler { + + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + return Response.success(); + } + + @Override + protected Response update(CloudFormationCustomResourceEvent event, Context context) { + return Response.success(); + } + + @Override + protected Response delete(CloudFormationCustomResourceEvent event, Context context) { + return Response.success(); + } +} From 38f323d0fbd9b5f89ed484e29cbe4c45c66313c1 Mon Sep 17 00:00:00 2001 From: Mark Sailes Date: Wed, 1 Mar 2023 20:14:30 +0000 Subject: [PATCH 03/14] cf fix --- powertools-cloudformation/pom.xml | 6 +++++ .../CloudFormationResponse.java | 26 ++++++++++++++++--- .../cloudformation/EndToEndTest.java | 3 --- .../src/test/resources/logback.xml | 11 ++++++++ 4 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 powertools-cloudformation/src/test/resources/logback.xml diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml index 42ef703e0..9bbc2a4c3 100644 --- a/powertools-cloudformation/pom.xml +++ b/powertools-cloudformation/pom.xml @@ -98,6 +98,12 @@ wiremock-jre8 test + + ch.qos.logback + logback-classic + 1.2.6 + test + \ No newline at end of file diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index c300a48c0..512f4372a 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -6,6 +6,8 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.PropertyNamingStrategies; import com.fasterxml.jackson.databind.node.ObjectNode; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import software.amazon.awssdk.http.Header; import software.amazon.awssdk.http.HttpExecuteRequest; import software.amazon.awssdk.http.HttpExecuteResponse; @@ -32,6 +34,8 @@ */ class CloudFormationResponse { + private static final Logger LOG = LoggerFactory.getLogger(CloudFormationResponse.class); + /** * Internal representation of the payload to be sent to the event target URL. Retains all properties of the payload * except for "Data". This is done so that the serialization of the non-"Data" properties and the serialization of @@ -52,6 +56,20 @@ static class ResponseBody { private final String logicalResourceId; private final boolean noEcho; + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("ResponseBody{"); + sb.append("status='").append(status).append('\''); + sb.append(", reason='").append(reason).append('\''); + sb.append(", physicalResourceId='").append(physicalResourceId).append('\''); + sb.append(", stackId='").append(stackId).append('\''); + sb.append(", requestId='").append(requestId).append('\''); + sb.append(", logicalResourceId='").append(logicalResourceId).append('\''); + sb.append(", noEcho=").append(noEcho); + sb.append('}'); + return sb.toString(); + } + ResponseBody(CloudFormationCustomResourceEvent event, Response.Status responseStatus, String physicalResourceId, @@ -201,21 +219,21 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, Context context, Response resp) throws CustomResourceResponseException { try { - // Silent success String reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName(); if (resp == null) { ResponseBody body = new ResponseBody(event, Response.Status.SUCCESS, context.getLogStreamName(), false, reason); + LOG.debug("ResponseBody: {}", body); ObjectNode node = body.toObjectNode(null); return new StringInputStream(node.toString()); } else { String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() : context.getLogStreamName(); - - ResponseBody body = new ResponseBody( - event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), reason); + ResponseBody body = new ResponseBody(event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), reason); + LOG.debug("ResponseBody: {}", body); ObjectNode node = body.toObjectNode(resp.getJsonNode()); return new StringInputStream(node.toString()); } } catch (RuntimeException e) { + LOG.error(e.getMessage()); throw new CustomResourceResponseException("Unable to generate response body.", e); } } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java index ee752327f..1851cc695 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java @@ -1,6 +1,3 @@ -// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. -// SPDX-License-Identifier: MIT-0 - package software.amazon.lambda.powertools.cloudformation; import com.amazonaws.services.lambda.runtime.ClientContext; diff --git a/powertools-cloudformation/src/test/resources/logback.xml b/powertools-cloudformation/src/test/resources/logback.xml new file mode 100644 index 000000000..8c752522e --- /dev/null +++ b/powertools-cloudformation/src/test/resources/logback.xml @@ -0,0 +1,11 @@ + + + + %d{HH:mm:ss.SSS} [%thread] %-5level %logger{36} - %msg%n + + + + + + + \ No newline at end of file From 458757aadfb370b64e3d88b6107d99b3faa27ad4 Mon Sep 17 00:00:00 2001 From: Mark Sailes Date: Fri, 3 Mar 2023 13:43:59 +0000 Subject: [PATCH 04/14] cf fix --- .../powertools/cloudformation/Response.java | 2 ++ .../cloudformation/EndToEndTest.java | 35 +++++++++++++------ ...ava => RuntimeExceptionThrownHandler.java} | 4 +-- 3 files changed, 29 insertions(+), 12 deletions(-) rename powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/{UpdateCausesRuntimeException.java => RuntimeExceptionThrownHandler.java} (85%) diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java index 9d67fc8a9..0a076d792 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java @@ -148,6 +148,7 @@ public static Builder builder() { * * @return a failed Response with no value. */ + @Deprecated public static Response failed() { return new Response(null, Status.FAILED, null, false); } @@ -179,6 +180,7 @@ public static Response failed(String physicalResourceId) { * * @return a success Response with no physicalResourceId value. */ + @Deprecated public static Response success() { return new Response(null, Status.SUCCESS, null, false); } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java index 1851cc695..66bbf2bd5 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java @@ -9,7 +9,7 @@ import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.junit.jupiter.api.Test; import software.amazon.lambda.powertools.cloudformation.handlers.NoPhysicalResourceIdSetHandler; -import software.amazon.lambda.powertools.cloudformation.handlers.UpdateCausesRuntimeException; +import software.amazon.lambda.powertools.cloudformation.handlers.RuntimeExceptionThrownHandler; import java.util.UUID; @@ -20,23 +20,38 @@ public class EndToEndTest { public static final String PHYSICAL_RESOURCE_ID = UUID.randomUUID().toString(); + public static final String LOG_STREAM_NAME = "FakeLogStreamName"; @Test void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdating(WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); - UpdateCausesRuntimeException handler = new UpdateCausesRuntimeException(); + RuntimeExceptionThrownHandler handler = new RuntimeExceptionThrownHandler(); CloudFormationCustomResourceEvent updateEvent = updateEventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); - Response response = handler.handleRequest(updateEvent, new FakeContext()); + handler.handleRequest(updateEvent, new FakeContext()); - assertThat(response).isNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'FAILED')]")) .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) ); } - // Existing functionality - could well be incorrect + @Test + void runtimeExceptionThrownOnCreateSendsLogStreamNameAsPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor(put("/").willReturn(ok())); + + RuntimeExceptionThrownHandler handler = new RuntimeExceptionThrownHandler(); + CloudFormationCustomResourceEvent createEvent = baseEvent(wmRuntimeInfo.getHttpPort()) + .withRequestType("Create") + .build(); + handler.handleRequest(createEvent, new FakeContext()); + + verify(putRequestedFor(urlPathMatching("/")) + .withRequestBody(matchingJsonPath("[?(@.Status == 'FAILED')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]")) + ); + } + @Test void physicalResourceIdSetAsLogStreamOnUpdateWhenCustomerDoesntProvideAPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); @@ -48,7 +63,7 @@ void physicalResourceIdSetAsLogStreamOnUpdateWhenCustomerDoesntProvideAPhysicalR assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == 'FakeLogStreamName')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]")) ); } @@ -63,12 +78,12 @@ void physicalResourceIdSetAsLogStreamOnDeleteWhenCustomerDoesntProvideAPhysicalR assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == 'FakeLogStreamName')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]")) ); } @Test - void createNewResourceBecausePhysicalResourceIdNoSetByCustomerOnCreate(WireMockRuntimeInfo wmRuntimeInfo) { + void createNewResourceBecausePhysicalResourceIdNotSetByCustomerOnCreate(WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); @@ -80,7 +95,7 @@ void createNewResourceBecausePhysicalResourceIdNoSetByCustomerOnCreate(WireMockR assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == 'FakeLogStreamName')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]")) ); } @@ -125,7 +140,7 @@ public String getLogGroupName() { @Override public String getLogStreamName() { - return "FakeLogStreamName"; + return LOG_STREAM_NAME; } @Override diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/UpdateCausesRuntimeException.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java similarity index 85% rename from powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/UpdateCausesRuntimeException.java rename to powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java index 9ac589233..1b1dafc2e 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/UpdateCausesRuntimeException.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java @@ -5,11 +5,11 @@ import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; import software.amazon.lambda.powertools.cloudformation.Response; -public class UpdateCausesRuntimeException extends AbstractCustomResourceHandler { +public class RuntimeExceptionThrownHandler extends AbstractCustomResourceHandler { @Override protected Response create(CloudFormationCustomResourceEvent event, Context context) { - return null; + throw new RuntimeException("failure"); } @Override From 16963eef8c5271e16b38d52c25906ca921e68b67 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Thu, 9 Mar 2023 10:13:29 +0100 Subject: [PATCH 05/14] Cfn fix - variant 2 (#2) * Variant 2: Use the request's physicalResourceId when in doubt and it isn't null --- .../CloudFormationResponse.java | 5 +++- .../cloudformation/EndToEndTest.java | 24 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index 512f4372a..b4bd113cb 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -226,7 +226,10 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, ObjectNode node = body.toObjectNode(null); return new StringInputStream(node.toString()); } else { - String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() : context.getLogStreamName(); + + String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() : + event.getPhysicalResourceId() != null? event.getPhysicalResourceId() : context.getLogStreamName(); + ResponseBody body = new ResponseBody(event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), reason); LOG.debug("ResponseBody: {}", body); ObjectNode node = body.toObjectNode(resp.getJsonNode()); diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java index 66bbf2bd5..2606144ff 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java @@ -8,6 +8,8 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import software.amazon.lambda.powertools.cloudformation.handlers.NoPhysicalResourceIdSetHandler; import software.amazon.lambda.powertools.cloudformation.handlers.RuntimeExceptionThrownHandler; @@ -22,6 +24,20 @@ public class EndToEndTest { public static final String PHYSICAL_RESOURCE_ID = UUID.randomUUID().toString(); public static final String LOG_STREAM_NAME = "FakeLogStreamName"; + @Test + void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(WireMockRuntimeInfo wmRuntimeInfo) { + stubFor(put("/").willReturn(ok())); + + NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); + CloudFormationCustomResourceEvent updateEvent = updateEventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); + handler.handleRequest(updateEvent, new FakeContext()); + + verify(putRequestedFor(urlPathMatching("/")) + .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) + ); + } + @Test void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdating(WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); @@ -63,12 +79,12 @@ void physicalResourceIdSetAsLogStreamOnUpdateWhenCustomerDoesntProvideAPhysicalR assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) ); } - @Test - void physicalResourceIdSetAsLogStreamOnDeleteWhenCustomerDoesntProvideAPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { + @Test + void physicalResourceIdSetFromRequestOnDeleteWhenCustomerDoesntProvideAPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); @@ -78,7 +94,7 @@ void physicalResourceIdSetAsLogStreamOnDeleteWhenCustomerDoesntProvideAPhysicalR assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + LOG_STREAM_NAME + "')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) ); } From ec4bd983e0fc5736ff5aea5c41677d1b960983c3 Mon Sep 17 00:00:00 2001 From: Mark Sailes Date: Thu, 9 Mar 2023 09:26:20 +0000 Subject: [PATCH 06/14] feedback changes --- pom.xml | 6 ++++ powertools-cloudformation/pom.xml | 1 - .../AbstractCustomResourceHandler.java | 4 ++- .../CloudFormationResponse.java | 30 ++++++++++--------- 4 files changed, 25 insertions(+), 16 deletions(-) diff --git a/pom.xml b/pom.xml index 44669547e..91014ac87 100644 --- a/pom.xml +++ b/pom.xml @@ -283,6 +283,12 @@ 1.1.1 test + + ch.qos.logback + logback-classic + 1.2.6 + test + com.github.tomakehurst wiremock-jre8 diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml index 9bbc2a4c3..5366cdc26 100644 --- a/powertools-cloudformation/pom.xml +++ b/powertools-cloudformation/pom.xml @@ -101,7 +101,6 @@ ch.qos.logback logback-classic - 1.2.6 test diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java index e2d5a1206..e721651a0 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/AbstractCustomResourceHandler.java @@ -66,8 +66,10 @@ public final Response handleRequest(CloudFormationCustomResourceEvent event, Con LOG.error("Unable to generate response. Sending empty failure to {}", responseUrl, rse); try { // If the customers code throws an exception, Powertools should respond in a way that doesn't - // change the CloudFormation resources. A failure is sent with the existing PhysicalResourceId + // change the CloudFormation resources. + // In the case of a Update or Delete, a failure is sent with the existing PhysicalResourceId // indicating no change. + // In the case of a Create, null will be set and changed to the Lambda LogStreamName before sending. client.send(event, context, Response.failed(event.getPhysicalResourceId())); } catch (Exception e) { // unable to generate response AND send the failure diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index b4bd113cb..6565eb422 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -56,20 +56,6 @@ static class ResponseBody { private final String logicalResourceId; private final boolean noEcho; - @Override - public String toString() { - final StringBuffer sb = new StringBuffer("ResponseBody{"); - sb.append("status='").append(status).append('\''); - sb.append(", reason='").append(reason).append('\''); - sb.append(", physicalResourceId='").append(physicalResourceId).append('\''); - sb.append(", stackId='").append(stackId).append('\''); - sb.append(", requestId='").append(requestId).append('\''); - sb.append(", logicalResourceId='").append(logicalResourceId).append('\''); - sb.append(", noEcho=").append(noEcho); - sb.append('}'); - return sb.toString(); - } - ResponseBody(CloudFormationCustomResourceEvent event, Response.Status responseStatus, String physicalResourceId, @@ -129,6 +115,20 @@ ObjectNode toObjectNode(JsonNode dataNode) { } return node; } + + @Override + public String toString() { + final StringBuffer sb = new StringBuffer("ResponseBody{"); + sb.append("status='").append(status).append('\''); + sb.append(", reason='").append(reason).append('\''); + sb.append(", physicalResourceId='").append(physicalResourceId).append('\''); + sb.append(", stackId='").append(stackId).append('\''); + sb.append(", requestId='").append(requestId).append('\''); + sb.append(", logicalResourceId='").append(logicalResourceId).append('\''); + sb.append(", noEcho=").append(noEcho); + sb.append('}'); + return sb.toString(); + } } private final SdkHttpClient client; @@ -213,6 +213,8 @@ protected Map> headers(int contentLength) { /** * Returns the response body as an input stream, for supplying with the HTTP request to the custom resource. * + * If PhysicalResourceId is null at this point it will be replaced with the Lambda LogStreamName. + * * @throws CustomResourceResponseException if unable to generate the response stream */ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, From 40d9c1639c193adae2fdd3c40013155b882626fd Mon Sep 17 00:00:00 2001 From: Mark Sailes Date: Thu, 9 Mar 2023 09:32:20 +0000 Subject: [PATCH 07/14] feedback changes --- .../{EndToEndTest.java => CloudFormationIntegrationTest.java} | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) rename powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/{EndToEndTest.java => CloudFormationIntegrationTest.java} (98%) diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java similarity index 98% rename from powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java rename to powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java index 2606144ff..9d50f0eb0 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/EndToEndTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java @@ -8,8 +8,6 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; import software.amazon.lambda.powertools.cloudformation.handlers.NoPhysicalResourceIdSetHandler; import software.amazon.lambda.powertools.cloudformation.handlers.RuntimeExceptionThrownHandler; @@ -19,7 +17,7 @@ import static org.assertj.core.api.Assertions.assertThat; @WireMockTest -public class EndToEndTest { +public class CloudFormationIntegrationTest { public static final String PHYSICAL_RESOURCE_ID = UUID.randomUUID().toString(); public static final String LOG_STREAM_NAME = "FakeLogStreamName"; From c5ef2d6f380605bfafc8de685bc9b70b46cf36f8 Mon Sep 17 00:00:00 2001 From: Mark Sailes <45629314+msailes@users.noreply.github.com> Date: Thu, 9 Mar 2023 19:54:38 +0000 Subject: [PATCH 08/14] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Jérôme Van Der Linden <117538+jeromevdl@users.noreply.github.com> --- .../amazon/lambda/powertools/cloudformation/Response.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java index 0a076d792..0205e4283 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java @@ -145,6 +145,8 @@ public static Builder builder() { * is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes * the update as a replacement and sends a delete request to the old resource. For more information, * see AWS::CloudFormation::CustomResource. + + @deprecated this method is not safe. Provide a physicalResourceId. * * @return a failed Response with no value. */ @@ -178,6 +180,7 @@ public static Response failed(String physicalResourceId) { * the update as a replacement and sends a delete request to the old resource. For more information, * see AWS::CloudFormation::CustomResource. * + * @deprecated this method is not safe. Provide a physicalResourceId. * @return a success Response with no physicalResourceId value. */ @Deprecated From 996ba118e14135b830100e4ca4227c4e3cc13cad Mon Sep 17 00:00:00 2001 From: Mark Sailes Date: Thu, 9 Mar 2023 19:58:19 +0000 Subject: [PATCH 09/14] formatting fix --- .../amazon/lambda/powertools/cloudformation/Response.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java index 0205e4283..6dd636a73 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java @@ -145,9 +145,8 @@ public static Builder builder() { * is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes * the update as a replacement and sends a delete request to the old resource. For more information, * see AWS::CloudFormation::CustomResource. - - @deprecated this method is not safe. Provide a physicalResourceId. * + * @deprecated this method is not safe. Provide a physicalResourceId. * @return a failed Response with no value. */ @Deprecated From 078dfa5fc66a864b40c527c04af89a0fb17316ed Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Fri, 10 Mar 2023 14:29:06 +0100 Subject: [PATCH 10/14] Fix test name --- .../cloudformation/CloudFormationIntegrationTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java index 9d50f0eb0..de83e8857 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java @@ -67,7 +67,7 @@ void runtimeExceptionThrownOnCreateSendsLogStreamNameAsPhysicalResourceId(WireMo } @Test - void physicalResourceIdSetAsLogStreamOnUpdateWhenCustomerDoesntProvideAPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { + void physicalResourceIdSetFromRequestOnUpdateWhenCustomerDoesntProvideAPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); From 235adf96ac659a8db20cf706f324166fa93b547f Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Fri, 10 Mar 2023 19:43:09 +0100 Subject: [PATCH 11/14] Add tests - user handler returns a physicalResourceId --- .../CloudFormationIntegrationTest.java | 46 +++++++++++++++++++ .../PhysicalResourceIdSetHandler.java | 32 +++++++++++++ 2 files changed, 78 insertions(+) create mode 100644 powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java index de83e8857..fdaa05194 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java @@ -8,7 +8,10 @@ import com.github.tomakehurst.wiremock.junit5.WireMockRuntimeInfo; import com.github.tomakehurst.wiremock.junit5.WireMockTest; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import software.amazon.lambda.powertools.cloudformation.handlers.NoPhysicalResourceIdSetHandler; +import software.amazon.lambda.powertools.cloudformation.handlers.PhysicalResourceIdSetHandler; import software.amazon.lambda.powertools.cloudformation.handlers.RuntimeExceptionThrownHandler; import java.util.UUID; @@ -22,6 +25,11 @@ public class CloudFormationIntegrationTest { public static final String PHYSICAL_RESOURCE_ID = UUID.randomUUID().toString(); public static final String LOG_STREAM_NAME = "FakeLogStreamName"; +// public CloudFormationIntegrationTest(WireMockRuntimeInfo wmRuntimeInfo) { +// +// this.wmRuntimeInfo = wmRuntimeInfo; +// } + @Test void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); @@ -113,6 +121,44 @@ void createNewResourceBecausePhysicalResourceIdNotSetByCustomerOnCreate(WireMock ); } + @ParameterizedTest + @ValueSource(strings = {"Create", "Update", "Delete"}) + void physicalResourceIdReturnedFromSuccessToCloudformation(String requestType, WireMockRuntimeInfo wmRuntimeInfo) { + + String physicalResourceId = UUID.randomUUID().toString(); + + PhysicalResourceIdSetHandler handler = new PhysicalResourceIdSetHandler(physicalResourceId, true); + CloudFormationCustomResourceEvent createEvent = baseEvent(wmRuntimeInfo.getHttpPort()) + .withRequestType(requestType) + .build(); + Response response = handler.handleRequest(createEvent, new FakeContext()); + + assertThat(response).isNotNull(); + verify(putRequestedFor(urlPathMatching("/")) + .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + physicalResourceId + "')]")) + ); + } + + @ParameterizedTest + @ValueSource(strings = {"Create", "Update", "Delete"}) + void physicalResourceIdReturnedFromFailedToCloudformation(String requestType, WireMockRuntimeInfo wmRuntimeInfo) { + + String physicalResourceId = UUID.randomUUID().toString(); + + PhysicalResourceIdSetHandler handler = new PhysicalResourceIdSetHandler(physicalResourceId, false); + CloudFormationCustomResourceEvent createEvent = baseEvent(wmRuntimeInfo.getHttpPort()) + .withRequestType(requestType) + .build(); + Response response = handler.handleRequest(createEvent, new FakeContext()); + + assertThat(response).isNotNull(); + verify(putRequestedFor(urlPathMatching("/")) + .withRequestBody(matchingJsonPath("[?(@.Status == 'FAILED')]")) + .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + physicalResourceId + "')]")) + ); + } + private static CloudFormationCustomResourceEvent updateEventWithPhysicalResourceId(int httpPort, String physicalResourceId) { CloudFormationCustomResourceEvent.CloudFormationCustomResourceEventBuilder builder = baseEvent(httpPort); diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java new file mode 100644 index 000000000..51f520a3d --- /dev/null +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/PhysicalResourceIdSetHandler.java @@ -0,0 +1,32 @@ +package software.amazon.lambda.powertools.cloudformation.handlers; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent; +import software.amazon.lambda.powertools.cloudformation.AbstractCustomResourceHandler; +import software.amazon.lambda.powertools.cloudformation.Response; + +public class PhysicalResourceIdSetHandler extends AbstractCustomResourceHandler { + + private final String physicalResourceId; + private final boolean callsSucceed; + + public PhysicalResourceIdSetHandler(String physicalResourceId, boolean callsSucceed) { + this.physicalResourceId = physicalResourceId; + this.callsSucceed = callsSucceed; + } + + @Override + protected Response create(CloudFormationCustomResourceEvent event, Context context) { + return callsSucceed? Response.success(physicalResourceId) : Response.failed(physicalResourceId); + } + + @Override + protected Response update(CloudFormationCustomResourceEvent event, Context context) { + return callsSucceed? Response.success(physicalResourceId) : Response.failed(physicalResourceId); + } + + @Override + protected Response delete(CloudFormationCustomResourceEvent event, Context context) { + return callsSucceed? Response.success(physicalResourceId) : Response.failed(physicalResourceId); + } +} From d9e89592d299404e4ec48c86ffe172f5f0fb9e73 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Fri, 10 Mar 2023 19:51:32 +0100 Subject: [PATCH 12/14] Generalized some more tests --- .../CloudFormationIntegrationTest.java | 35 ++++++++++++------- .../RuntimeExceptionThrownHandler.java | 2 +- 2 files changed, 23 insertions(+), 14 deletions(-) diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java index fdaa05194..9335f55e6 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java @@ -25,18 +25,20 @@ public class CloudFormationIntegrationTest { public static final String PHYSICAL_RESOURCE_ID = UUID.randomUUID().toString(); public static final String LOG_STREAM_NAME = "FakeLogStreamName"; -// public CloudFormationIntegrationTest(WireMockRuntimeInfo wmRuntimeInfo) { -// -// this.wmRuntimeInfo = wmRuntimeInfo; -// } - - @Test - void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(WireMockRuntimeInfo wmRuntimeInfo) { + @ParameterizedTest + @ValueSource(strings = {"Update", "Delete"}) + void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(String requestType, WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); - CloudFormationCustomResourceEvent updateEvent = updateEventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); - handler.handleRequest(updateEvent, new FakeContext()); + int httpPort = wmRuntimeInfo.getHttpPort(); + + CloudFormationCustomResourceEvent event = baseEvent(httpPort) + .withPhysicalResourceId(PHYSICAL_RESOURCE_ID) + .withRequestType(requestType) + .build(); + + handler.handleRequest(event, new FakeContext()); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) @@ -44,13 +46,20 @@ void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(Wi ); } - @Test - void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdating(WireMockRuntimeInfo wmRuntimeInfo) { + @ParameterizedTest + @ValueSource(strings = {"Update", "Delete"}) + void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdatingOrDeleting(String requestType, WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); RuntimeExceptionThrownHandler handler = new RuntimeExceptionThrownHandler(); - CloudFormationCustomResourceEvent updateEvent = updateEventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); - handler.handleRequest(updateEvent, new FakeContext()); + int httpPort = wmRuntimeInfo.getHttpPort(); + + CloudFormationCustomResourceEvent event = baseEvent(httpPort) + .withPhysicalResourceId(PHYSICAL_RESOURCE_ID) + .withRequestType(requestType) + .build(); + + handler.handleRequest(event, new FakeContext()); verify(putRequestedFor(urlPathMatching("/")) .withRequestBody(matchingJsonPath("[?(@.Status == 'FAILED')]")) diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java index 1b1dafc2e..ee5be77b8 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.java @@ -19,6 +19,6 @@ protected Response update(CloudFormationCustomResourceEvent event, Context conte @Override protected Response delete(CloudFormationCustomResourceEvent event, Context context) { - return null; + throw new RuntimeException("failure"); } } From f6112756482f0ee2434796fef73f449a5bf4d824 Mon Sep 17 00:00:00 2001 From: Scott Gerring Date: Fri, 10 Mar 2023 19:54:31 +0100 Subject: [PATCH 13/14] More parameterized testing --- .../CloudFormationIntegrationTest.java | 26 +++++++------------ 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java index 9335f55e6..06463308c 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java @@ -83,28 +83,20 @@ void runtimeExceptionThrownOnCreateSendsLogStreamNameAsPhysicalResourceId(WireMo ); } - @Test - void physicalResourceIdSetFromRequestOnUpdateWhenCustomerDoesntProvideAPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { + @ParameterizedTest + @ValueSource(strings = {"Update", "Delete"}) + void physicalResourceIdSetFromRequestOnUpdateOrDeleteWhenCustomerDoesntProvideAPhysicalResourceId(String requestType, WireMockRuntimeInfo wmRuntimeInfo) { stubFor(put("/").willReturn(ok())); NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); - CloudFormationCustomResourceEvent updateEvent = updateEventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); - Response response = handler.handleRequest(updateEvent, new FakeContext()); - - assertThat(response).isNotNull(); - verify(putRequestedFor(urlPathMatching("/")) - .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]")) - .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]")) - ); - } + int httpPort = wmRuntimeInfo.getHttpPort(); - @Test - void physicalResourceIdSetFromRequestOnDeleteWhenCustomerDoesntProvideAPhysicalResourceId(WireMockRuntimeInfo wmRuntimeInfo) { - stubFor(put("/").willReturn(ok())); + CloudFormationCustomResourceEvent event = baseEvent(httpPort) + .withPhysicalResourceId(PHYSICAL_RESOURCE_ID) + .withRequestType(requestType) + .build(); - NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler(); - CloudFormationCustomResourceEvent createEvent = deleteEventWithPhysicalResourceId(wmRuntimeInfo.getHttpPort(), PHYSICAL_RESOURCE_ID); - Response response = handler.handleRequest(createEvent, new FakeContext()); + Response response = handler.handleRequest(event, new FakeContext()); assertThat(response).isNotNull(); verify(putRequestedFor(urlPathMatching("/")) From c0e599e6831cca33b9288fc58600691f085fce2a Mon Sep 17 00:00:00 2001 From: Mark Sailes Date: Tue, 14 Mar 2023 14:15:41 +0000 Subject: [PATCH 14/14] pr feedback --- .../powertools/cloudformation/CloudFormationResponse.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java index 6565eb422..39a86293b 100644 --- a/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java +++ b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java @@ -164,7 +164,7 @@ SdkHttpClient getClient() { */ public HttpExecuteResponse send(CloudFormationCustomResourceEvent event, Context context) throws IOException, CustomResourceResponseException { - return send(event, context, Response.success(context.getLogGroupName())); + return send(event, context, null); } /** @@ -223,7 +223,9 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, try { String reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName(); if (resp == null) { - ResponseBody body = new ResponseBody(event, Response.Status.SUCCESS, context.getLogStreamName(), false, reason); + String physicalResourceId = event.getPhysicalResourceId() != null? event.getPhysicalResourceId() : context.getLogStreamName(); + + ResponseBody body = new ResponseBody(event, Response.Status.SUCCESS, physicalResourceId, false, reason); LOG.debug("ResponseBody: {}", body); ObjectNode node = body.toObjectNode(null); return new StringInputStream(node.toString());