diff --git a/lib/Serverless.js b/lib/Serverless.js index 0a49ac05d..fb04d714a 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -32,17 +32,17 @@ class Serverless { this._awsAdminSecretKey = config.awsAdminSecretKey; this._version = require('./../package.json').version; this._projectRootPath = SUtils.getProjectPath(process.cwd()); - this._project = false; - this._meta = false; this.actions = {}; this.hooks = {}; this.commands = {}; + this.classes = { + Project: require('./ServerlessProject'), + Module: require('./ServerlessModule'), + }; - // If within private, add further queued data + // If project if (this._projectRootPath) { - this._project = SUtils.getProject(this._projectRootPath); - // 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 @@ -54,44 +54,61 @@ class Serverless { this._awsAdminSecretKey = process.env.SERVERLESS_ADMIN_AWS_SECRET_ACCESS_KEY; } - // Load Plugins: Defaults + // Load Plugins: Framework Defaults let defaults = require('./Actions.json'); this._loadPlugins(__dirname, defaults.plugins); // Load Plugins: Project - if (this._projectRootPath && this._project.plugins) { - this._loadPlugins(this._projectRootPath, this._project.plugins); + if (this._projectRootPath && SUtils.fileExistsSync(path.join(this._projectRootPath, 's-project.json'))) { + let projectJson = require(path.join(this._projectRootPath, 's-project.json')); + if (projectJson.plugins) this._loadPlugins(this._projectRootPath, projectJson.plugins); } + + let prj = new this.classes.Project(this, {}); + console.log(prj.get()); } /** - * Validate Project - * Ensures: - * - A Valid Serverless Project is found - * - Project's s-project.json has one valid region and stage + * Load Plugins + * - @param relDir string path to start from when rel paths are specified + * - @param pluginMetadata [{path:'path (re or loadable npm mod',config{}}] */ - validateProject() { + _loadPlugins(relDir, pluginMetadata) { let _this = this; - // Check for root path - if (!this._projectRootPath) { - return BbPromise.reject(new SError('Must be in a Serverless project', SError.errorCodes.NOT_IN_SERVERLESS_PROJECT)); + for (let pluginMetadatum of pluginMetadata) { + + // Find Plugin + let PluginClass; + if (pluginMetadatum.path.indexOf('.') == 0) { + + // Load non-npm plugin from the private plugins folder + let pluginAbsPath = path.join(relDir, pluginMetadatum.path); + SUtils.sDebug('Attempting to load plugin from ' + pluginAbsPath); + PluginClass = require(pluginAbsPath); + PluginClass = PluginClass(SPlugin, __dirname); + } else { + + // Load plugin from either custom or node_modules in plugins folder + if (SUtils.dirExistsSync(path.join(relDir, 'plugins', 'custom', pluginMetadatum.path))) { + PluginClass = require(path.join(relDir, 'plugins', 'custom', pluginMetadatum.path)); + PluginClass = PluginClass(SPlugin, __dirname); + } else if (SUtils.dirExistsSync(path.join(relDir, 'plugins', 'node_modules', pluginMetadatum.path))) { + PluginClass = require(path.join(relDir, 'plugins', 'node_modules', pluginMetadatum.path)); + PluginClass = PluginClass(SPlugin, __dirname); + } + } + + // Load Plugin + if (!PluginClass) { + console.log('WARNING: This plugin was requested by this project but could not be found: ' + pluginMetadatum.path); + } else { + SUtils.sDebug(PluginClass.getName() + ' plugin loaded'); + this.addPlugin(new PluginClass(_this)); + } } - - //TODO: Validate "meta" folder exists w/ essential variable files, or auto-create here temporarily to help transition users. - //TODO: Check _project has at least one region and a stage - - // Check for AWS API Keys - if (!this._awsAdminKeyId || !this._awsAdminSecretKey) { - return BbPromise.reject(new SError( - 'Missing AWS API Keys', - SError.errorCodes.INVALID_PROJECT_SERVERLESS - )); - } - - return BbPromise.resolve(); } /** @@ -173,16 +190,6 @@ class Serverless { // Add Action this.actions[config.handler] = function(evt) { - // Get Meta Object, Populate Variables - if (_this._projectRootPath) { - _this._meta = SUtils.getMeta(_this._projectRootPath); - - //_this._project = SUtils.populateVariables(_this._project, _this._meta, { - // type: 'private' - //}); - //console.log(_this._project); - } - // Add pre hooks, action, then post hooks to queued let queue = _this.hooks[config.handler + 'Pre']; @@ -210,9 +217,7 @@ class Serverless { } /** - * Register Hook - * @param hook - * @param config + * Add Hook */ addHook(hook, config) { @@ -222,8 +227,6 @@ class Serverless { /** * Add Plugin - * @param ServerlessPlugin class object - * @returns {Promise} */ addPlugin(ServerlessPlugin) { @@ -232,74 +235,6 @@ class Serverless { ServerlessPlugin.registerHooks(), ]); } - - /** - * Execute Pre/Post shell based hook - * @param fullScriptPath - * @returns {Promise.} return code - * @private - * TODO Re-implement this - */ - - //_executeShellHook(fullScriptPath) { - // SCli.log(`Executing shell hook ${fullScriptPath}...`); - // - // try { - // let rc = exec(fullScriptPath, {silent: false}).code; - // if (rc !== 0) { - // return BbPromise.reject(new SError(`ERROR executing shell hook ${fullScriptPath}. RC: ${rc}...`, SError.errorCodes.UNKNOWN)); - // } - // } catch (e) { - // console.error(e); - // return BbPromise.reject(new SError(`ERROR executing shell hook ${fullScriptPath}. Threw error. RC: ${rc}...`, SError.errorCodes.UNKNOWN)); - // } - // - // return BbPromise.resolve(rc); - //} - - /** - * Load Plugins - * @param relDir string path to start from when rel paths are specified - * @param pluginMetadata [{path:'path (re or loadable npm mod',config{}}] - * @private - */ - - _loadPlugins(relDir, pluginMetadata) { - - let _this = this; - - for (let pluginMetadatum of pluginMetadata) { - - // Find Plugin - let PluginClass; - if (pluginMetadatum.path.indexOf('.') == 0) { - - // Load non-npm plugin from the private plugins folder - let pluginAbsPath = path.join(relDir, pluginMetadatum.path); - SUtils.sDebug('Attempting to load plugin from ' + pluginAbsPath); - PluginClass = require(pluginAbsPath); - PluginClass = PluginClass(SPlugin, __dirname); - } else { - - // Load plugin from either custom or node_modules in plugins folder - if (SUtils.dirExistsSync(path.join(relDir, 'plugins', 'custom', pluginMetadatum.path))) { - PluginClass = require(path.join(relDir, 'plugins', 'custom', pluginMetadatum.path)); - PluginClass = PluginClass(SPlugin, __dirname); - } else if (SUtils.dirExistsSync(path.join(relDir, 'plugins', 'node_modules', pluginMetadatum.path))) { - PluginClass = require(path.join(relDir, 'plugins', 'node_modules', pluginMetadatum.path)); - PluginClass = PluginClass(SPlugin, __dirname); - } - } - - // Load Plugin - if (!PluginClass) { - console.log('WARNING: This plugin was requested by this project but could not be found: ' + pluginMetadatum.path); - } else { - SUtils.sDebug(PluginClass.getName() + ' plugin loaded'); - this.addPlugin(new PluginClass(_this)); - } - } - } } -module.exports = Serverless; +module.exports = Serverless; \ No newline at end of file diff --git a/lib/ServerlessFunction.js b/lib/ServerlessFunction.js new file mode 100644 index 000000000..994f0ae74 --- /dev/null +++ b/lib/ServerlessFunction.js @@ -0,0 +1,96 @@ +'use strict'; + +/** + * Serverless Function Class + * - options.path format is: "moduleFolder/functionFolder#functionName" + */ + +const SError = require('./ServerlessError'), + SUtils = require('./utils/index'), + SCli = require('./utils/cli'), + awsMisc = require('./utils/aws/Misc'), + extend = require('util')._extend, + path = require('path'), + fs = require('fs'), + BbPromise = require('bluebird'); + +class ServerlessFunction { + + /** + * Constructor + */ + + constructor(Serverless, options) { + this.S = Serverless; + this.load(options.path); + } + + /** + * Load + * - Load from source (i.e., file system); + */ + + load(functionPath) { + + let _this = this; + + //TODO: Validate Path (ensure it has '/' and '#') + + // Defaults + _this._populated = false; + _this.data = {}; + _this.data.endpoints = []; + + // If no project path exists, return + if (!functionPath) return; + + let func = SUtils.readAndParseJsonSync(path.join( + _this.S._projectRootPath, + 'back', + 'modules', + functionPath.split('/')[0], + 'functions', + functionPath.split('/')[1].split('#')[0], + 's-function.json')); + + func = func.functions[functionPath.split('#')[1]]; + func.name = functionPath.split('#')[1]; // Add name, for consistency + + _this = extend(_this.data, func); + } + + /** + * Get + * - Return data + */ + + get() { + return this.data; + } + + /** + * Set + * - Update data + */ + + set(data) { + + // TODO: Validate data + + this.data = data; + } + + /** + * Populate + * - Fill in templates then variables + */ + + populate(stage, region) { + + this._populated = true; + // TODO: Implement via SUtils class which can be used in Project, Module & Function Classes + } +} + +module.exports = ServerlessFunction; + diff --git a/lib/ServerlessModule.js b/lib/ServerlessModule.js new file mode 100644 index 000000000..c9f0d8f8b --- /dev/null +++ b/lib/ServerlessModule.js @@ -0,0 +1,130 @@ +'use strict'; + +/** + * Serverless Module Class + * - options.path format is: "moduleFolder" + */ + +const SError = require('./ServerlessError'), + SUtils = require('./utils/index'), + SCli = require('./utils/cli'), + awsMisc = require('./utils/aws/Misc'), + ServerlessFunction = require('./ServerlessFunction'), + extend = require('util')._extend, + path = require('path'), + fs = require('fs'), + BbPromise = require('bluebird'); + +class ServerlessModule { + + /** + * Constructor + */ + + constructor(Serverless, options) { + this.S = Serverless; + this.load(options.path); + } + + /** + * Load + * - Load from source (i.e., file system); + */ + + load(modulePath) { + + let _this = this; + + // TODO: Validate Module path. Should consist of Module 'serverless-users' name only. + + // Defaults + _this._populated = false; + _this.data = {}; + _this.data.name = 'serverless' + SUtils.generateShortId(6); + _this.data.version = '0.0.1'; + _this.data.profile = 'aws-v' + require('../package.json').version; + _this.data.location = 'https://github.com/...'; + _this.data.author = ''; + _this.data.description = 'A Serverless Module'; + _this.data.custom = {}; + _this.data.functions = {}; + _this.data.cloudFormation = { + resources: {}, + lambdaIamPolicyDocumentStatements: [] + }; + + // If no project path exists, return + if (!modulePath) return; + + let module = SUtils.readAndParseJsonSync(path.join( + _this.S._projectRootPath, + 'back', + 'modules', + modulePath, + 's-module.json')); + + // Add Functions + module.functions = {}; + let functionList = fs.readdirSync(path.join( + _this.S._projectRootPath, + 'back', + 'modules', + modulePath, + 'functions')); + + for (let i = 0; i < functionList.length; i++) { + + let funcFile = SUtils.readAndParseJsonSync(path.join( + _this.S._projectRootPath, + 'back', + 'modules', + modulePath, + 'functions', + functionList[i], + 's-function.json')); + + Object.keys(funcFile.functions).forEach(function(funcKey) { + let func = new ServerlessFunction(_this.S, { path: modulePath + '/' + functionList[i] + '#' + funcKey }); + func = func.get(); + module.functions[func.name] = func; + }); + } + + _this = extend(_this.data, module); + } + + /** + * Get + * - Return data + */ + + get() { + return this.data; + } + + /** + * Set + * - Update data + */ + + set(data) { + + // TODO: Validate data + + this.data = data; + } + + /** + * Populate + * - Fill in templates then variables + */ + + populate(stage, region) { + + this._populated = true; + // TODO: Implement via SUtils class which can be used in Project, Module & Function Classes + } +} + +module.exports = ServerlessModule; + diff --git a/lib/ServerlessProject.js b/lib/ServerlessProject.js new file mode 100644 index 000000000..d7078773f --- /dev/null +++ b/lib/ServerlessProject.js @@ -0,0 +1,171 @@ +'use strict'; + +/** + * Serverless Project Class + */ + +const SError = require('./ServerlessError'), + SUtils = require('./utils/index'), + SCli = require('./utils/cli'), + awsMisc = require('./utils/aws/Misc'), + ServerlessModule = require('./ServerlessModule'), + extend = require('util')._extend, + path = require('path'), + fs = require('fs'), + BbPromise = require('bluebird'); + +class ServerlessProject { + + /** + * Constructor + */ + + constructor(Serverless, options) { + this.S = Serverless; + this.load(this.S._projectRootPath); + } + + /** + * Load + * - Load from source (i.e., file system); + */ + + load(projectPath) { + + let _this = this; + + // Defaults + _this._populated = false; + _this.data = {}; + _this.data.name = 'serverless' + SUtils.generateShortId(6); + _this.data.version = '0.0.1'; + _this.data.profile = 'serverless-v' + require('../package.json').version; + _this.data.location = 'https://github.com/...'; + _this.data.author = ''; + _this.data.description = 'A Serverless Project'; + _this.data.custom = {}; + _this.data.modules = {}; + _this.data.plugins = []; + _this.data.cloudFormation = { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "The AWS CloudFormation template for this Serverless application's resources outside of Lambdas and Api Gateway", + "Resources": { + "IamRoleLambda": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": [ + "lambda.amazonaws.com" + ] + }, + "Action": [ + "sts:AssumeRole" + ] + } + ] + }, + "Path": "/" + } + }, + "IamPolicyLambda": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyName": "${stage}-${projectName}-lambda", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": "arn:aws:logs:${region}:*:" + } + ] + }, + "Roles": [ + { + "Ref": "IamRoleLambda" + } + ], + "Groups": [ + { + "Ref": "IamGroupLambda" + } + ] + } + } + }, + "Outputs": { + "IamRoleArnLambda": { + "Description": "ARN of the lambda IAM role", + "Value": { + "Fn::GetAtt": [ + "IamRoleLambda", + "Arn" + ] + } + } + } + }; + + // If no project path exists, return + if (!projectPath) return; + + // Get Project JSON + let project = SUtils.readAndParseJsonSync(path.join(projectPath, 's-project.json')); + + // Add Modules & Functions + project.modules = {}; + let moduleList = fs.readdirSync(path.join(projectPath, 'back', 'modules')); + + for (let i = 0; i < moduleList.length; i++) { + let module = new ServerlessModule(_this.S, { path: moduleList[i] }); + module = module.get(); + project.modules[module.name] = module; + } + + _this = extend(_this.data, project); + } + + /** + * Get + * - Return data + */ + + get() { + return this.data; + } + + /** + * Set + * - Update data + */ + + set(data) { + + // TODO: Validate data + + this.data = data; + } + + /** + * Populate + * - Fill in templates then variables + */ + + populate(stage, region) { + + this._populated = true; + // TODO: Implement via SUtils class which can be used in Project, Module & Function Classes + } +} + +module.exports = ServerlessProject; \ No newline at end of file diff --git a/lib/actions/CodeDeployLambdaNodeJs.js b/lib/actions/CodeDeployLambdaNodeJs.js index 84fbc1943..28e4d77bc 100644 --- a/lib/actions/CodeDeployLambdaNodeJs.js +++ b/lib/actions/CodeDeployLambdaNodeJs.js @@ -142,7 +142,7 @@ module.exports = function(SPlugin, serverlessPath) { return _this.S3.sPutLambdaZip( evt.region.regionBucket, - _this.S._project.name, + _this.S.data.project.get('name'), evt.stage, evt.function.name, fs.createReadStream(evt.function.pathCompressed)) @@ -168,7 +168,7 @@ module.exports = function(SPlugin, serverlessPath) { lambdaVersion; var params = { - FunctionName: _this.Lambda.sGetLambdaName(_this.S._project, evt.function), + FunctionName: _this.Lambda.sGetLambdaName(_this.S.data.project.get('name'), evt.function.name), Qualifier: '$LATEST' }; @@ -193,11 +193,11 @@ module.exports = function(SPlugin, serverlessPath) { S3Bucket: evt.function.s3Bucket, S3Key: evt.function.s3Key }, - FunctionName: _this.Lambda.sGetLambdaName(_this.S._project, evt.function), /* required */ + FunctionName: _this.Lambda.sGetLambdaName(_this.S.data.project.get('name'), evt.function.name), /* required */ Handler: evt.function.handler, /* required */ Role: evt.region.iamRoleArnLambda, /* required */ Runtime: evt.function.module.runtime, /* required */ - Description: 'Serverless Lambda function for project: ' + _this.S._project.name, + Description: 'Serverless Lambda function for project: ' + _this.S.data.project.get('name'), MemorySize: evt.function.memorySize, Publish: true, // Required by Serverless Framework & recommended by AWS Timeout: evt.function.timeout diff --git a/lib/actions/CodeEventDeployLambda.js b/lib/actions/CodeEventDeployLambda.js index 5b688543f..4b1ffaef8 100644 --- a/lib/actions/CodeEventDeployLambda.js +++ b/lib/actions/CodeEventDeployLambda.js @@ -57,7 +57,7 @@ module.exports = function(SPlugin, serverlessPath) { async.eachLimit(evt.function.events, 5, function (event, cb) { let params = { - FunctionName: _this.Lambda.sGetLambdaName(_this.S._project, evt.function), + FunctionName: _this.Lambda.sGetLambdaName(_this.S.data.project.get('name'), evt.function.name), EventSourceArn: event.eventSourceArn, StartingPosition: event.startingPosition, BatchSize: event.batchSize, diff --git a/lib/actions/EndpointBuildApiGateway.js b/lib/actions/EndpointBuildApiGateway.js index 3b22c782b..e7f8695b2 100644 --- a/lib/actions/EndpointBuildApiGateway.js +++ b/lib/actions/EndpointBuildApiGateway.js @@ -198,7 +198,7 @@ module.exports = function(SPlugin, serverlessPath) { let _this = this; let params = { - FunctionName: _this.Lambda.sGetLambdaName(_this.S._project, evt.endpoint.function), /* required */ + FunctionName: _this.Lambda.sGetLambdaName(_this.S.data.project.get('name'), evt.endpoint.function.name), /* required */ Qualifier: evt.stage }; diff --git a/lib/actions/EnvGet.js b/lib/actions/EnvGet.js index 49224c286..7f8b46ad5 100644 --- a/lib/actions/EnvGet.js +++ b/lib/actions/EnvGet.js @@ -101,9 +101,8 @@ usage: serverless env get`, } } - return _this.S.validateProject() + return _this._prompt() .bind(_this) - .then(_this._prompt) .then(_this._validateAndPrepare) .then(_this._getEnvVar) .then(function() { diff --git a/lib/actions/EnvList.js b/lib/actions/EnvList.js index 424b41fc9..b930ec57a 100644 --- a/lib/actions/EnvList.js +++ b/lib/actions/EnvList.js @@ -95,9 +95,8 @@ Usage: serverless env list`, } } - return _this.S.validateProject() + return _this._prompt() .bind(_this) - .then(_this._prompt) .then(_this._validateAndPrepare) .then(function(evt) { return evt; diff --git a/lib/actions/EnvSet.js b/lib/actions/EnvSet.js index 9f7c2cca1..4d06b402d 100644 --- a/lib/actions/EnvSet.js +++ b/lib/actions/EnvSet.js @@ -105,9 +105,8 @@ usage: serverless env set`, } } - return _this.S.validateProject() + return _this._prompt() .bind(_this) - .then(_this._prompt) .then(_this._validateAndPrepare) .then(_this._setEnvVar) .then(function() { diff --git a/lib/actions/EnvUnset.js b/lib/actions/EnvUnset.js index 54bb66881..25d5fc829 100644 --- a/lib/actions/EnvUnset.js +++ b/lib/actions/EnvUnset.js @@ -99,9 +99,8 @@ usage: serverless env unset`, } } - return _this.S.validateProject() + return _this._prompt() .bind(_this) - .then(_this._prompt) .then(_this._validateAndPrepare) .then(_this._unsetEnvVar) .then(function() { diff --git a/lib/actions/FunctionCreate.js b/lib/actions/FunctionCreate.js index 6fd455508..4c1454fd6 100644 --- a/lib/actions/FunctionCreate.js +++ b/lib/actions/FunctionCreate.js @@ -98,9 +98,8 @@ usage: serverless function create `, if (_this.S.cli.options.nonInteractive) _this.S._interactive = false; } - return _this.S.validateProject() + return _this._promptModuleFunction() .bind(_this) - .then(_this._promptModuleFunction) .then(_this._validateAndPrepare) .then(_this._createFunctionSkeleton) .then(function() { diff --git a/lib/actions/FunctionDeploy.js b/lib/actions/FunctionDeploy.js index 6ed05f568..15fa49d25 100644 --- a/lib/actions/FunctionDeploy.js +++ b/lib/actions/FunctionDeploy.js @@ -19,6 +19,7 @@ */ module.exports = function(SPlugin, serverlessPath) { + const path = require('path'), SError = require(path.join(serverlessPath, 'ServerlessError')), SUtils = require(path.join(serverlessPath, 'utils/index')), @@ -96,7 +97,7 @@ module.exports = function(SPlugin, serverlessPath) { if (_this.S.cli) { // Options - evt = JSON.parse(JSON.stringify(this.S.cli.options)); // Important: Clone objects, don't refer to them + evt = JSON.parse(JSON.stringify(_this.S.cli.options)); // Important: Clone objects, don't refer to them // Option - Non-interactive if (_this.S.cli.options.nonInteractive) _this.S._interactive = false diff --git a/lib/actions/ModuleCreate.js b/lib/actions/ModuleCreate.js index 0d72d6b1b..e446fc374 100644 --- a/lib/actions/ModuleCreate.js +++ b/lib/actions/ModuleCreate.js @@ -99,9 +99,8 @@ usage: serverless module create`, } } - return _this.S.validateProject() + return _this._promptModuleFunction() .bind(_this) - .then(_this._promptModuleFunction) .then(_this._validateAndPrepare) .then(_this._createModuleSkeleton) .then(function() { diff --git a/lib/actions/ModuleInstall.js b/lib/actions/ModuleInstall.js index 2bda2b8e3..af70f3a49 100644 --- a/lib/actions/ModuleInstall.js +++ b/lib/actions/ModuleInstall.js @@ -78,9 +78,8 @@ usage: serverless module install `, _this.evt.url = this.S.cli.params[0]; } - return this.S.validateProject() + return _this._downloadModule() .bind(_this) - .then(_this._downloadModule) .then(_this._validateAndPrepare) .then(_this._installModule) .then(_this._updateCfTemplate) @@ -222,6 +221,7 @@ usage: serverless module install `, */ _updateCfTemplate() { + let _this = this, projectCfPath = path.join(_this.S._projectRootPath, 'cloudformation'), cfExtensionPoints = SUtils.readAndParseJsonSync(path.join(_this.evt.pathTempModule, 's-module.json')).cloudFormation; diff --git a/lib/actions/ProjectCreate.js b/lib/actions/ProjectCreate.js index eb0bb2a73..98c1b15e5 100644 --- a/lib/actions/ProjectCreate.js +++ b/lib/actions/ProjectCreate.js @@ -108,7 +108,7 @@ module.exports = function(SPlugin, serverlessPath) { // If CLI, parse arguments if (_this.S.cli) { - _this.evt = JSON.parse(JSON.stringify(this.S.cli.options)); // Important: Clone objects, don't refer to them + _this.evt = JSON.parse(JSON.stringify(_this.S.cli.options)); // Important: Clone objects, don't refer to them if (_this.S.cli.options.nonInteractive) _this.S._interactive = false; } @@ -410,7 +410,6 @@ module.exports = function(SPlugin, serverlessPath) { */ _createProjectBucket() { - SCli.log('Creating a project region bucket on S3: ' + this.evt.projectBucket + '...'); return this.S3.sCreateBucket(this.evt.projectBucket); } @@ -423,18 +422,6 @@ module.exports = function(SPlugin, serverlessPath) { let _this = this; - //TODO: Move to RegionCreate - //if (cfStackData) { - // for (let i = 0; i < cfStackData.Outputs.length; i++) { - // if (cfStackData.Outputs[i].OutputKey === 'IamRoleArnLambda') { - // _this.evt.iamRoleLambdaArn = cfStackData.Outputs[i].OutputValue; - // } - // } - // - // // Save StackName to Evt - // _this.evt.stageCfStack = cfStackData.StackName; - //} - // Create s-project.json let prjJson = SUtils.readAndParseJsonSync(path.join(_this._templatesDir, 's-project.json')); prjJson.name = _this.evt.name; diff --git a/lib/actions/RegionCreate.js b/lib/actions/RegionCreate.js index 9eddd1dd1..c12eea4ca 100644 --- a/lib/actions/RegionCreate.js +++ b/lib/actions/RegionCreate.js @@ -95,14 +95,12 @@ usage: serverless region create`, if (_this.S.cli.options.nonInteractive) _this.S._interactive = false; } - return _this.S.validateProject() + return _this._prompt() .bind(_this) - .then(_this._prompt) .then(_this._validateAndPrepare) .then(_this._initAWS) .then(_this._putEnvFile) - .then(_this._putCfFile) - .then(_this._createCfStack) + .then(_this._resourcesDeploy) .then(function() { SCli.log('Successfully created region ' + _this.evt.region + ' within stage ' + _this.evt.stage); return _this.evt; @@ -167,6 +165,9 @@ usage: serverless region create`, this.S._meta.private.stages[this.evt.stage].regions[this.evt.region] = { variables: {} }; + + // Save Meta before deploying resources + SUtils.saveMeta(this.S._projectRootPath, this.S._meta); } /** @@ -212,7 +213,16 @@ SERVERLESS_PROJECT_NAME=${this.S._project.name}`; */ _resourcesDeploy() { - // TODO: Call Resources Deploy subaction + + let _this = this; + + let newEvent = { + stage: 'development', + region: _this.evt.region, + _subaction: true + }; + + return _this.S.actions.resourcesDeploy(newEvent); } } diff --git a/lib/actions/ResourcesDeploy.js b/lib/actions/ResourcesDeploy.js index f95f21ee5..c5fc5443a 100644 --- a/lib/actions/ResourcesDeploy.js +++ b/lib/actions/ResourcesDeploy.js @@ -9,8 +9,6 @@ * region (String) the name of the region you want to deploy resources to. Must exist in provided stage. */ -//TODO: Refactor how resources work... Check the Road Map for more information - module.exports = function(SPlugin, serverlessPath) { const path = require('path'), @@ -89,9 +87,8 @@ usage: serverless resources deploy`, if (_this.S.cli.options.nonInteractive) _this.S._interactive = false; } - return this.S.validateProject() + return _this._prompt() .bind(_this) - .then(_this._prompt) .then(_this._validateAndPrepare) .then(_this._updateResources) .then(() => { @@ -131,7 +128,7 @@ usage: serverless resources deploy`, * Validate & Prepare */ - _validateAndPrepare(){ + _validateAndPrepare() { let _this = this; @@ -156,7 +153,7 @@ usage: serverless resources deploy`, } // Validate region: make sure region exists in stage - if (!_this.S._meta.project.stages[_this.evt.stage].regions[_this.evt.region]) { + if (!_this.S._meta.private.stages[_this.evt.stage].regions[_this.evt.region]) { return BbPromise.reject(new SError('Region "' + _this.evt.region + '" does not exist in stage "' + _this.evt.stage + '"')); } @@ -189,58 +186,46 @@ usage: serverless resources deploy`, }; _this.CF = require('../utils/aws/CloudFormation')(awsConfig); - return _this.CF.sUpdateResourcesStack( + // Create or update CF Stack + return _this.CF.sCreateOrUpdateResourcesStack( _this.S, _this.evt.stage, - _this.evt.region.region) + _this.evt.region.region, + 'update') .then(cfData => { + + // Monitor CF Update return _this.CF.sMonitorCf(cfData, 'update'); }) .catch(function(e) { if (e.message.indexOf('does not exist') !== -1) { - // If Resources stack does not exit, create it. - return _this.CF.sPutCfFile( - _this.S._projectRootPath, - _this.evt.region.regionBucket, - _this.S._project.name, + return _this.CF.sCreateOrUpdateResourcesStack( + _this.S, _this.evt.stage, - _this.evt.region, - 'resources') - .then(function(cfTemplateUrl) { + _this.evt.region.region, + 'create') + .then(cfData => { - let projResoucesCfPath = path.join(_this.S._projectRootPath, 'cloudformation', 'resources-cf.json'), - cfTemplate = SUtils.readAndParseJsonSync(projResoucesCfPath); + // Monitor CF Create + return _this.CF.sMonitorCf(cfData, 'create') + .then(cfStackData => { - // Create CF resources stack - return _this.CF.sCreateResourcesStack( - _this.S._projectRootPath, - _this.S._project.name, - _this.evt.stage, - _this.S._project.domain, - '', - cfTemplateUrl - ) - .then(cfData => { - return _this.CF.sMonitorCf(cfData, 'create') - .then(cfStackData => { + _this._spinner.stop(true); - _this._spinner.stop(true); - - if (cfStackData) { - for (let i = 0; i < cfStackData.Outputs.length; i++) { - if (cfStackData.Outputs[i].OutputKey === 'IamRoleArnLambda') { - _this.evt.region.iamRoleArnLambda = cfStackData.Outputs[i].OutputValue; - } - } - } - }); + if (cfStackData) { + for (let i = 0; i < cfStackData.Outputs.length; i++) { + if (cfStackData.Outputs[i].OutputKey === 'IamRoleArnLambda') { + _this.evt.region.iamRoleArnLambda = cfStackData.Outputs[i].OutputValue; + } + } + } }); }); } else { - if( e.message.indexOf('No updates are to be performed.') !== -1) { + if (e.message.indexOf('No updates are to be performed.') !== -1) { return BbPromise.resolve({}); } else { return BbPromise.reject(new SError(e)); diff --git a/lib/actions/StageCreate.js b/lib/actions/StageCreate.js index 009b6f0ee..b3b528623 100644 --- a/lib/actions/StageCreate.js +++ b/lib/actions/StageCreate.js @@ -94,9 +94,8 @@ usage: serverless stage create`, if (_this.S.cli.options.nonInteractive) _this.S._interactive = false; } - return _this.S.validateProject() + return _this._prompt() .bind(_this) - .then(_this._prompt) .then(_this._validateAndPrepare) .then(_this._createRegion) .then(function() { diff --git a/lib/utils/aws/CloudFormation.js b/lib/utils/aws/CloudFormation.js index da1bfff51..29dcba1b7 100644 --- a/lib/utils/aws/CloudFormation.js +++ b/lib/utils/aws/CloudFormation.js @@ -10,8 +10,8 @@ let BbPromise = require('bluebird'), os = require('os'), async = require('async'), AWS = require('aws-sdk'), - SUtils = require('../../utils'), - SError = require('../../ServerlessError'), + SUtils = require('../../utils'), + SError = require('../../ServerlessError'), fs = require('fs'); // Promisify fs module. This adds "Async" to the end of every method @@ -100,8 +100,15 @@ module.exports = function(config) { }); }; + /** + * Get Lambda PHysical ID From Logical + * @param logicalIds + * @param lambdaResourceSummaries + * @returns {Array} + */ CloudFormation.sGetLambdaPhysicalsFromLogicals = function(logicalIds, lambdaResourceSummaries) { + let lambdaPhysicalIds = []; for (let lid of logicalIds) { let foundLambda = lambdaResourceSummaries.find(element=> { @@ -122,15 +129,20 @@ module.exports = function(config) { * Put CF File On S3 */ - CloudFormation.sPutCfFile = function(projRootPath, bucketName, projName, projStage, projRegion) { + CloudFormation.sPutCfFile = function(Serverless, stage, region) { let S3 = require('./S3')(config); + // Get Resources + let cfTemplate = SUtils.getResources(Serverless._project); + + console.log("here", cfTemplate); + let d = new Date(), - cfPath = path.join(projRootPath, 'meta', 'private', 'resources', 's-resources-cf.json'), - key = ['Serverless', projName, projStage, projRegion, 'resources/' + 's-resources-cf'].join('/') + '@' + d.getTime() + '.json', + cfPath = path.join(Serverless._projectRootPath, 'meta', 'private', 'resources', 's-resources-cf.json'), + key = ['Serverless', Serverless._project.name, stage, region, 'resources/' + 's-resources-cf'].join('/') + '@' + d.getTime() + '.json', params = { - Bucket: bucketName, + Bucket: Serverless._meta.private.variables.projectBucket, Key: key, ACL: 'private', ContentType: 'application/json', @@ -138,89 +150,56 @@ module.exports = function(config) { }; return S3.putObjectPromised(params) - .then(function() { + .then(function() { - // TemplateURL is an https:// URL. You force us to lookup endpt vs bucket/key attrs!?!? wtf not cool - let s3 = new AWS.S3(); - return 'https://' + s3.endpoint.hostname + `/${bucketName}/${key}`; - }); + // TemplateURL is an https:// URL. You force us to lookup endpt vs bucket/key attrs!?!? wtf not cool + let s3 = new AWS.S3(); + return 'https://' + s3.endpoint.hostname + `/${Serverless._meta.private.variables.projectBucket}/${key}`; + }); }; /** - * Create Resources Stack + * Create Or Update Resources Stack */ - CloudFormation.sCreateResourcesStack = function( - projRootPath, - projName, - projStage, - projDomain, - projNotificationEmail, - templateUrl) { + CloudFormation.sCreateOrUpdateResourcesStack = function(Serverless, stage, region, type) { - let _this = this; - let stackName = CloudFormation.sGetResourcesStackName(projStage, projName); - let params = { - StackName: stackName, - Capabilities: [ - 'CAPABILITY_IAM', - ], - TemplateURL: templateUrl, - OnFailure: 'ROLLBACK', - Parameters: [], - Tags: [{ - Key: 'STAGE', - Value: projStage, - }] - }; + let _this = this; + let stackName = CloudFormation.sGetResourcesStackName(stage, Serverless._project.name); - // Create CloudFormation Stack - return CloudFormation.createStackPromised(params); - }; + // Put CF file on S3 + return CloudFormation.sPutCfFile( + Serverless, + stage, + region) + .then(function(templateUrl) { - /** - * Update Resources Stack - */ + // CF Params + let params = { + StackName: stackName, + Capabilities: [ + 'CAPABILITY_IAM', + ], + UsePreviousTemplate: false, + Parameters: [], + Tags: [{ + Key: 'STAGE', + Value: stage, + }], + TemplateURL: templateUrl + }; - CloudFormation.sUpdateResourcesStack = function(Serverless, stage, region) { + // Create or Update + if (type == 'create') { - let _this = this, - projRootPath = Serverless._projectRootPath, - bucketName = SUtils.getRegionConfig(Serverless._projectJson, stage, region).regionBucket, - projName = Serverless._projectJson.name; + return CloudFormation.createStackPromised(params); - let stackName = CloudFormation.sGetResourcesStackName(stage, projName); + } else if (type == 'update') { - let params = { - StackName: stackName, - Capabilities: [ - 'CAPABILITY_IAM', - ], - UsePreviousTemplate: false, - Parameters: [ - { - ParameterKey: 'ProjectName', - ParameterValue: projName, - UsePreviousValue: false, - }, - { - ParameterKey: 'Stage', - ParameterValue: stage, - UsePreviousValue: false, - }, - { - ParameterKey: 'DataModelStage', - ParameterValue: stage, - UsePreviousValue: false, - }, - ], - }; - - return CloudFormation.sPutCfFile(projRootPath, bucketName, projName, stage, 'resources') - .then(function(templateUrl) { - params.TemplateURL = templateUrl; - return CloudFormation.updateStackPromised(params); - }); + params.OnFailure = 'ROLLBACK'; + return CloudFormation.updateStackPromised(params); + } + }); }; /** @@ -251,36 +230,36 @@ module.exports = function(config) { stackData = null; async.whilst( - function() { - return stackStatus !== stackStatusComplete; - }, + function() { + return stackStatus !== stackStatusComplete; + }, - function(callback) { - setTimeout(function() { - let params = { - StackName: cfData.StackId, - }; - CloudFormation.describeStacksPromised(params) - .then(function(data) { - stackData = data; - stackStatus = stackData.Stacks[0].StackStatus; + function(callback) { + setTimeout(function() { + let params = { + StackName: cfData.StackId, + }; + CloudFormation.describeStacksPromised(params) + .then(function(data) { + stackData = data; + stackStatus = stackData.Stacks[0].StackStatus; - SUtils.sDebug('CF stack status: ', stackStatus); + SUtils.sDebug('CF stack status: ', stackStatus); - if (!stackStatus || validStatuses.indexOf(stackStatus) === -1) { - let prefix = createOrUpdate.slice(0,-1); - return reject(new SError( - `Something went wrong while ${prefix}ing your cloudformation`)); - } else { - return callback(); - } - }); - }, checkFreq); - }, + if (!stackStatus || validStatuses.indexOf(stackStatus) === -1) { + let prefix = createOrUpdate.slice(0,-1); + return reject(new SError( + `Something went wrong while ${prefix}ing your cloudformation`)); + } else { + return callback(); + } + }); + }, checkFreq); + }, - function() { - return resolve(stackData.Stacks[0]); - } + function() { + return resolve(stackData.Stacks[0]); + } ); }); diff --git a/lib/utils/aws/Lambda.js b/lib/utils/aws/Lambda.js index ce2ff586a..5feb6620f 100644 --- a/lib/utils/aws/Lambda.js +++ b/lib/utils/aws/Lambda.js @@ -26,8 +26,8 @@ module.exports = function(config) { * Get Lambda Name */ - Lambda.sGetLambdaName = function(projectJson, functionJson) { - return projectJson.name + '-' + functionJson.name; + Lambda.sGetLambdaName = function(projectName, functionName) { + return projectName + '-' + functionName; }; /** diff --git a/lib/utils/index.js b/lib/utils/index.js index 9487d9173..1082eb852 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -30,7 +30,8 @@ module.exports.supportedRuntimes = { }; /** - * Get Root Path + * Get Project Path + * - Returns path string */ exports.getProjectPath = function(startDir) { @@ -64,56 +65,10 @@ exports.getProjectPath = function(startDir) { return projRootPath; }; -/** - * GetProject - * - Create a project javascript object from al JSON files - */ - -exports.getProject = function(projectRootPath) { - - let _this = this, - project = {}; - - // Get Project JSON - project = _this.readAndParseJsonSync(path.join(projectRootPath, 's-project.json')); - - // Add Modules & Functions - project.modules = {}; - let moduleList = fs.readdirSync(path.join(projectRootPath, 'back', 'modules')); - for (let i = 0; i < moduleList.length; i++) { - try { - - let module = _this.readAndParseJsonSync(path.join(projectRootPath, 'back', 'modules', moduleList[i], 's-module.json')); - project.modules[module.name] = module; - project.modules[module.name].pathModule = path.join('back', 'modules', moduleList[i], 's-module.json'); - project.modules[module.name].functions = {}; - - // Get Functions - let moduleFolders = fs.readdirSync(path.join(projectRootPath, 'back', 'modules', moduleList[i])); - for (let j = 0; j < moduleFolders.length; j++) { - - // Check functionPath exists - if (_this.fileExistsSync(path.join(projectRootPath, 'back', 'modules', moduleList[i], moduleFolders[j], 's-function.json'))) { - let funcs = _this.readAndParseJsonSync(path.join(projectRootPath, 'back', 'modules', moduleList[i], moduleFolders[j], 's-function.json')); - - for (let k = 0; k < Object.keys(funcs.functions).length; k++) { - project.modules[module.name].functions[Object.keys(funcs.functions)[k]] = funcs.functions[Object.keys(funcs.functions)[k]]; - project.modules[module.name].functions[Object.keys(funcs.functions)[k]].name = Object.keys(funcs.functions)[k]; - project.modules[module.name].functions[Object.keys(funcs.functions)[k]].pathFunction = path.join('back', 'modules', moduleList[i], moduleFolders[j], 's-function.json'); - } - } - } - } catch (e) { - console.log(e); - } - } - - return project; -}; - /** * Get Meta - * - Get Project Meta Information + * - Load Project Meta Information from file system + * - TODO: Validate against Serverless JSON "Meta" Schema */ exports.getMeta = function(projectRootPath) { @@ -179,11 +134,15 @@ exports.getMeta = function(projectRootPath) { if (_this.dirExistsSync(path.join(projectRootPath, 'meta', 'public'))) _getVariables('public'); if (_this.dirExistsSync(path.join(projectRootPath, 'meta', 'private'))) _getVariables('private'); + // TODO: Validate Meta from JSON "META" Schema + return projectMeta; }; /** * Save Meta + * - Saves Meta data to file system + * - TODO: Validate Meta from JSON "META" Schema before saving */ exports.saveMeta = function(projectRootPath, projectMeta) { @@ -220,71 +179,56 @@ exports.saveMeta = function(projectRootPath, projectMeta) { }; /** - * Populate Variables - * - variable names are provided with the ${someVar} syntax. Ex. {projectName: '${someVar}'} - * - variables can be of any type, but since they're referenced in JSON, variable names should always be in string quotes: - * - Ex. {projectName: '${someVar}'} - someVar can be an obj, or array...etc - * - you can provide more than one variable in a string. Ex. {projectName: '${firstName}-${lastName}-project'} - * - we get the value of the variable based on stage/region provided. If both are provided, we'll get the region specific value, - * - if only stage is provided, we'll get the stage specific value, if none is provided, we'll get the private or public specific value (depending on the type property of the option obj) - * - example options: {type: 'private', stage: 'development', region: 'us-east-1'} - **/ + * GetResources + * - Dynamically Create CloudFormation resources from s-project.json and s-module.json + * - Returns Aggregated CloudFormation Template + */ -exports.populateVariables = function(project, meta, options) { +exports.getResources = function(projectJson) { - options.type = options.type || 'private'; // private is the default type + let cfTemplate = JSON.parse(JSON.stringify(projectJson.cloudFormation)); - // Validate providing region without stage is invalid! - if (!options.stage) options.region = null; + // Loop through modules and aggregate resources + for (let i = 0; i < Object.keys(projectJson.modules).length; i++) { - // Validate Stage exists - if (typeof options.stage != 'undefined' && !meta.private.stages[options.stage]) { - throw Error('Stage doesnt exist!'); - } + let moduleJson = projectJson.modules[Object.keys(projectJson.modules)[i]]; - // Validate Region exists - if (typeof options.region != 'undefined' && !meta.private.stages[options.stage].regions[options.region]) { - throw Error('Region doesnt exist in the provided stage!'); - } + // If no cloudFormation in module, skip... + if (!moduleJson.cloudFormation) continue; - // Traverse the whole project and replace variables - traverse(project).forEach(function(projectVal) { + // Merge Lambda Policy Statements + if (moduleJson.cloudFormation.lambdaIamPolicyDocumentStatements && + moduleJson.cloudFormation.lambdaIamPolicyDocumentStatements.length > 0) { + SCli.log('Merging in Lambda IAM Policy statements from s-module'); - // check if the current string is a variable - if (typeof(projectVal) === 'string' && projectVal.match(/\${([^{}]*)}/g) != null) { - - let newVal = projectVal; - - // get all ${variable} - projectVal.match(/\${([^{}]*)}/g).forEach(function(variableSyntax) { - let variableName = variableSyntax.replace('${', '').replace('}', ''); - - let value; - if (options.stage && options.region) { - value = meta[options.type].stages[options.stage].regions[options.region].variables[variableName]; - - } else if (options.stage) { - value = meta[options.type].stages[options.stage].variables[variableName]; - - } else if (!options.stage && !options.region) { - value = meta[options.type].variables[variableName]; - } - - if (typeof value === 'undefined') { - throw Error('Variable Doesnt exist!'); - } else if (typeof value === 'string') { - newVal = newVal.replace(variableSyntax, value); - } else { - newVal = value; - } + moduleJson.cloudFormation.lambdaIamPolicyDocumentStatements.forEach(function(policyStmt) { + cfTemplate.Resources.IamPolicyLambda.Properties.PolicyDocument.Statement.push(policyStmt); }); - - this.update(newVal); - } - }); - return project; + // Merge resources + if (moduleJson.cloudFormation.resources) { + + let cfResourceKeys = Object.keys(moduleJson.cloudFormation.resources); + + if (cfResourceKeys.length > 0) { + SCli.log('Merging in CF Resources from s-module'); + } + + cfResourceKeys.forEach(function (resourceKey) { + if (cfTemplate.Resources[resourceKey]) { + SCli.log( + chalk.bgYellow.white(' WARN ') + + chalk.magenta(` Resource key ${resourceKey} already defined in ${file}. Overwriting...`) + ); + } + + cfTemplate.Resources[resourceKey] = moduleJson.cloudFormation.resources[resourceKey]; + }); + } + } + + return cfTemplate; }; /** @@ -713,6 +657,12 @@ exports.writeFile = function(filePath, contents) { }); }; +/** + * Generate Short ID + * @param maxLen + * @returns {string} + */ + exports.generateShortId = function(maxLen) { return shortid.generate().replace(/\W+/g, '').substring(0, maxLen).replace(/[_-]/g, ''); }; @@ -734,13 +684,13 @@ exports.generateProjectBucketName = function(region, projectDomain) { * Given list of project stage objects, extract given region */ -exports.getRegionConfig = function(projectMeta, stage, regionName) { +exports.getRegionConfig = function(projectMeta, stage, region) { - if (!projectMeta.project.stages[stage].regions[region]) { - throw new SError(`Could not find region ${regionName}`, SError.errorCodes.UNKNOWN); + if (!projectMeta.private.stages[stage].regions[region]) { + throw new SError(`Could not find region ${region}`, SError.errorCodes.UNKNOWN); } - return projectMeta.project.stages[stage].regions[region]; + return projectMeta.private.stages[stage].regions[region]; }; exports.dirExistsSync = function(path) { diff --git a/other/examples/meta.json b/other/examples/meta.json index ee392d1fa..198da7ad6 100644 --- a/other/examples/meta.json +++ b/other/examples/meta.json @@ -13,16 +13,6 @@ "variables": {} }, "public": { - "stages": { - "development": { - "regions": { - "us-east-1": { - "variables": {} - } - }, - "variables": {} - } - }, "variables": {} } } \ No newline at end of file diff --git a/other/examples/project.json b/other/examples/project.json index 203baf79e..c266ad5d7 100644 --- a/other/examples/project.json +++ b/other/examples/project.json @@ -131,7 +131,9 @@ } ] } - } + }, + + "templates": {} } } } \ No newline at end of file diff --git a/package.json b/package.json index 2af9069a4..05a480c1a 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "dotenv": "^1.2.0", "download": "^4.2.0", "expand-home-dir": "0.0.2", + "immutable": "^3.7.6", "insert-module-globals": "^6.5.2", "keypress": "^0.2.1", "minimist": "^1.2.0",