Skip to content

Commit

Permalink
Added on the fly unzipping of packaged archives if the CodeUri ends w…
Browse files Browse the repository at this point in the history
…ith .jar or .zip. Fixes #37
  • Loading branch information
PaulMaddox committed Aug 18, 2017
1 parent 18e2574 commit 52b312c
Show file tree
Hide file tree
Showing 12 changed files with 386 additions and 2 deletions.
1 change: 1 addition & 0 deletions .gitignore
Expand Up @@ -3,3 +3,4 @@ dist/
.vscode/
.cache/
./aws-sam-local
debug
35 changes: 35 additions & 0 deletions README.md
Expand Up @@ -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)
Expand Down Expand Up @@ -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.
Expand Down
110 changes: 108 additions & 2 deletions runtime.go
@@ -1,6 +1,7 @@
package main

import (
"archive/zip"
"io"
"log"
"os"
Expand Down Expand Up @@ -47,6 +48,7 @@ type Runtime struct {
Name string
Image string
Cwd string
DecompressedCwd string
Function resources.AWSServerlessFunction
EnvVarOverrides map[string]string
DebugPort string
Expand Down Expand Up @@ -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)
}
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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

}
1 change: 1 addition & 0 deletions samples/java/.gitignore
@@ -0,0 +1 @@
target/*
45 changes: 45 additions & 0 deletions 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
15 changes: 15 additions & 0 deletions 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
62 changes: 62 additions & 0 deletions samples/java/pom.xml
@@ -0,0 +1,62 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.aws.codestar.projecttemplates</groupId>
<artifactId>HelloWorld</artifactId>
<version>1.0</version>
<packaging>jar</packaging>
<name>A sample Java Spring web service created with AWS CodeStar.</name>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.4.3.RELEASE</version>
</parent>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
<scope>provided</scope>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.1.0</version>
</dependency>
</dependencies>

<build>
<pluginManagement>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-shade-plugin</artifactId>
<version>2.3</version>
<configuration>
<createDependencyReducedPom>false</createDependencyReducedPom>
</configuration>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>shade</goal>
</goals>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</pluginManagement>
</build>
</project>
@@ -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);
}
}

0 comments on commit 52b312c

Please sign in to comment.