serverless/lib/Serverless.js
2016-03-09 20:31:03 +07:00

463 lines
12 KiB
JavaScript

'use strict';
require('shelljs/global');
const path = require('path'),
_ = require('lodash'),
SCli = require('./utils/cli'),
SError = require('./Error'),
SPlugin = require('./Plugin'),
BbPromise = require('bluebird'),
dotenv = require('dotenv');
let SUtils;
// Global Bluebird Config
BbPromise.onPossiblyUnhandledRejection(function(error) {
throw error;
});
BbPromise.longStackTraces();
const supportedRuntimes = [
require('./RuntimeNode'),
require('./RuntimePython27')
];
/**
* Serverless Base Class
*/
class Serverless {
constructor(config) {
// Add version
this._version = require('./../package.json').version;
this._pipeline = null;
// Set Default Config
this.config = {
interactive: false,
serverlessPath: __dirname
};
this.classes = {
ProviderAws: require('./ProviderAws'),
Project: require('./Project'),
Function: require('./Function'),
Endpoint: require('./Endpoint'),
Event: require('./Event'),
Stage: require('./Stage'),
Region: require('./Region'),
Variables: require('./Variables'),
Templates: require('./Templates'),
Resources: require('./Resources'),
RuntimeNode: require('./RuntimeNode'),
RuntimePython27: require('./RuntimePython27')
};
// Add Config Settings
this.updateConfig(config);
// Add Defaults
this.providers = {};
this.actions = {};
this.hooks = {};
this.commands = {};
this.runtimes = [];
this.cli = null;
this.utils = require('./utils/index');
SUtils = this.utils;
supportedRuntimes.forEach(R => this.addRuntime(new R(this)));
this.initProviders();
}
/**
* Init
* - Initializes project
* - Returns a Promise
*/
init() {
let _this = this;
return BbPromise.try(function() {
if (_this.hasProject()) {
_this._project = new _this.classes.Project(_this);
return _this._project.load()
.then(function() {
// Load Admin ENV information
require('dotenv').config({
silent: true, // Don't display dotenv load failures for admin.env if we already have the required environment variables
path: path.join(_this.getProject().getRootPath(), 'admin.env')
});
});
}
})
.then(function() {
// Load Plugins: Framework Defaults
let defaults = require('./Actions.json');
_this._loadPlugins(__dirname, defaults.plugins);
_this.loadProjectPlugins();
});
}
// TODO: Remove Backward Compatibility. Many CI/CD systems are using _init() still.
_init() {
return this.init();
}
updateConfig(config) {
this.config = _.assign(this.config, config);
}
getConfig() {
return this.config;
}
getServerlessPath() {
return this.config.serverlessPath;
}
/**
* Project
*/
hasProject() {
return this.config.projectPath != undefined;
}
getProject() {
return this._project;
}
setProject( project ) {
this._project = project;
}
/**
* Providers
*/
initProviders() {
this.providers.aws = new this.classes.ProviderAws(this, this.config);
}
getProvider() {
return this.providers.aws;
}
hasProvider(name) {
return this.providers[name.toLowerCase()] != undefined;
}
/**
* Execute
*/
_execute(actionQueue, evt, config) {
let _this = this;
// If no queue, create one
if (!_this._pipeline) {
_this._pipeline = BbPromise.try(function() {
if (_this.cli) {
// If CLI...
// Set up evt.options
evt = {
options: _.assign(_this.cli.options, _this.cli.params)
};
} else {
// If Programmatic...
// If no options object, auto-set options
if (typeof evt.options === 'undefined' && Object.keys(evt).length) evt = { options: evt };
}
})
.then(function() {
return actionQueue.reduce(function (previous, current) {
return previous.then(current);
}, BbPromise.resolve(_this.middleware(evt, config)));
})
.catch(SError, function(e) {
_this._reset();
throw e;
process.exit(e.messageId);
})
.error(function(e) {
console.error(e);
_this._reset();
process.exit(1);
})
.finally(function() {
_this._reset();
});
return _this._pipeline;
} else {
// Otherwise, return promises in existing queue
return actionQueue.reduce(function (previous, current) {
return previous.then(current);
}, BbPromise.resolve(_this.middleware(evt, config)));
}
}
/**
* Middleware
*/
middleware(evt, config) {
// Always have properties
if (!evt.options) evt.options = {};
if (!evt.data) evt.data = {};
return evt;
}
/**
* Reset
*/
_reset() {
this._pipeline = null;
}
/**
* Load Project Plugins
*/
loadProjectPlugins() {
if( this.hasProject() ) {
this._loadPlugins( this.getProject().getRootPath(), this.getProject().getAllPlugins() );
}
}
/**
* Load Plugins
* - @param relDir string path to start from when rel paths are specified
* - @param pluginMetadata [{path:'path (re or loadable npm mod',config{}}]
*/
_loadPlugins(relDir, pluginMetadata) {
let _this = this;
for (let pluginMetadatum of pluginMetadata) {
// Find Plugin
let PluginClass;
if (pluginMetadatum.indexOf('.') > -1 ) {
// Load non-npm plugin from the private plugins folder
let pluginAbsPath = path.join(relDir, pluginMetadatum);
SUtils.sDebug('Attempting to load plugin from ' + pluginAbsPath);
PluginClass = require(pluginAbsPath);
PluginClass = PluginClass(SPlugin, __dirname);
} else {
// Load plugin from either plugins or node_modules folder
if (SUtils.dirExistsSync(path.join(relDir, 'node_modules', pluginMetadatum))) {
PluginClass = require(path.join(relDir, 'node_modules', pluginMetadatum));
PluginClass = PluginClass(SPlugin, __dirname);
}
}
// Load Plugin
if (PluginClass) {
SUtils.sDebug(PluginClass.getName() + ' plugin loaded');
this.addPlugin(new PluginClass(_this));
}
}
}
/**
* Command
*/
command(argv) {
let _this = this;
// Set CLI
_this.cli = {
context: null,
action: null,
options: {},
params: {},
raw: argv
};
// If debug option, set to debug mode
if (_this.cli.raw && _this.cli.raw.d) process.env.DEBUG = true;
SUtils.sDebug('CLI raw input: ', _this.cli.raw);
// If version command, return version
if (_this.cli.raw._[0] === 'version' || _this.cli.raw._[0] === 'v' | argv.v===true || argv.version===true) {
console.log(_this._version);
return BbPromise.resolve();
}
// Get Context & Action
_this.cli.context = _this.cli.raw._[0];
_this.cli.action = _this.cli.raw._[1];
// Show Help - if no context action, "help" or "h" is specified as params or options
if (_this.cli.raw._.length === 0 ||
_this.cli.raw._[0] === 'help' ||
_this.cli.raw._[0] === 'h' ||
_this.cli.raw.help ||
_this.cli.raw.h)
{
if (!_this.commands[_this.cli.context]) {
return SCli.generateMainHelp(_this.commands);
} else if (_this.commands[_this.cli.context] && !_this.commands[_this.cli.context][_this.cli.action]) {
return SCli.generateContextHelp(_this.cli.context, _this.commands);
} else if (_this.commands[_this.cli.context] && _this.commands[_this.cli.context][_this.cli.action]) {
return SCli.generateActionHelp(_this.commands[_this.cli.context][_this.cli.action]);
}
}
// If command not found, throw error
if (!_this.commands[_this.cli.context]) {
return BbPromise.reject(new SError('In the command you just typed, the "' + _this.cli.context + '" is valid but "' + _this.cli.action + '" is not. Enter "serverless help" to see the actions for this context.'));
}
if (!_this.commands[_this.cli.context][_this.cli.action]) {
return BbPromise.reject(new SError('Command not found. Enter "serverless help" to see all available commands.'));
}
// if not in project root and not creating project, throw error
if (!this.hasProject() && _this.cli.context != 'project') {
return BbPromise.reject(new SError('This command can only be run inside a Serverless project.'));
}
// Get Command Config
let cmdConfig = _this.commands[_this.cli.context][_this.cli.action];
// Options - parse using command config
cmdConfig.options.map(opt => {
_this.cli.options[opt.option] = (_this.cli.raw[opt.option] ? _this.cli.raw[opt.option] : (_this.cli.raw[opt.shortcut] || null));
});
// Params - remove context and contextAction strings from params array
let params = _this.cli.raw._.filter(v => {
return ([cmdConfig.context, cmdConfig.contextAction].indexOf(v) == -1);
});
// Params - parse params using command config
if (cmdConfig.parameters) {
cmdConfig.parameters.forEach(function(parameter) {
if (parameter.position.indexOf('->') == -1) {
_this.cli.params[parameter.parameter] = params.splice(parameter.position, parameter.position + 1);
_this.cli.params[parameter.parameter] = _this.cli.params[parameter.parameter][0];
} else {
_this.cli.params[parameter.parameter] = params.splice(parameter.position.split('->')[0], (parameter.position.split('->')[1] ? parameter.position.split('->')[1] : params.length));
}
});
}
SUtils.sDebug('CLI processed input: ', _this.cli);
_this.actions[cmdConfig.handler].apply(_this, {});
}
/**
* Add action
* @param action must return an ES6 BbPromise that is resolved or rejected
* @param config
*/
addAction(action, config) {
let _this = this;
// Add Hooks Array
this.hooks[config.handler + 'Pre'] = [];
this.hooks[config.handler + 'Post'] = [];
// Handle optional configuration
config.options = config.options || [];
config.parameters = config.parameters || [];
// Add Action
this.actions[config.handler] = function(evt) {
// Add pre hooks, action, then post hooks to queued
let queue = _this.hooks[config.handler + 'Pre'];
// Prevent duplicate actions from being added
if (queue.indexOf(action) === -1) queue.push(action);
// Use _execute()
return _this._execute(queue.concat(_this.hooks[config.handler + 'Post']), evt, config);
};
// Add command
if (config.context && config.contextAction) {
if (!this.commands[config.context]) {
this.commands[config.context] = {};
}
this.commands[config.context][config.contextAction] = config;
}
}
/**
* Add Hook
*/
addHook(hook, config) {
let name = config.action + (config.event.charAt(0).toUpperCase() + config.event.slice(1));
this.hooks[name].push(hook);
}
/**
* Add Plugin
*/
addPlugin(ServerlessPlugin) {
return BbPromise.all([
ServerlessPlugin.registerActions(),
ServerlessPlugin.registerHooks()
]);
}
/**
* Add Runtime
*/
addRuntime(runtime) {
this.runtimes.push(runtime);
}
getRuntime(runtimeName) {
return _.find(this.runtimes, (r) => r.getName() === runtimeName);
}
}
module.exports = Serverless;