new region/stage: refactor

This commit is contained in:
Austen Collins 2015-09-13 11:25:43 -07:00
parent 4425149b2c
commit 19087a4a4f
10 changed files with 371 additions and 238 deletions

View File

@ -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 <stage>')
.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 {

View File

@ -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;
});
}

View File

@ -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)
};

View File

@ -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' },
];

View File

@ -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);
});
}
});
};

View File

@ -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));
});

View File

@ -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);

View File

@ -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');
});

View File

@ -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);
});
});
});
});

View File

@ -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": [