Skip to content

Commit 7434bf9

Browse files
moizshMoiz Sharafphipag
authored
feat(cfn-custom-resource): Add optional 'reason' field for detailed failure reporting (#1758)
* feat(cfn-custom-resource): add optional 'reason' field for detailed failure reporting in CloudFormation custom resource responses --------- Co-authored-by: Moiz Sharaf <[email protected]> Co-authored-by: Philipp Page <[email protected]>
1 parent 4061087 commit 7434bf9

File tree

5 files changed

+93
-5
lines changed

5 files changed

+93
-5
lines changed

examples/powertools-examples-core/kotlin/build.gradle.kts

+4-4
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,9 @@ dependencies {
1414
implementation("com.fasterxml.jackson.core:jackson-databind:2.17.2")
1515
implementation("com.amazonaws:aws-lambda-java-events:3.11.0")
1616
implementation("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.17.2")
17-
aspect("software.amazon.lambda:powertools-tracing:1.18.0")
18-
aspect("software.amazon.lambda:powertools-logging:1.18.0")
19-
aspect("software.amazon.lambda:powertools-metrics:1.18.0")
17+
aspect("software.amazon.lambda:powertools-tracing:1.19.0")
18+
aspect("software.amazon.lambda:powertools-logging:1.19.0")
19+
aspect("software.amazon.lambda:powertools-metrics:1.19.0")
2020
testImplementation("junit:junit:4.13.2")
2121
implementation("org.jetbrains.kotlin:kotlin-stdlib-jdk8")
2222
}
@@ -36,4 +36,4 @@ tasks.compileTestKotlin {
3636
// If using JDK 11 or higher, use the following instead:
3737
//kotlin {
3838
// jvmToolchain(11)
39-
//}
39+
//}

powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponse.java

+4-1
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,7 @@
3636
import software.amazon.awssdk.http.SdkHttpMethod;
3737
import software.amazon.awssdk.http.SdkHttpRequest;
3838
import software.amazon.awssdk.utils.StringInputStream;
39+
import software.amazon.awssdk.utils.StringUtils;
3940

4041
/**
4142
* 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,
148149
ObjectNode node = body.toObjectNode(null);
149150
return new StringInputStream(node.toString());
150151
} else {
151-
152+
if (!StringUtils.isBlank(resp.getReason())) {
153+
reason = resp.getReason();
154+
}
152155
String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() :
153156
event.getPhysicalResourceId() != null ? event.getPhysicalResourceId() :
154157
context.getLogStreamName();

powertools-cloudformation/src/main/java/software/amazon/lambda/powertools/cloudformation/Response.java

+39
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
import java.util.HashMap;
2020
import java.util.Map;
2121
import java.util.stream.Collectors;
22+
import software.amazon.awssdk.utils.StringUtils;
2223

2324
/**
2425
* 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 {
3031
private final Status status;
3132
private final String physicalResourceId;
3233
private final boolean noEcho;
34+
private final String reason;
3335

3436
private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho) {
3537
this.jsonNode = jsonNode;
3638
this.status = status;
3739
this.physicalResourceId = physicalResourceId;
3840
this.noEcho = noEcho;
41+
this.reason = null;
42+
}
43+
44+
private Response(JsonNode jsonNode, Status status, String physicalResourceId, boolean noEcho, String reason) {
45+
this.jsonNode = jsonNode;
46+
this.status = status;
47+
this.physicalResourceId = physicalResourceId;
48+
this.noEcho = noEcho;
49+
this.reason = reason;
3950
}
4051

4152
/**
@@ -149,6 +160,15 @@ public boolean isNoEcho() {
149160
return noEcho;
150161
}
151162

163+
/**
164+
* The reason for the failure.
165+
*
166+
* @return a potentially null reason
167+
*/
168+
public String getReason() {
169+
return reason;
170+
}
171+
152172
/**
153173
* Includes all Response attributes, including its value in JSON format
154174
*
@@ -161,6 +181,7 @@ public String toString() {
161181
attributes.put("Status", status);
162182
attributes.put("PhysicalResourceId", physicalResourceId);
163183
attributes.put("NoEcho", noEcho);
184+
attributes.put("Reason", reason);
164185
return attributes.entrySet().stream()
165186
.map(entry -> entry.getKey() + " = " + entry.getValue())
166187
.collect(Collectors.joining(",", "[", "]"));
@@ -182,6 +203,7 @@ public static class Builder {
182203
private Status status;
183204
private String physicalResourceId;
184205
private boolean noEcho;
206+
private String reason;
185207

186208
private Builder() {
187209
}
@@ -263,6 +285,20 @@ public Builder noEcho(boolean noEcho) {
263285
return this;
264286
}
265287

288+
/**
289+
* Reason for the response.
290+
* Reason is optional for Success responses, but required for Failed responses.
291+
* If not provided it will be replaced with cloudwatch log stream name.
292+
*
293+
* @param reason if null, the default reason will be used
294+
* @return a reference to this builder
295+
*/
296+
297+
public Builder reason(String reason) {
298+
this.reason = reason;
299+
return this;
300+
}
301+
266302
/**
267303
* Builds a Response object for the value.
268304
*
@@ -277,6 +313,9 @@ public Response build() {
277313
node = mapper.valueToTree(value);
278314
}
279315
Status responseStatus = this.status != null ? this.status : Status.SUCCESS;
316+
if (StringUtils.isNotBlank(this.reason)) {
317+
return new Response(node, responseStatus, physicalResourceId, noEcho, reason);
318+
}
280319
return new Response(node, responseStatus, physicalResourceId, noEcho);
281320
}
282321
}

powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/CloudFormationResponseTest.java

+23
Original file line numberDiff line numberDiff line change
@@ -324,4 +324,27 @@ void responseBodyStreamFailedResponse() throws Exception {
324324
"}";
325325
assertThat(stream.getString()).isEqualTo(expectedJson);
326326
}
327+
328+
@Test
329+
void responseBodyStreamFailedResponseWithReason() throws Exception {
330+
CloudFormationCustomResourceEvent event = mockCloudFormationCustomResourceEvent();
331+
Context context = mock(Context.class);
332+
CloudFormationResponse cfnResponse = testableCloudFormationResponse();
333+
String failureReason = "Failed test reason";
334+
Response failedResponseWithReason = Response.builder().
335+
status(Response.Status.FAILED).reason(failureReason).build();
336+
StringInputStream stream = cfnResponse.responseBodyStream(event, context, failedResponseWithReason);
337+
338+
String expectedJson = "{" +
339+
"\"Status\":\"FAILED\"," +
340+
"\"Reason\":\"" + failureReason + "\"," +
341+
"\"PhysicalResourceId\":null," +
342+
"\"StackId\":null," +
343+
"\"RequestId\":null," +
344+
"\"LogicalResourceId\":null," +
345+
"\"NoEcho\":false," +
346+
"\"Data\":null" +
347+
"}";
348+
assertThat(stream.getString()).isEqualTo(expectedJson);
349+
}
327350
}

powertools-cloudformation/src/test/java/software/amazon/lambda/powertools/cloudformation/ResponseTest.java

+23
Original file line numberDiff line numberDiff line change
@@ -33,11 +33,13 @@ void defaultValues() {
3333
assertThat(response.getStatus()).isEqualTo(Response.Status.SUCCESS);
3434
assertThat(response.getPhysicalResourceId()).isNull();
3535
assertThat(response.isNoEcho()).isFalse();
36+
assertThat(response.getReason()).isNull();
3637

3738
assertThat(response.toString()).contains("JSON = null");
3839
assertThat(response.toString()).contains("Status = SUCCESS");
3940
assertThat(response.toString()).contains("PhysicalResourceId = null");
4041
assertThat(response.toString()).contains("NoEcho = false");
42+
assertThat(response.toString()).contains("Reason = null");
4143
}
4244

4345
@Test
@@ -61,6 +63,27 @@ void explicitNullValues() {
6163
assertThat(response.toString()).contains("NoEcho = false");
6264
}
6365

66+
@Test
67+
void explicitReasonWithDefaultValues() {
68+
String reason = "test";
69+
Response response = Response.builder()
70+
.reason(reason)
71+
.build();
72+
assertThat(response).isNotNull();
73+
assertThat(response.getJsonNode()).isNull();
74+
assertThat(response.getStatus()).isEqualTo(Response.Status.SUCCESS);
75+
assertThat(response.getPhysicalResourceId()).isNull();
76+
assertThat(response.isNoEcho()).isFalse();
77+
assertThat(response.getReason()).isNotNull();
78+
assertThat(response.getReason()).isEqualTo(reason);
79+
80+
assertThat(response.toString()).contains("JSON = null");
81+
assertThat(response.toString()).contains("Status = SUCCESS");
82+
assertThat(response.toString()).contains("PhysicalResourceId = null");
83+
assertThat(response.toString()).contains("NoEcho = false");
84+
assertThat(response.toString()).contains("Reason = "+reason);
85+
}
86+
6487
@Test
6588
void customNonJsonRelatedValues() {
6689
Response response = Response.builder()

0 commit comments

Comments
 (0)