Skip to content

fix(cloudformation-module): Use physicalResourceId when not provided by custom resource #1082

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 15 commits into from
Mar 14, 2023
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 @@ -146,7 +164,7 @@ SdkHttpClient getClient() {
*/
public HttpExecuteResponse send(CloudFormationCustomResourceEvent event,
Context context) throws IOException, CustomResourceResponseException {
return send(event, context, null);
return send(event, context, Response.success(context.getLogGroupName()));
}

/**
Expand Down Expand Up @@ -195,23 +213,32 @@ 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);
ResponseBody body = new ResponseBody(event, Response.Status.SUCCESS, context.getLogStreamName(), 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,69 @@ 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.
*
* @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.
*
* @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