Erik Erikson bf8d57fa9f Improve Stage and Region Usage
Remove the errant (but understandable) distributed usage of region and stage settings.  This otherwise locks in a multitude of bugs around the improper algorithm for selecting (given all context) the proper region or stage setting.  Instead, all code should use the centralized algorithm for determining such values.  This creates a strange first and second class configuration concept but these two are sufficiently varied and complex in their creation and use that this seems appropriate.
2017-12-11 16:39:44 -08:00

336 lines
10 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;
const handlerPath = handler.split('.')[0];
const handlerName = handler.split('.')[1];
if (runtime.startsWith('nodejs')) {
return this.invokeLocalNodeJs(
handlerPath,
handlerName,
this.options.data,
this.options.context);
}
if (runtime === 'python2.7' || runtime === 'python3.6') {
return this.invokeLocalPython(
process.platform === 'win32' ? 'python.exe' : runtime,
handlerPath,
handlerName,
this.options.data,
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 & 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,
[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(input);
python.stdin.end();
python.on('close', () => resolve());
});
}
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;
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 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;
}
return lambda(event, context, callback);
}
}
module.exports = AwsInvokeLocal;