diff --git a/lib/Serverless.js b/lib/Serverless.js index 80340b995..515e17865 100644 --- a/lib/Serverless.js +++ b/lib/Serverless.js @@ -33,6 +33,7 @@ class Serverless { this._version = require('./../package.json').version; this._projectRootPath = SUtils.getProjectPath(process.cwd()); this._project = false; + this._meta = false; this.actions = {}; this.hooks = {}; this.commands = {}; @@ -46,12 +47,12 @@ class Serverless { this._loadPlugins(this._projectRootPath, this._project.plugins); } - // If within project, add further queued data + // If within private, add further queued data if (this._projectRootPath) { // Get Project Information this._project = SUtils.getProject(this._projectRootPath); - this._meta = SUtils.getProjectMeta(this._projectRootPath); + this._meta = SUtils.getMeta(this._projectRootPath); // Load Admin ENV information require('dotenv').config({ @@ -63,7 +64,6 @@ class Serverless { this._awsAdminKeyId = process.env.SERVERLESS_ADMIN_AWS_ACCESS_KEY_ID; this._awsAdminSecretKey = process.env.SERVERLESS_ADMIN_AWS_SECRET_ACCESS_KEY; } - console.log(this._meta.project.stages.development.regions); } /** @@ -268,7 +268,7 @@ class Serverless { let PluginClass; if (pluginMetadatum.path.indexOf('.') == 0) { - // Load non-npm plugin from the project plugins folder + // 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); diff --git a/lib/ServerlessPlugin.js b/lib/ServerlessPlugin.js index c85dc09a3..1c37e3b56 100644 --- a/lib/ServerlessPlugin.js +++ b/lib/ServerlessPlugin.js @@ -228,7 +228,7 @@ class ServerlessPlugin { cliPromptSelectStage(message, stage, addLocalStage) { let _this = this, - stages = Object.keys(_this.S._meta.project.stages); + stages = Object.keys(_this.S._meta.private.stages); // Resolve stage if provided if (stage) return BbPromise.resolve(stage); @@ -236,7 +236,7 @@ class ServerlessPlugin { // Skip if not interactive if (!_this.S._interactive) return BbPromise.resolve(); - // if project has 1 stage, skip prompt + // if private has 1 stage, skip prompt if (stages.length === 1) { return BbPromise.resolve(stages[0]); } @@ -274,8 +274,8 @@ class ServerlessPlugin { if (stage === 'local') return BbPromise.resolve('local'); // If stage has one region, skip prompt and return that instead - if (stage && Object.keys(_this.S._meta.project.stages[stage].regions).length === 1 && existing) { - return BbPromise.resolve(Object.keys(_this.S._meta.project.stages[stage].regions)[0]); + if (stage && Object.keys(_this.S._meta.private.stages[stage].regions).length === 1 && existing) { + return BbPromise.resolve(Object.keys(_this.S._meta.private.stages[stage].regions)[0]); } // Skip if not interactive or stage is local @@ -286,8 +286,8 @@ class ServerlessPlugin { // if stage is provided, limit region list if (stage){ - // Make sure stage exists in project - if (!_this.S._meta.project.stages[stage]) { + // Make sure stage exists in private + if (!_this.S._meta.private.stages[stage]) { return BbPromise.reject(new SError('Stage ' + stage + ' does not exist in your project', SError.errorCodes.UNKNOWN)); } @@ -296,18 +296,18 @@ class ServerlessPlugin { // List only regions in stage regionChoices = []; - Object.keys(_this.S._meta.project.stages[stage].regions).forEach(function(region) { + Object.keys(_this.S._meta.private.stages[stage].regions).forEach(function(region) { regionChoices.push(region) }); } else { // Make sure there are regions left in stage - if (Object.keys(_this.S._meta.project.stages[stage].regions).length === 4) { + if (Object.keys(_this.S._meta.private.stages[stage].regions).length === 4) { return BbPromise.reject(new SError('Stage ' + stage + ' already have all possible regions.', SError.errorCodes.UNKNOWN)); } // List only regions NOT in stage - Object.keys(_this.S._meta.project.stages[stage].regions).forEach(function(regionInStage) { + Object.keys(_this.S._meta.private.stages[stage].regions).forEach(function(regionInStage) { let index = regionChoices.indexOf(regionInStage.region); regionChoices.splice(index, 1); }); diff --git a/lib/actions/EndpointDeploy.js b/lib/actions/EndpointDeploy.js index cf9485555..fef724a08 100644 --- a/lib/actions/EndpointDeploy.js +++ b/lib/actions/EndpointDeploy.js @@ -225,7 +225,7 @@ module.exports = function(SPlugin, serverlessPath) { // If no region specified, deploy to all regions in stage if (!evt.regions.length) { - evt.regions = Object.keys(this.S._meta.project.stages[evt.stage].regions); + evt.regions = Object.keys(this.S._meta.private.stages[evt.stage].regions); } // Delete region for neatness diff --git a/lib/actions/EnvGet.js b/lib/actions/EnvGet.js index fb9d928c0..49224c286 100644 --- a/lib/actions/EnvGet.js +++ b/lib/actions/EnvGet.js @@ -157,7 +157,7 @@ usage: serverless env get`, } // validate stage: make sure stage exists - if (!_this.S._meta.project.stages[_this.evt.stage] && _this.evt.stage != 'local') { + if (!_this.S._meta.private.stages[_this.evt.stage] && _this.evt.stage != 'local') { return BbPromise.reject(new SError('Stage ' + _this.evt.stage + ' does not exist in your project', SError.errorCodes.UNKNOWN)); } @@ -165,7 +165,7 @@ usage: serverless env get`, if (_this.evt.stage != 'local' && _this.evt.region != 'all') { // 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 + '"')); } } diff --git a/lib/actions/EnvList.js b/lib/actions/EnvList.js index b21ea5773..424b41fc9 100644 --- a/lib/actions/EnvList.js +++ b/lib/actions/EnvList.js @@ -144,7 +144,7 @@ Usage: serverless env list`, } // Validate stage: make sure stage exists - if (!_this.S._meta.project.stages[_this.evt.stage] && _this.evt.stage != 'local') { + if (!_this.S._meta.private.stages[_this.evt.stage] && _this.evt.stage != 'local') { return BbPromise.reject(new SError('Stage ' + _this.evt.stage + ' does not exist in your project', SError.errorCodes.UNKNOWN)); } @@ -152,7 +152,7 @@ Usage: serverless env list`, if (_this.evt.stage != 'local' && _this.evt.region != 'all') { // 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 + '"')); } } diff --git a/lib/actions/EnvSet.js b/lib/actions/EnvSet.js index 08b0fc686..3414c711f 100644 --- a/lib/actions/EnvSet.js +++ b/lib/actions/EnvSet.js @@ -168,7 +168,7 @@ usage: serverless env set`, } // Validate stage: make sure stage exists - if (!_this.S._meta.project.stages[_this.evt.stage] && _this.evt.stage != 'local') { + if (!_this.S._meta.private.stages[_this.evt.stage] && _this.evt.stage != 'local') { return BbPromise.reject(new SError('Stage ' + _this.evt.stage + ' does not exist in your project', SError.errorCodes.UNKNOWN)); } @@ -176,7 +176,7 @@ usage: serverless env set`, if (_this.evt.stage != 'local' && _this.evt.region != 'all') { // validate region: make sure region exists in stage - if (!_this.S._meta.project.stages[_this.evt.stage].regions[region]) { + if (!_this.S._meta.private.stages[_this.evt.stage].regions[region]) { return BbPromise.reject(new SError('Region "' + _this.evt.region + '" does not exist in stage "' + _this.evt.stage + '"')); } } diff --git a/lib/actions/EnvUnset.js b/lib/actions/EnvUnset.js index 31c7c9b0d..39ab88bfd 100644 --- a/lib/actions/EnvUnset.js +++ b/lib/actions/EnvUnset.js @@ -165,7 +165,7 @@ usage: serverless env unset`, if (_this.evt.stage != 'local' && _this.evt.region != 'all') { // 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 + '"')); } } diff --git a/lib/actions/FunctionDeploy.js b/lib/actions/FunctionDeploy.js index e86b351a5..6ed05f568 100644 --- a/lib/actions/FunctionDeploy.js +++ b/lib/actions/FunctionDeploy.js @@ -226,7 +226,7 @@ module.exports = function(SPlugin, serverlessPath) { // If no region specified, deploy to all regions in stage if (!evt.regions.length) { - evt.regions = Object.keys(this.S._meta.project.stages); + evt.regions = Object.keys(this.S._meta.private.stages); } return evt; diff --git a/lib/actions/ProjectCreate.js b/lib/actions/ProjectCreate.js index 47eca3983..868f9daea 100644 --- a/lib/actions/ProjectCreate.js +++ b/lib/actions/ProjectCreate.js @@ -7,7 +7,7 @@ * - Generates scaffolding for the new project in CWD * - Creates a new project S3 bucket and puts env and CF files * - Creates CF stack by default, unless noExeCf option is set to true - * - Generates the final s-project.json file + * - Generates project JSON files * * Event Properties: * - name (String) a name for new project @@ -20,14 +20,15 @@ */ module.exports = function(SPlugin, serverlessPath) { - const path = require('path'), - SError = require( path.join( serverlessPath, 'ServerlessError' ) ), - SCli = require( path.join( serverlessPath, 'utils/cli' ) ), - SUtils = require( path.join( serverlessPath, 'utils' ) ), - os = require('os'), - fs = require('fs'), - BbPromise = require('bluebird'), - awsMisc = require( path.join( serverlessPath, 'utils/aws/Misc' ) ); + + const path = require('path'), + SError = require( path.join( serverlessPath, 'ServerlessError' ) ), + SCli = require( path.join( serverlessPath, 'utils/cli' ) ), + SUtils = require( path.join( serverlessPath, 'utils' ) ), + os = require('os'), + fs = require('fs'), + BbPromise = require('bluebird'), + awsMisc = require( path.join( serverlessPath, 'utils/aws/Misc' ) ); BbPromise.promisifyAll(fs); @@ -100,7 +101,7 @@ module.exports = function(SPlugin, serverlessPath) { let _this = this; - if(evt) { + if (evt) { _this.evt = evt; _this.S._interactive = false; } @@ -108,15 +109,12 @@ 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 - if (_this.S.cli.options.nonInteractive) { - _this.S._interactive = false; - } + + if (_this.S.cli.options.nonInteractive) _this.S._interactive = false; } // Add default runtime - if (!_this.evt.runtime) { - _this.evt.runtime = 'nodejs'; - } + if (!_this.evt.runtime) _this.evt.runtime = 'nodejs'; // Always create "development" stage on ProjectCreate _this.evt.stage = 'development'; @@ -345,7 +343,7 @@ module.exports = function(SPlugin, serverlessPath) { } // Set Serverless Regional Bucket - this.evt.regionBucket = SUtils.generateRegionBucketName(this.evt.region, this.evt.domain); + this.evt.projectBucket = SUtils.generateProjectBucketName(this.evt.region, this.evt.domain); return BbPromise.resolve(); } @@ -376,11 +374,18 @@ module.exports = function(SPlugin, serverlessPath) { ) .then(function() { + // Create Folders + fs.mkdirSync(path.join(_this._projectRootPath, 'back', 'modules')); + fs.mkdirSync(path.join(_this._projectRootPath, 'meta')); + fs.mkdirSync(path.join(_this._projectRootPath, 'meta', 'private')); + fs.mkdirSync(path.join(_this._projectRootPath, 'meta', 'public')); + fs.mkdirSync(path.join(_this._projectRootPath, 'meta', 'private', 'variables')); + fs.mkdirSync(path.join(_this._projectRootPath, 'meta', 'public', 'variables')); + fs.mkdirSync(path.join(_this._projectRootPath, 'meta', 'private', 'resources')); + fs.mkdirSync(path.join(_this._projectRootPath, 'plugins')); + fs.mkdirSync(path.join(_this._projectRootPath, 'plugins', 'custom')); + return BbPromise.all([ - fs.mkdirAsync(path.join(_this._projectRootPath, 'back', 'modules')), - fs.mkdirAsync(path.join(_this._projectRootPath, 'plugins')).then(function(){ - return fs.mkdirAsync(path.join(_this._projectRootPath, 'plugins', 'custom')); - }), SUtils.writeFile(path.join(_this._projectRootPath, 'admin.env'), adminEnv), SUtils.writeFile(path.join(_this._projectRootPath, 'README.md'), readme), SUtils.generateResourcesCf( @@ -401,8 +406,8 @@ module.exports = function(SPlugin, serverlessPath) { */ _createProjectBucket() { - SCli.log('Creating a project region bucket on S3: ' + this.evt.regionBucket + '...'); - return this.S3.sCreateBucket(this.evt.regionBucket); + SCli.log('Creating a project region bucket on S3: ' + this.evt.projectBucket + '...'); + return this.S3.sCreateBucket(this.evt.projectBucket); } /** @@ -416,7 +421,7 @@ module.exports = function(SPlugin, serverlessPath) { SERVERLESS_PROJECT_NAME=${this.evt.name}`; return this.S3.sPutEnvFile( - this.evt.regionBucket, + this.evt.projectBucket, this.evt.name, this.evt.stage, envFileContents); @@ -429,7 +434,7 @@ module.exports = function(SPlugin, serverlessPath) { _putCfFile() { return this.CF.sPutCfFile( this._projectRootPath, - this.evt.regionBucket, + this.evt.projectBucket, this.evt.name, this.evt.stage, 'resources'); @@ -466,8 +471,7 @@ module.exports = function(SPlugin, serverlessPath) { _this.evt.stage, _this.evt.domain, _this.evt.notificationEmail, - cfTemplateURL - ) + cfTemplateURL) .then(cfData => { return _this.CF.sMonitorCf(cfData, 'create') .then(cfStackData => { @@ -496,21 +500,38 @@ module.exports = function(SPlugin, serverlessPath) { _this.evt.stageCfStack = cfStackData.StackName; } - let prjJson = SUtils.readAndParseJsonSync(path.join(_this._templatesDir, 's-project.json')); - - prjJson.stages[_this.evt.stage] = [{ - region: _this.evt.region, - iamRoleArnLambda: _this.evt.iamRoleLambdaArn || '', - regionBucket: _this.evt.regionBucket - }]; - - prjJson.name = _this.evt.name; - prjJson.domain = _this.evt.domain; - + // Create s-project.json + let prjJson = SUtils.readAndParseJsonSync(path.join(_this._templatesDir, 's-project.json')); + prjJson.name = _this.evt.name; + prjJson.description = 'A brand new Serverless project'; fs.writeFileSync(path.join(_this._projectRootPath, 's-project.json'), JSON.stringify(prjJson, null, 2)); - return prjJson; + // Save Meta + _this._meta = { + private: { + stages: {}, + variables: { + domain: _this.evt.domain, + projectBucket: _this.evt.projectBucket + } + }, + public: { + stages: {}, + variables: {} + }, + }; + _this._meta.private.stages[_this.evt.stage] = { + regions: {}, + variables: {} + }; + _this._meta.private.stages[_this.evt.stage].regions[_this.evt.region] = { + variables: { + stackName: _this.evt.stageCfStack, + iamRoleLambdaArn: _this.evt.iamRoleLambdaArn + } + }; + SUtils.saveMeta(_this._projectRootPath, _this._meta); } } diff --git a/lib/actions/RegionCreate.js b/lib/actions/RegionCreate.js index 63c7f757c..50c00aa6c 100644 --- a/lib/actions/RegionCreate.js +++ b/lib/actions/RegionCreate.js @@ -153,7 +153,7 @@ usage: serverless region create`, } // validate stage: make sure stage exists - if (!_this.S._meta.project.stages[_this.evt.stage]) { + if (!_this.S._meta.private.stages[_this.evt.stage]) { return BbPromise.reject(new SError('Stage ' + _this.evt.stage + ' does not exist in your project', SError.errorCodes.UNKNOWN)); } @@ -192,7 +192,7 @@ usage: serverless region create`, */ _createRegionBucket() { - this.evt.regionBucket = SUtils.generateRegionBucketName(this.evt.region, this.S._meta.variables.domain); + this.evt.regionBucket = SUtils.generateProjectBucketName(this.evt.region, this.S._meta.variables.domain); SCli.log('Creating a region bucket on S3: ' + this.evt.regionBucket + '...'); return this.S3.sCreateBucket(this.evt.regionBucket); } @@ -286,7 +286,7 @@ SERVERLESS_PROJECT_NAME=${this.S._project.name}`; _this.evt.stageCfStack = cfStackData.StackName; } - _this.S._meta.project.stages[_this.evt.stage].regions[_this.evt.region] = { + _this.S._meta.private.stages[_this.evt.stage].regions[_this.evt.region] = { regionBucket: _this.evt.regionBucket, stackName: cfStackData.StackName, iamRoleArnLambda: _this.evt.iamRoleArnLambda @@ -302,7 +302,7 @@ SERVERLESS_PROJECT_NAME=${this.S._project.name}`; return SUtils.writeFile( path.join(_this.S._projectRootPath, 'meta', 'variables', variableFile), - JSON.stringify(_this.S._meta.project.stages[_this.evt.stage].regions[_this.evt.region], null, 2) + JSON.stringify(_this.S._meta.private.stages[_this.evt.stage].regions[_this.evt.region], null, 2) ); } } diff --git a/lib/actions/ResourcesDeploy.js b/lib/actions/ResourcesDeploy.js index c17ab7c97..c58b5d1a8 100644 --- a/lib/actions/ResourcesDeploy.js +++ b/lib/actions/ResourcesDeploy.js @@ -145,7 +145,7 @@ usage: serverless resources deploy`, } // validate stage: make sure stage exists - if (!_this.S._meta.project.stages[_this.evt.stage] && _this.evt.stage != 'local') { + if (!_this.S._meta.private.stages[_this.evt.stage] && _this.evt.stage != 'local') { return BbPromise.reject(new SError('Stage ' + _this.evt.stage + ' does not exist in your project', SError.errorCodes.UNKNOWN)); } diff --git a/lib/actions/StageCreate.js b/lib/actions/StageCreate.js index 2f25102e1..9f0bdd074 100644 --- a/lib/actions/StageCreate.js +++ b/lib/actions/StageCreate.js @@ -179,7 +179,7 @@ usage: serverless stage create`, } // validate stage: Ensure stage doesn't already exist - if (this.S._meta.project.stages[this.evt.stage]) { + if (this.S._meta.private.stages[this.evt.stage]) { return BbPromise.reject(new SError('Stage ' + this.evt.stage + ' already exists', SError.errorCodes.UNKNOWN)); } @@ -231,7 +231,7 @@ usage: serverless stage create`, // Check if project name is in AllowedValues if (cfTemplate.Parameters.ProjectName.AllowedValues.indexOf(this.S._project.name) == -1) { - cfTemplate.Parameters.ProjectName.AllowedValues.push(this.S._meta.project.name); + cfTemplate.Parameters.ProjectName.AllowedValues.push(this.S._project.name); } // Write it @@ -247,7 +247,7 @@ usage: serverless stage create`, */ _createRegionBucket() { - this.evt.regionBucket = SUtils.generateRegionBucketName(this.evt.region, this.S._project.domain); + this.evt.regionBucket = SUtils.generateProjectBucketName(this.evt.region, this.S._project.domain); SCli.log('Creating a region bucket on S3: ' + this.evt.regionBucket + '...'); return this.S3.sCreateBucket(this.evt.regionBucket); } @@ -296,7 +296,7 @@ SERVERLESS_PROJECT_NAME=${this.S._project.name}`; let stackName = _this.CF.sGetResourcesStackName(_this.evt.stage, _this.S._project.name); SCli.log(`Remember to run CloudFormation manually to create stack with name: ${stackName}`); - SCli.log('After creating CF stack, remember to put the IAM role outputs and regionBucket in your project s-project.json in the correct stage/region.'); + SCli.log('After creating CF stack, remember to put the IAM role outputs and regionBucket in your private s-project.json in the correct stage/region.'); return BbPromise.resolve(); } diff --git a/lib/templates/gitignore b/lib/templates/gitignore index f897b5a6b..5c628da07 100644 --- a/lib/templates/gitignore +++ b/lib/templates/gitignore @@ -37,4 +37,5 @@ node_modules #SERVERLESS STUFF admin.env -.env \ No newline at end of file +.env +meta/private \ No newline at end of file diff --git a/lib/templates/lambdas-cf.json b/lib/templates/lambdas-cf.json deleted file mode 100644 index 3669c6592..000000000 --- a/lib/templates/lambdas-cf.json +++ /dev/null @@ -1,29 +0,0 @@ -{ - "AWSTemplateFormatVersion": "2010-09-09", - "Description": "The AWS CloudFormation template for this Serverless application's Lambda functions", - "Parameters": { - "LambdaRoleArn": { - "Type": "String", - "Default": "" - } - }, - "Resources": { - "lTemplate": { - "Type": "AWS::Lambda::Function", - "Properties": { - "Code": { - "S3Bucket": "", - "S3Key": "" - }, - "Description": "", - "Handler": "", - "MemorySize": 1024, - "Role": { - "Ref": "LambdaRoleArn" - }, - "Runtime": "", - "Timeout": 6 - } - } - } -} diff --git a/lib/templates/resources-cf.json b/lib/templates/resources-cf.json index 5389703f4..8bc9fd9ae 100644 --- a/lib/templates/resources-cf.json +++ b/lib/templates/resources-cf.json @@ -70,23 +70,6 @@ "Path": "/" } }, - "IamInstanceProfileLambda": { - "Type": "AWS::IAM::InstanceProfile", - "Properties": { - "Path": "/", - "Roles": [ - { - "Ref": "IamRoleLambda" - } - ] - } - }, - "IamGroupLambda": { - "Type": "AWS::IAM::Group", - "Properties": { - "Path": "/" - } - }, "IamPolicyLambda": { "Type": "AWS::IAM::Policy", "Properties": { @@ -153,4 +136,4 @@ } } } -} +} \ No newline at end of file diff --git a/lib/templates/s-project.json b/lib/templates/s-project.json index 748ed5573..1f37e551b 100644 --- a/lib/templates/s-project.json +++ b/lib/templates/s-project.json @@ -1,12 +1,150 @@ { "name": "", "version": "0.0.1", - "profile": "serverless-0", + "profile": "serverless-0.1", "location": "https://github.com/...", "author": "", "description": "", - "domain": "", - "stages": {}, "custom": {}, - "plugins": [] + "modules": {}, + "plugins": [], + "cloudFormation" : { + "AWSTemplateFormatVersion": "2010-09-09", + "Description": "The AWS CloudFormation template for this Serverless application's resources outside of Lambdas and Api Gateway", + "Parameters": { + "ProjectName": { + "Type": "String", + "AllowedValues": [] + }, + "ProjectDomain": { + "Type": "String", + "Default": "myapp.com" + }, + "Stage": { + "Type": "String", + "AllowedValues": [ + ] + }, + "DataModelStage": { + "Type": "String", + "AllowedValues": [ + ] + }, + "NotificationEmail": { + "Type": "String", + "Default": "you@you.com" + }, + "DynamoRWThroughput": { + "Type": "String", + "Default": "1" + } + }, + "Metadata": { + "AWS::CloudFormation::Interface": { + "ParameterGroups": [ + { + "Label": {"default": "Project Settings"}, + "Parameters": ["ProjectName", "ProjectDomain", "Stage", "DataModelStage"] + }, + { + "Label": {"default": "Monitoring"}, + "Parameters": ["NotificationEmail"] + }, + { + "Label": {"default": "Database Settings (DynamoDB)"}, + "Parameters": ["DynamoRWThroughput"] + } + ] + } + }, + "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": { + "Fn::Join": [ + "_-_", + [ + { + "Ref": "Stage" + }, + { + "Ref": "ProjectName" + }, + "lambda" + ] + ] + }, + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Resource": { + "Fn::Join": [ + ":", + [ + "arn:aws:logs", + { + "Ref": "AWS::Region" + }, + "*:*" + ] + ] + } + } + ] + }, + "Roles": [ + { + "Ref": "IamRoleLambda" + } + ], + "Groups": [ + { + "Ref": "IamGroupLambda" + } + ] + } + } + }, + "Outputs": { + "IamRoleArnLambda": { + "Description": "ARN of the lambda IAM role", + "Value": { + "Fn::GetAtt": [ + "IamRoleLambda", + "Arn" + ] + } + } + } + } } \ No newline at end of file diff --git a/lib/utils/aws/CloudFormation.js b/lib/utils/aws/CloudFormation.js index ad32163e6..4ad3b2751 100644 --- a/lib/utils/aws/CloudFormation.js +++ b/lib/utils/aws/CloudFormation.js @@ -126,12 +126,12 @@ module.exports = function(config) { let S3 = require('./S3')(config); - if (['lambdas', 'resources'].indexOf(type) == -1) { - BbPromise.reject(new SError(`Type ${type} invalid. Must be lambdas or resources`, SError.errorCodes.UNKNOWN)); + if (['resources'].indexOf(type) == -1) { + BbPromise.reject(new SError(`Type ${type} invalid. Must be resources only`, SError.errorCodes.UNKNOWN)); } let d = new Date(), - cfPath = path.join(projRootPath, 'cloudformation', type + '-cf.json'), + cfPath = path.join(projRootPath, 'meta', 'private', 'resources', 's-' + type + '-cf.json'), key = ['Serverless', projName, projStage, 'cloudformation/' + type].join('/') + '@' + d.getTime() + '.json', params = { Bucket: bucketName, diff --git a/lib/utils/index.js b/lib/utils/index.js index 0ad0b2e7f..206a9e51b 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -83,7 +83,7 @@ exports.getProject = function(projectRootPath) { try { let module = _this.readAndParseJsonSync(path.join(projectRootPath, 'back', 'modules', moduleList[i], 's-module.json')); - project.modules[module.name] = module; + project.modules[module.name] = module; project.modules[module.name].pathModule = path.join('back', 'modules', moduleList[i], 's-module.json'); project.modules[module.name].functions = {}; @@ -111,76 +111,110 @@ exports.getProject = function(projectRootPath) { }; /** - * GetProjectMeta + * Get Meta * - Get Project Meta Information */ -exports.getProjectMeta = function(projectRootPath) { +exports.getMeta = function(projectRootPath) { let _this = this, projectMeta = { - project: { + private: { stages: {}, variables: {} + }, + public: { + variables: {} } }; - // Create "meta" folder if does not exist - if (!_this.dirExistsSync(path.join(projectRootPath, 'meta'))) { - fs.mkdirSync(path.join(projectRootPath, 'meta')); - } + // Re-usable function to traverse public or private variable folders + let _getVariables = function(type) { - // Create "meta/variables" folder if does not exist - if (!_this.dirExistsSync(path.join(projectRootPath, 'meta', 'variables'))) { - fs.mkdirSync(path.join(projectRootPath, 'meta', 'variables')); - } + let variableFiles = fs.readdirSync(path.join(projectRootPath, 'meta', type, 'variables')); + for (let i = 0; i < variableFiles.length; i++) { - // Get Variables - let variableFiles = fs.readdirSync(path.join(projectRootPath, 'meta', 'variables')); - for (let i = 0; i < variableFiles.length; i++) { + let variableFile = _this.readAndParseJsonSync(path.join(projectRootPath, 'meta', type, 'variables', variableFiles[i])); - let variableFile = _this.readAndParseJsonSync(path.join(projectRootPath, 'meta', 'variables', variableFiles[i])); + // Parse file name to get stage/region + let file = variableFiles[i].replace('s-variables-', '').replace('.json', ''); - // Parse file name to get stage/region - let file = variableFiles[i].replace('s-variables-', '').replace('.json', ''); + if (file === 'common') { - if (file === 'common') { + // Set Common variables + projectMeta[type].variables = _this.readAndParseJsonSync(path.join(projectRootPath, 'meta', type, 'variables', variableFiles[i])); - // Set Common variables - projectMeta.project.variables = _this.readAndParseJsonSync(path.join(projectRootPath, 'meta', 'variables', variableFiles[i])); - } else { + } else { - // Set Stage/Region variables - file = file.split('-'); - if (!projectMeta.project.stages[file[0]]) projectMeta.project.stages[file[0]] = { - regions: {}, - variables: {} - }; - - if (file.length === 1) { - - // Set Stage Variables - projectMeta.project.stages[file[0]].variables = _this.readAndParseJsonSync(path.join(projectRootPath, 'meta', 'variables', variableFiles[i])); - - } else if (file.length === 2) { - - // Set Stage-Region Variables - let region; - if (file[1] === 'useast1') region = 'us-east-1'; - if (file[1] === 'uswest2') region = 'us-west-2'; - if (file[1] === 'euwest1') region = 'eu-west-1'; - if (file[1] === 'apnortheast1') region = 'ap-northeast-1'; - if (!projectMeta.project.stages[file[0]].regions[region]) projectMeta.project.stages[file[0]].regions[region] = { - variables: _this.readAndParseJsonSync(path.join(projectRootPath, 'meta', 'variables', variableFiles[i])) + // Set Stage/Region variables + file = file.split('-'); + if (!projectMeta[type].stages) projectMeta[type].stages = {}; + if (!projectMeta[type].stages[file[0]]) projectMeta[type].stages[file[0]] = { + regions: {}, + variables: {} }; - projectMeta.project.stages[file[0]].regions[region].variables.region = region; + + if (file.length === 1) { + + // Set Stage Variables + projectMeta[type].stages[file[0]].variables = _this.readAndParseJsonSync(path.join(projectRootPath, 'meta', type, 'variables', variableFiles[i])); + + } else if (file.length === 2) { + + // Set Stage-Region Variables + let region; + if (file[1] === 'useast1') region = 'us-east-1'; + if (file[1] === 'uswest2') region = 'us-west-2'; + if (file[1] === 'euwest1') region = 'eu-west-1'; + if (file[1] === 'apnortheast1') region = 'ap-northeast-1'; + if (!projectMeta[type].stages[file[0]].regions[region]) projectMeta[type].stages[file[0]].regions[region] = { + variables: _this.readAndParseJsonSync(path.join(projectRootPath, 'meta', type, 'variables', variableFiles[i])) + }; + } } } - } + }; + + if (_this.dirExistsSync(path.join(projectRootPath, 'meta', 'public'))) _getVariables('public'); + if (_this.dirExistsSync(path.join(projectRootPath, 'meta', 'private'))) _getVariables('private'); return projectMeta; }; +/** + * Save Meta + */ + +exports.saveMeta = function(projectRootPath, projectMeta) { + + // Re-usable function to save public or private variables + let _saveVariables = function(type) { + + // Save Common Variables + fs.writeFileSync(path.join(projectRootPath, 'meta', type, 'variables', 's-variables-common.json'), + JSON.stringify(projectMeta[type].variables, null, 2)); + + for (let i = 0; i < Object.keys(projectMeta[type].stages).length; i++) { + + let stage = projectMeta[type].stages[Object.keys(projectMeta[type].stages)[i]]; + + // Save Stage Variables + fs.writeFileSync(path.join(projectRootPath, 'meta', type, 'variables', 's-variables-' + Object.keys(projectMeta[type].stages)[i] + '.json'), + JSON.stringify(stage.variables, null, 2)); + + // Save Stage Region Variables + for (let j = 0; j < Object.keys(stage.regions).length; j++) { + fs.writeFileSync(path.join(projectRootPath, 'meta', type, 'variables', 's-variables-' + Object.keys(projectMeta[type].stages)[i] + '-' + Object.keys(stage.regions)[j].replace(/-/g, '') + '.json'), + JSON.stringify(stage.regions[Object.keys(stage.regions)[j]].variables, null, 2)); + } + } + }; + + if (this.dirExistsSync(path.join(projectRootPath, 'meta', 'public'))) _saveVariables('public'); + if (this.dirExistsSync(path.join(projectRootPath, 'meta', 'private'))) _saveVariables('private'); + +}; + /** * Execute (Command) */ @@ -615,7 +649,7 @@ exports.generateShortId = function(maxLen) { * Generate JawsBucket Name */ -exports.generateRegionBucketName = function(region, projectDomain) { +exports.generateProjectBucketName = function(region, projectDomain) { // Sanitize region = region.trim().replace(/-/g, '').toLowerCase(); @@ -685,6 +719,7 @@ exports.npmInstall = function(dir) { */ exports.generateResourcesCf = function(projRootPath, projName, projDomain, stage, region, notificationEmail) { + let cfTemplate = require('../templates/resources-cf'); cfTemplate.Parameters.ProjectName.Default = projName; @@ -694,13 +729,12 @@ exports.generateResourcesCf = function(projRootPath, projName, projDomain, stage cfTemplate.Parameters.Stage.AllowedValues = [stage]; cfTemplate.Parameters.DataModelStage.AllowedValues = [stage]; - cfTemplate.Parameters.NotificationEmail.Default = notificationEmail; + cfTemplate.Parameters.NotificationEmail.Default = notificationEmail; cfTemplate.Description = projName + ' resources'; return this.writeFile( - path.join(projRootPath, 'cloudformation', 'resources-cf.json'), - JSON.stringify(cfTemplate, null, 2) - ); + path.join(projRootPath, 'meta', 'private', 'resources', 's-resources-cf.json'), + JSON.stringify(cfTemplate, null, 2)); }; /** diff --git a/other/examples/meta.json b/other/examples/meta.json index bc4afe150..198da7ad6 100644 --- a/other/examples/meta.json +++ b/other/examples/meta.json @@ -1,5 +1,5 @@ { - "project": { + "private": { "stages": { "development": { "regions": { @@ -11,5 +11,8 @@ } }, "variables": {} + }, + "public": { + "variables": {} } } \ No newline at end of file diff --git a/tests/test_utils.js b/tests/test_utils.js index f64a6e140..a70b08fd6 100644 --- a/tests/test_utils.js +++ b/tests/test_utils.js @@ -11,9 +11,9 @@ let fs = require('fs'), SUtils = require('../lib/utils'); /** - * Create test project + * Create test private * @param config see tests/config.js - * @param npmInstallDirs list of dirs relative to project root to execute npm install on + * @param npmInstallDirs list of dirs relative to private root to execute npm install on * @returns {Promise} full path to proj temp dir that was just created */ @@ -27,14 +27,14 @@ module.exports.createTestProject = function(config, npmInstallDirs) { // Create Test Project let tmpProjectPath = path.join(os.tmpdir(), projectName); - SUtils.sDebug('test_utils', 'Creating test project in ' + tmpProjectPath + '\n'); + SUtils.sDebug('test_utils', 'Creating test private in ' + tmpProjectPath + '\n'); // Delete test folder if already exists if (fs.existsSync(tmpProjectPath)) { rimraf.sync(tmpProjectPath); } - // Copy test project to temp directory + // Copy test private to temp directory fs.mkdirSync(tmpProjectPath); wrench.copyDirSyncRecursive(path.join(__dirname, './test-prj'), tmpProjectPath, { forceDelete: true, @@ -42,12 +42,12 @@ module.exports.createTestProject = function(config, npmInstallDirs) { let lambdasCF = SUtils.readAndParseJsonSync(__dirname + '/../lib/templates/lambdas-cf.json'), resourcesCF = SUtils.readAndParseJsonSync(__dirname + '/../lib/templates/resources-cf.json'), - projectJSON = SUtils.readAndParseJsonSync(path.join(tmpProjectPath, 's-project.json')); + projectJSON = SUtils.readAndParseJsonSync(path.join(tmpProjectPath, 's-private.json')); // Delete Lambda Template delete lambdasCF.Resources.lTemplate; - // Add project name to AllowedValues + // Add private name to AllowedValues resourcesCF.Parameters.ProjectName.AllowedValues.push(projectName); // Add stages to AllowedValues @@ -69,10 +69,10 @@ module.exports.createTestProject = function(config, npmInstallDirs) { projectJSON.stages[projectStage] = [{ region: projectRegion, iamRoleArnLambda: projectLambdaIAMRole, - regionBucket: SUtils.generateRegionBucketName(projectRegion, projectDomain) + regionBucket: SUtils.generateProjectBucketName(projectRegion, projectDomain) },]; - fs.writeFileSync(path.join(tmpProjectPath, 's-project.json'), JSON.stringify(projectJSON, null, 2)); + fs.writeFileSync(path.join(tmpProjectPath, 's-private.json'), JSON.stringify(projectJSON, null, 2)); // Write Admin.env file let adminEnv = 'SERVERLESS_ADMIN_AWS_ACCESS_KEY_ID=' @@ -81,7 +81,7 @@ module.exports.createTestProject = function(config, npmInstallDirs) { + process.env.TEST_SERVERLESS_AWS_SECRET_KEY + os.EOL; fs.writeFileSync(path.join(tmpProjectPath, 'admin.env'), adminEnv); - //Need to run npm install on the test project, they recommend NOT doing this programatically + //Need to run npm install on the test private, they recommend NOT doing this programatically //https://github.com/npm/npm#using-npm-programmatically if (npmInstallDirs) { diff --git a/tests/tests/actions/FunctionCreate.js b/tests/tests/actions/FunctionCreate.js index 4e861e41c..07baccaac 100644 --- a/tests/tests/actions/FunctionCreate.js +++ b/tests/tests/actions/FunctionCreate.js @@ -2,7 +2,7 @@ /** * Test: Function Create Action - * - Creates a new project in your system's temp directory + * - Creates a new private in your system's temp directory * - Creates a new Function inside the "users" module */ diff --git a/tests/tests/actions/ModuleCreate.js b/tests/tests/actions/ModuleCreate.js index 5d1641d75..93f6bb2b9 100644 --- a/tests/tests/actions/ModuleCreate.js +++ b/tests/tests/actions/ModuleCreate.js @@ -2,8 +2,8 @@ /** * Test: Module Create Action - * - Creates a new project in your system's temp directory - * - Creates a new Module inside test project + * - Creates a new private in your system's temp directory + * - Creates a new Module inside test private */ let Serverless = require('../../../lib/Serverless.js'), diff --git a/tests/tests/actions/ModuleInstall.js b/tests/tests/actions/ModuleInstall.js index deb1c0158..864f3b0ed 100644 --- a/tests/tests/actions/ModuleInstall.js +++ b/tests/tests/actions/ModuleInstall.js @@ -2,7 +2,7 @@ /** * Test: Module Install Action - * - Creates a new project in your system's temp directory + * - Creates a new private in your system's temp directory * - Installs module-test Module from github using the ModuleInstall action * - asserts that the Module was installed correctly */ diff --git a/tests/tests/actions/ProjectCreate.js b/tests/tests/actions/ProjectCreate.js index 1e3c98984..c1aa80aa2 100644 --- a/tests/tests/actions/ProjectCreate.js +++ b/tests/tests/actions/ProjectCreate.js @@ -2,8 +2,8 @@ /** * Test: Project Create Action - * - Creates a new project in your system's temp directory - * - Deletes the CF stack created by the project + * - Creates a new private in your system's temp directory + * - Deletes the CF stack created by the private */ let Serverless = require('../../../lib/Serverless'), @@ -118,7 +118,7 @@ describe('Test action: Project Create', function() { }); describe('Project Create', function() { - it('should create a new project in temp directory', function(done) { + it('should create a new private in temp directory', function(done) { this.timeout(0); @@ -139,7 +139,7 @@ describe('Test action: Project Create', function() { validateEvent(evt); // Validate Project JSON - let projectJson = utils.readAndParseJsonSync(path.join(os.tmpdir(), name, 's-project.json')); + let projectJson = utils.readAndParseJsonSync(path.join(os.tmpdir(), name, 's-private.json')); let region = false; for (let i = 0; i < projectJson.stages.development.length; i++) { diff --git a/tests/tests/actions/ResourcesDeploy.js b/tests/tests/actions/ResourcesDeploy.js index a1bdd8bb0..b5de6e193 100644 --- a/tests/tests/actions/ResourcesDeploy.js +++ b/tests/tests/actions/ResourcesDeploy.js @@ -2,8 +2,8 @@ /** * Test: Resources Deploy Action - * - Creates a new project in your system's temp directory - * - Makes a tiny update to the project's CF template + * - Creates a new private in your system's temp directory + * - Makes a tiny update to the private's CF template * - Deploy new CF template * - Deploy/Rollback the original CF template for cleaning */