diff --git a/.gitignore b/.gitignore index cfa6b239db..9e9a7e00ce 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,4 @@ dist/ .vscode/ .cache/ ./aws-sam-local +debug diff --git a/README.md b/README.md index 0d041945f9..7683a1e200 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,7 @@ - [Package and Deploy to Lambda](#package-and-deploy-to-lambda) - [Getting started](#getting-started) - [Advanced](#advanced) + - [Compiled Languages (Java)](#compiled-languages-java) - [IAM Credentials](#iam-credentials) - [Lambda Environment Variables](#lambda-environment-variables) - [Environment Variable file](#environment-variable-file) @@ -285,6 +286,40 @@ $ sam deploy --template-file ./packaged.yaml --stack-name mystack --capabilities ## Advanced +### Compiled Languages (Java) + +To use SAM Local with compiled languages, such as Java that require a packaged artifact (e.g. a JAR, or ZIP), you can specify the location of the artifact with the `AWS::Serverless::Function` `CodeUri` property in your SAM template. + +For example: + +``` +AWSTemplateFormatVersion: 2010-09-09 +Transform: AWS::Serverless-2016-10-31 + +Resources: + ExampleJavaFunction: + Type: AWS::Serverless::Function + Properties: + Handler: com.example.HelloWorldHandler + CodeUri: ./target/HelloWorld-1.0.jar + Runtime: java8 +``` + +You should then build your JAR file using your normal build process. Please note that JAR files used with AWS Lambda should be a shaded JAR file (or uber jar) containing all of the function dependencies. + +``` +// Build the JAR file +$ mvn package shade:shade + +// Invoke with SAM Local +$ echo '{ "some": "input" }' | sam local invoke + +// Or start local API Gateway simulator +$ sam local start-api +``` + +You can find a full Java example in the [samples/java](samples/java) folder + ### IAM Credentials SAM Local will invoke functions with your locally configured IAM credentials. diff --git a/runtime.go b/runtime.go index 65449a262a..6995b1bade 100644 --- a/runtime.go +++ b/runtime.go @@ -1,6 +1,7 @@ package main import ( + "archive/zip" "io" "log" "os" @@ -47,6 +48,7 @@ type Runtime struct { Name string Image string Cwd string + DecompressedCwd string Function resources.AWSServerlessFunction EnvVarOverrides map[string]string DebugPort string @@ -199,15 +201,25 @@ func overrideHostConfig(cfg *container.HostConfig) error { } func (r *Runtime) getHostConfig() (*container.HostConfig, error) { + + // Check if there is a decompressed archive directory we should + // be using instead of the normal working directory (e.g. if a + // ZIP/JAR archive was specified as the CodeUri) + mount := r.Cwd + if r.DecompressedCwd != "" { + mount = r.DecompressedCwd + } + host := &container.HostConfig{ Resources: container.Resources{ Memory: int64(r.Function.MemorySize() * 1024 * 1024), }, Binds: []string{ - fmt.Sprintf("%s:/var/task:ro", r.Cwd), + fmt.Sprintf("%s:/var/task:ro", mount), }, PortBindings: r.getDebugPortBindings(), } + if err := overrideHostConfig(host); err != nil { log.Print(err) } @@ -222,6 +234,18 @@ func (r *Runtime) Invoke(event string) (io.Reader, io.Reader, error) { log.Printf("Invoking %s (%s)\n", r.Function.Handler(), r.Name) + // If the CodeUri has been specified as a .jar or .zip file, unzip it on the fly + if strings.HasSuffix(r.Cwd, ".jar") || strings.HasSuffix(r.Cwd, ".zip") { + log.Printf("Decompressing %s into runtime container...\n", filepath.Base(r.Cwd)) + decompressedDir, err := decompressArchive(r.Cwd) + if err != nil { + log.Printf("ERROR: Failed to decompress archive: %s\n", err) + return nil, nil, fmt.Errorf("failed to decompress archive: %s", err) + } + r.DecompressedCwd = decompressedDir + + } + env := getEnvironmentVariables(r.Function, r.EnvVarOverrides) // Define the container options @@ -517,11 +541,21 @@ func toStringMaybe(value interface{}) (string, bool) { // CleanUp removes the Docker container used by this runtime func (r *Runtime) CleanUp() { + + // Stop the Lambda timeout timer if r.TimeoutTimer != nil { r.TimeoutTimer.Stop() } + + // Remove the container r.Client.ContainerKill(r.Context, r.ID, "SIGKILL") r.Client.ContainerRemove(r.Context, r.ID, types.ContainerRemoveOptions{}) + + // Remove any decompressed archive if there was one (e.g. ZIP/JAR) + if r.DecompressedCwd != "" { + os.RemoveAll(r.DecompressedCwd) + } + } // demuxDockerStream takes a Docker attach stream, and parses out stdout/stderr @@ -591,7 +625,79 @@ func getWorkingDir(basedir string, codeuri string, checkWorkingDirExist bool) (s // Windows uses \ as the path delimiter, but Docker requires / as the path delimiter. dir = filepath.ToSlash(dir) - return dir, nil } + +// decompressArchive unzips a ZIP archive to a temporary directory and returns +// the temporary directory name, or an error +func decompressArchive(src string) (string, error) { + + // Create a temporary directory just for this decompression (dirname: OS tmp directory + unix timestamp)) + tmpdir := os.TempDir() + + // By default on OSX, os.TempDir() returns a directory in /var/folders/. + // This sits outside the default Docker Shared Files directories, however + // /var/folders is just a symlink to /private/var/folders/, so use that instead + if strings.HasPrefix(tmpdir, "/var/folders") { + tmpdir = "/private" + tmpdir + } + + dest := filepath.Join(tmpdir, "aws-sam-local-"+strconv.FormatInt(time.Now().UnixNano(), 10)) + + var filenames []string + + r, err := zip.OpenReader(src) + if err != nil { + return dest, err + } + defer r.Close() + + for _, f := range r.File { + + rc, err := f.Open() + if err != nil { + return dest, err + } + defer rc.Close() + + // Store filename/path for returning and using later on + fpath := filepath.Join(dest, f.Name) + filenames = append(filenames, fpath) + + if f.FileInfo().IsDir() { + + // Make Folder + os.MkdirAll(fpath, os.ModePerm) + + } else { + + // Make File + var fdir string + if lastIndex := strings.LastIndex(fpath, string(os.PathSeparator)); lastIndex > -1 { + fdir = fpath[:lastIndex] + } + + err = os.MkdirAll(fdir, os.ModePerm) + if err != nil { + log.Fatal(err) + return dest, err + } + f, err := os.OpenFile( + fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode()) + if err != nil { + return dest, err + } + defer f.Close() + + _, err = io.Copy(f, rc) + if err != nil { + return dest, err + } + + } + } + + return dest, nil + +} diff --git a/samples/java/.gitignore b/samples/java/.gitignore new file mode 100644 index 0000000000..e420ee4ba0 --- /dev/null +++ b/samples/java/.gitignore @@ -0,0 +1 @@ +target/* diff --git a/samples/java/README.md b/samples/java/README.md new file mode 100644 index 0000000000..e2cfb82be8 --- /dev/null +++ b/samples/java/README.md @@ -0,0 +1,45 @@ +Welcome to the AWS CodeStar sample web service +============================================== + +This sample code helps get you started with a simple Java web service using +AWS Lambda and Amazon API Gateway. + +What's Here +----------- + +This sample includes: + +* README.md - this file +* buildspec.yml - this file is used by AWS CodeBuild to build the web + service +* pom.xml - this file is the Maven Project Object Model for the web service +* src/ - this directory contains your Java service source files +* template.yml - this file contains the Serverless Application Model (SAM) used + by AWS Cloudformation to deploy your application to AWS Lambda and Amazon API + Gateway. + + +What Do I Do Next? +------------------ + +If you have checked out a local copy of your AWS CodeCommit repository you can +start making changes to the sample code. We suggest making a small change to +index.py first, so you can see how changes pushed to your project's repository +in AWS CodeCommit are automatically picked up by your project pipeline and +deployed to AWS Lambda and Amazon API Gateway. (You can watch the pipeline +progress on your AWS CodeStar project dashboard.) Once you've seen how that +works, start developing your own code, and have fun! + +Learn more about Serverless Application Model (SAM) and how it works here: +https://github.com/awslabs/serverless-application-model/blob/master/HOWTO.md + +AWS Lambda Developer Guide: +http://docs.aws.amazon.com/lambda/latest/dg/deploying-lambda-apps.html + +Learn more about AWS CodeStar by reading the user guide, and post questions and +comments about AWS CodeStar on our forum. + +AWS CodeStar User Guide: +http://docs.aws.amazon.com/codestar/latest/userguide/welcome.html + +AWS CodeStar Forum: https://forums.aws.amazon.com/forum.jspa?forumID=248 diff --git a/samples/java/buildspec.yml b/samples/java/buildspec.yml new file mode 100644 index 0000000000..1b8a006adb --- /dev/null +++ b/samples/java/buildspec.yml @@ -0,0 +1,15 @@ +version: 0.2 + +phases: + build: + commands: + - echo Entering build phase... + - echo Build started on `date` + - mvn package shade:shade + - mv target/HelloWorld-1.0.jar . + - unzip HelloWorld-1.0.jar + - rm -rf target src buildspec.yml pom.xml HelloWorld-1.0.jar + - aws cloudformation package --template template.yml --s3-bucket $S3_BUCKET --output-template template-export.json +artifacts: + files: + - template-export.json \ No newline at end of file diff --git a/samples/java/pom.xml b/samples/java/pom.xml new file mode 100644 index 0000000000..c78dfa7bfa --- /dev/null +++ b/samples/java/pom.xml @@ -0,0 +1,62 @@ + + 4.0.0 + com.aws.codestar.projecttemplates + HelloWorld + 1.0 + jar + A sample Java Spring web service created with AWS CodeStar. + + org.springframework.boot + spring-boot-starter-parent + 1.4.3.RELEASE + + + 1.8 + 1.8 + + + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-tomcat + provided + + + com.amazonaws + aws-lambda-java-core + 1.1.0 + + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + + package + + shade + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/samples/java/src/main/java/com/aws/codestar/projecttemplates/Application.java b/samples/java/src/main/java/com/aws/codestar/projecttemplates/Application.java new file mode 100644 index 0000000000..b6702649bd --- /dev/null +++ b/samples/java/src/main/java/com/aws/codestar/projecttemplates/Application.java @@ -0,0 +1,19 @@ +package com.aws.codestar.projecttemplates; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +/** Simple class to start up the application. + * + * @SpringBootApplication adds: + * @Configuration + * @EnableAutoConfiguration + * @ComponentScan + */ +@SpringBootApplication +public class Application { + + public static void main(String[] args) { + SpringApplication.run(Application.class, args); + } +} diff --git a/samples/java/src/main/java/com/aws/codestar/projecttemplates/GatewayResponse.java b/samples/java/src/main/java/com/aws/codestar/projecttemplates/GatewayResponse.java new file mode 100644 index 0000000000..0d3bb005a0 --- /dev/null +++ b/samples/java/src/main/java/com/aws/codestar/projecttemplates/GatewayResponse.java @@ -0,0 +1,33 @@ +package com.aws.codestar.projecttemplates; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * POJO containing response object for API Gateway. + */ +public class GatewayResponse { + + private final String body; + private final Map headers; + private final int statusCode; + + public GatewayResponse(final String body, final Map headers, final int statusCode) { + this.statusCode = statusCode; + this.body = body; + this.headers = Collections.unmodifiableMap(new HashMap<>(headers)); + } + + public String getBody() { + return body; + } + + public Map getHeaders() { + return headers; + } + + public int getStatusCode() { + return statusCode; + } +} diff --git a/samples/java/src/main/java/com/aws/codestar/projecttemplates/handler/HelloWorldHandler.java b/samples/java/src/main/java/com/aws/codestar/projecttemplates/handler/HelloWorldHandler.java new file mode 100644 index 0000000000..d437d09328 --- /dev/null +++ b/samples/java/src/main/java/com/aws/codestar/projecttemplates/handler/HelloWorldHandler.java @@ -0,0 +1,24 @@ +package com.aws.codestar.projecttemplates.handler; + +import com.amazonaws.services.lambda.runtime.Context; +import com.amazonaws.services.lambda.runtime.RequestHandler; + +import java.util.HashMap; +import java.util.Map; + +import com.aws.codestar.projecttemplates.GatewayResponse; + +/** + * Handler for requests to Lambda function. + */ +public class HelloWorldHandler implements RequestHandler { + + public Object handleRequest(final Object input, final Context context) { + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + headers.put("X-Custom-Header", "application/json"); + System.err.println("This is a test log message 1"); + System.err.println("This is a test log message 2"); + return new GatewayResponse("{ \"Output\": \"Hello World!\"}", headers, 200); + } +} diff --git a/samples/java/template.yml b/samples/java/template.yml new file mode 100644 index 0000000000..c88021c26c --- /dev/null +++ b/samples/java/template.yml @@ -0,0 +1,42 @@ +AWSTemplateFormatVersion: 2010-09-09 +Transform: +- AWS::Serverless-2016-10-31 +- AWS::CodeStar + +Parameters: + ProjectId: + Type: String + Description: AWS CodeStar projectID used to associate new resources to team members + +Resources: + GetHelloWorld: + Type: AWS::Serverless::Function + Properties: + Handler: com.aws.codestar.projecttemplates.handler.HelloWorldHandler + CodeUri: ./target/HelloWorld-1.0.jar + Runtime: java8 + Role: + Fn::ImportValue: + !Join ['-', [!Ref 'ProjectId', !Ref 'AWS::Region', 'LambdaTrustRole']] + Events: + GetEvent: + Type: Api + Properties: + Path: / + Method: get + + PostHelloWorld: + Type: AWS::Serverless::Function + Properties: + Handler: com.aws.codestar.projecttemplates.handler.HelloWorldHandler + CodeUri: ./target/HelloWorld-1.0.jar + Runtime: java8 + Role: + Fn::ImportValue: + !Join ['-', [!Ref 'ProjectId', !Ref 'AWS::Region', 'LambdaTrustRole']] + Events: + PostEvent: + Type: Api + Properties: + Path: / + Method: post \ No newline at end of file diff --git a/start.go b/start.go index fc7c5cb513..89a28f4fdd 100644 --- a/start.go +++ b/start.go @@ -170,6 +170,7 @@ func start(c *cli.Context) { stdoutTxt, stderrTxt, err := runt.Invoke(eventJSON) if err != nil { + log.Printf("ERROR: %s\n", err) w.WriteHeader(http.StatusInternalServerError) w.Write([]byte(`{ "message": "Internal server error" }`)) return