From b089b1c4b10c0c5ff98a6be90d48a329187c1741 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 31 Mar 2016 20:13:24 +0700 Subject: [PATCH 1/3] Allows `serverless function run` to read from stdin, solves #900 --- lib/Function.js | 6 +- lib/Runtime.js | 28 ++- lib/RuntimeNode.js | 52 ++--- lib/RuntimePython27.js | 11 +- lib/actions/FunctionRun.js | 436 ++++++++++++++++++++----------------- 5 files changed, 273 insertions(+), 260 deletions(-) diff --git a/lib/Function.js b/lib/Function.js index 03d246eff..db90cedab 100644 --- a/lib/Function.js +++ b/lib/Function.js @@ -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) { diff --git a/lib/Runtime.js b/lib/Runtime.js index 24ff93cd9..bf8449b00 100644 --- a/lib/Runtime.js +++ b/lib/Runtime.js @@ -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}`); diff --git a/lib/RuntimeNode.js b/lib/RuntimeNode.js index 07883cada..2ae502117 100644 --- a/lib/RuntimeNode.js +++ b/lib/RuntimeNode.js @@ -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 BbPromise.try(() => { // Call Function - functionCall(functionEvent, context(func, (err, result) => { - + functionCall(event, context(func, (err, result) => { SCli.log(`-----------------`); // Show error @@ -92,20 +70,20 @@ module.exports = function(S) { SCli.log(err.message); SCli.log(err.stack); - return resolve({ + return { status: 'error', response: err.message, error: err - }); + }; } // Show success response SCli.log(chalk.bold('Success! - This Response Was Returned:')); SCli.log(JSON.stringify(result, null, 4)); - return resolve({ + return { status: 'success', response: result - }); + }; })); }) }) diff --git a/lib/RuntimePython27.js b/lib/RuntimePython27.js index 334f81935..05088b42c 100644 --- a/lib/RuntimePython27.js +++ b/lib/RuntimePython27.js @@ -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 ]; diff --git a/lib/actions/FunctionRun.js b/lib/actions/FunctionRun.js index d7aa42ea2..668b4b0bd 100644 --- a/lib/actions/FunctionRun.js +++ b/lib/actions/FunctionRun.js @@ -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 service’s 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 service’s 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 ); }; \ No newline at end of file From 6c2bf9a8b20c82b2490a2ab9dd51d4c1cb6355d1 Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 31 Mar 2016 21:10:14 +0700 Subject: [PATCH 2/3] fix promises --- lib/RuntimeNode.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/lib/RuntimeNode.js b/lib/RuntimeNode.js index 2ae502117..124ba88f9 100644 --- a/lib/RuntimeNode.js +++ b/lib/RuntimeNode.js @@ -59,7 +59,7 @@ module.exports = function(S) { // to ensure that they are accessible in the global context. const functionCall = require(functionFile)[functionHandler]; - return BbPromise.try(() => { + return new BbPromise((resolve) => { // Call Function functionCall(event, context(func, (err, result) => { SCli.log(`-----------------`); @@ -70,20 +70,20 @@ module.exports = function(S) { SCli.log(err.message); SCli.log(err.stack); - return { + resolve({ status: 'error', response: err.message, error: err - }; + }); } // Show success response SCli.log(chalk.bold('Success! - This Response Was Returned:')); SCli.log(JSON.stringify(result, null, 4)); - return { + return resolve({ status: 'success', response: result - }; + }); })); }) }) From 9ae4dd6fb87231a13e58cbdbc3b8d48a4280fa9e Mon Sep 17 00:00:00 2001 From: Egor Kislitsyn Date: Thu, 31 Mar 2016 21:14:14 +0700 Subject: [PATCH 3/3] adds missing `return` statement --- lib/RuntimeNode.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/RuntimeNode.js b/lib/RuntimeNode.js index 124ba88f9..05522c985 100644 --- a/lib/RuntimeNode.js +++ b/lib/RuntimeNode.js @@ -70,7 +70,7 @@ module.exports = function(S) { SCli.log(err.message); SCli.log(err.stack); - resolve({ + return resolve({ status: 'error', response: err.message, error: err