diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000..c150aabe4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM amazonlinux:2017.03.1.20170812 as graalvm +# install graal to amazon linux. +ENV LANG=en_US.UTF-8 + +RUN yum install -y gcc gcc-c++ libc6-dev zlib1g-dev curl bash zlib zlib-devel zip \ + && rm -rf /var/cache/yum + +# Install JDK8 with backported JVMCI +ENV JDK_FILE openjdk-8u242-jvmci-20.1-b01-linux-amd64 +ENV JDK_DIR openjdk1.8.0_242-jvmci-20.1-b01 +RUN curl -4 -L https://github.com/graalvm/openjdk8-jvmci-builder/releases/download/jvmci-20.1-b01/${JDK_FILE}.tar.gz -o /tmp/jdk.tar.gz +RUN tar -zxvf /tmp/jdk.tar.gz -C /tmp \ + && mv /tmp/${JDK_DIR} /usr/lib/jdk +ENV JAVA_HOME /usr/lib/jdk + +# Download and install GraalVM 20.0.0 + native image +ENV GRAAL_VERSION 20.0.0 +ENV GRAAL_FILENAME graalvm-ce-java8-linux-amd64-${GRAAL_VERSION}.tar.gz +RUN curl -4 -L https://github.com/graalvm/graalvm-ce-builds/releases/download/vm-${GRAAL_VERSION}/${GRAAL_FILENAME} -o /tmp/${GRAAL_FILENAME} +RUN tar -zxvf /tmp/${GRAAL_FILENAME} -C /tmp \ + && mv /tmp/graalvm-ce-java8-${GRAAL_VERSION} /usr/lib/graalvm +RUN /usr/lib/graalvm/bin/gu install native-image + +# Download and install maven +ENV MAVEN_VERSION 3.6.3 +RUN curl -4 -L http://apache.forsale.plus/maven/maven-3/3.6.3/binaries/apache-maven-${MAVEN_VERSION}-bin.tar.gz -o /tmp/maven.tar.gz +RUN tar -zxvf /tmp/maven.tar.gz -C /tmp \ + && mv /tmp/apache-maven-${MAVEN_VERSION} /usr/lib/maven + +RUN rm -rf /tmp/* + +ENV PATH /usr/lib/graalvm/bin:/usr/lib/maven/bin:${PATH} + +COPY . /usr/lib/sjc +RUN cd /usr/lib/sjc && mvn install -DskipTests -Djacoco.minCoverage=0.1 + +VOLUME ["/func"] +WORKDIR /func +ENTRYPOINT ["/func/scripts/graalvm-build.sh"] \ No newline at end of file diff --git a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java index d31ae257b..1d896a31a 100644 --- a/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java +++ b/aws-serverless-java-container-core/src/main/java/com/amazonaws/serverless/proxy/internal/LambdaContainerHandler.java @@ -24,7 +24,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectReader; import com.fasterxml.jackson.databind.ObjectWriter; -import com.fasterxml.jackson.module.afterburner.AfterburnerModule; +//import com.fasterxml.jackson.module.afterburner.AfterburnerModule; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -80,9 +80,15 @@ public abstract class LambdaContainerHandler handler; + + public Runtime(SpringBootLambdaContainerHandler handler) { + this(handler, MAX_RETRIES); + } + + public Runtime(SpringBootLambdaContainerHandler handler, int retries) { + maxRetries = retries; + this.handler = handler; + } + + public void start() { + RuntimeClient client = new RuntimeClient(); + start(client); + } + + public void start(RuntimeClient client) throws RuntimeException { + while (true) { + InvocationRequest req = getNextRequest(client); + if (req == null) { + throw new RuntimeException("Could not fetch next event after " + maxRetries + " attempts"); + } + + ByteArrayOutputStream os = new ByteArrayOutputStream(); + try { + handleRequest(req.getEvent(), os, req.getContext()); + } catch (IOException e) { + e.printStackTrace(); + postErrorResponse(client, req.getContext().getAwsRequestId(), e); + } + + postResponse(client, req.getContext().getAwsRequestId(), os); + } + } + + private InvocationRequest getNextRequest(RuntimeClient client) { + InvocationRequest req; + for (int i = 0; i < maxRetries; i++) { + try { + req = client.getNextEvent(); + return req; + } catch (RuntimeClientException e) { + if (!e.isRetriable()) { + throw new RuntimeException("Failed to fetch next event", e); + } + } + } + return null; + } + + private void postResponse(RuntimeClient client, String reqId, OutputStream os) { + for (int i = 0; i < maxRetries; i++) { + try { + client.postInvocationResponse(reqId, os); + return; + } catch (RuntimeClientException ex) { + if (!ex.isRetriable()) { + throw new RuntimeException("Failed to post invocation response", ex); + } + } + } + throw new RuntimeException("Failed to post invocation response " + maxRetries + " times"); + } + + private void postErrorResponse(RuntimeClient client, String reqId, Throwable e) { + for (int i = 0; i < maxRetries; i++) { + try { + client.postInvocationError(reqId, e); + return; + } catch (RuntimeClientException ex) { + if (!ex.isRetriable()) { + throw new RuntimeException("Failed to post invocation error", ex); + } + } + } + throw new RuntimeException("Failed to post invocation error response " + maxRetries + " times"); + } + + @Override + public void handleRequest(InputStream inputStream, OutputStream outputStream, Context context) throws IOException { + handler.proxyStream(inputStream, outputStream, context); + } +} diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClient.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClient.java new file mode 100644 index 000000000..54e0f14cd --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClient.java @@ -0,0 +1,188 @@ +package com.amazonaws.serverless.runtime; + +import com.amazonaws.serverless.proxy.internal.LambdaContainerHandler; +import com.amazonaws.serverless.proxy.internal.SecurityUtils; +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.io.IOUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.ws.rs.core.HttpHeaders; +import java.io.*; +import java.net.HttpURLConnection; +import java.net.MalformedURLException; +import java.net.URL; + +public class RuntimeClient { + public static final String API_VERSION = "2018-06-01"; + public static final String API_PROTOCOL = "http"; + public static final String API_HOST_VAR_NAME = "AWS_LAMBDA_RUNTIME_API"; + + private static final String REQUEST_ID_HEADER = "Lambda-Runtime-Aws-Request-Id"; + private static final String DEADLINE_HEADER = "Lambda-Runtime-Deadline-Ms"; + private static final String FUNCTION_ARN_HEADER = "Lambda-Runtime-Invoked-Function-Arn"; + private static final String TRACE_ID_HEADER = "Lambda-Runtime-Trace-Id"; + + private final Logger log = LoggerFactory.getLogger(RuntimeClient.class); + + private final String apiHost; + private final URL nextEventUrl; + + public RuntimeClient() { + try { + apiHost = System.getenv(API_HOST_VAR_NAME); + nextEventUrl = new URL(API_PROTOCOL + "://" + apiHost + "/" + API_VERSION + "/runtime/invocation/next"); + InvocationContext.prepareContext(); + } catch (SecurityException e) { + log.error("Security Exception while reading runtime API host environment variable", e); + e.printStackTrace(); + throw new RuntimeException("Failed to initialize runtime client", e); + } catch (MalformedURLException e) { + log.error("Could not construct URL for runtime API endpoint", e); + e.printStackTrace(); + throw new RuntimeException("Failed to initialize runtime client", e); + } catch (NumberFormatException e) { + log.error("Could not parse memory limit from environment variable", e); + e.printStackTrace(); + throw new RuntimeException("Failed to initialize runtime client", e); + } + } + + public InvocationRequest getNextEvent() throws RuntimeClientException { + try { + HttpURLConnection conn = (HttpURLConnection)nextEventUrl.openConnection(); + conn.setRequestMethod("GET"); + conn.setReadTimeout(0); + conn.setConnectTimeout(0); + conn.setRequestProperty(HttpHeaders.ACCEPT, "application/json"); + + if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) { + String errMsg = "Could not retrieve next event. Status code: " + conn.getResponseCode(); + if (conn.getErrorStream() != null) { + errMsg += "\n" + String.join("\n", IOUtils.readLines(conn.getErrorStream())); + } + throw new RuntimeClientException( + errMsg, + null, + true); + } + + InvocationRequest req = new InvocationRequest(); + req.setEvent(conn.getInputStream()); + + String reqId = conn.getHeaderField(REQUEST_ID_HEADER); + long deadline = Long.parseLong(conn.getHeaderField(DEADLINE_HEADER)); + String arn = conn.getHeaderField(FUNCTION_ARN_HEADER); + String trace = conn.getHeaderField(TRACE_ID_HEADER); + if (reqId == null || arn == null || trace == null) { + throw new RuntimeClientException("Could not read event header fields", null, true); + } + req.setContext(new InvocationContext(reqId, deadline, arn, trace)); + + return req; + } catch (IOException e) { + log.error("Error while requesting next event", e); + throw new RuntimeClientException("Error while requesting next event", e, true); + } catch (NumberFormatException e) { + log.error("Could not parse deadline ms value", e); + throw new RuntimeClientException("Error while requesting next event", e, true); + } + } + + @SuppressFBWarnings("CRLF_INJECTION_LOGS") + public void postInvocationResponse(String reqId, OutputStream out) throws RuntimeClientException { + try { + URL responseUrl = new URL(API_PROTOCOL + "://" + apiHost + "/" + API_VERSION + "/runtime/invocation/" + reqId + "/response"); + HttpURLConnection conn = (HttpURLConnection)responseUrl.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + + if (out instanceof ByteArrayOutputStream) { + conn.getOutputStream().write(((ByteArrayOutputStream)out).toByteArray()); + } else { + throw new RuntimeClientException("Only byte array output streams are supported " + out.getClass().getName(), null, false); + } + conn.getOutputStream().close(); + if (conn.getResponseCode() != HttpURLConnection.HTTP_ACCEPTED) { + throw new RuntimeClientException("Could not send invocation response (" + conn.getResponseCode() + ")", null, true); + } + } catch (MalformedURLException e) { + log.error("Could not construct invocation response url for " + SecurityUtils.crlf(reqId), e); + throw new RuntimeClientException("Error while posting invocation response", e, false); + } catch (IOException e) { + log.error("Could not write to runtime API connection for " + SecurityUtils.crlf(reqId), e); + throw new RuntimeClientException("Error while posting invocation response", e, true); + } + } + + @SuppressFBWarnings("CRLF_INJECTION_LOGS") + public void postInvocationError(String reqId, Throwable error) throws RuntimeClientException { + try { + URL errorUrl = new URL(API_PROTOCOL + "://" + apiHost + "/" + API_VERSION + "/runtime/invocation/" + reqId + "/error"); + HttpURLConnection conn = (HttpURLConnection)errorUrl.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + + InvocationError err = new InvocationError(error.getMessage(), error.getClass().getSimpleName()); + conn.getOutputStream().write(LambdaContainerHandler.getObjectMapper().writeValueAsBytes(err)); + conn.getOutputStream().close(); + if (conn.getResponseCode() != HttpURLConnection.HTTP_ACCEPTED) { + throw new RuntimeClientException("Could not send invocation response (" + conn.getResponseCode() + ")", null, true); + } + } catch (MalformedURLException e) { + log.error("Could not construct invocation error url for " + SecurityUtils.crlf(reqId), e); + throw new RuntimeClientException("Error while posting invocation error", e, false); + } catch (IOException e) { + log.error("Could not write to runtime API connection for " + SecurityUtils.crlf(reqId), e); + throw new RuntimeClientException("Error while posting invocation error", e, true); + } + } + + public void reportInitError(Throwable error) throws RuntimeClientException { + try { + URL errorUrl = new URL(API_PROTOCOL + "://" + apiHost + "/" + API_VERSION + "/runtime/init/error"); + HttpURLConnection conn = (HttpURLConnection)errorUrl.openConnection(); + conn.setRequestMethod("POST"); + conn.setDoOutput(true); + + InvocationError err = new InvocationError(error.getMessage(), error.getClass().getSimpleName()); + conn.getOutputStream().write(LambdaContainerHandler.getObjectMapper().writeValueAsBytes(err)); + conn.getOutputStream().close(); + if (conn.getResponseCode() != HttpURLConnection.HTTP_ACCEPTED) { + throw new RuntimeClientException("Could not send invocation response (" + conn.getResponseCode() + ")", null, true); + } + } catch (MalformedURLException e) { + log.error("Could not construct init error url", e); + throw new RuntimeClientException("Error while posting init error", e, false); + } catch (IOException e) { + log.error("Could not write init error to runtime API", e); + throw new RuntimeClientException("Error while posting init error", e, true); + } + } + + static class InvocationError { + private String errorMessage; + private String errorType; + + public InvocationError(String errorMessage, String errorType) { + this.errorMessage = errorMessage; + this.errorType = errorType; + } + + public String getErrorMessage() { + return errorMessage; + } + + public void setErrorMessage(String errorMessage) { + this.errorMessage = errorMessage; + } + + public String getErrorType() { + return errorType; + } + + public void setErrorType(String errorType) { + this.errorType = errorType; + } + } +} diff --git a/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClientException.java b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClientException.java new file mode 100644 index 000000000..830979084 --- /dev/null +++ b/aws-serverless-java-container-springboot2/src/main/java/com/amazonaws/serverless/runtime/RuntimeClientException.java @@ -0,0 +1,22 @@ +package com.amazonaws.serverless.runtime; + +public class RuntimeClientException extends Exception { + private boolean retriable; + + public RuntimeClientException(String msg, Throwable e) { + this(msg, e, false); + } + + public RuntimeClientException(String msg, Throwable e, boolean retriable) { + super(msg, e); + this.retriable = retriable; + } + + public boolean isRetriable() { + return retriable; + } + + public void setRetriable(boolean retriable) { + this.retriable = retriable; + } +} diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java index 811a1ab55..1d8a15e08 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/WebFluxAppTest.java @@ -14,6 +14,7 @@ import org.junit.runner.RunWith; import org.junit.runners.Parameterized; +import javax.ws.rs.core.HttpHeaders; import java.util.Arrays; import java.util.Collection; diff --git a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java index a04604618..d0ee6967e 100644 --- a/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java +++ b/aws-serverless-java-container-springboot2/src/test/java/com/amazonaws/serverless/proxy/spring/webfluxapp/MessageController.java @@ -4,7 +4,6 @@ import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RestController; -import org.springframework.web.servlet.config.annotation.EnableWebMvc; import reactor.core.publisher.Flux; @RestController diff --git a/samples/springboot2/native-pet-store/.gitignore b/samples/springboot2/native-pet-store/.gitignore new file mode 100644 index 000000000..3160a3360 --- /dev/null +++ b/samples/springboot2/native-pet-store/.gitignore @@ -0,0 +1 @@ +src/main/resources/META-INF/native-image/* diff --git a/samples/springboot2/native-pet-store/README.md b/samples/springboot2/native-pet-store/README.md new file mode 100644 index 000000000..d6012a6f0 --- /dev/null +++ b/samples/springboot2/native-pet-store/README.md @@ -0,0 +1,40 @@ +# Serverless Spring Boot 2 example for GraalVM Native Image +**Note that this is an experimental sample and the APIs it relies on should not be used in production. There will likely be breaking changes as the features stabilize!** + +This experimental Pet Store example uses [Spring Boot's](https://projects.spring.io/spring-boot/) support for native compilation with [GraalVM](https://www.graalvm.org/) and executes as a custom runtime in AWS Lambda. + +The application uses Docker to compile a native executable compatible with AWS Lambda's runtime environment and can be deployed in an AWS account using the [Serverless Application Model](https://github.com/awslabs/serverless-application-model). The `template.yml` file in the root folder contains the application definition. + +## Pre-requisites +* [Docker](https://www.docker.com/) +* [AWS CLI](https://aws.amazon.com/cli/) +* [SAM CLI](https://github.com/awslabs/aws-sam-cli) + +## Deployment +To make it easy to build our samples with GraalVM, We have published a Docker image that includes GraalVM version 20.0.0, the JDK 8 with backported compiler interfaces (JVMCI), Maven 3.6.3, and the latest snapshot version of Serverless Java Container. + +The container image expects the Maven project - the directory containing the `pom.xml` file - to be mounted to the `/func` volume and will execute `/func/scripts/graalvm-build.sh` as it entrypoint. + +From the `native-pet-store` sample folder, use a shell to run the build process with Docker +```bash +$ docker run --rm -v $PWD:/func sapessi/aws-serverless-java-container-graalvm-build +``` + +To deploy the application in your AWS account, you can use the SAM CLI's guided deployment process and follow the instructions on the screen + +``` +$ sam deploy --guided +``` + +Once the deployment completes, the SAM CLI will print out the stack's outputs, including the new application URL. You can use `curl` or a web browser to make a call to the URL + +``` +... +--------------------------------------------------------------------------------------------------------- +OutputKey-Description OutputValue +--------------------------------------------------------------------------------------------------------- +PetStoreApi - URL for application https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +--------------------------------------------------------------------------------------------------------- + +$ curl https://xxxxxxxxxx.execute-api.us-west-2.amazonaws.com/pets +``` \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/pom.xml b/samples/springboot2/native-pet-store/pom.xml new file mode 100644 index 000000000..6b69e4d95 --- /dev/null +++ b/samples/springboot2/native-pet-store/pom.xml @@ -0,0 +1,126 @@ + + + 4.0.0 + + com.amazonaws.serverless.sample + native-serverless-springboot2-example + 1.0-SNAPSHOT + + + + + spring-milestone + Spring milestone + https://repo.spring.io/milestone + + + + + + + spring-milestone + Spring milestone + https://repo.spring.io/milestone + + + + + 1.8 + 1.8 + com.amazonaws.serverless.sample.springboot2.Application + 5 + 2.3.0.M4 + + + + org.springframework.boot + spring-boot-starter-parent + 2.3.0.M4 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + + + + + + org.springframework + spring-context-indexer + + + + org.springframework.experimental + spring-graal-native + 0.6.1.RELEASE + + + + javax.validation + validation-api + 2.0.1.Final + + + + com.amazonaws.serverless + aws-serverless-java-container-springboot2 + 1.6-SNAPSHOT + + + com.fasterxml.jackson.module + jackson-module-afterburner + + + + + + + + + graalvm + + + + org.graalvm.nativeimage + native-image-maven-plugin + 20.0.0 + + + + -J-Xmx${docker.memory.gb}g -Dspring.graal.remove-unused-autoconfig=true -Dspring.graal.remove-yaml-support=true --no-fallback --allow-incomplete-classpath --report-unsupported-elements-at-runtime -H:+ReportExceptionStackTraces -H:EnableURLProtocols=http --no-server + + + + + native-image + + package + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + + diff --git a/samples/springboot2/native-pet-store/scripts/bootstrap b/samples/springboot2/native-pet-store/scripts/bootstrap new file mode 100644 index 000000000..14791d4d8 --- /dev/null +++ b/samples/springboot2/native-pet-store/scripts/bootstrap @@ -0,0 +1,4 @@ +#!/bin/sh +set -euo pipefail + +./$_HANDLER \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/scripts/graalvm-build.sh b/samples/springboot2/native-pet-store/scripts/graalvm-build.sh new file mode 100644 index 000000000..f8d5c0af7 --- /dev/null +++ b/samples/springboot2/native-pet-store/scripts/graalvm-build.sh @@ -0,0 +1,32 @@ +#!/bin/sh +echo "Starting GraalVM build" + +METADATA_FOLDER=src/main/resources/META-INF/native-image + +mvn clean package + +if [[ $? -ne 0 ]]; then + echo "Maven build failed" + exit 1 +fi + +mkdir -p $METADATA_FOLDER + +java -agentlib:native-image-agent=config-output-dir=$METADATA_FOLDER \ + -Dagentrun=true -jar target/native-serverless-springboot2-example-1.0-SNAPSHOT.jar + +mvn -Pgraalvm clean package + +if [[ $? -ne 0 ]]; then + echo "Maven build failed" + exit 1 +fi + +mkdir /func/target/lambda + +# $(find ./target -maxdepth 1 -type f -not -name "*.jar*") +GRAAL_EXECUTABLE=com.amazonaws.serverless.sample.springboot2.application +mv /func/target/$GRAAL_EXECUTABLE /func/target/lambda/$GRAAL_EXECUTABLE +chmod +x /func/target/lambda/$GRAAL_EXECUTABLE + +cp /func/scripts/bootstrap /func/target/lambda \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/src/assembly/bin.xml b/samples/springboot2/native-pet-store/src/assembly/bin.xml new file mode 100644 index 000000000..1e085057d --- /dev/null +++ b/samples/springboot2/native-pet-store/src/assembly/bin.xml @@ -0,0 +1,27 @@ + + lambda-package + + zip + + false + + + + ${project.build.directory}${file.separator}lib + lib + + tomcat-embed* + + + + + ${project.build.directory}${file.separator}classes + + ** + + ${file.separator} + + + \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java new file mode 100644 index 000000000..2f2918054 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/Application.java @@ -0,0 +1,38 @@ +package com.amazonaws.serverless.sample.springboot2; + +import com.amazonaws.serverless.exceptions.ContainerInitializationException; +import com.amazonaws.serverless.proxy.internal.testutils.AwsProxyRequestBuilder; +import com.amazonaws.serverless.proxy.internal.testutils.MockLambdaContext; +import com.amazonaws.serverless.proxy.model.AwsProxyRequest; +import com.amazonaws.serverless.proxy.model.AwsProxyResponse; +import com.amazonaws.serverless.proxy.spring.SpringBootLambdaContainerHandler; +import com.amazonaws.serverless.proxy.spring.SpringBootProxyHandlerBuilder; +import com.amazonaws.serverless.runtime.Runtime; +import com.amazonaws.serverless.sample.springboot2.controller.PetsController; + +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Import; + + +@SpringBootApplication(proxyBeanMethods = false) +@Import({ PetsController.class }) +public class Application { + private static ConfigurableApplicationContext ctx; + + public static void main(String[] args) throws ContainerInitializationException { + SpringBootLambdaContainerHandler handler = + new SpringBootProxyHandlerBuilder() + .defaultProxy() + .servletApplication() + .springBootApplication(Application.class) + .buildAndInitialize(); + if (System.getProperty("agentrun") != null) { + AwsProxyRequest testReq = new AwsProxyRequestBuilder("/pets", "GET").build(); + handler.proxy(testReq, new MockLambdaContext()); + } else { + Runtime lambdaRuntime = new Runtime(handler); + lambdaRuntime.start(); + } + } +} \ No newline at end of file diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/controller/PetsController.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/controller/PetsController.java new file mode 100644 index 000000000..ff3dda64e --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/controller/PetsController.java @@ -0,0 +1,79 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot2.controller; + + + +import com.amazonaws.serverless.sample.springboot2.model.Pet; +import com.amazonaws.serverless.sample.springboot2.model.PetData; + +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; +import org.springframework.web.servlet.config.annotation.EnableWebMvc; + +import java.security.Principal; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + + +@RestController +@EnableWebMvc +public class PetsController { + @RequestMapping(path = "/pets", method = RequestMethod.POST) + public Pet createPet(@RequestBody Pet newPet) { + if (newPet.getName() == null || newPet.getBreed() == null) { + return null; + } + + Pet dbPet = newPet; + dbPet.setId(UUID.randomUUID().toString()); + return dbPet; + } + + @RequestMapping(path = "/pets", method = RequestMethod.GET) + public List listPets(@RequestParam("limit") Optional limit, Principal principal) { + int queryLimit = 10; + if (limit.isPresent()) { + queryLimit = limit.get(); + } + + List outputPets = new ArrayList<>(queryLimit); + + for (int i = 0; i < queryLimit; i++) { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setName(PetData.getRandomName()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + outputPets.add(newPet); + } + + return outputPets; + } + + @RequestMapping(path = "/pets/{petId}", method = RequestMethod.GET) + public Pet listPets() { + Pet newPet = new Pet(); + newPet.setId(UUID.randomUUID().toString()); + newPet.setBreed(PetData.getRandomBreed()); + newPet.setDateOfBirth(PetData.getRandomDoB()); + newPet.setName(PetData.getRandomName()); + return newPet; + } + +} diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java new file mode 100644 index 000000000..5c3482e40 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/filter/CognitoIdentityFilter.java @@ -0,0 +1,69 @@ +package com.amazonaws.serverless.sample.springboot2.filter; + + +import com.amazonaws.serverless.proxy.RequestReader; +import com.amazonaws.serverless.proxy.model.AwsProxyRequestContext; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import javax.servlet.Filter; +import javax.servlet.FilterChain; +import javax.servlet.FilterConfig; +import javax.servlet.ServletException; +import javax.servlet.ServletRequest; +import javax.servlet.ServletResponse; + +import java.io.IOException; + + +/** + * Simple Filter implementation that looks for a Cognito identity id in the API Gateway request context + * and stores the value in a request attribute. The filter is registered with aws-serverless-java-container + * in the onStartup method from the {@link com.amazonaws.serverless.sample.springboot2.StreamLambdaHandler} class. + */ +public class CognitoIdentityFilter implements Filter { + public static final String COGNITO_IDENTITY_ATTRIBUTE = "com.amazonaws.serverless.cognitoId"; + + private static Logger log = LoggerFactory.getLogger(CognitoIdentityFilter.class); + + @Override + public void init(FilterConfig filterConfig) + throws ServletException { + // nothing to do in init + } + + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) + throws IOException, ServletException { + Object apiGwContext = servletRequest.getAttribute(RequestReader.API_GATEWAY_CONTEXT_PROPERTY); + if (apiGwContext == null) { + log.warn("API Gateway context is null"); + filterChain.doFilter(servletRequest, servletResponse); + return; + } + if (!AwsProxyRequestContext.class.isAssignableFrom(apiGwContext.getClass())) { + log.warn("API Gateway context object is not of valid type"); + filterChain.doFilter(servletRequest, servletResponse); + } + + AwsProxyRequestContext ctx = (AwsProxyRequestContext)apiGwContext; + if (ctx.getIdentity() == null) { + log.warn("Identity context is null"); + filterChain.doFilter(servletRequest, servletResponse); + } + String cognitoIdentityId = ctx.getIdentity().getCognitoIdentityId(); + if (cognitoIdentityId == null || "".equals(cognitoIdentityId.trim())) { + log.warn("Cognito identity id in request is null"); + } + servletRequest.setAttribute(COGNITO_IDENTITY_ATTRIBUTE, cognitoIdentityId); + filterChain.doFilter(servletRequest, servletResponse); + } + + + @Override + public void destroy() { + // nothing to do in destroy + } +} diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Error.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Error.java new file mode 100644 index 000000000..a2278ed02 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Error.java @@ -0,0 +1,29 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot2.model; + +public class Error { + private String message; + + public Error(String errorMessage) { + message = errorMessage; + } + + public String getMessage() { + return message; + } + + public void setMessage(String message) { + this.message = message; + } +} diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Pet.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Pet.java new file mode 100644 index 000000000..5d61e6bc9 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/Pet.java @@ -0,0 +1,59 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot2.model; + +import java.util.Date; + + +public class Pet { + private String id; + private String breed; + private String name; + private Date dateOfBirth; + + public Pet() { + + } + + public String getId() { + return id; + } + + public void setId(String id) { + this.id = id; + } + + public String getBreed() { + return breed; + } + + public void setBreed(String breed) { + this.breed = breed; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public Date getDateOfBirth() { + return dateOfBirth; + } + + public void setDateOfBirth(Date dateOfBirth) { + this.dateOfBirth = dateOfBirth; + } +} diff --git a/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/PetData.java b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/PetData.java new file mode 100644 index 000000000..34a324211 --- /dev/null +++ b/samples/springboot2/native-pet-store/src/main/java/com/amazonaws/serverless/sample/springboot2/model/PetData.java @@ -0,0 +1,117 @@ +/* + * Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance + * with the License. A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES + * OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions + * and limitations under the License. + */ +package com.amazonaws.serverless.sample.springboot2.model; + + +import java.util.ArrayList; +import java.util.Calendar; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.List; +import java.util.concurrent.ThreadLocalRandom; + + +public class PetData { + private static List breeds = new ArrayList<>(); + static { + breeds.add("Afghan Hound"); + breeds.add("Beagle"); + breeds.add("Bernese Mountain Dog"); + breeds.add("Bloodhound"); + breeds.add("Dalmatian"); + breeds.add("Jack Russell Terrier"); + breeds.add("Norwegian Elkhound"); + } + + private static List names = new ArrayList<>(); + static { + names.add("Bailey"); + names.add("Bella"); + names.add("Max"); + names.add("Lucy"); + names.add("Charlie"); + names.add("Molly"); + names.add("Buddy"); + names.add("Daisy"); + names.add("Rocky"); + names.add("Maggie"); + names.add("Jake"); + names.add("Sophie"); + names.add("Jack"); + names.add("Sadie"); + names.add("Toby"); + names.add("Chloe"); + names.add("Cody"); + names.add("Bailey"); + names.add("Buster"); + names.add("Lola"); + names.add("Duke"); + names.add("Zoe"); + names.add("Cooper"); + names.add("Abby"); + names.add("Riley"); + names.add("Ginger"); + names.add("Harley"); + names.add("Roxy"); + names.add("Bear"); + names.add("Gracie"); + names.add("Tucker"); + names.add("Coco"); + names.add("Murphy"); + names.add("Sasha"); + names.add("Lucky"); + names.add("Lily"); + names.add("Oliver"); + names.add("Angel"); + names.add("Sam"); + names.add("Princess"); + names.add("Oscar"); + names.add("Emma"); + names.add("Teddy"); + names.add("Annie"); + names.add("Winston"); + names.add("Rosie"); + } + + public static List getBreeds() { + return breeds; + } + + public static List getNames() { + return names; + } + + public static String getRandomBreed() { + return breeds.get(ThreadLocalRandom.current().nextInt(0, breeds.size() - 1)); + } + + public static String getRandomName() { + return names.get(ThreadLocalRandom.current().nextInt(0, names.size() - 1)); + } + + public static Date getRandomDoB() { + GregorianCalendar gc = new GregorianCalendar(); + + int year = ThreadLocalRandom.current().nextInt( + Calendar.getInstance().get(Calendar.YEAR) - 15, + Calendar.getInstance().get(Calendar.YEAR) + ); + + gc.set(Calendar.YEAR, year); + + int dayOfYear = ThreadLocalRandom.current().nextInt(1, gc.getActualMaximum(Calendar.DAY_OF_YEAR)); + + gc.set(Calendar.DAY_OF_YEAR, dayOfYear); + return gc.getTime(); + } +} diff --git a/samples/springboot2/native-pet-store/template.yml b/samples/springboot2/native-pet-store/template.yml new file mode 100644 index 000000000..4d26c3fb3 --- /dev/null +++ b/samples/springboot2/native-pet-store/template.yml @@ -0,0 +1,32 @@ +AWSTemplateFormatVersion: '2010-09-09' +Transform: AWS::Serverless-2016-10-31 +Description: Example Pet Store API written with SpringBoot with the aws-serverless-java-container library + +Globals: + Api: + # API Gateway regional endpoints + EndpointConfiguration: REGIONAL + +Resources: + PetStoreFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.amazonaws.serverless.sample.springboot2.application + Runtime: provided + CodeUri: target/lambda + MemorySize: 256 + Policies: AWSLambdaBasicExecutionRole + Timeout: 60 + Events: + HttpApiEvent: + Type: HttpApi + Properties: + TimeoutInMillis: 3000 + PayloadFormatVersion: '1.0' + +Outputs: + SpringBootPetStoreApi: + Description: URL for application + Value: !Sub 'https://${ServerlessHttpApi}.execute-api.${AWS::Region}.amazonaws.com/pets' + Export: + Name: SpringBootPetStoreApi