From 19087a4a4f7e5b8a4302a4622ee3bba394fa05a9 Mon Sep 17 00:00:00 2001 From: Austen Collins Date: Sun, 13 Sep 2015 11:25:43 -0700 Subject: [PATCH] new region/stage: refactor --- bin/jaws | 29 ++-- lib/commands/env.js | 2 +- lib/commands/logs.js | 18 --- lib/commands/new_project.js | 2 +- lib/commands/new_region_stage.js | 191 ------------------------- lib/commands/new_stage_region.js | 234 +++++++++++++++++++++++++++++++ lib/utils/aws.js | 21 +-- tests/all.js | 4 +- tests/cli/new_stage_region.js | 104 ++++++++++++++ tests/test-prj/jaws-cf.json | 4 +- 10 files changed, 371 insertions(+), 238 deletions(-) delete mode 100644 lib/commands/logs.js delete mode 100644 lib/commands/new_region_stage.js create mode 100644 lib/commands/new_stage_region.js create mode 100644 tests/cli/new_stage_region.js diff --git a/bin/jaws b/bin/jaws index 21a1df72e..27a00459a 100755 --- a/bin/jaws +++ b/bin/jaws @@ -32,6 +32,7 @@ program if (type == 'project') { + // New Project var CmdNewProject = require('../lib/commands/new_project'); handleExit(CmdNewProject.run( args.name, @@ -42,15 +43,19 @@ program args.profile, args.noCf )); - } else if (type == 'region' || type == 'stage') { - var theCmd = require('../lib/commands/new_region_stage'), - isCreateRegion = (type == 'region'); - if (!regionName || !stageName) { - console.error("Region and stage name required. Stage name will be the 1st stage primed in new region"); - process.exit(1); - } - handleExit(theCmd.create(isCreateRegion, JAWS, regionName, stageName, options.dontExeCf)); + } else if (type == 'region' || type == 'stage') { + + // New Region/Stage + var CmdNewStageRegion = require('../lib/commands/new_stage_region'); + handleExit(CmdNewStageRegion.run( + JAWS, + type, + args.stage, + args.region, + args.noCf + )); + } else { console.error('Unsupported type ' + type + '. Must be project|region|stage'); process.exit(1); @@ -208,14 +213,6 @@ program handleExit(theCmd.run(JAWS)); }); -program - .command('logs ') - .description('Get logs for the lambda function in the specified stage in your current working directory.') - .action(function(stage) { - var theCmd = require('../lib/commands/logs'); - handleExit(theCmd.logs(JAWS, stage)); - }); - if (process.argv.length == 2) { program.outputHelp(); } else { diff --git a/lib/commands/env.js b/lib/commands/env.js index 86e5a78e6..9716b9f19 100644 --- a/lib/commands/env.js +++ b/lib/commands/env.js @@ -23,6 +23,7 @@ var JawsError = require('../jaws-error'), * @param projectName * @param stage */ + module.exports.getEnvFileAsMap = function(JAWS, stage) { var deferred; @@ -40,7 +41,6 @@ module.exports.getEnvFileAsMap = function(JAWS, stage) { if (!s3ObjData.Body) { return ''; } - return s3ObjData.Body; }); } diff --git a/lib/commands/logs.js b/lib/commands/logs.js deleted file mode 100644 index 2d369c9a3..000000000 --- a/lib/commands/logs.js +++ /dev/null @@ -1,18 +0,0 @@ -'use strict'; - -/** - * JAWS Command: logs - * - Fetches logs for your lambdas - */ - -var JawsError = require('../jaws-error'), - Promise = require('bluebird'), - path = require('path'), - fs = require('fs'), - AWS = require('../utils/aws'); - -Promise.promisifyAll(fs); - -module.exports.logs = function(JAWS, stage) { -//TODO: need help here. Want realtime log stream consuming (not poll if possible) -}; diff --git a/lib/commands/new_project.js b/lib/commands/new_project.js index b1c309d5d..4a5ca9392 100644 --- a/lib/commands/new_project.js +++ b/lib/commands/new_project.js @@ -212,7 +212,7 @@ CMD.prototype._prompt = Promise.method(function() { var choices = [ { key: '1) ', label: 'us-east-1', value: 'us-east-1' }, { key: '2) ', label: 'us-west-1', value: 'us-west-1' }, - { key: '3) ', label: 'eu-east-1', value: 'eu-west-1' }, + { key: '3) ', label: 'eu-west-1', value: 'eu-west-1' }, { key: '4) ', label: 'ap-northeast-1', value: 'ap-northeast-1' }, ]; diff --git a/lib/commands/new_region_stage.js b/lib/commands/new_region_stage.js deleted file mode 100644 index 27b4faa87..000000000 --- a/lib/commands/new_region_stage.js +++ /dev/null @@ -1,191 +0,0 @@ -'use strict'; - -/** - * Does one of the following: - * -Creates a new region, primed with one stage - * -Creates new stage in existing region - */ - -var JawsError = require('../jaws-error'), - Promise = require('bluebird'), - fs = require('fs'), - path = require('path'), - AWSUtils = require('../utils/aws'), - utils = require('../utils'), - Spinner = require('cli-spinner').Spinner; - -Promise.promisifyAll(fs); - -function _validateNewRegion(JAWS, regionName, stageName) { - if (!JAWS._meta.projectJson.project || !JAWS._meta.projectJson.project.stages) { - return Promise.reject(new JawsError('Project has no existing stages object defined', JawsError.errorCodes.UNKNOWN)); - } - - //Make sure region is not already defined - var stages = JAWS._meta.projectJson.project.stages; - - if (!stages[stageName]) { - return Promise.reject(new JawsError( - 'Stage ' + stageName + ' is invalid (not defined in jaws.json:project.stages)', - JawsError.errorCodes.UNKNOWN)); - } - - Object.keys(stages).forEach(function(stageName) { - if (stages[stageName].region == regionName) { - return Promise.reject(new JawsError('Region ' + regionName + ' already defined in stage ' + stageName)); - } - }); - - return Promise.resolve(); -} - -function _validateNewStage(JAWS, stageName) { - if (!JAWS._meta.projectJson.project || !JAWS._meta.projectJson.project.stages) { - return Promise.reject(new JawsError('Project has no existing stages object defined', JawsError.errorCodes.UNKNOWN)); - } - - //Make sure stage is not already defined - var stages = JAWS._meta.projectJson.project.stages; - - if (stages[stageName]) { - return Promise.reject(new JawsError('Stage ' + stageName + ' already exists in jaws.json:project.stages')); - } - - //Make sure the stage is not already defined in s3 env var - dont want to overwrite it - var envCmd = require('./env'); - return envCmd.getEnvFileAsMap(JAWS, stageName) - .then(function(envMap) { - if (Object.keys(envMap).length > 0) { - throw new JawsError( - 'Stage ' + stageName + ' can not be created as an env var file already exists', - JawsError.errorCodes.INVALID_RESOURCE_NAME - ); - } - }); -} - -function _updateJawsProjJson(JAWS, regionName, stageName, lambdaArn, apiArn) { - var stages = JAWS._meta.projectJson.project.stages, - projJawsJsonPath = path.join(JAWS._meta.projectRootPath, 'jaws.json'), - regionObj = { - region: regionName, - iamRoleArnLambda: lambdaArn || '', - iamRoleArnApiGateway: apiArn || '', - }; - - if (stages[stageName]) { - stages[stageName].push(regionObj); - } else { - stages[stageName] = regionObj; - } - - return utils.writeFile(projJawsJsonPath, JSON.stringify(JAWS._meta.projectJson, null, 2)); -} - -function _createCfStack(JAWS, region, stage) { - var message = 'JAWS is now going to create an AWS CloudFormation Stack for the "' + stage + - '" stage of your JAWS project in the new ' + region + ' region. This takes around 5 minutes. Sit tight!'; - var spinner = new Spinner('%s Creating CloudFormation Stack...'); - - console.log(message); - spinner.setSpinnerString('|/-\\'); - spinner.start(); - - return AWSUtils.cfCreateStack( - JAWS._meta.profile, - region, - JAWS._meta.projectRootPath, - JAWS._meta.projectJson.name, - stage, - '' //TODO: read email out of existing jaws-cf.json? - ) - .then(function(cfData) { - return AWSUtils.monitorCfCreate(cfData, JAWS._meta.profile, region, spinner); - }); -} - -/** - * Update Project JSON arns - * - * @param cfOutputs - * @param {Jaws} JAWS - * @param regionName - * @param stageName - * @returns {Promise} - * @private - */ -function _updateProjectJsonArns(cfOutputs, JAWS, regionName, stageName) { - - var iamRoleArnLambda, - iamRoleArnApiGateway; - - for (var i = 0; i < cfOutputs.length; i++) { - if (cfOutputs[i].OutputKey === 'IamRoleArnLambda') { - iamRoleArnLambda = cfOutputs[i].OutputValue; - } - - if (cfOutputs[i].OutputKey === 'IamRoleArnApiGateway') { - iamRoleArnApiGateway = cfOutputs[i].OutputValue; - } - } - - return _updateJawsProjJson(JAWS, regionName, stageName, iamRoleArnLambda, iamRoleArnApiGateway); -} - -/** - * Create env file skeletion for new stage - * - * @param {Jaws} JAWS - * @param stageName - * @private - */ -function _createEnvFile(JAWS, stageName) { - var envFileContents = 'JAWS_STAGE=' + stageName + '\nJAWS_DATA_MODEL_PREFIX=' + stageName; - return AWSUtils.putEnvFile( - JAWS._meta.profile, - JAWS._meta.projectJson.project.envVarBucket.region, - JAWS._meta.projectJson.project.envVarBucket.name, - JAWS._meta.projectJson.name, - stageName, - envFileContents); -} - -/** - * - * @param {boolean} isCreateRegion if false means we are creating stage in existing region - * @param {Jaws} JAWS - * @param regionName - * @param stageName the stage to prime in the new region - * @param noExeCf don't execute CloudFormation at the end - * @returns {*} - */ -module.exports.create = function(isCreateRegion, JAWS, regionName, stageName, noExeCf) { - var deferred; - if (isCreateRegion) { - utils.logIfVerbose('Creating new region'); - deferred = _validateNewRegion(JAWS, regionName, stageName); - } else { - utils.logIfVerbose('Creating new stage'); - deferred = _validateNewStage(JAWS, stageName) - .then(function() { - return _createEnvFile(JAWS, stageName); - } - ); - } - - return deferred - .then(function() { - if (noExeCf) { - utils.logIfVerbose('No exec cf specified, updating proj jaws.json only'); - - console.log('Successfully created. CloudFormation file can be run manually'); - console.log('After creating CF stack, remember to put the IAM role outputs in your project jaws.json'); - return _updateJawsProjJson(JAWS, regionName, stageName); - } else { - return _createCfStack(JAWS, regionName, stageName) - .then(function(cfOutputs) { - return _updateProjectJsonArns(cfOutputs, JAWS, regionName, stageName); - }); - } - }); -}; diff --git a/lib/commands/new_stage_region.js b/lib/commands/new_stage_region.js new file mode 100644 index 000000000..93459a194 --- /dev/null +++ b/lib/commands/new_stage_region.js @@ -0,0 +1,234 @@ +'use strict'; + +/** + * JAWS Command: New Region/Stage + * -Creates a new region, primed with one stage + * -Creates new stage in existing region + */ + +var JawsError = require('../jaws-error'), + JawsCli = require('../utils/cli'), + Promise = require('bluebird'), + fs = require('fs'), + path = require('path'), + AWSUtils = require('../utils/aws'), + utils = require('../utils'), + Spinner = require('cli-spinner').Spinner; + +Promise.promisifyAll(fs); + +/** + * Run + */ + +module.exports.run = function(JAWS, type, stage, region, noCf) { + var command = new CMD(JAWS, type, stage, region, noCf); + return command.run(); +}; + +/** + * Command Class + * @constructor + */ + +function CMD(JAWS, type, stage, region, noCf) { + this._JAWS = JAWS; + this._type = type; + this._stage = stage; + this._region = region; + this._noCf = noCf; +} + +/** + * CMD: Run + */ + +CMD.prototype.run = Promise.method(function() { + + var _this = this; + + // Status + if (_this._type === 'stage') JawsCli.log('Creating new stage "' + _this._stage + '"...'); + if (_this._type === 'region') JawsCli.log('Creating new region within stage "' + _this._stage + '"...'); + + return Promise.try(function() {}) + .bind(_this) + .then(_this._promptRegion) + .then(_this._validate) + .then(_this._createEnvFile) + .then(_this._createCfStack) + .then(_this._updateProjectJson); +}); + +/** + * CMD: Prompt: Region + */ + +CMD.prototype._promptRegion = Promise.method(function() { + + var _this = this; + + // If region exists, skip + if (_this._region) return; + + var regions = [ + 'us-east-1', + 'us-west-1', + 'eu-west-1', + 'ap-northeast-1', + ]; + + // Create Choices + var choices = []; + for (var i = 0; i < (regions.length + 1); i++) { + choices.push({ + key: (i + 1) + ') ', + value: regions[i], + label: regions[i], + }); + } + + return JawsCli.select('Choose a region within this stage: ', choices, false) + .then(function(results) { + _this._region = [results[0].value]; + }); +}); + +/** + * CMD: Validate + */ + +CMD.prototype._validate = Promise.method(function() { + + var _this = this; + + // Check project config is valid + if (!_this._JAWS._meta.projectJson.project || !_this._JAWS._meta.projectJson.project.stages) { + throw new JawsError('Project\'s jaws.json is malformed or has no existing stages object defined'); + } + + // Check stage and region have been submitted + if (!_this._stage || !_this._region) { + throw new JawsError('Stage and region are required'); + } + + // Stage Validations + if (_this._type === 'stage') { + + // Make sure stage is not already defined + if (_this._JAWS._meta.projectJson.project.stages[_this._stage]) { + throw new JawsError('Stage "' + _this._stage + '" is already defined in this project'); + } + + // Make sure stage is not already defined in s3 env var - don't want to overwrite it + var envCmd = require('./env'); + return envCmd.getEnvFileAsMap(_this._JAWS, _this._stage) + .then(function(envMap) { + if (Object.keys(envMap).length > 0) { + throw new JawsError( 'Stage "' + _this._stage + '" can not be created as an env var file already exists'); + } + }); + } + + // Region Validations + if (_this._type === 'region') { + + // Make sure region is not already defined + var regions = Object.keys(_this._JAWS._meta.projectJson.project.stages[_this._stage]); + if (regions.indexOf(_this._region) > -1) { + throw new JawsError('Region "' + _this._region + '" is already defined in the stage "' + _this._stage + '"'); + } + } + + return Promise.resolve(); +}); + +/** + * CMD: Create ENV File + */ + +CMD.prototype._createEnvFile = Promise.method(function() { + + var _this = this; + + // If type is not stage, skip this + if (_this._type !== 'stage') return; + + var envFileContents = 'JAWS_STAGE=' + _this._stage + + '\nJAWS_DATA_MODEL_PREFIX=' + _this._stage; + + return AWSUtils.putEnvFile( + _this._JAWS._meta.profile, + _this._JAWS._meta.projectJson.project.envVarBucket.region, + _this._JAWS._meta.projectJson.project.envVarBucket.name, + _this._JAWS._meta.projectJson.name, + _this._stage, + envFileContents); +}); + +/** + * CMD: Create CF Stack + */ + +CMD.prototype._createCfStack = Promise.method(function() { + + var _this = this; + + // Start loading icon + var spinner = JawsCli.spinner('Creating CloudFormation Stack for stage "' + + _this._stage + + '" and region "' + + _this._region + + '". This doesn\'t cost anything, but it takes a few minutes...'); + spinner.start(); + + return AWSUtils.cfCreateStack( + _this._JAWS._meta.profile, + _this._region, + _this._JAWS._meta.projectRootPath, + _this._JAWS._meta.projectJson.name, + _this._stage, + '' // TODO: read email out of existing jaws-cf.json? + ) + .then(function(cfData) { + return AWSUtils.monitorCfCreate(cfData, _this._JAWS._meta.profile, _this._region) + .then(function(cfData) { + _this._cfData = cfData; + spinner.stop(true); + JawsCli.log('CloudFormation Stack "' + cfData.StackName + '" successfully created.'); + }); + }); +}); + +/** + * CMD: Update Project JSON + */ + +CMD.prototype._updateProjectJson = Promise.method(function() { + + var _this = this; + + var regionObj = { + region: _this._region, + }; + + for (var i = 0; i < _this._cfData.Outputs.length; i++) { + if (_this._cfData.Outputs[i].OutputKey === 'IamRoleArnLambda') { + regionObj.IamRoleArnLambda = _this._cfData.Outputs[i].OutputValue; + } + + if (_this._cfData.Outputs[i].OutputKey === 'IamRoleArnApiGateway') { + regionObj.iamRoleArnApiGateway = _this._cfData.Outputs[i].OutputValue; + } + } + + if (_this._JAWS._meta.projectJson.project.stages[_this._stage]) { + _this._JAWS._meta.projectJson.project.stages[_this._stage].push(regionObj); + } else { + _this._JAWS._meta.projectJson.project.stages[_this._stage] = [regionObj]; + } + + return utils.writeFile( + path.join(_this._JAWS._meta.projectRootPath, 'jaws.json'), + JSON.stringify(_this._JAWS._meta.projectJson, null, 2)); +}); \ No newline at end of file diff --git a/lib/utils/aws.js b/lib/utils/aws.js index 6ec1aa16d..1e64d5274 100644 --- a/lib/utils/aws.js +++ b/lib/utils/aws.js @@ -157,6 +157,7 @@ exports.iamGetRole = function(awsProfile, awsRegion, roleName) { * @param projNotificationEmail * @returns {Promise} */ + exports.cfCreateStack = function(awsProfile, awsRegion, projRootPath, projName, projStage, projNotificationEmail) { var _this = this; @@ -218,12 +219,19 @@ exports.cfCreateStack = function(awsProfile, awsRegion, projRootPath, projName, } else { return resolve(data); } - }); }); }; -exports.monitorCfCreate = function(cfData, awsProfile, region, spinner) { +/** + * CloudFormation: Monitor CF Create + * @param cfData + * @param awsProfile + * @param region + * @returns {bluebird|exports|module.exports} + */ + +exports.monitorCfCreate = function(cfData, awsProfile, region) { var _this = this; return new Promise(function(resolve, reject) { @@ -247,8 +255,6 @@ exports.monitorCfCreate = function(cfData, awsProfile, region, spinner) { stackStatus = stackData.Stacks[0].StackStatus; if (!stackStatus || ['CREATE_IN_PROGRESS', 'CREATE_COMPLETE'].indexOf(stackStatus) === -1) { - - spinner.stop(true); return reject(new JawsError( 'Something went wrong while creating your JAWS resources', JawsError.errorCodes.UNKNOWN)); @@ -260,15 +266,14 @@ exports.monitorCfCreate = function(cfData, awsProfile, region, spinner) { }, function() { - // Stop Spinner, inform - spinner.stop(true); - console.log('CloudFormation Stack ' + stackData.Stacks[0].StackName + ' successfully created.'); - return resolve(stackData.Stacks[0].Outputs); + //console.log('CloudFormation Stack ' + stackData.Stacks[0].StackName + ' successfully created.'); + return resolve(stackData.Stacks[0]); } ); }); }; + exports.createBucket = function(awsProfile, awsRegion, bucketName) { this.configAWS(awsProfile, awsRegion); diff --git a/tests/all.js b/tests/all.js index 6298cfad2..5e7cbd7ce 100644 --- a/tests/all.js +++ b/tests/all.js @@ -22,8 +22,10 @@ describe('AllTests', function() { /** * Tests below create AWS Resources */ + + require('./cli/new_stage_region'); //require('./cli/dash'); //require('./cli/deploy_lambda'); //require('./cli/deploy_endpoint'); - require('./cli/new_project'); + //require('./cli/new_project'); }); \ No newline at end of file diff --git a/tests/cli/new_stage_region.js b/tests/cli/new_stage_region.js new file mode 100644 index 000000000..37935265e --- /dev/null +++ b/tests/cli/new_stage_region.js @@ -0,0 +1,104 @@ +'use strict'; + +/** + * JAWS Test: Dash Command + */ + +var Jaws = require('../../lib/index.js'), + CmdNewStageRegion = require('../../lib/commands/new_stage_region'), + JawsError = require('../../lib/jaws-error'), + testUtils = require('../test_utils'), + Promise = require('bluebird'), + path = require('path'), + shortid = require('shortid'), + assert = require('chai').assert; + +var config = require('../config'), + projPath, + JAWS; + +var tempStage = 'temp-' + shortid.generate(); +var tempRegion1 = 'us-west-1'; +var tempRegion2 = 'eu-west-1'; + +describe('Test "new stage/region" command', function() { + + before(function(done) { + this.timeout(0); + + // Tag All Lambdas & Endpoints + return Promise.try(function() { + + // Create Test Project + projPath = testUtils.createTestProject( + config.name, + config.region, + config.stage, + config.iamRoleArnLambda, + config.iamRoleArnApiGateway, + config.envBucket); + process.chdir(path.join(projPath, 'back')); + + // Instantiate JAWS + JAWS = new Jaws(); + }) + .then(function() { + return done(); + }); + }); + + after(function(done) { + done(); + }); + + describe('Positive tests', function() { + + it('Create New Stage', function(done) { + this.timeout(0); + + CmdNewStageRegion.run(JAWS, 'stage', tempStage, tempRegion1, false) + .then(function() { + var jawsJson = require(path.join(process.cwd(), '../jaws.json')); + var region = false; + for (var i = 0; i < jawsJson.project.stages[tempStage].length; i++) { + var stage = jawsJson.project.stages[tempStage][i]; + if (stage.region === tempRegion1) { + region = stage.region; + } + } + assert.isTrue(region !== false); + done(); + }) + .catch(JawsError, function(e) { + done(e); + }) + .error(function(e) { + done(e); + }); + }); + + it('Create New region', function(done) { + this.timeout(0); + + CmdNewStageRegion.run(JAWS, 'region', tempStage, tempRegion2, false) + .then(function() { + var jawsJson = require(path.join(process.cwd(), '../jaws.json')); + var region = false; + for (var i = 0; i < jawsJson.project.stages[tempStage].length; i++) { + var stage = jawsJson.project.stages[tempStage][i]; + if (stage.region === tempRegion2) { + region = stage.region; + } + } + assert.isTrue(region !== false); + done(); + }) + .catch(JawsError, function(e) { + done(e); + }) + .error(function(e) { + done(e); + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/test-prj/jaws-cf.json b/tests/test-prj/jaws-cf.json index 7392cf992..325b51bad 100644 --- a/tests/test-prj/jaws-cf.json +++ b/tests/test-prj/jaws-cf.json @@ -207,7 +207,7 @@ } }, "Outputs": { - "IamRoleLambdaArn": { + "IamRoleArnLambda": { "Description": "ARN of the lambda IAM role", "Value": { "Fn::GetAtt": [ @@ -216,7 +216,7 @@ ] } }, - "IamRoleApiGatewayArn": { + "IamRoleArnApiGateway": { "Description": "ARN of the api gateway IAM role", "Value": { "Fn::GetAtt": [