Merge pull request #924 from minibikini/master

Allows `serverless function run` to read from stdin, solves #900
This commit is contained in:
Austen 2016-03-31 20:18:27 -07:00
commit f9042cd1c8
5 changed files with 269 additions and 256 deletions

View File

@ -14,7 +14,7 @@ module.exports = function(S) {
constructor(data, filePath) {
super();
this._class = 'Function';
this._config = config || {};
this._filePath = filePath;
@ -234,8 +234,8 @@ module.exports = function(S) {
return this.getRuntime().scaffold(this);
}
run(stage, region) {
return this.getRuntime().run(this, stage, region);
run(stage, region, event) {
return this.getRuntime().run(this, stage, region, event);
}
build(pathDist, stage, region) {

View File

@ -31,7 +31,7 @@ module.exports = function(S) {
* - Run the function in this runtime
*/
run(func, stage, region) {
run(func, stage, region, event) {
return BbPromise.reject(new SError(`Runtime "${this.getName()}" should implement "run()" method`));
}
@ -92,23 +92,21 @@ module.exports = function(S) {
*/
copyFunction(func, pathDist, stage, region) {
return BbPromise.try(() => {
// Status
S.utils.sDebug(`"${stage} - ${region} - ${func.getName()}": Copying in dist dir ${pathDist}`);
// Status
S.utils.sDebug(`"${stage} - ${region} - ${func.getName()}": Copying in dist dir ${pathDist}`);
// Extract the root of the lambda package from the handler property
let handlerFullPath = func.getRootPath(func.handler.split('/')[func.handler.split('/').length - 1]).replace(/\\/g, '/');
// Extract the root of the lambda package from the handler property
let handlerFullPath = func.getRootPath(_.last(func.handler.split('/'))).replace(/\\/g, '/');
// Check handler is correct
if (handlerFullPath.indexOf(func.handler) == -1) {
throw new SError('This function\'s handler is invalid and not in the file system: ' + func.handler);
}
// Check handler is correct
if (!handlerFullPath.endsWith(func.handler)) {
return BbPromise.reject(new SError(`This function's handler is invalid and not in the file system: ` + func.handler));
}
let packageRoot = handlerFullPath.replace(func.handler, '');
let packageRoot = handlerFullPath.replace(func.handler, '');
return fse.copySync(packageRoot, pathDist, {
filter: this._processExcludePatterns(func, pathDist, stage, region)
});
return fse.copyAsync(packageRoot, pathDist, {
filter: this._processExcludePatterns(func, pathDist, stage, region)
});
}
@ -144,7 +142,7 @@ module.exports = function(S) {
return !excludePatterns.some(sRegex => {
let re = new RegExp(sRegex),
matches = re.exec(filePath),
willExclude = (matches && matches.length > 0) ? true : false;
willExclude = matches && matches.length > 0;
if (willExclude) {
S.utils.sDebug(`"${stage} - ${region} - ${func.name}": Excluding - ${filePath}`);

View File

@ -45,45 +45,23 @@ module.exports = function(S) {
* - Run this function locally
*/
run(func, stage, region) {
run(func, stage, region, event) {
let _this = this,
functionEvent,
functionCall;
return this.getEnvVars(func, stage, region)
// Add ENV vars (from no stage/region) to environment
.then(envVars => _.merge(process.env, envVars))
.then(() => {
const handlerArr = func.handler.split('/').pop().split('.'),
functionFile = func.getRootPath(handlerArr[0] + '.js'),
functionHandler = handlerArr[1];
return BbPromise.try(function () {
// Load Event
functionEvent = S.utils.readFileSync(func.getRootPath('event.json'));
// Load Function
let handlerArr = func.handler.split('/').pop().split('.'),
functionFile = func.getRootPath(handlerArr[0] + '.js'),
functionHandler = handlerArr[1];
return BbPromise.resolve([ functionFile, functionHandler ]);
})
// Setting env vars is a side-effect here and does not change the promise chain result (so tap instead of then)
.tap(() => {
_this.getEnvVars(func, stage, region)
.then(function (envVars) {
// Add ENV vars (from no stage/region) to environment
for (var key in envVars) {
process.env[key] = envVars[key];
}
});
})
// Separate bundled promise chain result here
.spread((functionFile, functionHandler) => {
// Load function handler. This has to be done after env vars are set
// to ensure that they are accessible in the global context.
functionCall = require(functionFile)[functionHandler];
return new BbPromise(resolve => {
const functionCall = require(functionFile)[functionHandler];
return new BbPromise((resolve) => {
// Call Function
functionCall(functionEvent, context(func, (err, result) => {
functionCall(event, context(func, (err, result) => {
SCli.log(`-----------------`);
// Show error

View File

@ -44,20 +44,17 @@ module.exports = function(S) {
* Run
*/
run(func, stage, region) {
run(func, stage, region, event) {
return BbPromise.all([
S.utils.readFile(func.getRootPath('event.json')),
this.getEnvVars(func, stage, region)
])
.spread((functionEvent, env) => {
return this.getEnvVars(func, stage, region)
.then((env) => {
const handlerArr = func.handler.split('/').pop().split('.'),
functionFile = func.getRootPath(handlerArr[0] + '.py'),
functionHandler = handlerArr[1],
result = {};
const childArgs = [
'--event', JSON.stringify(functionEvent),
'--event', JSON.stringify(event),
'--handler-path', functionFile,
'--handler-function', functionHandler
];

View File

@ -5,211 +5,251 @@
* - Runs the function in the CWD for local testing
*/
module.exports = function(S) {
module.exports = function(S) {
const path = require('path'),
SError = require(S.getServerlessPath('Error')),
SCli = require(S.getServerlessPath('utils/cli')),
SUtils = S.utils,
BbPromise = require('bluebird'),
chalk = require('chalk');
const path = require('path'),
SError = require(S.getServerlessPath('Error')),
SCli = require(S.getServerlessPath('utils/cli')),
SUtils = S.utils,
BbPromise = require('bluebird'),
chalk = require('chalk');
/**
* FunctionRun Class
*/
class FunctionRun extends S.classes.Plugin {
class FunctionRun extends S.classes.Plugin {
static getName() {
return 'serverless.core.' + this.name;
}
registerActions() {
S.addAction(this.functionRun.bind(this), {
handler: 'functionRun',
description: `Runs the service locally. Reads the services runtime and passes it off to a runtime-specific runner`,
context: 'function',
contextAction: 'run',
options: [
{
option: 'region',
shortcut: 'r',
description: 'region you want to run your function in'
},
{
option: 'stage',
shortcut: 's',
description: 'stage you want to run your function in'
},
{
option: 'runDeployed',
shortcut: 'd',
description: 'invoke deployed function'
},
{
option: 'invocationType',
shortcut: 'i',
description: 'Valid Values: Event | RequestResponse | DryRun . Default is RequestResponse'
},
{
option: 'log',
shortcut: 'l',
description: 'Show the log output'
}
],
parameters: [
{
parameter: 'name',
description: 'The name of the function you want to run',
position: '0'
}
]
});
return BbPromise.resolve();
}
/**
* Action
*/
functionRun(evt) {
let _this = this;
_this.evt = evt;
// Flow
return this._prompt()
.bind(_this)
.then(_this._validateAndPrepare)
.then(function() {
// Run local or deployed
if (_this.evt.options.runDeployed) {
return _this._runDeployed();
} else {
return _this._runLocal();
}
})
.then(() => this.evt);
}
_prompt() {
if (!S.config.interactive || this.evt.options.stage) return BbPromise.resolve();
return this.cliPromptSelectStage('Function Run - Choose a stage: ', this.evt.options.stage, false)
.then(stage => this.evt.options.stage = stage)
.then(() => this.cliPromptSelectRegion('Select a region: ', false, true, this.evt.options.region, this.evt.options.stage) )
.then(region => this.evt.options.region = region);
}
/**
* Validate And Prepare
*/
_validateAndPrepare() {
let _this = this;
// If CLI and path is not specified, deploy from CWD if Function
if (S.cli && !_this.evt.options.name) {
// Get all functions in CWD
if (!SUtils.fileExistsSync(path.join(process.cwd(), 's-function.json'))) {
return BbPromise.reject(new SError('You must be in a function folder to run it'));
}
_this.evt.options.name = process.cwd().split(path.sep)[process.cwd().split(path.sep).length - 1];
}
_this.function = S.getProject().getFunction(_this.evt.options.name);
// Missing function
if (!_this.function) return BbPromise.reject(new SError(`Function ${_this.evt.options.name} does not exist in your project.`));
return BbPromise.resolve();
}
/**
* Run Local
*/
_runLocal() {
if (!this.evt.options.name) {
return BbPromise.reject(new SError('Please provide a function name to run'));
}
SCli.log(`Running ${this.evt.options.name}...`);
return this.function.run(this.evt.options.stage, this.evt.options.region)
.then(result => this.evt.data.result = result);
}
/**
* Run Deployed
*/
_runDeployed() {
let _this = this;
_this.evt.options.invocationType = _this.evt.options.invocationType || 'RequestResponse';
_this.evt.options.region = _this.evt.options.region || S.getProject().getAllRegions(_this.evt.options.stage)[0].name;
if (_this.evt.options.invocationType !== 'RequestResponse') {
_this.evt.options.logType = 'None';
} else {
_this.evt.options.logType = _this.evt.options.log ? 'Tail' : 'None'
}
// validate stage: make sure stage exists
if (!S.getProject().validateStageExists(_this.evt.options.stage)) {
return BbPromise.reject(new SError('Stage ' + _this.evt.options.stage + ' does not exist in your project', SError.errorCodes.UNKNOWN));
}
// validate region: make sure region exists in stage
if (!S.getProject().validateRegionExists(_this.evt.options.stage, _this.evt.options.region)) {
return BbPromise.reject(new SError('Region "' + _this.evt.options.region + '" does not exist in stage "' + _this.evt.options.stage + '"'));
}
// Invoke Lambda
let params = {
FunctionName: _this.function.getDeployedName({ stage: _this.evt.options.stage, region: _this.evt.options.region }),
// ClientContext: new Buffer(JSON.stringify({x: 1, y: [3,4]})).toString('base64'),
InvocationType: _this.evt.options.invocationType,
LogType: _this.evt.options.logType,
Payload: new Buffer(JSON.stringify(SUtils.readFileSync(_this.function.getRootPath('event.json')))),
Qualifier: _this.evt.options.stage
};
return S.getProvider('aws')
.request('Lambda', 'invoke', params, _this.evt.options.stage, _this.evt.options.region)
.then( reply => {
let color = !reply.FunctionError ? 'white' : 'red';
if (reply.Payload) {
let payload = JSON.parse(reply.Payload)
S.config.interactive && console.log(chalk[color](JSON.stringify(payload, null, ' ')));
_this.evt.data.result = {
status: 'success',
response: payload
};
}
if (reply.LogResult) {
console.log(chalk.gray('--------------------------------------------------------------------'));
let logResult = new Buffer(reply.LogResult, 'base64').toString();
logResult.split('\n').forEach( line => {
console.log(SCli.formatLambdaLogEvent(line));
});
}
})
.catch(function(e) {
_this.evt.data.result = {
status: 'error',
message: e.message,
stack: e.stack
};
BbPromise.reject(e);
});
}
static getName() {
return 'serverless.core.' + this.name;
}
return( FunctionRun );
registerActions() {
S.addAction(this.functionRun.bind(this), {
handler: 'functionRun',
description: `Runs the service locally. Reads the services runtime and passes it off to a runtime-specific runner`,
context: 'function',
contextAction: 'run',
options: [
{
option: 'region',
shortcut: 'r',
description: 'region you want to run your function in'
},
{
option: 'stage',
shortcut: 's',
description: 'stage you want to run your function in'
},
{
option: 'runDeployed',
shortcut: 'd',
description: 'invoke deployed function'
},
{
option: 'invocationType',
shortcut: 'i',
description: 'Valid Values: Event | RequestResponse | DryRun . Default is RequestResponse'
},
{
option: 'log',
shortcut: 'l',
description: 'Show the log output'
}
],
parameters: [
{
parameter: 'name',
description: 'The name of the function you want to run',
position: '0'
}
]
});
return BbPromise.resolve();
}
/**
* Action
*/
functionRun(evt) {
this.evt = evt;
// Flow
return this._prompt()
.bind(this)
.then(this._validateAndPrepare)
.then(() => {
// Run local or deployed
if (this.evt.options.runDeployed) {
return this._runDeployed();
} else {
return this._runLocal();
}
})
.then(() => this.evt);
}
_prompt() {
if (!S.config.interactive || this.evt.options.stage) return BbPromise.resolve();
return this.cliPromptSelectStage('Function Run - Choose a stage: ', this.evt.options.stage, false)
.then(stage => this.evt.options.stage = stage)
.then(() => this.cliPromptSelectRegion('Select a region: ', false, true, this.evt.options.region, this.evt.options.stage) )
.then(region => this.evt.options.region = region);
}
/**
* Validate And Prepare
*/
_validateAndPrepare() {
// If CLI and path is not specified, deploy from CWD if Function
if (S.cli && !this.evt.options.name) {
// Get all functions in CWD
if (!SUtils.fileExistsSync(path.join(process.cwd(), 's-function.json'))) {
return BbPromise.reject(new SError('You must be in a function folder to run it'));
}
this.evt.options.name = SUtils.fileReadSync(path.join(process.cwd(), 's-function.json')).name
}
this.function = S.getProject().getFunction(this.evt.options.name);
// Missing function
if (!this.function) return BbPromise.reject(new SError(`Function ${this.evt.options.name} does not exist in your project.`));
return this._getEventFromStdIn()
.then(event => event || S.utils.readFile(this.function.getRootPath('event.json')))
.then(event => this.evt.data.event = event);
}
/**
* Get event data from STDIN
* If
*/
_getEventFromStdIn() {
return new BbPromise((resolve, reject) => {
const stdin = process.stdin;
const chunks = [];
const onReadable = () => {
const chunk = stdin.read();
if (chunk !== null) chunks.push(chunk);
};
const onEnd = () => {
try {
resolve(JSON.parse(chunks.join('')));
} catch(e) {
reject(new SError("Invalid event JSON"));
}
};
stdin.setEncoding('utf8');
stdin.on('readable', onReadable);
stdin.on('end', onEnd);
setTimeout((() => {
stdin.removeListener('readable', onReadable);
stdin.removeListener('end', onEnd);
stdin.end()
resolve()
}), 5);
});
}
/**
* Run Local
*/
_runLocal() {
const name = this.evt.options.name;
const stage = this.evt.options.stage;
const region = this.evt.options.region;
const event = this.evt.data.event;
if (!name) return BbPromise.reject(new SError('Please provide a function name to run'));
SCli.log(`Running ${name}...`);
return this.function.run(stage, region, event)
.then(result => this.evt.data.result = result);
}
/**
* Run Deployed
*/
_runDeployed() {
const stage = this.evt.options.stage;
this.evt.options.invocationType = this.evt.options.invocationType || 'RequestResponse';
this.evt.options.region = this.evt.options.region || S.getProject().getAllRegions(stage)[0].name;
const region = this.evt.options.region;
if (this.evt.options.invocationType !== 'RequestResponse') {
this.evt.options.logType = 'None';
} else {
this.evt.options.logType = this.evt.options.log ? 'Tail' : 'None'
}
// validate stage: make sure stage exists
if (!S.getProject().validateStageExists(stage)) {
return BbPromise.reject(new SError(`Stage "${stage}" does not exist in your project`, SError.errorCodes.UNKNOWN));
}
// validate region: make sure region exists in stage
if (!S.getProject().validateRegionExists(stage, region)) {
return BbPromise.reject(new SError(`Region "${region}" does not exist in stage "${stage}"`));
}
// Invoke Lambda
let params = {
FunctionName: this.function.getDeployedName({ stage, region }),
// ClientContext: new Buffer(JSON.stringify({x: 1, y: [3,4]})).toString('base64'),
InvocationType: this.evt.options.invocationType,
LogType: this.evt.options.logType,
Payload: new Buffer(JSON.stringify(this.evt.data.event)),
Qualifier: stage
};
return S.getProvider('aws')
.request('Lambda', 'invoke', params, stage, region)
.then( reply => {
const color = !reply.FunctionError ? 'white' : 'red';
if (reply.Payload) {
const response = JSON.parse(reply.Payload);
if (S.config.interactive) console.log(chalk[color](JSON.stringify(response, null, 4)));
this.evt.data.result = {
response,
status: 'success'
};
}
if (reply.LogResult) {
console.log(chalk.gray('--------------------------------------------------------------------'));
const logResult = new Buffer(reply.LogResult, 'base64').toString();
logResult.split('\n').forEach( line => console.log(SCli.formatLambdaLogEvent(line)) );
}
})
.catch(e => {
this.evt.data.result = {
status: 'error',
message: e.message,
stack: e.stack
};
return BbPromise.reject(e);
});
}
}
return( FunctionRun );
};