diff --git a/examples/powertools-examples-core/kotlin/build.gradle.kts b/examples/powertools-examples-core/kotlin/build.gradle.kts index 170e5b6d8..94dcbe071 100644 --- a/examples/powertools-examples-core/kotlin/build.gradle.kts +++ b/examples/powertools-examples-core/kotlin/build.gradle.kts @@ -14,9 +14,9 @@ dependencies { implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2") implementation("com.amazonaws:aws-lambda-java-events:3.11.0") implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2") - aspect("software.amazon.lambda:powertools-tracing:1.18.0") - aspect("software.amazon.lambda:powertools-logging:1.18.0") - aspect("software.amazon.lambda:powertools-metrics:1.18.0") + aspect("software.amazon.lambda:powertools-tracing:1.19.0") + aspect("software.amazon.lambda:powertools-logging:1.19.0") + aspect("software.amazon.lambda:powertools-metrics:1.19.0") testImplementation("junit:junit:4.13.2") implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8") } @@ -36,4 +36,4 @@ tasks.compileTestKotlin { // If using JDK 11 or higher, use the following instead: //kotlin { // jvmToolchain(11) -//} \ 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 2f020aa25..404137802 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 @@ -36,6 +36,7 @@ import software.amazon.awssdk.http.SdkHttpMethod; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.utils.StringInputStream; +import software.amazon.awssdk.utils.StringUtils; /** * Client for sending responses to AWS CloudFormation custom resources by way of a response URL, which is an Amazon S3 @@ -148,7 +149,9 @@ StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event, ObjectNode node = body.toObjectNode(null); return new StringInputStream(node.toString()); } else { - + if (!StringUtils.isBlank(resp.getReason())) { + reason = resp.getReason(); + } String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() : event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() : context.getLogStreamName(); 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 fe18000d4..94372ac97 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 @@ -19,6 +19,7 @@ import java.util.HashMap; import java.util.Map; import java.util.stream.Collectors; +import software.amazon.awssdk.utils.StringUtils; /** * Models the arbitrary data to be sent to the custom resource in response to a CloudFormation event. This object @@ -30,12 +31,22 @@ public class Response { private final Status status; private final String physicalResourceId; private final boolean noEcho; + private final String reason; private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho) { this.jsonNode = jsonNode; this.status = status; this.physicalResourceId = physicalResourceId; this.noEcho = noEcho; + this.reason = null; + } + + private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho, String reason) { + this.jsonNode = jsonNode; + this.status = status; + this.physicalResourceId = physicalResourceId; + this.noEcho = noEcho; + this.reason = reason; } /** @@ -149,6 +160,15 @@ public boolean isNoEcho() { return noEcho; } + /** + * The reason for the failure. + * + * @return a potentially null reason + */ + public String getReason() { + return reason; + } + /** * Includes all Response attributes, including its value in JSON format * @@ -161,6 +181,7 @@ public String toString() { attributes.put("Status", status); attributes.put("PhysicalResourceId", physicalResourceId); attributes.put("NoEcho", noEcho); + attributes.put("Reason", reason); return attributes.entrySet().stream() .map(entry -> entry.getKey() + " = " + entry.getValue()) .collect(Collectors.joining(",", "[", "]")); @@ -182,6 +203,7 @@ public static class Builder { private Status status; private String physicalResourceId; private boolean noEcho; + private String reason; private Builder() { } @@ -263,6 +285,20 @@ public Builder noEcho(boolean noEcho) { return this; } + /** + * Reason for the response. + * Reason is optional for Success responses, but required for Failed responses. + * If not provided it will be replaced with cloudwatch log stream name. + * + * @param reason if null, the default reason will be used + * @return a reference to this builder + */ + + public Builder reason(String reason) { + this.reason = reason; + return this; + } + /** * Builds a Response object for the value. * @@ -277,6 +313,9 @@ public Response build() { node = mapper.valueToTree(value); } Status responseStatus = this.status != null ? this.status : Status.SUCCESS; + if (StringUtils.isNotBlank(this.reason)) { + return new Response(node, responseStatus, physicalResourceId, noEcho, reason); + } return new Response(node, responseStatus, physicalResourceId, noEcho); } } 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 51f0e95f9..938de74d8 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 @@ -324,4 +324,27 @@ void responseBodyStreamFailedResponse() throws Exception { "}"; assertThat(stream.getString()).isEqualTo(expectedJson); } + + @Test + void responseBodyStreamFailedResponseWithReason() throws Exception { + CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent(); + Context context = mock(Context.class); + CloudFormationResponse cfnResponse = testableCloudFormationResponse(); + String failureReason = "Failed test reason"; + Response failedResponseWithReason = Response.builder(). + status(Response.Status.FAILED).reason(failureReason).build(); + StringInputStream stream = cfnResponse.responseBodyStream(event, context, failedResponseWithReason); + + String expectedJson = "{" + + "\"Status\":\"FAILED\"," + + "\"Reason\":\"" + failureReason + "\"," + + "\"PhysicalResourceId\":null," + + "\"StackId\":null," + + "\"RequestId\":null," + + "\"LogicalResourceId\":null," + + "\"NoEcho\":false," + + "\"Data\":null" + + "}"; + assertThat(stream.getString()).isEqualTo(expectedJson); + } } diff --git a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java index 37fe73d0f..42457d918 100644 --- a/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java +++ b/powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java @@ -33,11 +33,13 @@ void defaultValues() { assertThat(response.getStatus()).isEqualTo(Response.Status.SUCCESS); assertThat(response.getPhysicalResourceId()).isNull(); assertThat(response.isNoEcho()).isFalse(); + assertThat(response.getReason()).isNull(); assertThat(response.toString()).contains("JSON = null"); assertThat(response.toString()).contains("Status = SUCCESS"); assertThat(response.toString()).contains("PhysicalResourceId = null"); assertThat(response.toString()).contains("NoEcho = false"); + assertThat(response.toString()).contains("Reason = null"); } @Test @@ -61,6 +63,27 @@ void explicitNullValues() { assertThat(response.toString()).contains("NoEcho = false"); } + @Test + void explicitReasonWithDefaultValues() { + String reason = "test"; + Response response = Response.builder() + .reason(reason) + .build(); + assertThat(response).isNotNull(); + assertThat(response.getJsonNode()).isNull(); + assertThat(response.getStatus()).isEqualTo(Response.Status.SUCCESS); + assertThat(response.getPhysicalResourceId()).isNull(); + assertThat(response.isNoEcho()).isFalse(); + assertThat(response.getReason()).isNotNull(); + assertThat(response.getReason()).isEqualTo(reason); + + assertThat(response.toString()).contains("JSON = null"); + assertThat(response.toString()).contains("Status = SUCCESS"); + assertThat(response.toString()).contains("PhysicalResourceId = null"); + assertThat(response.toString()).contains("NoEcho = false"); + assertThat(response.toString()).contains("Reason = "+reason); + } + @Test void customNonJsonRelatedValues() { Response response = Response.builder()