diff --git a/pom.xml b/pom.xml
index aca2162e1..91014ac87 100644
--- a/pom.xml
+++ b/pom.xml
@@ -283,6 +283,18 @@
1.1.1
test
+
+ ch.qos.logback
+ logback-classic
+ 1.2.6
+ test
+
+
+ com.github.tomakehurst
+ wiremock-jre8
+ 2.35.0
+ test
+
diff --git a/powertools-cloudformation/pom.xml b/powertools-cloudformation/pom.xml
index 4946d9424..5366cdc26 100644
--- a/powertools-cloudformation/pom.xml
+++ b/powertools-cloudformation/pom.xml
@@ -93,6 +93,16 @@
assertj-core
test
+
+ com.github.tomakehurst
+ wiremock-jre8
+ test
+
+
+ ch.qos.logback
+ logback-classic
+ 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..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
@@ -65,7 +65,12 @@ 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.
+ // 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
LOG.error("Unable to send failure response to {}.", responseUrl, e);
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..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
@@ -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
@@ -53,14 +57,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();
@@ -111,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;
@@ -195,23 +213,34 @@ 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,
Context context,
Response resp) throws CustomResourceResponseException {
try {
+ 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);
+ 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());
} else {
- ResponseBody body = new ResponseBody(
- event, context, resp.getStatus(), resp.getPhysicalResourceId(), resp.isNoEcho());
+
+ 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());
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/main/java/software/amazon/lambda/powertools/cloudformation/Response.java b/powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java
index 3ae6b9296..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
@@ -138,23 +138,71 @@ public static Builder builder() {
}
/**
- * Creates an empty, failed Response.
+ * 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.
+ *
+ * @deprecated this method is not safe. Provide a physicalResourceId.
* @return a failed Response with no value.
*/
+ @Deprecated
public static Response failed() {
return new Response(null, Status.FAILED, null, false);
}
/**
- * Creates an empty, successful 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(String physicalResourceId) {
+ return new Response(null, Status.FAILED, physicalResourceId, false);
+ }
+
+ /**
+ * Creates a successful 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.
+ *
+ * @deprecated this method is not safe. Provide a physicalResourceId.
+ * @return a success Response with no physicalResourceId value.
+ */
+ @Deprecated
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/CloudFormationIntegrationTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java
new file mode 100644
index 000000000..06463308c
--- /dev/null
+++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationIntegrationTest.java
@@ -0,0 +1,247 @@
+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 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;
+
+import static com.github.tomakehurst.wiremock.client.WireMock.*;
+import static org.assertj.core.api.Assertions.assertThat;
+
+@WireMockTest
+public class CloudFormationIntegrationTest {
+
+ public static final String PHYSICAL_RESOURCE_ID = UUID.randomUUID().toString();
+ public static final String LOG_STREAM_NAME = "FakeLogStreamName";
+
+ @ParameterizedTest
+ @ValueSource(strings = {"Update", "Delete"})
+ void physicalResourceIdTakenFromRequestForUpdateOrDeleteWhenUserSpecifiesNull(String requestType, WireMockRuntimeInfo wmRuntimeInfo) {
+ stubFor(put("/").willReturn(ok()));
+
+ NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler();
+ 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')]"))
+ .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]"))
+ );
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"Update", "Delete"})
+ void physicalResourceIdDoesNotChangeWhenRuntimeExceptionThrownWhenUpdatingOrDeleting(String requestType, WireMockRuntimeInfo wmRuntimeInfo) {
+ stubFor(put("/").willReturn(ok()));
+
+ RuntimeExceptionThrownHandler handler = new RuntimeExceptionThrownHandler();
+ 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')]"))
+ .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]"))
+ );
+ }
+
+ @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 + "')]"))
+ );
+ }
+
+ @ParameterizedTest
+ @ValueSource(strings = {"Update", "Delete"})
+ void physicalResourceIdSetFromRequestOnUpdateOrDeleteWhenCustomerDoesntProvideAPhysicalResourceId(String requestType, WireMockRuntimeInfo wmRuntimeInfo) {
+ stubFor(put("/").willReturn(ok()));
+
+ NoPhysicalResourceIdSetHandler handler = new NoPhysicalResourceIdSetHandler();
+ int httpPort = wmRuntimeInfo.getHttpPort();
+
+ CloudFormationCustomResourceEvent event = baseEvent(httpPort)
+ .withPhysicalResourceId(PHYSICAL_RESOURCE_ID)
+ .withRequestType(requestType)
+ .build();
+
+ Response response = handler.handleRequest(event, new FakeContext());
+
+ assertThat(response).isNotNull();
+ verify(putRequestedFor(urlPathMatching("/"))
+ .withRequestBody(matchingJsonPath("[?(@.Status == 'SUCCESS')]"))
+ .withRequestBody(matchingJsonPath("[?(@.PhysicalResourceId == '" + PHYSICAL_RESOURCE_ID + "')]"))
+ );
+ }
+
+ @Test
+ void createNewResourceBecausePhysicalResourceIdNotSetByCustomerOnCreate(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 == '" + LOG_STREAM_NAME + "')]"))
+ );
+ }
+
+ @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);
+
+ 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 {
+ @Override
+ public String getAwsRequestId() {
+ return null;
+ }
+
+ @Override
+ public String getLogGroupName() {
+ return null;
+ }
+
+ @Override
+ public String getLogStreamName() {
+ return LOG_STREAM_NAME;
+ }
+
+ @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/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/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();
+ }
+}
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);
+ }
+}
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
new file mode 100644
index 000000000..ee5be77b8
--- /dev/null
+++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/handlers/RuntimeExceptionThrownHandler.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 RuntimeExceptionThrownHandler extends AbstractCustomResourceHandler {
+
+ @Override
+ protected Response create(CloudFormationCustomResourceEvent event, Context context) {
+ throw new RuntimeException("failure");
+ }
+
+ @Override
+ protected Response update(CloudFormationCustomResourceEvent event, Context context) {
+ throw new RuntimeException("failure");
+ }
+
+ @Override
+ protected Response delete(CloudFormationCustomResourceEvent event, Context context) {
+ throw new RuntimeException("failure");
+ }
+}
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