Skip to content
8 changes: 4 additions & 4 deletions examples/powertools-examples-core/kotlin/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
Expand All @@ -36,4 +36,4 @@ tasks.compileTestKotlin {
// If using JDK 11 or higher, use the following instead:
//kotlin {
// jvmToolchain(11)
//}
//}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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;
}

/**
Expand Down Expand Up @@ -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
*
Expand All @@ -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(",", "[", "]"));
Expand All @@ -182,6 +203,7 @@ public static class Builder {
private Status status;
private String physicalResourceId;
private boolean noEcho;
private String reason;

private Builder() {
}
Expand Down Expand Up @@ -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.
*
Expand All @@ -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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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()
Expand Down
Loading