diff --git a/README.md b/README.md index 0b58f64..d64b42a 100644 --- a/README.md +++ b/README.md @@ -1,44 +1,102 @@ -# serverless-offline-direct-lambda -A Serverless Offline plugin that exposes lambdas with no API Gateway event via HTTP, to allow offline direct lambda-to-lambda interactions. - -Note - this requires the plugin 'serverless-offline'. - -To include in your project, add the following to the plugins section in serverless.yml: - -``` -- serverless-offline-direct-lambda -``` - -To run: - -``` -servlerless offline start -``` - -(calling the command 'start' is necessary to trigger the plugin, simply running 'serverless online' does not trigger the start hooks). - -The plugin will create api-gateway proxies for all lambdas with *no* triggering events. - -You will see output like this: - -``` -$ sls offline start -Serverless: Running Serverless Offline with direct lambda support -Serverless: Starting Offline: dev/us-east-1. - -Serverless: Routes for myLambda: -Serverless: (none) - -Serverless: Routes for my-project-dev-myLambda_proxy: -Serverless: POST /proxy/my-project-dev-myLambda -``` - -The body of the POST should match the JSON data that would ordinarily be passed in a lambda-to-lambda call. i.e. -``` -{ - "Payload":... -} -``` - -On the client side, abstract the decision to use a direct AWS.lambda.invoke() call or an http call to the proxy using: -https://github.com/civicteam/lambda-wrapper +# serverless-offline-direct-lambda +A Serverless Offline plugin that exposes lambdas with no API Gateway event via HTTP, to allow offline direct lambda-to-lambda interactions. + +## Setup +Note - this requires the plugin 'serverless-offline'. + +To include in your project, add the following to the plugins section in serverless.yml: + +``` +- serverless-offline-direct-lambda +``` + +You may also want to change the port that the plugin runs on - you can do this by specifying the following custom config in your serverless yml file: + +```yml +custom: + serverless-offline: + port: 4000 +``` + +## Running & Calling + +To run: + +``` +servlerless offline start +``` + +(calling the command 'start' is necessary to trigger the plugin, simply running 'serverless online' does not trigger the start hooks). + +The plugin will create api-gateway proxies for all lambdas with *no* triggering events. + +You will see output like this: + +```bash +export AWS_SDK_USED=node + +sls offline start + +Serverless: Running Serverless Offline with direct lambda support +Serverless: Starting Offline: dev/us-east-1. + +Serverless: Routes for myLambda: +Serverless: (none) + +Serverless: Routes for my-project-dev-myLambda_proxy: +Serverless: POST /proxy/my-project-dev-myLambda +Serverless: POST /2015-03-31/functions/my-project-dev-myLambda/invocations +``` + +### Calling via HTTP Post: + +The body of the POST should match the JSON data that would ordinarily be passed in a lambda-to-lambda call. i.e. + +```bash +curl -X POST \ + http://localhost:4000/proxy/my-project-dev-myLambda \ + -H 'Cache-Control: no-cache' \ + -H 'Content-Type: application/json' \ + -d '{ + "some-key": "some-value", + "other-key": false +}' +``` + +### Invoking the function via the AWS SDK: + +You may also invoke the function by using the AWS SDK on your client side... +This can be done by specifying a custom "endpoint" in your Lambda configuration like so: + +**Note:** the AWS SDK for NodeJS actually sends a different content type header on it's request to the Lambda API then all the other AWS SDK's (Python, Rails etc).. You will need to `export AWS_SDK_USED=node` before running the `serverless offline` if you wish to use this with the NodeJS AWS SDK. + +```javascript + +var AWS = require('aws-sdk'); +AWS.config.region = 'us-east-1'; + +let lambda = new AWS.Lambda({ + region: 'us-east-1', + endpoint: 'http://localhost:4000' +}) + +var lambda_args = { + "some-key": "some-value", + "other-key": false +} + +var params = { + FunctionName: 'my-project-dev-myLambda', // the lambda function we are going to invoke + Payload: JSON.stringify(lambda_args) +}; + +lambda.invoke(params, function(err, data) { + if (err) { + console.error(err); + } else { + console.dir(data); + } +}) + +``` + diff --git a/index.js b/index.js index f8683f1..0dd87b2 100644 --- a/index.js +++ b/index.js @@ -3,6 +3,72 @@ const packagePath = 'node_modules/serverless-offline-direct-lambda'; const handlerPath = `proxy.js`; +var AWS_SDK_USED = process.env.AWS_SDK_USED || 'rails'; +function AWS_SDK_METHOD(functionBeingProxied, location) { + + if(AWS_SDK_USED == 'node') { + + // Additional support to call the function from the AWS SDK (NodeJS) directly... + var AWS_SDK_NODE_METHOD = { + http: { + method: 'POST', + // This is the path to the Lambda API.. + path: `2015-03-31/functions/${functionBeingProxied.name}/invocations`, + integration: 'lambda', + request: { + template: { + // NB: AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json' + 'binary/octet-stream': JSON.stringify( + { + location, + body: "$input.body", + targetHandler : functionBeingProxied.handler, + } + ) + } + }, + response: { + headers: { + "Content-Type": "application/json" + } + } + } + }; + return AWS_SDK_NODE_METHOD; + + } else { + + // Additional support to call the function from the All other SDK's (Don't ask why AWS did it like this ......) + var AWS_SDK_RAILS_METHOD = { + http: { + method: 'POST', + // This is the path to the Lambda API.. + path: `2015-03-31/functions/${functionBeingProxied.name}/invocations`, + integration: 'lambda', + request: { + template: { + // NB: AWS SDK for NodeJS specifies as 'binary/octet-stream' not 'application/json' + 'application/json': JSON.stringify( + { + location, + body: "$input.json('$')", + targetHandler : functionBeingProxied.handler, + } + ) + } + }, + response: { + headers: { + "Content-Type": "application/json" + } + } + } + }; + return AWS_SDK_RAILS_METHOD; + } + +}; + class ServerlessPlugin { constructor(serverless, options) { this.serverless = serverless; @@ -32,7 +98,7 @@ const addProxies = (functionsObject, location) => { // filter out functions with event config, // leaving just those intended for direct lambda-to-lambda invocation const functionObject = functionsObject[fn]; - if (!functionObject.events || functionObject.events.length == 0) { + if (!functionObject.events || functionObject.events.length === 0) { const pf = functionProxy(functionObject, location); functionsObject[pf.name] = pf; } @@ -44,6 +110,7 @@ const functionProxy = (functionBeingProxied, location) => ({ handler: `${packagePath}/proxy.handler`, environment: functionBeingProxied.environment, events: [ + // This is the original `/post/FUNCTION-NAME` from the plugin... { http: { method: 'POST', @@ -64,7 +131,11 @@ const functionProxy = (functionBeingProxied, location) => ({ headers: {} } } - } + }, + + // See methods above for further details + AWS_SDK_METHOD(functionBeingProxied, location) + ], package: { include: [handlerPath], diff --git a/proxy.js b/proxy.js index 39c508c..6c6846b 100644 --- a/proxy.js +++ b/proxy.js @@ -2,26 +2,26 @@ const serializeError = require('serialize-error'); const path = require('path'); function handler(event, context, callback) { - // extract the path to the handler (relative to the project root) - // and the function to call on the handler + const [targetHandlerFile, targetHandlerFunction] = event.targetHandler.split("."); - const target = require(path.resolve(__dirname, '../..', event.location, targetHandlerFile)); + const target = require(path.resolve(__dirname, '../..', targetHandlerFile)); - // call the target function target[targetHandlerFunction](event.body, context, (error, response) => { + if (error) { - callback(null, { - StatusCode: 500, - FunctionError: 'Handled', - Payload: serializeError(error) - }) + // Return Serverless error to AWS sdk + callback(null, { + StatusCode: 500, + FunctionError: 'Handled', + Payload: serializeError(error) + }) } else { - callback(null, { - StatusCode: 200, - Payload: JSON.stringify(response) - }) + // Return lambda function response to AWS SDK & pass through args from serverless. + callback(null, response) } }); + + } module.exports.handler = handler;