Skip to content

Commit b062a76

Browse files
authored
fix(cloudformation-module): Use physicalResourceId when not provided by custom resource (#1082)
1 parent 28113a1 commit b062a76

File tree

11 files changed

+460
-32
lines changed

11 files changed

+460
-32
lines changed

pom.xml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -283,6 +283,18 @@
283283
<version>1.1.1</version>
284284
<scope>test</scope>
285285
</dependency>
286+
<dependency>
287+
<groupId>ch.qos.logback</groupId>
288+
<artifactId>logback-classic</artifactId>
289+
<version>1.2.6</version>
290+
<scope>test</scope>
291+
</dependency>
292+
<dependency>
293+
<groupId>com.github.tomakehurst</groupId>
294+
<artifactId>wiremock-jre8</artifactId>
295+
<version>2.35.0</version>
296+
<scope>test</scope>
297+
</dependency>
286298
</dependencies>
287299
</dependencyManagement>
288300

powertools-cloudformation/pom.xml

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,16 @@
9393
<artifactId>assertj-core</artifactId>
9494
<scope>test</scope>
9595
</dependency>
96+
<dependency>
97+
<groupId>com.github.tomakehurst</groupId>
98+
<artifactId>wiremock-jre8</artifactId>
99+
<scope>test</scope>
100+
</dependency>
101+
<dependency>
102+
<groupId>ch.qos.logback</groupId>
103+
<artifactId>logback-classic</artifactId>
104+
<scope>test</scope>
105+
</dependency>
96106
</dependencies>
97107

98108
</project>

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

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,12 @@ public final Response handleRequest(CloudFormationCustomResourceEvent event, Con
6565
} catch (CustomResourceResponseException rse) {
6666
LOG.error("Unable to generate response. Sending empty failure to {}", responseUrl, rse);
6767
try {
68-
client.send(event, context, Response.failed());
68+
// If the customers code throws an exception, Powertools should respond in a way that doesn't
69+
// change the CloudFormation resources.
70+
// In the case of a Update or Delete, a failure is sent with the existing PhysicalResourceId
71+
// indicating no change.
72+
// In the case of a Create, null will be set and changed to the Lambda LogStreamName before sending.
73+
client.send(event, context, Response.failed(event.getPhysicalResourceId()));
6974
} catch (Exception e) {
7075
// unable to generate response AND send the failure
7176
LOG.error("Unable to send failure response to {}.", responseUrl, e);

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

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@
66
import com.fasterxml.jackson.databind.ObjectMapper;
77
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
88
import com.fasterxml.jackson.databind.node.ObjectNode;
9+
import org.slf4j.Logger;
10+
import org.slf4j.LoggerFactory;
911
import software.amazon.awssdk.http.Header;
1012
import software.amazon.awssdk.http.HttpExecuteRequest;
1113
import software.amazon.awssdk.http.HttpExecuteResponse;
@@ -32,6 +34,8 @@
3234
*/
3335
class CloudFormationResponse {
3436

37+
private static final Logger LOG = LoggerFactory.getLogger(CloudFormationResponse.class);
38+
3539
/**
3640
* Internal representation of the payload to be sent to the event target URL. Retains all properties of the payload
3741
* except for "Data". This is done so that the serialization of the non-"Data" properties and the serialization of
@@ -53,14 +57,14 @@ static class ResponseBody {
5357
private final boolean noEcho;
5458

5559
ResponseBody(CloudFormationCustomResourceEvent event,
56-
Context context,
5760
Response.Status responseStatus,
5861
String physicalResourceId,
59-
boolean noEcho) {
62+
boolean noEcho,
63+
String reason) {
6064
Objects.requireNonNull(event, "CloudFormationCustomResourceEvent cannot be null");
61-
Objects.requireNonNull(context, "Context cannot be null");
62-
this.physicalResourceId = physicalResourceId != null ? physicalResourceId : context.getLogStreamName();
63-
this.reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName();
65+
66+
this.physicalResourceId = physicalResourceId;
67+
this.reason = reason;
6468
this.status = responseStatus == null ? Response.Status.SUCCESS.name() : responseStatus.name();
6569
this.stackId = event.getStackId();
6670
this.requestId = event.getRequestId();
@@ -111,6 +115,20 @@ ObjectNode toObjectNode(JsonNode dataNode) {
111115
}
112116
return node;
113117
}
118+
119+
@Override
120+
public String toString() {
121+
final StringBuffer sb = new StringBuffer("ResponseBody{");
122+
sb.append("status='").append(status).append('\'');
123+
sb.append(", reason='").append(reason).append('\'');
124+
sb.append(", physicalResourceId='").append(physicalResourceId).append('\'');
125+
sb.append(", stackId='").append(stackId).append('\'');
126+
sb.append(", requestId='").append(requestId).append('\'');
127+
sb.append(", logicalResourceId='").append(logicalResourceId).append('\'');
128+
sb.append(", noEcho=").append(noEcho);
129+
sb.append('}');
130+
return sb.toString();
131+
}
114132
}
115133

116134
private final SdkHttpClient client;
@@ -195,23 +213,34 @@ protected Map<String, List<String>> headers(int contentLength) {
195213
/**
196214
* Returns the response body as an input stream, for supplying with the HTTP request to the custom resource.
197215
*
216+
* If PhysicalResourceId is null at this point it will be replaced with the Lambda LogStreamName.
217+
*
198218
* @throws CustomResourceResponseException if unable to generate the response stream
199219
*/
200220
StringInputStream responseBodyStream(CloudFormationCustomResourceEvent event,
201221
Context context,
202222
Response resp) throws CustomResourceResponseException {
203223
try {
224+
String reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName();
204225
if (resp == null) {
205-
ResponseBody body = new ResponseBody(event, context, Response.Status.SUCCESS, null, false);
226+
String physicalResourceId = event.getPhysicalResourceId() != null? event.getPhysicalResourceId() : context.getLogStreamName();
227+
228+
ResponseBody body = new ResponseBody(event, Response.Status.SUCCESS, physicalResourceId, false, reason);
229+
LOG.debug("ResponseBody: {}", body);
206230
ObjectNode node = body.toObjectNode(null);
207231
return new StringInputStream(node.toString());
208232
} else {
209-
ResponseBody body = new ResponseBody(
210-
event, context, resp.getStatus(), resp.getPhysicalResourceId(), resp.isNoEcho());
233+
234+
String physicalResourceId = resp.getPhysicalResourceId() != null ? resp.getPhysicalResourceId() :
235+
event.getPhysicalResourceId() != null? event.getPhysicalResourceId() : context.getLogStreamName();
236+
237+
ResponseBody body = new ResponseBody(event, resp.getStatus(), physicalResourceId, resp.isNoEcho(), reason);
238+
LOG.debug("ResponseBody: {}", body);
211239
ObjectNode node = body.toObjectNode(resp.getJsonNode());
212240
return new StringInputStream(node.toString());
213241
}
214242
} catch (RuntimeException e) {
243+
LOG.error(e.getMessage());
215244
throw new CustomResourceResponseException("Unable to generate response body.", e);
216245
}
217246
}

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

Lines changed: 51 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -138,23 +138,71 @@ public static Builder builder() {
138138
}
139139

140140
/**
141-
* Creates an empty, failed Response.
141+
* Creates a failed Response with no physicalResourceId set. Powertools will set the physicalResourceId to the
142+
* Lambda LogStreamName
142143
*
144+
* The value returned for a PhysicalResourceId can change custom resource update operations. If the value returned
145+
* is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes
146+
* the update as a replacement and sends a delete request to the old resource. For more information,
147+
* see AWS::CloudFormation::CustomResource.
148+
*
149+
* @deprecated this method is not safe. Provide a physicalResourceId.
143150
* @return a failed Response with no value.
144151
*/
152+
@Deprecated
145153
public static Response failed() {
146154
return new Response(null, Status.FAILED, null, false);
147155
}
148156

149157
/**
150-
* Creates an empty, successful Response.
158+
* Creates a failed Response with a given physicalResourceId.
151159
*
152-
* @return a failed Response with no value.
160+
* @param physicalResourceId The value must be a non-empty string and must be identical for all responses for the
161+
* same resource.
162+
* The value returned for a PhysicalResourceId can change custom resource update
163+
* operations. If the value returned is the same, it is considered a normal update. If the
164+
* value returned is different, AWS CloudFormation recognizes the update as a replacement
165+
* and sends a delete request to the old resource. For more information,
166+
* see AWS::CloudFormation::CustomResource.
167+
* @return a failed Response with physicalResourceId
153168
*/
169+
public static Response failed(String physicalResourceId) {
170+
return new Response(null, Status.FAILED, physicalResourceId, false);
171+
}
172+
173+
/**
174+
* Creates a successful Response with no physicalResourceId set. Powertools will set the physicalResourceId to the
175+
* Lambda LogStreamName
176+
*
177+
* The value returned for a PhysicalResourceId can change custom resource update operations. If the value returned
178+
* is the same, it is considered a normal update. If the value returned is different, AWS CloudFormation recognizes
179+
* the update as a replacement and sends a delete request to the old resource. For more information,
180+
* see AWS::CloudFormation::CustomResource.
181+
*
182+
* @deprecated this method is not safe. Provide a physicalResourceId.
183+
* @return a success Response with no physicalResourceId value.
184+
*/
185+
@Deprecated
154186
public static Response success() {
155187
return new Response(null, Status.SUCCESS, null, false);
156188
}
157189

190+
/**
191+
* Creates a successful Response with a given physicalResourceId.
192+
*
193+
* @param physicalResourceId The value must be a non-empty string and must be identical for all responses for the
194+
* same resource.
195+
* The value returned for a PhysicalResourceId can change custom resource update
196+
* operations. If the value returned is the same, it is considered a normal update. If the
197+
* value returned is different, AWS CloudFormation recognizes the update as a replacement
198+
* and sends a delete request to the old resource. For more information,
199+
* see AWS::CloudFormation::CustomResource.
200+
* @return a success Response with physicalResourceId
201+
*/
202+
public static Response success(String physicalResourceId) {
203+
return new Response(null, Status.SUCCESS, physicalResourceId, false);
204+
}
205+
158206
private final JsonNode jsonNode;
159207
private final Status status;
160208
private final String physicalResourceId;

0 commit comments

Comments
 (0)