Merge pull request #4199 from RafalWilinski/invoke-local-maven

Invoke Local: Java Support
This commit is contained in:
Philipp Muens 2017-09-18 12:34:16 +02:00 committed by GitHub
commit 60838b74e9
9 changed files with 532 additions and 4 deletions

View File

@ -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

View File

@ -1 +1,5 @@
*.pyc
java/target
java/.project
java/.settings
java/.classpath

View File

@ -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;

View File

@ -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);
})
);
});
});
});

View File

@ -0,0 +1,2 @@
Manifest-version: 1.0
Main-Class: com.serverless.InvokeBridge

View File

@ -0,0 +1,71 @@
<?xml version="1.0" encoding="UTF-8"?>
<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/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.serverless</groupId>
<artifactId>invoke-bridge</artifactId>
<version>1.0</version>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-core</artifactId>
<version>1.1.0</version>
</dependency>
<dependency>
<groupId>com.amazonaws</groupId>
<artifactId>aws-lambda-java-log4j</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-core</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.8.5</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-annotations</artifactId>
<version>2.8.5</version>
</dependency>
</dependencies>
<build>
<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>
<configuration>
<transformers>
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
<mainClass>com.serverless.InvokeBridge</mainClass>
</transformer>
</transformers>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

View File

@ -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<String, String> getCustom() {
return null;
}
public Map<String, String> 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;
}
}

View File

@ -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<String, Object> parsedInput = parseInput(getInput());
HashMap<String, Object> eventMap = (HashMap<String, Object>) 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<String, Object> parsedInput) {
HashMap<String, Object> contextMap = (HashMap<String, Object>) 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<String, Object> event, Context context) throws Exception {
Method[] methods = this.clazz.getDeclaredMethods();
return methods[1].invoke(this.instance, event, context);
}
private HashMap<String, Object> parseInput(String input) throws IOException {
TypeReference<HashMap<String,Object>> typeRef = new TypeReference<HashMap<String,Object>>() {};
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();
}
}

View File

@ -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<Map<String, Object>, ApiGatewayRe
@Override
public ApiGatewayResponse handleRequest(Map<String, Object> 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()