From 54f4e5a6b605e6c2a66bab3e716c8d35043a5a55 Mon Sep 17 00:00:00 2001 From: Austen Collins Date: Thu, 24 Sep 2015 08:19:03 -0700 Subject: [PATCH 1/4] deploy resources: start command --- bin/jaws | 4 + lib/commands/dash.js | 43 ++++---- lib/commands/deploy_lambda.js | 3 +- lib/commands/deploy_resources.js | 177 +++++++++++++++++++++++++++++++ 4 files changed, 200 insertions(+), 27 deletions(-) create mode 100644 lib/commands/deploy_resources.js diff --git a/bin/jaws b/bin/jaws index dcc67b00b..5d1534320 100755 --- a/bin/jaws +++ b/bin/jaws @@ -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); diff --git a/lib/commands/dash.js b/lib/commands/dash.js index 6c7b23df6..a081f146e 100644 --- a/lib/commands/dash.js +++ b/lib/commands/dash.js @@ -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]; } }); diff --git a/lib/commands/deploy_lambda.js b/lib/commands/deploy_lambda.js index fbab287af..8461eb057 100644 --- a/lib/commands/deploy_lambda.js +++ b/lib/commands/deploy_lambda.js @@ -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) { diff --git a/lib/commands/deploy_resources.js b/lib/commands/deploy_resources.js new file mode 100644 index 000000000..295913f38 --- /dev/null +++ b/lib/commands/deploy_resources.js @@ -0,0 +1,177 @@ +'use strict'; + +/** + * JAWS Command: deploy resources + * - Deploys project's API Gateway REST API to the specified stage and one or all regions + */ + + +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, allTagged) { + var command = new CMD(JAWS, stage, region, allTagged); + return command.run(); +}; + +/** + * CMD Class + * @param JAWS + * @param stage + * @param region + * @param allTagged + * @constructor + */ + +function CMD(JAWS, stage, region, allTagged) { + var _this = this; + _this._stage = stage; + _this._allTagged = allTagged; + _this._JAWS = JAWS; + _this._prjJson = JAWS._meta.projectJson; + _this._prjRootPath = JAWS._meta.projectRootPath; + _this._prjCreds = JAWS._meta.credentials; + + 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(function() { + // If !allTagged, tag current directory + if (!_this._allTagged) { + return CMDtag.tag('endpoint', null, false); + } + }) + .then(_this._promptStage) + .then(_this._promptRegions) + .then(function() { + return _this._regions; + }) + .each(function(regionJson) { + + JawsCli.log('Endpoint Deployer: Deploying endpoint(s) to region "' + regionJson.region + '"...'); + + var deployer = new ApiDeployer( + _this._JAWS, + _this._stage, + regionJson, + _this._prjRootPath, + _this._prjJson, + _this._prjCreds + ); + + return deployer.deploy() + .then(function(url) { + JawsCli.log('Endpoint Deployer: Endpoints for stage "' + + _this._stage + + '" successfully deployed to API Gateway in the region "' + + regionJson.region + + '". Access them @ ' + + url); + }); + }) + .then(function() { + // Untag All tagged endpoints + return _this._allTagged ? CMDtag.tagAll(_this._JAWS, 'endpoint', true) : CMDtag.tag('endpoint', null, true); + }); +}); + +/** + * CMD: Prompt Stage + */ + +CMD.prototype._promptStage = Promise.method(function() { + + var _this = this; + + // If stage, skip + if (_this._stage) return; + + var stages = Object.keys(_this._prjJson.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); +}); + From a2dce9c8be94f39f6f71d2fbaa22d0c512ff056a Mon Sep 17 00:00:00 2001 From: Austen Collins Date: Thu, 24 Sep 2015 12:03:21 -0700 Subject: [PATCH 2/4] deploy resources: complete command --- lib/commands/deploy_lambda.js | 2 +- lib/commands/deploy_resources.js | 130 ++++++++++++++++++++++--------- lib/commands/new_project.js | 13 ++++ lib/commands/new_stage_region.js | 2 +- lib/utils/aws.js | 45 ++++++++++- tests/all.js | 1 + tests/cli/deploy_resources.js | 57 ++++++++++++++ 7 files changed, 209 insertions(+), 41 deletions(-) create mode 100644 tests/cli/deploy_resources.js diff --git a/lib/commands/deploy_lambda.js b/lib/commands/deploy_lambda.js index 8461eb057..73c7f3361 100644 --- a/lib/commands/deploy_lambda.js +++ b/lib/commands/deploy_lambda.js @@ -288,7 +288,7 @@ Deployer.prototype.deploy = Promise.method(function() { }) .then(function() { spinner.stop(true); - }) + }); } }) .then(function() { diff --git a/lib/commands/deploy_resources.js b/lib/commands/deploy_resources.js index 295913f38..49f5f14e5 100644 --- a/lib/commands/deploy_resources.js +++ b/lib/commands/deploy_resources.js @@ -2,10 +2,9 @@ /** * JAWS Command: deploy resources - * - Deploys project's API Gateway REST API to the specified stage and one or all regions + * - Deploys project's resources-cf.json */ - var JawsError = require('../jaws-error'), JawsCli = require('../utils/cli'), Promise = require('bluebird'), @@ -27,8 +26,8 @@ Promise.promisifyAll(fs); * @returns {*} */ -module.exports.run = function(JAWS, stage, region, allTagged) { - var command = new CMD(JAWS, stage, region, allTagged); +module.exports.run = function(JAWS, stage, region) { + var command = new CMD(JAWS, stage, region); return command.run(); }; @@ -37,18 +36,13 @@ module.exports.run = function(JAWS, stage, region, allTagged) { * @param JAWS * @param stage * @param region - * @param allTagged * @constructor */ -function CMD(JAWS, stage, region, allTagged) { +function CMD(JAWS, stage, region) { var _this = this; _this._stage = stage; - _this._allTagged = allTagged; _this._JAWS = JAWS; - _this._prjJson = JAWS._meta.projectJson; - _this._prjRootPath = JAWS._meta.projectRootPath; - _this._prjCreds = JAWS._meta.credentials; if (region && stage) { _this._regions = _this._JAWS._meta.projectJson.stages[_this._stage].filter(function(r) { @@ -70,43 +64,34 @@ CMD.prototype.run = Promise.method(function() { // Flow return _this._JAWS.validateProject() .bind(_this) - .then(function() { - // If !allTagged, tag current directory - if (!_this._allTagged) { - return CMDtag.tag('endpoint', null, false); + .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(_this._promptStage) - .then(_this._promptRegions) .then(function() { return _this._regions; }) .each(function(regionJson) { - JawsCli.log('Endpoint Deployer: Deploying endpoint(s) to region "' + regionJson.region + '"...'); + JawsCli.log('Resources Deployer "' + + _this._stage + + '": Deploying resources to region "' + + regionJson.region + + '"...'); - var deployer = new ApiDeployer( + var deployer = new ResourceDeployer( _this._JAWS, _this._stage, - regionJson, - _this._prjRootPath, - _this._prjJson, - _this._prjCreds + regionJson ); - return deployer.deploy() - .then(function(url) { - JawsCli.log('Endpoint Deployer: Endpoints for stage "' - + _this._stage - + '" successfully deployed to API Gateway in the region "' - + regionJson.region - + '". Access them @ ' - + url); - }); - }) - .then(function() { - // Untag All tagged endpoints - return _this._allTagged ? CMDtag.tagAll(_this._JAWS, 'endpoint', true) : CMDtag.tag('endpoint', null, true); + return deployer.deploy(); }); }); @@ -121,7 +106,7 @@ CMD.prototype._promptStage = Promise.method(function() { // If stage, skip if (_this._stage) return; - var stages = Object.keys(_this._prjJson.stages); + var stages = Object.keys(_this._JAWS._meta.projectJson.stages); if (!stages.length) { throw new JawsError('You have no stages in this project'); } @@ -175,3 +160,76 @@ CMD.prototype._promptRegions = Promise.method(function() { 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); +}); diff --git a/lib/commands/new_project.js b/lib/commands/new_project.js index 3f35600a7..478ab97a4 100644 --- a/lib/commands/new_project.js +++ b/lib/commands/new_project.js @@ -144,6 +144,19 @@ CMD.prototype._prompt = Promise.method(function() { }, }; + // Prompt: notification email - for AWS alerts + _this.Prompter.override.notificationEmail = _this._notificationEmail; + _this._prompts.properties.notificationEmail = { + description: 'Enter an email to use for AWS alarms: '.yellow, + required: true, + message: 'Please enter a valid email', + default: 'you@yourapp.com', + conform: function(email) { + if (!email) return false; + return true; + }, + }; + // Prompt: s3 bucket - holds env vars for this project _this.Prompter.override.s3Bucket = _this._s3Bucket; _this._prompts.properties.s3Bucket = { diff --git a/lib/commands/new_stage_region.js b/lib/commands/new_stage_region.js index 914840c3c..90df023b9 100644 --- a/lib/commands/new_stage_region.js +++ b/lib/commands/new_stage_region.js @@ -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 + '"'); } diff --git a/lib/utils/aws.js b/lib/utils/aws.js index 416d4a137..96cbb10ea 100644 --- a/lib/utils/aws.js +++ b/lib/utils/aws.js @@ -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 @@ -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) { diff --git a/tests/all.js b/tests/all.js index 64cb5e208..fe550eba8 100644 --- a/tests/all.js +++ b/tests/all.js @@ -26,6 +26,7 @@ describe('AllTests', function() { */ //require('./cli/dash'); //require('./cli/deploy_lambda'); + //require('./cli/deploy_resources'); //require('./cli/deploy_endpoint'); //require('./cli/new_stage_region'); //require('./cli/new_project'); diff --git a/tests/cli/deploy_resources.js b/tests/cli/deploy_resources.js new file mode 100644 index 000000000..f44556ae7 --- /dev/null +++ b/tests/cli/deploy_resources.js @@ -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); + }); + }); + }); +}); \ No newline at end of file From 2a3efef9e2fd0f3561a1aa71bf356f1012f1b931 Mon Sep 17 00:00:00 2001 From: Austen Collins Date: Thu, 24 Sep 2015 12:44:05 -0700 Subject: [PATCH 3/4] resources: Change one of the JAWS standard params. aaHostedZone is now aaProjectDomain --- lib/commands/new_project.js | 123 +++++++++++++++++++++++--------- lib/templates/resources-cf.json | 8 +-- lib/utils/aws.js | 4 +- 3 files changed, 95 insertions(+), 40 deletions(-) diff --git a/lib/commands/new_project.js b/lib/commands/new_project.js index 478ab97a4..df9ac26e2 100644 --- a/lib/commands/new_project.js +++ b/lib/commands/new_project.js @@ -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'); @@ -49,6 +50,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 +58,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 +123,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,37 +137,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: notification email - for AWS alerts - _this.Prompter.override.notificationEmail = _this._notificationEmail; - _this._prompts.properties.notificationEmail = { - description: 'Enter an email to use for AWS alarms: '.yellow, - required: true, - message: 'Please enter a valid email', - default: 'you@yourapp.com', - conform: function(email) { - if (!email) return false; - return true; - }, - }; + var domainDescription = 'Enter a project domain: ' + + os.EOL + + ' - You don’t 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; - // 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', + _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); @@ -182,12 +174,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) { @@ -196,8 +246,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) { @@ -211,6 +264,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; @@ -253,7 +307,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; }); @@ -325,6 +379,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 diff --git a/lib/templates/resources-cf.json b/lib/templates/resources-cf.json index 8f3c2d134..60cc01a95 100644 --- a/lib/templates/resources-cf.json +++ b/lib/templates/resources-cf.json @@ -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" diff --git a/lib/utils/aws.js b/lib/utils/aws.js index 96cbb10ea..a7f638ed2 100644 --- a/lib/utils/aws.js +++ b/lib/utils/aws.js @@ -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', From 5b6c42641162582ab27c484e8b5f8a18cd94604c Mon Sep 17 00:00:00 2001 From: Austen Collins Date: Thu, 24 Sep 2015 12:54:41 -0700 Subject: [PATCH 4/4] tests: fix new project test to use 'DomainName' resources parameter --- lib/commands/new_project.js | 3 ++- tests/all.js | 12 ++++++------ tests/cli/new_project.js | 1 + tests/config.js | 1 + 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/lib/commands/new_project.js b/lib/commands/new_project.js index df9ac26e2..bda66c4c5 100644 --- a/lib/commands/new_project.js +++ b/lib/commands/new_project.js @@ -33,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, diff --git a/tests/all.js b/tests/all.js index fe550eba8..6d423adf0 100644 --- a/tests/all.js +++ b/tests/all.js @@ -15,11 +15,11 @@ 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 @@ -29,5 +29,5 @@ describe('AllTests', function() { //require('./cli/deploy_resources'); //require('./cli/deploy_endpoint'); //require('./cli/new_stage_region'); - //require('./cli/new_project'); + require('./cli/new_project'); }); \ No newline at end of file diff --git a/tests/cli/new_project.js b/tests/cli/new_project.js index b0fcbf720..01e4240f4 100644 --- a/tests/cli/new_project.js +++ b/tests/cli/new_project.js @@ -38,6 +38,7 @@ describe('Test new command', function() { config.newName, config.stage, config.usEast1Bucket, + config.domain, config.region, config.notifyEmail, config.profile, diff --git a/tests/config.js b/tests/config.js index fa6ca8815..d2e74f526 100644 --- a/tests/config.js +++ b/tests/config.js @@ -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',