Skip to content
Merged
12 changes: 12 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,18 @@
<version>1.1.1</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.6</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<version>2.35.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>

Expand Down
10 changes: 10 additions & 0 deletions powertools-cloudformation/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.tomakehurst</groupId>
<artifactId>wiremock-jre8</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

</project>
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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();
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -195,23 +213,34 @@ protected Map<String, List<String>> 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);
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
Loading