diff --git a/docs/providers/aws/cli-reference/invoke-local.md b/docs/providers/aws/cli-reference/invoke-local.md index e52daead2..20a618a98 100644 --- a/docs/providers/aws/cli-reference/invoke-local.md +++ b/docs/providers/aws/cli-reference/invoke-local.md @@ -96,7 +96,9 @@ This example will pass the json context in the `lib/context.json` file (relative ### Limitations -Currently, `invoke local` only supports the NodeJs and Python runtimes. +Currently, `invoke local` only supports the NodeJs, Python & Java runtimes. + +**Note:** In order to get correct output when using Java runtime, your Response class must implement `toString()` method. ## Resource permissions diff --git a/lib/plugins/aws/invokeLocal/.gitignore b/lib/plugins/aws/invokeLocal/.gitignore index 0d20b6487..376be9d3e 100644 --- a/lib/plugins/aws/invokeLocal/.gitignore +++ b/lib/plugins/aws/invokeLocal/.gitignore @@ -1 +1,5 @@ *.pyc +java/target +java/.project +java/.settings +java/.classpath diff --git a/lib/plugins/aws/invokeLocal/index.js b/lib/plugins/aws/invokeLocal/index.js index 927c212a1..2bdb17365 100644 --- a/lib/plugins/aws/invokeLocal/index.js +++ b/lib/plugins/aws/invokeLocal/index.js @@ -2,6 +2,7 @@ const BbPromise = require('bluebird'); const _ = require('lodash'); +const fs = BbPromise.promisifyAll(require('fs')); const path = require('path'); const validate = require('../lib/validate'); const chalk = require('chalk'); @@ -136,8 +137,17 @@ class AwsInvokeLocal { this.options.context); } + if (runtime === 'java8') { + return this.invokeLocalJava( + 'java', + handler, + this.serverless.service.package.artifact, + this.options.data, + this.options.context); + } + throw new this.serverless.classes - .Error('You can only invoke Node.js & Python functions locally.'); + .Error('You can only invoke Node.js, Python & Java functions locally.'); } invokeLocalPython(runtime, handlerPath, handlerName, event, context) { @@ -161,6 +171,68 @@ class AwsInvokeLocal { }); } + callJavaBridge(artifactPath, className, input) { + return new BbPromise((resolve) => fs.statAsync(artifactPath).then(() => { + const java = spawn('java', [ + `-DartifactPath=${artifactPath}`, + `-DclassName=${className}`, + '-jar', + path.join(__dirname, 'java', 'target', 'invoke-bridge-1.0.jar'), + ]); + + this.serverless.cli.log([ + 'In order to get human-readable output,', + ' please implement "toString()" method of your "ApiGatewayResponse" object.', + ].join('')); + + java.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); + java.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); + java.stdin.write(input); + java.stdin.end(); + java.on('close', () => resolve()); + }).catch(() => { + throw new Error(`Artifact ${artifactPath} doesn't exists, please compile it first.`); + })); + } + + invokeLocalJava(runtime, className, artifactPath, event, customContext) { + const timeout = Number(this.options.functionObj.timeout) + || Number(this.serverless.service.provider.timeout) + || 6; + const context = { + name: this.options.functionObj.name, + version: 'LATEST', + logGroupName: this.provider.naming.getLogGroupName(this.options.functionObj.name), + timeout, + }; + const input = JSON.stringify({ + event: event || {}, + context: customContext || context, + }); + + const javaBridgePath = path.join(__dirname, 'java'); + const executablePath = path.join(javaBridgePath, 'target'); + + return new BbPromise(resolve => fs.statAsync(executablePath) + .then(() => this.callJavaBridge(artifactPath, className, input)) + .then(resolve) + .catch(() => { + const mvn = spawn('mvn', [ + 'package', + '-f', + path.join(javaBridgePath, 'pom.xml'), + ]); + + this.serverless.cli + .log('Building Java bridge, first invocation might take a bit longer.'); + + mvn.stderr.on('data', (buf) => this.serverless.cli.consoleLog(`mvn - ${buf.toString()}`)); + mvn.stdin.end(); + + mvn.on('close', () => this.callJavaBridge(artifactPath, className, input).then(resolve)); + })); + } + invokeLocalNodeJs(handlerPath, handlerName, event, customContext) { let lambda; diff --git a/lib/plugins/aws/invokeLocal/index.test.js b/lib/plugins/aws/invokeLocal/index.test.js index 4222be2c6..8e73d67ec 100644 --- a/lib/plugins/aws/invokeLocal/index.test.js +++ b/lib/plugins/aws/invokeLocal/index.test.js @@ -3,6 +3,9 @@ const expect = require('chai').expect; const sinon = require('sinon'); const path = require('path'); +const mockRequire = require('mock-require'); +const EventEmitter = require('events'); +const fse = require('fs-extra'); const AwsInvokeLocal = require('./index'); const AwsProvider = require('../provider/awsProvider'); const Serverless = require('../../../Serverless'); @@ -276,12 +279,15 @@ describe('AwsInvokeLocal', () => { describe('#invokeLocal()', () => { let invokeLocalNodeJsStub; let invokeLocalPythonStub; + let invokeLocalJavaStub; beforeEach(() => { invokeLocalNodeJsStub = sinon.stub(awsInvokeLocal, 'invokeLocalNodeJs').resolves(); invokeLocalPythonStub = sinon.stub(awsInvokeLocal, 'invokeLocalPython').resolves(); + invokeLocalJavaStub = + sinon.stub(awsInvokeLocal, 'invokeLocalJava').resolves(); awsInvokeLocal.serverless.service.service = 'new-service'; awsInvokeLocal.options = { @@ -298,6 +304,7 @@ describe('AwsInvokeLocal', () => { afterEach(() => { awsInvokeLocal.invokeLocalNodeJs.restore(); awsInvokeLocal.invokeLocalPython.restore(); + awsInvokeLocal.invokeLocalJava.restore(); }); it('should call invokeLocalNodeJs when no runtime is set', () => awsInvokeLocal.invokeLocal() @@ -352,12 +359,26 @@ describe('AwsInvokeLocal', () => { {}, undefined )).to.be.equal(true); - delete awsInvokeLocal.options.functionObj.runtime; }); }); - it('throw error when using runtime other than Node.js or Python', () => { + it('should call invokeLocalJava when java8 runtime is set', () => { awsInvokeLocal.options.functionObj.runtime = 'java8'; + return awsInvokeLocal.invokeLocal() + .then(() => { + expect(invokeLocalJavaStub.calledOnce).to.be.equal(true); + expect(invokeLocalJavaStub.calledWithExactly( + 'java', + 'handler.hello', + undefined, + {}, + undefined + )).to.be.equal(true); + }); + }); + + it('throw error when using runtime other than Node.js or Python', () => { + awsInvokeLocal.options.functionObj.runtime = 'invalid-runtime'; expect(() => awsInvokeLocal.invokeLocal()).to.throw(Error); delete awsInvokeLocal.options.functionObj.runtime; }); @@ -507,4 +528,183 @@ describe('AwsInvokeLocal', () => { }); }); }); + + describe('#callJavaBridge()', () => { + let awsInvokeLocalMocked; + let writeChildStub; + let endChildStub; + + beforeEach(() => { + writeChildStub = sinon.stub(); + endChildStub = sinon.stub(); + + mockRequire('child_process', { + spawn: () => ({ + stderr: new EventEmitter().on('data', () => {}), + stdout: new EventEmitter().on('data', () => {}), + stdin: { + write: writeChildStub, + end: endChildStub, + }, + on: (key, callback) => callback(), + }), + }); + + // Remove Node.js internal "require cache" contents and re-require ./index.js + delete require.cache[require.resolve('./index')]; + delete require.cache[require.resolve('child_process')]; + + const AwsInvokeLocalMocked = require('./index'); // eslint-disable-line global-require + + serverless.setProvider('aws', new AwsProvider(serverless)); + awsInvokeLocalMocked = new AwsInvokeLocalMocked(serverless, options); + + awsInvokeLocalMocked.options = { + stage: 'dev', + function: 'first', + functionObj: { + handler: 'handler.hello', + name: 'hello', + timeout: 4, + }, + data: {}, + }; + }); + + afterEach(() => { + delete require.cache[require.resolve('./index')]; + delete require.cache[require.resolve('child_process')]; + }); + + it('spawns java process with correct arguments', () => + awsInvokeLocalMocked.callJavaBridge( + __dirname, + 'com.serverless.Handler', + '{}' + ).then(() => { + expect(writeChildStub.calledOnce).to.be.equal(true); + expect(endChildStub.calledOnce).to.be.equal(true); + expect(writeChildStub.calledWithExactly('{}')).to.be.equal(true); + }) + ); + }); + + describe('#invokeLocalJava()', () => { + const bridgePath = path.join(__dirname, 'java', 'target'); + let callJavaBridgeStub; + + beforeEach(() => { + fse.mkdirsSync(bridgePath); + callJavaBridgeStub = sinon.stub(awsInvokeLocal, 'callJavaBridge').resolves(); + awsInvokeLocal.options = { + stage: 'dev', + function: 'first', + functionObj: { + handler: 'handler.hello', + name: 'hello', + timeout: 4, + }, + data: {}, + }; + }); + + afterEach(() => { + awsInvokeLocal.callJavaBridge.restore(); + fse.removeSync(bridgePath); + }); + + it('should invoke callJavaBridge when bridge is built', () => + awsInvokeLocal.invokeLocalJava( + 'java', + 'com.serverless.Handler', + __dirname, + {} + ).then(() => { + expect(callJavaBridgeStub.calledOnce).to.be.equal(true); + expect(callJavaBridgeStub.calledWithExactly( + __dirname, + 'com.serverless.Handler', + JSON.stringify({ + event: {}, + context: { + name: 'hello', + version: 'LATEST', + logGroupName: '/aws/lambda/hello', + timeout: 4, + }, + }) + )).to.be.equal(true); + }) + ); + + describe('when attempting to build the Java bridge', () => { + let awsInvokeLocalMocked; + let callJavaBridgeMockedStub; + + beforeEach(() => { + mockRequire('child_process', { + spawn: () => ({ + stderr: new EventEmitter().on('data', () => {}), + stdout: new EventEmitter().on('data', () => {}), + stdin: { + write: () => {}, + end: () => {}, + }, + on: (key, callback) => callback(), + }), + }); + + // Remove Node.js internal "require cache" contents and re-require ./index.js + delete require.cache[require.resolve('./index')]; + delete require.cache[require.resolve('child_process')]; + + const AwsInvokeLocalMocked = require('./index'); // eslint-disable-line global-require + + serverless.setProvider('aws', new AwsProvider(serverless)); + awsInvokeLocalMocked = new AwsInvokeLocalMocked(serverless, options); + callJavaBridgeMockedStub = sinon.stub(awsInvokeLocalMocked, 'callJavaBridge').resolves(); + + awsInvokeLocalMocked.options = { + stage: 'dev', + function: 'first', + functionObj: { + handler: 'handler.hello', + name: 'hello', + timeout: 4, + }, + data: {}, + }; + }); + + afterEach(() => { + awsInvokeLocalMocked.callJavaBridge.restore(); + delete require.cache[require.resolve('./index')]; + delete require.cache[require.resolve('child_process')]; + }); + + it('if it\'s not present yet', () => + awsInvokeLocalMocked.invokeLocalJava( + 'java', + 'com.serverless.Handler', + __dirname, + {} + ).then(() => { + expect(callJavaBridgeMockedStub.calledOnce).to.be.equal(true); + expect(callJavaBridgeMockedStub.calledWithExactly( + __dirname, + 'com.serverless.Handler', + JSON.stringify({ + event: {}, + context: { + name: 'hello', + version: 'LATEST', + logGroupName: '/aws/lambda/hello', + timeout: 4, + }, + }) + )).to.be.equal(true); + }) + ); + }); + }); }); diff --git a/lib/plugins/aws/invokeLocal/java/MANIFEST.mf b/lib/plugins/aws/invokeLocal/java/MANIFEST.mf new file mode 100644 index 000000000..3cc01de12 --- /dev/null +++ b/lib/plugins/aws/invokeLocal/java/MANIFEST.mf @@ -0,0 +1,2 @@ +Manifest-version: 1.0 +Main-Class: com.serverless.InvokeBridge diff --git a/lib/plugins/aws/invokeLocal/java/pom.xml b/lib/plugins/aws/invokeLocal/java/pom.xml new file mode 100644 index 000000000..c47be034b --- /dev/null +++ b/lib/plugins/aws/invokeLocal/java/pom.xml @@ -0,0 +1,71 @@ + + + 4.0.0 + + com.serverless + invoke-bridge + 1.0 + + + 1.8 + 1.8 + + + + + com.amazonaws + aws-lambda-java-core + 1.1.0 + + + com.amazonaws + aws-lambda-java-log4j + 1.0.0 + + + com.fasterxml.jackson.core + jackson-core + 2.8.5 + + + com.fasterxml.jackson.core + jackson-databind + 2.8.5 + + + com.fasterxml.jackson.core + jackson-annotations + 2.8.5 + + + + + + + org.apache.maven.plugins + maven-shade-plugin + 2.3 + + false + + + + package + + shade + + + + + com.serverless.InvokeBridge + + + + + + + + + diff --git a/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/Context.java b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/Context.java new file mode 100644 index 000000000..b0c93945c --- /dev/null +++ b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/Context.java @@ -0,0 +1,86 @@ +package com.serverless; + +import com.amazonaws.services.lambda.runtime.Client; +import com.amazonaws.services.lambda.runtime.ClientContext; +import com.amazonaws.services.lambda.runtime.CognitoIdentity; +import com.amazonaws.services.lambda.runtime.LambdaLogger; + +import java.util.Map; + +public class Context implements com.amazonaws.services.lambda.runtime.Context { + private String name; + private String version; + private String logGroupName; + private long endTime; + + Context(String name, String version, String logGroupName, int timeout) { + this.name = name; + this.version = version; + this.logGroupName = logGroupName; + this.endTime = System.currentTimeMillis() + (timeout * 1000); + } + + public String getAwsRequestId() { + return "1234567890"; + } + + public String getLogGroupName() { + return this.logGroupName; + } + + public String getLogStreamName() { + return "LogStream_" + this.name; + } + + public String getFunctionName() { + return this.name; + } + + public String getFunctionVersion() { + return this.version; + } + + public String getInvokedFunctionArn() { + return "arn:aws:lambda:serverless:" + this.name; + } + + public CognitoIdentity getIdentity() { + return new CognitoIdentity() { + public String getIdentityId() { + return "1"; + } + + public String getIdentityPoolId() { + return "1"; + } + }; + } + + public ClientContext getClientContext() { + return new ClientContext() { + public Client getClient() { + return null; + } + + public Map getCustom() { + return null; + } + + public Map getEnvironment() { + return System.getenv(); + } + }; + } + + public int getRemainingTimeInMillis() { + return Math.max(0, (int) (this.endTime - System.currentTimeMillis())); + } + + public int getMemoryLimitInMB() { + return 1024; + } + + public LambdaLogger getLogger() { + return System.out::println; + } +} diff --git a/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java new file mode 100644 index 000000000..ab9629426 --- /dev/null +++ b/lib/plugins/aws/invokeLocal/java/src/main/java/com/serverless/InvokeBridge.java @@ -0,0 +1,88 @@ +package com.serverless; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.io.BufferedReader; +import java.io.File; +import java.io.IOException; +import java.io.InputStreamReader; +import java.lang.reflect.Method; +import java.net.URL; +import java.net.URLClassLoader; +import java.util.HashMap; + +public class InvokeBridge { + private File artifact; + private String className; + private Object instance; + private Class clazz; + + private InvokeBridge() { + this.artifact = new File(new File("."), System.getProperty("artifactPath")); + this.className = System.getProperty("className"); + + try { + HashMap parsedInput = parseInput(getInput()); + HashMap eventMap = (HashMap) parsedInput.get("event"); + + this.instance = this.getInstance(); + + System.out.println(this.invoke(eventMap, this.getContext(parsedInput)).toString()); + } catch (Exception e) { + e.printStackTrace(); + } + } + + private Context getContext(HashMap parsedInput) { + HashMap contextMap = (HashMap) parsedInput.get("context"); + + String name = (String) contextMap.getOrDefault("name", "functionName"); + String version = (String) contextMap.getOrDefault("version", "LATEST"); + String logGroupName = (String) contextMap.getOrDefault("logGroupName", "logGroup"); + int timeout = Integer.parseInt(String.valueOf(contextMap.getOrDefault("timeout", 5))); + + return new Context(name, version, logGroupName, timeout); + } + + private Object getInstance() throws Exception { + URL[] urls = {this.artifact.toURI().toURL()}; + URLClassLoader child = new URLClassLoader(urls, this.getClass().getClassLoader()); + + this.clazz = Class.forName(this.className, true, child); + + return this.clazz.newInstance(); + } + + private Object invoke(HashMap event, Context context) throws Exception { + Method[] methods = this.clazz.getDeclaredMethods(); + + return methods[1].invoke(this.instance, event, context); + } + + private HashMap parseInput(String input) throws IOException { + TypeReference> typeRef = new TypeReference>() {}; + ObjectMapper mapper = new ObjectMapper(); + + JsonNode jsonNode = mapper.readTree(input); + + return mapper.convertValue(jsonNode, typeRef); + } + + private String getInput() throws IOException { + BufferedReader streamReader = new BufferedReader(new InputStreamReader(System.in, "UTF-8")); + StringBuilder inputStringBuilder = new StringBuilder(); + String inputStr; + + while ((inputStr = streamReader.readLine()) != null) { + inputStringBuilder.append(inputStr); + } + + return inputStringBuilder.toString(); + } + + public static void main(String[] args) { + new InvokeBridge(); + } +} diff --git a/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java b/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java index 80a6f78e8..6d6c670e5 100644 --- a/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java +++ b/lib/plugins/create/templates/aws-java-maven/src/main/java/com/serverless/Handler.java @@ -3,6 +3,7 @@ package com.serverless; import java.util.Collections; import java.util.Map; +import org.apache.log4j.BasicConfigurator; import org.apache.log4j.Logger; import com.amazonaws.services.lambda.runtime.Context; @@ -14,6 +15,8 @@ public class Handler implements RequestHandler, ApiGatewayRe @Override public ApiGatewayResponse handleRequest(Map input, Context context) { + BasicConfigurator.configure(); + LOG.info("received: " + input); Response responseBody = new Response("Go Serverless v1.x! Your function executed successfully!", input); return ApiGatewayResponse.builder()