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