mirror of
https://github.com/serverless/serverless.git
synced 2026-01-25 15:07:39 +00:00
Merge pull request #924 from minibikini/master
Allows `serverless function run` to read from stdin, solves #900
This commit is contained in:
commit
f9042cd1c8
@ -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) {
|
||||
|
||||
@ -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}`);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
];
|
||||
|
||||
@ -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 );
|
||||
};
|
||||
Loading…
x
Reference in New Issue
Block a user