'use strict'; const BbPromise = require('bluebird'); const _ = require('lodash'); const path = require('path'); const validate = require('../lib/validate'); const chalk = require('chalk'); const stdin = require('get-stdin'); const spawn = require('child_process').spawn; class AwsInvokeLocal { constructor(serverless, options) { this.serverless = serverless; this.options = options || {}; this.provider = this.serverless.getProvider('aws'); Object.assign(this, validate); this.hooks = { 'invoke:local:invoke': () => BbPromise.bind(this) .then(this.extendedValidate) .then(this.loadEnvVars) .then(this.invokeLocal), }; } extendedValidate() { this.validate(); // validate function exists in service this.options.functionObj = this.serverless.service.getFunction(this.options.function); this.options.data = this.options.data || ''; return new BbPromise(resolve => { if (this.options.data) { resolve(); } else if (this.options.path) { const absolutePath = path.isAbsolute(this.options.path) ? this.options.path : path.join(this.serverless.config.servicePath, this.options.path); if (!this.serverless.utils.fileExistsSync(absolutePath)) { throw new this.serverless.classes.Error('The file you provided does not exist.'); } // if (absolutePath.endsWith('.js')) { // to support js - export as an input data this.options.data = require(absolutePath); // eslint-disable-line global-require } else { this.options.data = this.serverless.utils.readFileSync(absolutePath); } resolve(); } else { try { stdin().then(input => { this.options.data = input; resolve(); }); } catch (exception) { // resolve if no stdin was provided resolve(); } } }).then(() => { try { this.options.data = JSON.parse(this.options.data); } catch (exception) { // do nothing if it's a simple string or object already } }); } loadEnvVars() { const lambdaName = this.options.functionObj.name; const memorySize = Number(this.options.functionObj.memorySize) || Number(this.serverless.service.provider.memorySize) || 1024; const lambdaDefaultEnvVars = { PATH: '/usr/local/lib64/node-v4.3.x/bin:/usr/local/bin:/usr/bin/:/bin', LANG: 'en_US.UTF-8', LD_LIBRARY_PATH: '/usr/local/lib64/node-v4.3.x/lib:/lib64:/usr/lib64:/var/runtime:/var/runtime/lib:/var/task:/var/task/lib', // eslint-disable-line max-len LAMBDA_TASK_ROOT: '/var/task', LAMBDA_RUNTIME_DIR: '/var/runtime', AWS_REGION: this.options.region || _.get(this.serverless, 'service.provider.region'), AWS_DEFAULT_REGION: this.options.region || _.get(this.serverless, 'service.provider.region'), AWS_LAMBDA_LOG_GROUP_NAME: this.provider.naming.getLogGroupName(lambdaName), AWS_LAMBDA_LOG_STREAM_NAME: '2016/12/02/[$LATEST]f77ff5e4026c45bda9a9ebcec6bc9cad', AWS_LAMBDA_FUNCTION_NAME: lambdaName, AWS_LAMBDA_FUNCTION_MEMORY_SIZE: memorySize, AWS_LAMBDA_FUNCTION_VERSION: '$LATEST', NODE_PATH: '/var/runtime:/var/task:/var/runtime/node_modules', }; const providerEnvVars = this.serverless.service.provider.environment || {}; const functionEnvVars = this.options.functionObj.environment || {}; _.merge(process.env, lambdaDefaultEnvVars, providerEnvVars, functionEnvVars); return BbPromise.resolve(); } invokeLocal() { const runtime = this.options.functionObj.runtime || this.serverless.service.provider.runtime || 'nodejs4.3'; const handler = this.options.functionObj.handler; const handlerPath = handler.split('.')[0]; const handlerName = handler.split('.')[1]; if (runtime.startsWith('nodejs')) { return this.invokeLocalNodeJs( handlerPath, handlerName, this.options.data); } if (runtime === 'python2.7' || runtime === 'python3.6') { return this.invokeLocalPython( runtime, handlerPath, handlerName, this.options.data); } throw new this.serverless.classes .Error('You can only invoke Node.js & Python functions locally.'); } invokeLocalPython(runtime, handlerPath, handlerName, event) { if (process.env.VIRTUAL_ENV) { process.env.PATH = `${process.env.VIRTUAL_ENV}/bin:${process.env.PATH}`; } return new BbPromise(resolve => { const python = spawn(runtime, [path.join(__dirname, 'invoke.py'), handlerPath, handlerName], { env: process.env }); python.stdout.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); python.stderr.on('data', (buf) => this.serverless.cli.consoleLog(buf.toString())); python.stdin.write(JSON.stringify(event || {})); python.stdin.end(); python.on('close', () => resolve()); }); } invokeLocalNodeJs(handlerPath, handlerName, event) { let lambda; try { /* * we need require() here to load the handler from the file system * which the user has to supply by passing the function name */ const handlersContainer = require( // eslint-disable-line global-require path.join( this.serverless.config.servicePath, this.options.extraServicePath || '', handlerPath ) ); lambda = handlersContainer[handlerName]; } catch (error) { this.serverless.cli.consoleLog(error); process.exit(0); } const callback = (err, result) => { if (err) { let errorResult; if (err instanceof Error) { errorResult = { errorMessage: err.message, errorType: err.constructor.name, }; } else { errorResult = { errorMessage: err, }; } this.serverless.cli.consoleLog(chalk.red(JSON.stringify(errorResult, null, 4))); process.exitCode = 1; } else if (result) { if (result.headers && result.headers['Content-Type'] === 'application/json') { if (result.body) { try { Object.assign(result, { body: JSON.parse(result.body), }); } catch (e) { throw new Error('Content-Type of response is application/json but body is not json'); } } } this.serverless.cli.consoleLog(JSON.stringify(result, null, 4)); } }; const startTime = new Date(); const context = { awsRequestId: 'id', invokeid: 'id', logGroupName: this.provider.naming.getLogGroupName(this.options.functionObj.name), logStreamName: '2015/09/22/[HEAD]13370a84ca4ed8b77c427af260', functionVersion: 'HEAD', isDefaultFunctionVersion: true, functionName: this.options.functionObj.name, memoryLimitInMB: '1024', succeed(result) { return callback(null, result); }, fail(error) { return callback(error); }, done(error, result) { return callback(error, result); }, getRemainingTimeInMillis() { return (new Date()).valueOf() - startTime.valueOf(); }, }; return lambda(event, context, callback); } } module.exports = AwsInvokeLocal;