Skip to content

Commit b087302

Browse files
author
Joe Wolf
committed
Create Java cfn-response equivalent (aws-powertools#558)
Introduces Java implementation of cfn-response module based on NodeJS design. Put the sole class in a separate powertools-cloudformation Maven module. 100% code coverage Addresses aws-powertools#558
1 parent 1e8e9e3 commit b087302

File tree

4 files changed

+561
-0
lines changed

4 files changed

+561
-0
lines changed

pom.xml

+6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
<module>powertools-metrics</module>
3535
<module>powertools-parameters</module>
3636
<module>powertools-validation</module>
37+
<module>powertools-cloudformation</module>
3738
</modules>
3839

3940
<scm>
@@ -105,6 +106,11 @@
105106
<type>pom</type>
106107
<scope>import</scope>
107108
</dependency>
109+
<dependency>
110+
<groupId>software.amazon.awssdk</groupId>
111+
<artifactId>http-client-spi</artifactId>
112+
<version>${aws.sdk.version}</version>
113+
</dependency>
108114
<dependency>
109115
<groupId>io.burt</groupId>
110116
<artifactId>jmespath-jackson</artifactId>

powertools-cloudformation/pom.xml

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
<?xml version="1.0" encoding="UTF-8"?>
2+
<project xmlns="http://maven.apache.org/POM/4.0.0"
3+
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
4+
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
5+
<modelVersion>4.0.0</modelVersion>
6+
7+
<artifactId>powertools-cloudformation</artifactId>
8+
<packaging>jar</packaging>
9+
10+
<parent>
11+
<artifactId>powertools-parent</artifactId>
12+
<groupId>software.amazon.lambda</groupId>
13+
<version>1.7.3</version>
14+
</parent>
15+
16+
<name>AWS Lambda Powertools Java library Cloudformation</name>
17+
<description>
18+
A suite of utilities for AWS Lambda Functions that makes tracing with AWS X-Ray, structured logging and creating
19+
custom metrics asynchronously easier.
20+
</description>
21+
<url>https://aws.amazon.com/lambda/</url>
22+
<issueManagement>
23+
<system>GitHub Issues</system>
24+
<url>https://github.com/awslabs/aws-lambda-powertools-java/issues</url>
25+
</issueManagement>
26+
<scm>
27+
<url>https://github.com/awslabs/aws-lambda-powertools-java.git</url>
28+
</scm>
29+
<developers>
30+
<developer>
31+
<name>AWS Lambda Powertools team</name>
32+
<organization>Amazon Web Services</organization>
33+
<organizationUrl>https://aws.amazon.com/</organizationUrl>
34+
</developer>
35+
</developers>
36+
37+
<distributionManagement>
38+
<snapshotRepository>
39+
<id>ossrh</id>
40+
<url>https://aws.oss.sonatype.org/content/repositories/snapshots</url>
41+
</snapshotRepository>
42+
</distributionManagement>
43+
44+
<dependencies>
45+
<dependency>
46+
<groupId>software.amazon.awssdk</groupId>
47+
<artifactId>http-client-spi</artifactId>
48+
</dependency>
49+
<dependency>
50+
<groupId>com.amazonaws</groupId>
51+
<artifactId>aws-lambda-java-core</artifactId>
52+
</dependency>
53+
<dependency>
54+
<groupId>com.amazonaws</groupId>
55+
<artifactId>aws-lambda-java-events</artifactId>
56+
</dependency>
57+
<dependency>
58+
<groupId>com.fasterxml.jackson.core</groupId>
59+
<artifactId>jackson-databind</artifactId>
60+
</dependency>
61+
<dependency>
62+
<groupId>org.aspectj</groupId>
63+
<artifactId>aspectjrt</artifactId>
64+
</dependency>
65+
66+
<!-- Test dependencies -->
67+
<dependency>
68+
<groupId>org.junit.jupiter</groupId>
69+
<artifactId>junit-jupiter-api</artifactId>
70+
<scope>test</scope>
71+
</dependency>
72+
<dependency>
73+
<groupId>org.junit.jupiter</groupId>
74+
<artifactId>junit-jupiter-engine</artifactId>
75+
<scope>test</scope>
76+
</dependency>
77+
<dependency>
78+
<groupId>org.mockito</groupId>
79+
<artifactId>mockito-core</artifactId>
80+
<scope>test</scope>
81+
</dependency>
82+
<dependency>
83+
<groupId>org.assertj</groupId>
84+
<artifactId>assertj-core</artifactId>
85+
<scope>test</scope>
86+
</dependency>
87+
</dependencies>
88+
89+
</project>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,249 @@
1+
package software.amazon.lambda.powertools.cloudformation;
2+
3+
import com.amazonaws.services.lambda.runtime.Context;
4+
import com.amazonaws.services.lambda.runtime.events.CloudFormationCustomResourceEvent;
5+
import com.fasterxml.jackson.databind.ObjectMapper;
6+
import com.fasterxml.jackson.databind.PropertyNamingStrategies;
7+
import software.amazon.awssdk.http.Header;
8+
import software.amazon.awssdk.http.HttpExecuteRequest;
9+
import software.amazon.awssdk.http.HttpExecuteResponse;
10+
import software.amazon.awssdk.http.SdkHttpClient;
11+
import software.amazon.awssdk.http.SdkHttpMethod;
12+
import software.amazon.awssdk.http.SdkHttpRequest;
13+
import software.amazon.awssdk.utils.StringInputStream;
14+
15+
import java.io.IOException;
16+
import java.net.URI;
17+
import java.util.Collections;
18+
import java.util.HashMap;
19+
import java.util.List;
20+
import java.util.Map;
21+
22+
/**
23+
* Client for sending responses to AWS CloudFormation custom resources by way of a response URL, which is an Amazon S3
24+
* pre-signed URL.
25+
* <p>
26+
* See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/cfn-lambda-function-code-cfnresponsemodule.html
27+
*/
28+
public class CloudFormationResponse {
29+
30+
/**
31+
* Indicates if the function invoking send had successfully completed.
32+
*/
33+
public enum ResponseStatus {
34+
SUCCESS, FAILED
35+
}
36+
37+
/**
38+
* Representation of the payload to be sent to the event target URL.
39+
*/
40+
@SuppressWarnings("unused")
41+
static class ResponseBody {
42+
private final String status;
43+
private final String reason;
44+
private final String physicalResourceId;
45+
private final String stackId;
46+
private final String requestId;
47+
private final String logicalResourceId;
48+
private final boolean noEcho;
49+
private final Object data;
50+
51+
ResponseBody(CloudFormationCustomResourceEvent event,
52+
Context context,
53+
ResponseStatus responseStatus,
54+
Object responseData,
55+
String physicalResourceId,
56+
boolean noEcho) {
57+
requireNonNull(event, "CloudFormationCustomResourceEvent");
58+
requireNonNull(context, "Context");
59+
this.physicalResourceId = physicalResourceId != null ? physicalResourceId : context.getLogStreamName();
60+
this.reason = "See the details in CloudWatch Log Stream: " + context.getLogStreamName();
61+
this.status = responseStatus == null ? ResponseStatus.SUCCESS.name() : responseStatus.name();
62+
this.stackId = event.getStackId();
63+
this.requestId = event.getRequestId();
64+
this.logicalResourceId = event.getLogicalResourceId();
65+
this.noEcho = noEcho;
66+
this.data = responseData;
67+
}
68+
69+
public String getStatus() {
70+
return status;
71+
}
72+
73+
public String getReason() {
74+
return reason;
75+
}
76+
77+
public String getPhysicalResourceId() {
78+
return physicalResourceId;
79+
}
80+
81+
public String getStackId() {
82+
return stackId;
83+
}
84+
85+
public String getRequestId() {
86+
return requestId;
87+
}
88+
89+
public String getLogicalResourceId() {
90+
return logicalResourceId;
91+
}
92+
93+
public boolean isNoEcho() {
94+
return noEcho;
95+
}
96+
97+
public Object getData() {
98+
return data;
99+
}
100+
}
101+
102+
/*
103+
* Replace with java.util.Objects::requireNonNull when dropping JDK 8 support.
104+
*/
105+
private static <T> T requireNonNull(T arg, String argName) {
106+
if (arg == null) {
107+
throw new NullPointerException(argName + " cannot be null.");
108+
}
109+
return arg;
110+
}
111+
112+
private final SdkHttpClient client;
113+
private final ObjectMapper mapper;
114+
115+
/**
116+
* Creates a new CloudFormationResponse that uses the provided HTTP client and default JSON serialization format.
117+
*
118+
* @param client HTTP client to use for sending requests; cannot be null
119+
*/
120+
public CloudFormationResponse(SdkHttpClient client) {
121+
this(client, new ObjectMapper()
122+
.setPropertyNamingStrategy(new PropertyNamingStrategies.UpperCamelCaseStrategy()));
123+
}
124+
125+
/**
126+
* Creates a new CloudFormationResponse that uses the provided HTTP client and the JSON serialization mapper object.
127+
*
128+
* @param client HTTP client to use for sending requests; cannot be null
129+
* @param mapper for serializing response data; cannot be null
130+
*/
131+
protected CloudFormationResponse(SdkHttpClient client, ObjectMapper mapper) {
132+
this.client = requireNonNull(client, "SdkHttpClient");
133+
this.mapper = requireNonNull(mapper, "ObjectMapper");
134+
}
135+
136+
/**
137+
* Forwards a request containing a custom payload to the target resource specified by the event. The payload is
138+
* formed from the event and context data.
139+
*
140+
* @param event custom CF resource event
141+
* @param context used to specify when the function and any callbacks have completed execution, or to
142+
* access information from within the Lambda execution environment.
143+
* @param status whether the function successfully completed
144+
* @return the response object
145+
* @throws IOException when unable to generate or send the request
146+
*/
147+
public HttpExecuteResponse send(CloudFormationCustomResourceEvent event,
148+
Context context,
149+
ResponseStatus status) throws IOException {
150+
return send(event, context, status, null);
151+
}
152+
153+
/**
154+
* Forwards a request containing a custom payload to the target resource specified by the event. The payload is
155+
* formed from the event, status, and context data.
156+
*
157+
* @param event custom CF resource event
158+
* @param context used to specify when the function and any callbacks have completed execution, or to
159+
* access information from within the Lambda execution environment.
160+
* @param status whether the function successfully completed
161+
* @param responseData response data, e.g. a list of name-value pairs. May be null.
162+
* @return the response object
163+
* @throws IOException when unable to generate or send the request
164+
*/
165+
public HttpExecuteResponse send(CloudFormationCustomResourceEvent event,
166+
Context context,
167+
ResponseStatus status,
168+
Object responseData) throws IOException {
169+
return send(event, context, status, responseData, null);
170+
}
171+
172+
/**
173+
* Forwards a request containing a custom payload to the target resource specified by the event. The payload is
174+
* formed from the event, status, response, and context data.
175+
*
176+
* @param event custom CF resource event
177+
* @param context used to specify when the function and any callbacks have completed execution, or to
178+
* access information from within the Lambda execution environment.
179+
* @param status whether the function successfully completed
180+
* @param responseData response data, e.g. a list of name-value pairs. May be null.
181+
* @param physicalResourceId Optional. The unique identifier of the custom resource that invoked the function. By
182+
* default, the module uses the name of the Amazon CloudWatch Logs log stream that's
183+
* associated with the Lambda function.
184+
* @return the response object
185+
* @throws IOException when unable to generate or send the request
186+
*/
187+
public HttpExecuteResponse send(CloudFormationCustomResourceEvent event,
188+
Context context,
189+
ResponseStatus status,
190+
Object responseData,
191+
String physicalResourceId) throws IOException {
192+
return send(event, context, status, responseData, physicalResourceId, false);
193+
}
194+
195+
/**
196+
* Forwards a request containing a custom payload to the target resource specified by the event. The payload is
197+
* formed from the event, status, response, and context data.
198+
*
199+
* @param event custom CF resource event
200+
* @param context used to specify when the function and any callbacks have completed execution, or to
201+
* access information from within the Lambda execution environment.
202+
* @param status whether the function successfully completed
203+
* @param responseData response data, e.g. a list of name-value pairs. May be null.
204+
* @param physicalResourceId Optional. The unique identifier of the custom resource that invoked the function. By
205+
* default, the module uses the name of the Amazon CloudWatch Logs log stream that's
206+
* associated with the Lambda function.
207+
* @param noEcho Optional. Indicates whether to mask the output of the custom resource when it's
208+
* retrieved by using the Fn::GetAtt function. If set to true, all returned values are
209+
* masked with asterisks (*****), except for information stored in the locations specified
210+
* below. By default, this value is false.
211+
* @return the response object
212+
* @throws IOException when unable to generate or send the request
213+
*/
214+
public HttpExecuteResponse send(CloudFormationCustomResourceEvent event,
215+
Context context,
216+
ResponseStatus status,
217+
Object responseData,
218+
String physicalResourceId,
219+
boolean noEcho) throws IOException {
220+
ResponseBody body = new ResponseBody(event, context, status, responseData, physicalResourceId, noEcho);
221+
URI uri = URI.create(event.getResponseUrl());
222+
223+
// no need to explicitly close in-memory stream
224+
StringInputStream stream = new StringInputStream(mapper.writeValueAsString(body));
225+
SdkHttpRequest request = SdkHttpRequest.builder()
226+
.uri(uri)
227+
.method(SdkHttpMethod.PUT)
228+
.headers(headers(stream.available()))
229+
.build();
230+
HttpExecuteRequest httpExecuteRequest = HttpExecuteRequest.builder()
231+
.request(request)
232+
.contentStreamProvider(() -> stream)
233+
.build();
234+
return client.prepareRequest(httpExecuteRequest).call();
235+
}
236+
237+
/**
238+
* Generates HTTP headers to be supplied in the CloudFormation request.
239+
*
240+
* @param contentLength the length of the payload
241+
* @return HTTP headers
242+
*/
243+
protected Map<String, List<String>> headers(int contentLength) {
244+
Map<String, List<String>> headers = new HashMap<>();
245+
headers.put(Header.CONTENT_TYPE, Collections.emptyList()); // intentionally empty
246+
headers.put(Header.CONTENT_LENGTH, Collections.singletonList(Integer.toString(contentLength)));
247+
return headers;
248+
}
249+
}

0 commit comments

Comments
 (0)