mirror of
https://github.com/serverless/serverless.git
synced 2025-12-08 19:46:03 +00:00
369 lines
12 KiB
JavaScript
369 lines
12 KiB
JavaScript
'use strict';
|
|
|
|
const BbPromise = require('bluebird');
|
|
const _ = require('lodash');
|
|
const os = require('os');
|
|
const fs = BbPromise.promisifyAll(require('fs'));
|
|
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),
|
|
};
|
|
}
|
|
|
|
validateFile(filePath, key) {
|
|
const absolutePath = path.isAbsolute(filePath) ?
|
|
filePath :
|
|
path.join(this.serverless.config.servicePath, filePath);
|
|
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[key] = require(absolutePath); // eslint-disable-line global-require
|
|
} else {
|
|
this.options[key] = this.serverless.utils.readFileSync(absolutePath);
|
|
}
|
|
}
|
|
|
|
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.contextPath) {
|
|
this.validateFile(this.options.contextPath, 'context');
|
|
}
|
|
|
|
if (this.options.data) {
|
|
resolve();
|
|
} else if (this.options.path) {
|
|
this.validateFile(this.options.path, 'data');
|
|
|
|
resolve();
|
|
} else {
|
|
try {
|
|
stdin().then(input => {
|
|
this.options.data = input;
|
|
resolve();
|
|
});
|
|
} catch (exception) {
|
|
// resolve if no stdin was provided
|
|
resolve();
|
|
}
|
|
}
|
|
}).then(() => {
|
|
try {
|
|
if (!this.options.raw) {
|
|
this.options.data = JSON.parse(this.options.data);
|
|
this.options.context = JSON.parse(this.options.context);
|
|
}
|
|
} 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 = {
|
|
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.provider.getRegion(),
|
|
AWS_DEFAULT_REGION: this.provider.getRegion(),
|
|
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;
|
|
|
|
if (runtime.startsWith('nodejs')) {
|
|
const handlerPath = handler.split('.')[0];
|
|
const handlerName = handler.split('.')[1];
|
|
return this.invokeLocalNodeJs(
|
|
handlerPath,
|
|
handlerName,
|
|
this.options.data,
|
|
this.options.context);
|
|
}
|
|
|
|
if (runtime === 'python2.7' || runtime === 'python3.6') {
|
|
const handlerComponents = handler.split(/\./);
|
|
const handlerPath = handlerComponents.slice(0, -1).join('.');
|
|
const handlerName = handlerComponents.pop();
|
|
return this.invokeLocalPython(
|
|
process.platform === 'win32' ? 'python.exe' : runtime,
|
|
handlerPath,
|
|
handlerName,
|
|
this.options.data,
|
|
this.options.context);
|
|
}
|
|
|
|
if (runtime === 'java8') {
|
|
const className = handler.split('::')[0];
|
|
const handlerName = handler.split('::')[1] || 'handleRequest';
|
|
return this.invokeLocalJava(
|
|
'java',
|
|
className,
|
|
handlerName,
|
|
this.serverless.service.package.artifact,
|
|
this.options.data,
|
|
this.options.context);
|
|
}
|
|
|
|
throw new this.serverless.classes
|
|
.Error('You can only invoke Node.js, Python & Java functions locally.');
|
|
}
|
|
|
|
invokeLocalPython(runtime, handlerPath, handlerName, event, context) {
|
|
const input = JSON.stringify({
|
|
event: event || {},
|
|
context,
|
|
});
|
|
|
|
if (process.env.VIRTUAL_ENV) {
|
|
const runtimeDir = os.platform() === 'win32' ? 'Scripts' : 'bin';
|
|
process.env.PATH = [
|
|
path.join(process.env.VIRTUAL_ENV, runtimeDir),
|
|
path.delimiter,
|
|
process.env.PATH,
|
|
].join('');
|
|
}
|
|
|
|
return new BbPromise(resolve => {
|
|
const python = spawn(runtime,
|
|
['-u', path.join(__dirname, 'invoke.py'), handlerPath, handlerName],
|
|
{ env: process.env }, { shell: true });
|
|
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(input);
|
|
python.stdin.end();
|
|
python.on('close', () => resolve());
|
|
});
|
|
}
|
|
|
|
callJavaBridge(artifactPath, className, handlerName, input) {
|
|
return new BbPromise((resolve) => fs.statAsync(artifactPath).then(() => {
|
|
const java = spawn('java', [
|
|
`-DartifactPath=${artifactPath}`,
|
|
`-DclassName=${className}`,
|
|
`-DhandlerName=${handlerName}`,
|
|
'-jar',
|
|
path.join(__dirname, 'java', 'target', 'invoke-bridge-1.0.jar'),
|
|
], { shell: true });
|
|
|
|
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, handlerName, 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, handlerName, input))
|
|
.then(resolve)
|
|
.catch(() => {
|
|
const mvn = spawn('mvn', [
|
|
'package',
|
|
'-f',
|
|
path.join(javaBridgePath, 'pom.xml'),
|
|
], { shell: true });
|
|
|
|
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, handlerName, input)
|
|
.then(resolve));
|
|
}));
|
|
}
|
|
|
|
invokeLocalNodeJs(handlerPath, handlerName, event, customContext) {
|
|
this.serverless.cli.log('INVOKING INVOKE');
|
|
let lambda;
|
|
let pathToHandler;
|
|
let hasResponded = false;
|
|
try {
|
|
/*
|
|
* we need require() here to load the handler from the file system
|
|
* which the user has to supply by passing the function name
|
|
*/
|
|
pathToHandler = path.join(
|
|
this.serverless.config.servicePath,
|
|
this.options.extraServicePath || '',
|
|
handlerPath
|
|
);
|
|
const handlersContainer = require( // eslint-disable-line global-require
|
|
pathToHandler
|
|
);
|
|
lambda = handlersContainer[handlerName];
|
|
} catch (error) {
|
|
this.serverless.cli.consoleLog(chalk.red(JSON.stringify(error, null, 4)));
|
|
throw new Error(`Exception encountered when loading ${pathToHandler}`);
|
|
}
|
|
|
|
function handleError(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;
|
|
}
|
|
|
|
function handleResult(result) {
|
|
if (result instanceof Error) {
|
|
handleError.call(this, result);
|
|
return;
|
|
} else 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 callback = (err, result) => {
|
|
if (!hasResponded) {
|
|
hasResponded = true;
|
|
if (err) {
|
|
handleError.call(this, err);
|
|
} else if (result) {
|
|
handleResult.call(this, result);
|
|
}
|
|
}
|
|
};
|
|
|
|
const startTime = new Date();
|
|
const timeout = Number(this.options.functionObj.timeout)
|
|
|| Number(this.serverless.service.provider.timeout)
|
|
|| 6;
|
|
let 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 Math.max((timeout * 1000) - ((new Date()).valueOf() - startTime.valueOf()), 0);
|
|
},
|
|
};
|
|
|
|
if (customContext) {
|
|
context = customContext;
|
|
}
|
|
|
|
const maybeThennable = lambda(event, context, callback);
|
|
if (!_.isUndefined(maybeThennable) && _.isFunction(maybeThennable.then)) {
|
|
return maybeThennable
|
|
.then(
|
|
callback.bind(this, null),
|
|
callback.bind(this)
|
|
);
|
|
}
|
|
|
|
return maybeThennable;
|
|
}
|
|
}
|
|
|
|
module.exports = AwsInvokeLocal;
|