Merge branch 'master' of github.com:jaws-framework/JAWS

This commit is contained in:
doapp-ryanp 2015-09-24 15:20:14 -05:00
commit 4e90c7909e
12 changed files with 466 additions and 67 deletions

View File

@ -225,6 +225,10 @@ program
var theCmd = require('../lib/commands/deploy_lambda');
execute(theCmd.run(JAWS, stage, region, allTagged, options.dontExeCf));
break;
case 'resources':
var theCmd = require('../lib/commands/deploy_resources');
execute(theCmd.run(JAWS, stage, region));
break;
default:
console.error('Unsupported type ' + type + '. Must be endpoint|lambda|resources');
process.exit(1);

View File

@ -104,7 +104,7 @@ CMD.prototype.run = Promise.method(function() {
return CMDdeployLambda.run(
_this._JAWS,
_this._stage,
_this._regions,
(_this._regions.length > 1 ? null : _this._regions[0]),
true);
})
.then(function() {
@ -176,7 +176,7 @@ CMD.prototype._prepareResources = Promise.method(function() {
value: jsonPaths[i],
type: 'endpoint',
label: '/' + json.apiGateway.cloudFormation.Path
+ ' - ' + json.apiGateway.cloudFormation.Method,
+ ' - ' + json.apiGateway.cloudFormation.Method,
};
// Create path
@ -341,37 +341,30 @@ CMD.prototype._promptRegion = Promise.method(function() {
throw new JawsError('This stage has no regions');
}
_this._regions = regions;
// If stage only has 1 region, use it and skip prompt
if (regions.length === 1) {
_this._regions = regions;
return;
}
if (regions.length === 1) return;
// Create Choices
var choices = [];
for (var i = 0; i < (_this._regions.length + 1); i++) {
if (_this._regions[i]) {
choices.push({
key: (i + 1) + ') ',
value: _this._regions[i],
label: _this._regions[i],
});
} else {
// Push 'all regions' choice
choices.push({
key: (i + 1) + ') ',
value: 'all regions',
label: 'all regions',
});
}
for (var i = 0; i < _this._regions.length; i++) {
choices.push({
key: '',
value: _this._regions[i],
label: _this._regions[i],
});
}
choices.push({
key: '',
value: 'all regions',
label: 'all regions',
});
return JawsCli.select('Choose a region within this stage: ', choices, false)
.then(function(results) {
if (results[0].value === 'all regions') {
_this._regions = Object.keys(_this._JAWS._meta.projectJson.stages[_this._stage]);
} else {
if (results[0].value !== 'all regions') {
_this._regions = [results[0].value];
}
});

View File

@ -74,8 +74,7 @@ CMD.prototype.run = Promise.method(function() {
.then(_this._validate)
.then(_this._getTaggedLambdaPaths)
.then(function() {
utils.logIfVerbose("Deploying to stage:");
utils.logIfVerbose(_this._stage);
utils.logIfVerbose('Deploying to stage: ' + _this._stage);
return _this._regions;
})
.each(function(region) {
@ -289,7 +288,7 @@ Deployer.prototype.deploy = Promise.method(function() {
})
.then(function() {
spinner.stop(true);
})
});
}
})
.then(function() {

View File

@ -0,0 +1,235 @@
'use strict';
/**
* JAWS Command: deploy resources <stage> <region>
* - Deploys project's resources-cf.json
*/
var JawsError = require('../jaws-error'),
JawsCli = require('../utils/cli'),
Promise = require('bluebird'),
fs = require('fs'),
async = require('async'),
path = require('path'),
utils = require('../utils/index'),
AWSUtils = require('../utils/aws'),
CMDtag = require('./tag');
Promise.promisifyAll(fs);
/**
* Run
* @param JAWS
* @param stage
* @param region
* @param allTagged
* @returns {*}
*/
module.exports.run = function(JAWS, stage, region) {
var command = new CMD(JAWS, stage, region);
return command.run();
};
/**
* CMD Class
* @param JAWS
* @param stage
* @param region
* @constructor
*/
function CMD(JAWS, stage, region) {
var _this = this;
_this._stage = stage;
_this._JAWS = JAWS;
if (region && stage) {
_this._regions = _this._JAWS._meta.projectJson.stages[_this._stage].filter(function(r) {
return (r.region == region);
});
} else if (stage) {
_this._regions = _this._JAWS._meta.projectJson.stages[_this._stage];
}
}
/**
* CMD: Run
*/
CMD.prototype.run = Promise.method(function() {
var _this = this;
// Flow
return _this._JAWS.validateProject()
.bind(_this)
.then(_this._promptStage)
.then(function(answer) {
if (answer) _this._stage = answer[0].value;
})
.then(_this._promptRegions)
.then(function(answer) {
if (answer) {
_this._regions = [utils.getProjRegionConfigForStage(_this._JAWS, _this._stage, answer[0].value)];
}
})
.then(function() {
return _this._regions;
})
.each(function(regionJson) {
JawsCli.log('Resources Deployer "'
+ _this._stage
+ '": Deploying resources to region "'
+ regionJson.region
+ '"...');
var deployer = new ResourceDeployer(
_this._JAWS,
_this._stage,
regionJson
);
return deployer.deploy();
});
});
/**
* CMD: Prompt Stage
*/
CMD.prototype._promptStage = Promise.method(function() {
var _this = this;
// If stage, skip
if (_this._stage) return;
var stages = Object.keys(_this._JAWS._meta.projectJson.stages);
if (!stages.length) {
throw new JawsError('You have no stages in this project');
}
// If project has only one stage, skip select
if (stages.length === 1) {
_this._stage = stages[0];
return;
}
var choices = [];
for (var i = 0; i < stages.length; i++) {
choices.push({
key: '',
value: stages[i],
label: stages[i]
});
}
return JawsCli.select('Select a stage to deploy to: ', choices, false);
});
/**
* CMD: Prompt Regions
*/
CMD.prototype._promptRegions = Promise.method(function() {
var _this = this;
// If regions, skip
if (_this._regions && _this._regions.length) return;
var regions = _this._JAWS._meta.projectJson.stages[_this._stage];
// If stage has only one region, skip select
if (regions.length === 1) {
_this._regions = regions;
return;
}
var choices = [];
for (var i = 0; i < regions.length; i++) {
choices.push({
key: '',
value: regions[i].region,
label: regions[i].region,
});
}
return JawsCli.select('Select a region in this stage to deploy to: ', choices, false);
});
/**
* Resource Deployer
* @param JAWS
* @param stage
* @param region
* @constructor
*/
function ResourceDeployer(JAWS, stage, region) {
var _this = this;
_this._JAWS = JAWS;
_this._stage = stage;
_this._regionJson = region;
}
/**
* Resource Deployer: Deploy
*/
ResourceDeployer.prototype.deploy = Promise.method(function() {
var _this = this;
JawsCli.log('Resources Deployer "'
+ _this._stage
+ ' - '
+ _this._regionJson.region
+ '": Performing Cloudformation stack update. '
+ 'This could take a while depending on how many resources you are updating...');
var spinner = JawsCli.spinner();
spinner.start();
return _this._updateStack()
.bind(_this)
.then(function(cfData) {
return AWSUtils.monitorCf(cfData, _this._JAWS._meta.profile, _this._regionJson.region, 'update');
})
.then(function(data) {
spinner.stop(true);
JawsCli.log('Resources Deployer "'
+ _this._stage
+ ' - '
+ _this._regionJson.region
+ '": Cloudformation stack update completed successfully!');
})
.catch(function(error) {
spinner.stop(true);
JawsCli.log('Resources Deployer "'
+ _this._stage
+ ' - '
+ _this._regionJson.region
+ '": Cloudformation stack update failed because of the following error...');
console.log(error);
});
});
/**
* Resource Deployer: Update Stack
*/
ResourceDeployer.prototype._updateStack = Promise.method(function() {
var _this = this;
// Fetch Cloudformation template
return AWSUtils.cfUpdateResourcesStack(
_this._JAWS,
_this._stage,
_this._regionJson.region);
});

View File

@ -14,6 +14,7 @@ var JawsError = require('../jaws-error'),
fs = require('fs'),
path = require('path'),
os = require('os'),
chalk = require('chalk'),
AWSUtils = require('../utils/aws'),
utils = require('../utils'),
shortid = require('shortid');
@ -32,11 +33,12 @@ Promise.promisifyAll(fs);
* @returns {*}
*/
module.exports.run = function(name, stage, s3Bucket, region, notificationEmail, profile, noCf) {
module.exports.run = function(name, stage, s3Bucket, domain, region, notificationEmail, profile, noCf) {
var command = new CMD(
name,
stage,
s3Bucket,
domain,
notificationEmail,
region,
profile,
@ -49,6 +51,7 @@ module.exports.run = function(name, stage, s3Bucket, region, notificationEmail,
* @param name
* @param stage
* @param s3Bucket
* @param domain
* @param notificationEmail
* @param region
* @param profile
@ -56,10 +59,11 @@ module.exports.run = function(name, stage, s3Bucket, region, notificationEmail,
* @constructor
*/
function CMD(name, stage, s3Bucket, notificationEmail, region, profile, noCf) {
function CMD(name, stage, s3Bucket, domain, notificationEmail, region, profile, noCf) {
// Defaults
this._name = name ? name : null;
this._domain = domain ? domain : null;
this._stage = stage ? stage.toLowerCase().replace(/\W+/g, '').substring(0, 15) : null;
this._s3Bucket = s3Bucket;
this._notificationEmail = notificationEmail;
@ -120,10 +124,12 @@ CMD.prototype._prompt = Promise.method(function() {
var _this = this;
var nameDescription = 'Enter a project name: ' + os.EOL;
// Prompt: name (project name)
_this.Prompter.override.name = _this._name;
_this._prompts.properties.name = {
description: 'Enter a project name: '.yellow,
description: nameDescription.yellow,
default: 'jaws-' + shortid.generate().replace(/\W+/g, '').substring(0, 19).replace('_', ''),
message: 'Name must be only letters, numbers, underscores or dashes',
conform: function(name) {
@ -132,24 +138,24 @@ CMD.prototype._prompt = Promise.method(function() {
},
};
// Prompt: stage
_this.Prompter.override.stage = _this._stage;
_this._prompts.properties.stage = {
description: 'Enter a stage for this project: '.yellow,
default: 'dev',
message: 'Stage must be letters only',
conform: function(stage) {
var re = /^[a-zA-Z]+$/;
return re.test(stage);
},
};
// Prompt: domain - for AWS hosted zone and more
_this.Prompter.override.domain = _this._domain;
// Prompt: s3 bucket - holds env vars for this project
_this.Prompter.override.s3Bucket = _this._s3Bucket;
_this._prompts.properties.s3Bucket = {
description: 'Enter an S3 Bucket name to store this project\'s env vars and lambda zips (must be in same region as your lambdas): '.yellow,
default: 'jaws.yourapp.com',
message: 'Bucket name must only contain lowercase letters, numbers, periods and dashes',
var domainDescription = 'Enter a project domain: '
+ os.EOL
+ ' - You dont need to currently own this domain. '
+ os.EOL
+ ' - JAWS mostly uses it to namespace some project AWS resources (S3 Buckets). '
+ os.EOL
+ ' - However, if you do set up this domain as a Hosted Zone on Route 53 you will benefit from extra features'
+ os.EOL
+ ' - You can enter a placeholder for now and change this at any time.'
+ os.EOL;
_this._prompts.properties.domain = {
description: domainDescription.yellow,
default: 'myapp.com',
message: 'Domain must only contain lowercase letters, numbers, periods and dashes',
conform: function(bucket) {
var re = /^[a-z0-9-.]+$/;
return re.test(bucket);
@ -169,12 +175,70 @@ CMD.prototype._prompt = Promise.method(function() {
},
};
// Prompt: s3 bucket - holds env vars for this project
_this.Prompter.override.s3Bucket = _this._s3Bucket;
var s3BucketDescription = 'Enter a project S3 Bucket name: '
+ os.EOL
+ ' - JAWS uses an extra S3 bucket to store project ENV variables, lambda zips, and cloudformation templates.'
+ os.EOL
+ ' - This can be an existing bucket you reuse for all of your JAWS projects (must be in same region as your project).'
+ os.EOL
+ ' - If this doesn\'t exist, JAWS will create it automatically after these prompts in your project region.'
+ os.EOL
+ ' - You can enter a placeholder for now and change this at any time.'
+ os.EOL;
_this._prompts.properties.s3Bucket = {
description: s3BucketDescription.yellow,
default: 'jaws.myapp.com',
message: 'Bucket name must only contain lowercase letters, numbers, periods and dashes',
conform: function(bucket) {
var re = /^[a-z0-9-.]+$/;
return re.test(bucket);
},
};
// Prompt: stage
_this.Prompter.override.stage = _this._stage;
var stageDescription = 'Enter a stage for this project: ' + os.EOL;
_this._prompts.properties.stage = {
description: stageDescription.yellow,
default: 'dev',
message: 'Stage must be letters only',
conform: function(stage) {
var re = /^[a-zA-Z]+$/;
return re.test(stage);
},
};
// Prompt: notification email - for AWS alerts
_this.Prompter.override.notificationEmail = _this._notificationEmail;
var notificationEmailDescription = 'Enter an email to use for AWS alarms: ' + os.EOL;
_this._prompts.properties.notificationEmail = {
description: notificationEmailDescription.yellow,
required: true,
message: 'Please enter a valid email',
default: 'you@yourapp.com',
conform: function(email) {
if (!email) return false;
return true;
},
};
// Prompt: API Keys - Create an AWS profile by entering API keys
if (!utils.fileExistsSync(path.join(AWSUtils.getConfigDir(), 'credentials'))) {
_this.Prompter.override.awsAdminKeyId = _this._awsAdminKeyId;
var apiKeyDescription = 'Enter the ACCESS KEY ID for your Admin AWS IAM User: ' + os.EOL;
_this._prompts.properties.awsAdminKeyId = {
description: 'Enter the ACCESS KEY ID for your Admin AWS IAM User: '.yellow,
description: apiKeyDescription.yellow,
required: true,
message: 'Please enter a valid access key ID',
conform: function(key) {
@ -183,8 +247,11 @@ CMD.prototype._prompt = Promise.method(function() {
},
};
_this.Prompter.override.awsAdminSecretKey = _this._awsAdminSecretKey;
var apiSecretDescription = 'Enter the SECRET ACCESS KEY for your Admin AWS IAM User: ' + os.EOL;
_this._prompts.properties.awsAdminSecretKey = {
description: 'Enter the SECRET ACCESS KEY for your Admin AWS IAM User: '.yellow,
description: apiSecretDescription.yellow,
required: true,
message: 'Please enter a valid secret access key',
conform: function(key) {
@ -198,6 +265,7 @@ CMD.prototype._prompt = Promise.method(function() {
return _this.Prompter.getAsync(_this._prompts)
.then(function(answers) {
_this._name = answers.name;
_this._domain = answers.domain;
_this._stage = answers.stage.toLowerCase();
_this._s3Bucket = answers.s3Bucket;
_this._notificationEmail = answers.notificationEmail;
@ -240,7 +308,7 @@ CMD.prototype._prompt = Promise.method(function() {
});
}
return JawsCLI.select('Select a profile for your project: ', choices, false)
return JawsCLI.select('Select an AWS profile for your project: ', choices, false)
.then(function(results) {
_this._profile = results[0].value;
});
@ -312,6 +380,7 @@ CMD.prototype._createProjectDirectory = Promise.method(function() {
// Prepare CloudFormation template
var cfTemplate = utils.readAndParseJsonSync(__dirname + '/../templates/resources-cf.json');
cfTemplate.Parameters.aaProjectName.Default = _this._name;
cfTemplate.Parameters.aaProjectDomain.Default = _this._domain;
cfTemplate.Parameters.aaProjectName.AllowedValues = [_this._name];
cfTemplate.Parameters.aaStage.Default = _this._stage;
cfTemplate.Parameters.aaDataModelPrefix.Default = _this._stage; //to simplify bootstrap use same stage

View File

@ -215,7 +215,7 @@ CMD.prototype._validate = Promise.method(function() {
// Make sure region is not already defined
if (_this._JAWS._meta.projectJson.stages[_this._stage].some(function(r) {
return r.region == _this._region
return r.region == _this._region;
})) {
throw new JawsError('Region "' + _this._region + '" is already defined in the stage "' + _this._stage + '"');
}

View File

@ -5,16 +5,16 @@
"aaProjectName": {
"Type": "String"
},
"aaProjectDomain": {
"Type": "String",
"Default": "myapp.com"
},
"aaStage": {
"Type": "String"
},
"aaDataModelPrefix": {
"Type": "String"
},
"aaHostedZoneName": {
"Type": "String",
"Default": "myjawsproject.com"
},
"aaNotficationEmail": {
"Type": "String",
"Default": "you@you.com"

View File

@ -19,7 +19,7 @@ exports.validLambdaRegions = [
'us-east-1',
'us-west-2', //oregon
'eu-west-1', //Ireland
'ap-northeast-1' //Tokyo
'ap-northeast-1', //Tokyo
];
/**
@ -456,7 +456,7 @@ exports.cfUpdateLambdasStack = function(JAWS, stage, region, lambdaRoleArn) {
};
/**
* CloudFormation: Create Stack
* CloudFormation: Create Resources Stack
* @param awsProfile
* @param awsRegion
* @param projRootPath
@ -495,8 +495,8 @@ exports.cfCreateResourcesStack = function(awsProfile, awsRegion, projRootPath, p
ParameterValue: projStage,
UsePreviousValue: false,
}, {
ParameterKey: 'aaHostedZoneName',
ParameterValue: 'mydomain.com', //TODO: should we prompt for this?
ParameterKey: 'aaProjectDomain',
ParameterValue: 'mydomain.com',
UsePreviousValue: false,
}, {
ParameterKey: 'aaNotficationEmail',
@ -520,6 +520,45 @@ exports.cfCreateResourcesStack = function(awsProfile, awsRegion, projRootPath, p
});
};
/**
* CloudFormation: Update Resources Stack
* @param JAWS
* @param stage
* @param region
* @returns {*}
*/
exports.cfUpdateResourcesStack = function(JAWS, stage, region) {
var _this = this,
awsProfile = JAWS._meta.profile,
projRootPath = JAWS._meta.projectRootPath,
bucketName = JAWS._meta.projectJson.jawsBuckets[region],
projName = JAWS._meta.projectJson.name;
_this.configAWS(awsProfile, region);
var CF = Promise.promisifyAll(new AWS.CloudFormation({
apiVersion: '2010-05-15',
})),
stackName = _this.cfGetResourcesStackName(stage, projName);
var params = {
StackName: stackName,
Capabilities: [
'CAPABILITY_IAM',
],
UsePreviousTemplate: false,
Parameters: []
};
return _this.putCfFile(awsProfile, projRootPath, region, bucketName, projName, stage, 'resources')
.then(function(templateUrl) {
params.TemplateURL = templateUrl;
return CF.updateStackAsync(params);
});
};
/**
* CloudFormation: Monitor CF Stack Status (Create/Update)
* @param cfData
@ -878,7 +917,7 @@ exports.lambdaRemovePermission = function(awsProfile, awsRegion, functionName, s
var params = {
FunctionName: functionName, /* required */
StatementId: statementId /* required */
StatementId: statementId, /* required */
};
return new Promise(function(resolve, reject) {

View File

@ -15,18 +15,19 @@ describe('AllTests', function() {
});
//require tests vs inline so we can run sequentially
require('./cli/tag');
require('./cli/module_install');
require('./cli/env');
require('./cli/module_create');
require('./cli/run');
//require('./cli/tag');
//require('./cli/module_install');
//require('./cli/env');
//require('./cli/module_create');
//require('./cli/run');
/**
* Tests below create AWS Resources
*/
//require('./cli/dash');
//require('./cli/deploy_lambda');
//require('./cli/deploy_resources');
//require('./cli/deploy_endpoint');
//require('./cli/new_stage_region');
//require('./cli/new_project');
require('./cli/new_project');
});

View File

@ -0,0 +1,57 @@
'use strict';
/**
* JAWS Test: Deploy Resources
*/
var Jaws = require('../../lib/index.js'),
CmdDeployResources = require('../../lib/commands/deploy_resources'),
CmdTag = require('../../lib/commands/tag'),
JawsError = require('../../lib/jaws-error'),
testUtils = require('../test_utils'),
Promise = require('bluebird'),
path = require('path'),
assert = require('chai').assert,
config = require('../config'),
lambdaPaths = {},
projPath,
JAWS;
describe('Test deploy resources command', function() {
before(function(done) {
testUtils.createTestProject(
config.name,
config.region,
config.stage,
config.iamRoleArnLambda,
config.iamRoleArnApiGateway,
config.usEast1Bucket)
.then(function(pp) {
projPath = pp;
process.chdir(path.join(projPath));
JAWS = new Jaws();
})
.then(done);
});
describe('Positive tests', function() {
it('Deploy Resources', function(done) {
this.timeout(0);
CmdDeployResources.run(JAWS, config.stage, config.region)
.then(function() {
done();
})
.catch(JawsError, function(e) {
done(e);
})
.error(function(e) {
console.log(e);
done(e);
});
});
});
});

View File

@ -38,6 +38,7 @@ describe('Test new command', function() {
config.newName,
config.stage,
config.usEast1Bucket,
config.domain,
config.region,
config.notifyEmail,
config.profile,

View File

@ -10,6 +10,7 @@ process.env.JAWS_VERBOSE = true;
var config = {
name: 'test-prj',
domain: 'test.jawsapp.com',
notifyEmail: 'tester@jawsstack.com',
stage: 'unittest',
region: process.env.TEST_JAWS_REGION || 'us-east-1',