Merge pull request #102 from jaws-framework/v1.0-ux

V1.0 ux
This commit is contained in:
Austen 2015-09-12 15:31:25 -07:00
commit f2ce300eef
33 changed files with 1849 additions and 1030 deletions

View File

@ -7,6 +7,7 @@ var JawsError = require('../lib/jaws-error'),
program = require('commander'),
utils = require('../lib/utils'),
Promise = require('bluebird'),
minimist = require('minimist'),
handleExit = utils.handleExit;
var JAWS = new Jaws();
@ -20,31 +21,26 @@ program
.version(JAWS._meta.version);
program
.command('new <type> [regionName] [stageName]')
.description('Creates JAWS "project" or "region"/"stage" in existing project. Does so in cwd. ' +
'If type is region|stage, regionName and stageName are required. If region, the stageName is the stage that will' +
'be primed in the new region.')
.option('-d, --dont-exe-cf', 'Dont execute CloudFormation file')
//Things only valid for new project below:
.option('-p, --profile <profile name>', 'AWS profile to use (as defined in ~/.aws/credentials). Only valid for new project.')
.option('-n, --proj-name <name>', 'Project name. Only valid for new project.')
.option('-s, --stage <stage>', 'Stage to create. all lowercase Only valid for new project.')
.option('-r, --region <region>', 'Region lambda(s)&API(s) will be created in (Can add more later). Only valid for new project.')
.option('-3, --s3-bucket <bucketName>', 'S3 bucket to store stage env files. Key is JAWS/envVars/<projName>/<stage>. Bucket will be created if it DNE. Only valid for new project.')
.option('-e, --notification-email <email>', 'Email to be notified with stack alerts. Only valid for new project.')
.action(function(type, regionName, stageName, options) {
type = type.toLowerCase();
.command('new <type>')
.allowUnknownOption()
.description('Make a new "project", "stage", "region", "lambda" or "endpoint"')
.action(function() {
// Parse Args
var args = minimist(process.argv.slice(3));
var type = args._[0] ? args._[0].toLowerCase() : null;
if (type == 'project') {
var theCmd = require('../lib/commands/new_project');
handleExit(theCmd.create(
options.projName, // name is reserved in commander...
options.stage ? options.stage.toLowerCase() : null,
options.s3Bucket,
options.region,
options.notificationEmail,
options.profile,
options.dontExeCf
var CmdNewProject = require('../lib/commands/new_project');
handleExit(CmdNewProject.run(
args.name,
args.stage ? args.stage.toLowerCase() : null,
args.s3Bucket,
args.region,
args.email,
args.profile,
args.noCf
));
} else if (type == 'region' || type == 'stage') {
var theCmd = require('../lib/commands/new_region_stage'),
@ -66,7 +62,7 @@ program
.description('Create boilerplate structure for a new lambda or api gateway (or both)')
.option('-l, --lambda', 'will create the files needed for a lambda')
.option('-r, --lambda-runtime', 'only nodejs supported')
.option('-a, --api', 'will create the files needed for an api gateway configuration')
.option('-a, --endpoint', 'will create the files needed for an api gateway configuration')
.option('-b, --both', 'shorthand for -l -a')
.option('-f, --function-name', 'lambda functionName. Will ensure this is unique across your project.')
.option('-m, --resource-name', 'parent directory the functionName dir will be created in')
@ -76,13 +72,13 @@ program
options.lambda = true;
}
if (options.api || options.both) {
options.api = true;
if (options.endpoint || options.both) {
options.endpoint = true;
}
var theCmd = require('../lib/commands/generate');
handleExit(theCmd.run(
JAWS, options.lambda, options.api, options.functionName, options.resourceName, options.lambdaRuntime
JAWS, options.lambda, options.endpoint, options.functionName, options.resourceName, options.lambdaRuntime
));
});
@ -97,19 +93,19 @@ program
program
.command('tag [type]')
.description('Tag lambda function or api gateway resource (api) for deployment ' +
.description('Tag lambda function or api gateway resource (endpoint) for deployment ' +
'the next time deploy command is run. Type "lambda" is the default.')
.option('-u, --untag', 'un-tag lambda|api')
.option('-u, --untag', 'un-tag lambda|endpoint')
.option('-m, --multi', 'interactively select multiple')
.option('-a, --tag-all', 'tag all lambda|api functions in project')
.option('-l, --list-all', 'list all tagged lambda|api functions in project')
.option('-n, --untag-all', 'un-tag all lambda|api functions in project')
.option('-a, --tag-all', 'tag all lambda|endpoint functions in project')
.option('-l, --list-all', 'list all tagged lambda|endpoint functions in project')
.option('-n, --untag-all', 'un-tag all lambda|endpoint functions in project')
.action(function(type, options) {
type = type || 'lambda';
type = type.toLowerCase();
if (-1 == ['api', 'lambda'].indexOf(type)) {
console.error('Unsupported type ' + type + '. Must be api|lambda');
if (-1 == ['endpoint', 'lambda'].indexOf(type)) {
console.error('Unsupported type ' + type + '. Must be endpoint|lambda');
process.exit(1);
}
@ -129,8 +125,8 @@ program
});
program
.command('deploy <type> <stage> [region]')
.description('Deploy a lambda function (type lambda), a REST API (api), or provision AWS resources (resources) for the specified stage.' +
.command('deploy <type> [stage] [region]')
.description('Deploy a lambda function (type lambda), a REST API (endpoint), or provision AWS resources (resources) for the specified stage.' +
' By default will tag and deploy type at cwd')
.option('-t, --tags', 'Deploy all lambdas tagged as deployable in their jaws.json. Default is to just deploy cwd')
.option('-e, --all-at-once', 'By default, lambdas are deployed once at a time. This deploys all concurrently')
@ -138,19 +134,19 @@ program
type = type.toLowerCase();
switch (type) {
case 'api':
case 'endpoint':
var allTagged = (options.tags) ? true : false;
var theCmd = require('../lib/commands/deploy_api');
handleExit(theCmd.deployApi(JAWS, stage, region, allTagged));
var theCmd = require('../lib/commands/deploy_endpoint');
handleExit(theCmd.run(JAWS, stage, region, allTagged));
break;
case 'lambda':
var allTagged = (options.tags) ? true : false,
allAtOnce = (options.allAtOnce) ? true : false;
var theCmd = require('../lib/commands/deploy_lambda');
handleExit(theCmd.deployLambdas(JAWS, stage, allTagged, allAtOnce, region));
handleExit(theCmd.run(JAWS, stage, region, allTagged, allAtOnce));
break;
default:
console.error('Unsupported type ' + type + '. Must be api|lambda|resources');
console.error('Unsupported type ' + type + '. Must be endpoint|lambda|resources');
process.exit(1);
break;
}
@ -204,6 +200,14 @@ program
}
});
program
.command('dash')
.description('Check deployment status and deploy resources for a stage and region')
.action(function() {
var theCmd = require('../lib/commands/dash');
handleExit(theCmd.run(JAWS));
});
program
.command('logs <stage>')
.description('Get logs for the lambda function in the specified stage in your current working directory.')

368
lib/commands/dash.js Normal file
View File

@ -0,0 +1,368 @@
/**
* JAWS Command: dash
*/
var JawsError = require('../jaws-error'),
JawsCli = require('../utils/cli'),
Promise = require('bluebird'),
fs = require('fs'),
os = require('os'),
path = require('path'),
chalk = require('chalk'),
utils = require('../utils/index'),
CMDtag = require('./tag'),
CMDdeployLambda = require('./deploy_lambda'),
CMDdeployEndpoint = require('./deploy_endpoint');
/**
* Run
* @param JAWS
* @param stage
* @param regions
* @param allTagged
* @returns {Promise}
*/
module.exports.run = function(JAWS, stage, regions, allTagged) {
var command = new CMD(JAWS, stage, regions, allTagged);
return command.run();
};
/**
* Command Class
* @param JAWS
* @constructor
*/
function CMD(JAWS, stage, regions, allTagged) {
this._JAWS = JAWS;
this._allTagged = allTagged || false;
this._stage = stage || null;
this._regions = regions || [];
this._choices = [];
}
/**
* CMD: Run
*/
CMD.prototype.run = Promise.method(function() {
var _this = this;
_this._report = {
targetLambdas: 0,
targetEndpoints: 0
};
return Promise.try(function() {})
.bind(_this)
.then(function() {
// If !allTagged, Show Dashboard
if (!_this._allTagged) {
return Promise.try(function() {})
.bind(_this)
.then(_this._prepareResources)
.then(_this._prepareSummary)
.then(_this._renderDash)
.then(function(selectedResources) {
_this._resources = selectedResources;
if (!_this._resources.length) {
return false;
} else {
// TODO: Untag all other resources
return _this._resources;
}
})
.each(function(resource) {
if (resource.type === 'lambda') {
_this._report.targetLambdas++;
return CMDtag.tag('lambda', resource.value, false);
} else if (resource.type === 'endpoint') {
_this._report.targetEndpoints++;
return CMDtag.tag('endpoint', resource.value, false);
}
});
}
})
.then(_this._promptStage)
.then(_this._promptRegion)
.then(function() {
// Status
JawsCli.log(chalk.white('-------------------------------------------'));
JawsCli.log(chalk.white(' Dashboard: Deploying Lambdas...'));
JawsCli.log(chalk.white('-------------------------------------------'));
return CMDdeployLambda.run(
_this._JAWS,
_this._stage,
_this._regions,
true);
})
.then(function() {
// Status
JawsCli.log(chalk.white('-------------------------------------------'));
JawsCli.log(chalk.white(' Dashboard: Deploying Endpoints...'));
JawsCli.log(chalk.white('-------------------------------------------'));
return CMDdeployEndpoint.run(
_this._JAWS,
_this._stage,
_this._regions,
true);
})
.then(function() {
// Status
JawsCli.log(chalk.white('-------------------------------------------'));
JawsCli.log(chalk.white(' Dashboard: Deployments Completed'));
JawsCli.log(chalk.white('-------------------------------------------'));
});
});
/**
* CMD: Prepare Resources
*/
CMD.prototype._prepareResources = Promise.method(function() {
var _this = this;
return utils.findAllJawsJsons(_this._JAWS._meta.projectRootPath)
.then(function(jsonPaths) {
var hybrids = [];
var lambdas = [];
// Fetch and prepare json modules
for (var i = 0; i < jsonPaths.length; i++) {
// Add modules
var json = require(jsonPaths[i]);
var module = {};
// Add Lambda
if (json.lambda) {
module.lambda = {
key: ' L) ',
value: jsonPaths[i],
type: 'lambda',
label: json.lambda.functionName,
};
// Create path
var paths = jsonPaths[i].split('/');
paths = paths[paths.length - 3] + '/' + paths[paths.length - 2];
module.lambda.path = chalk.grey(paths);
}
// Add Endpoint
if (json.endpoint) {
module.endpoint = {
key: ' E) ',
value: jsonPaths[i],
type: 'endpoint',
label: '/' + json.endpoint.path + ' - ' + json.endpoint.method,
};
// Create path
var paths = jsonPaths[i].split('/');
paths = paths[paths.length - 3] + '/' + paths[paths.length - 2];
module.endpoint.path = chalk.grey(paths);
}
if (module.lambda && module.endpoint) hybrids.push(module);
if (module.lambda && !module.endpoint) lambdas.push(module);
}
// Sort hybrids by label/paths
hybrids.sort(function(a, b) {
return (a.label < b.label) ? -1 : (a.label > b.label) ? 1 : 0;
});
// Sort lambdas by label
lambdas.sort(function(a, b) {
return (a.label < b.label) ? -1 : (a.label > b.label) ? 1 : 0;
});
// Add Lambdas back in
var modules = lambdas.concat(hybrids);
// Prepare Choices
for (var i = 0; i < modules.length; i++) {
if (modules[i].lambda || modules[i].endpoint) {
_this._choices.push({
spacer: modules[i].lambda.path ? modules[i].lambda.path : modules[i].endpoint.path,
});
}
if (modules[i].lambda) {
_this._choices.push(modules[i].lambda);
}
if (modules[i].endpoint) {
_this._choices.push(modules[i].endpoint);
}
}
});
});
/**
* CMD: Prepare Summary
*/
CMD.prototype._prepareSummary = Promise.method(function() {
var _this = this;
var lambdaCount = 0;
var endpointCount = 0;
// Count Lambdas and Endpoints
for (var i = 0; i < _this._choices.length; i++) {
if (_this._choices[i].type === 'lambda') lambdaCount++;
if (_this._choices[i].type === 'endpoint') endpointCount++;
}
_this._summary = 'Dashboard for project "' + _this._JAWS._meta.projectJson.name + '"' + os.EOL
+ chalk.white.bold(' -------------------------------------------') + os.EOL
+ chalk.white(' Project Summary') + os.EOL
+ chalk.white.bold(' -------------------------------------------') + os.EOL
+ chalk.white(' Stages: ' + os.EOL);
// Add Stage Data
for (var stage in _this._JAWS._meta.projectJson.project.stages) {
_this._summary = _this._summary
+ chalk.white(' ' + stage + ' ');
for (var i = 0; i < _this._JAWS._meta.projectJson.project.stages[stage].length; i++) {
_this._summary = _this._summary
+ chalk.grey(_this._JAWS._meta.projectJson.project.stages[stage][i].region + ' ')
}
_this._summary = _this._summary + os.EOL;
}
_this._summary = _this._summary
+ chalk.white(' Lambdas: ' + lambdaCount) + os.EOL
+ chalk.white(' Endpoints: ' + endpointCount) + os.EOL
+ chalk.white.bold(' -------------------------------------------') + os.EOL
+ chalk.white(' Select Resources To Deploy') + os.EOL
+ chalk.white.bold(' -------------------------------------------');
});
/**
* CMD: Render Dash
*/
CMD.prototype._renderDash = Promise.method(function() {
var _this = this;
JawsCli.log(_this._summary);
return JawsCli.select(
null,
_this._choices,
true,
' Deploy Selected -->');
});
/**
* CMD: Prompt: Stage
*/
CMD.prototype._promptStage = Promise.method(function() {
var _this = this;
// If stage exists, skip
if (_this._stage) return;
var stages = Object.keys(_this._JAWS._meta.projectJson.project.stages);
// Check if project has stages
if (!stages.length) {
throw new JawsError('This project has no stages');
}
// If project only has 1 stage, skip prompt
if (stages.length === 1) {
_this._stage = stages[0];
return;
}
// Create Choices
var choices = [];
for (var i = 0; i < stages.length; i++) {
choices.push({
key: (i + 1) + ') ',
value: stages[i],
});
}
return JawsCli.select('Choose a stage: ', choices, false)
.then(function(results) {
_this._stage = results[0].value;
});
});
/**
* CMD: Prompt: Region
*/
CMD.prototype._promptRegion = Promise.method(function() {
var _this = this;
// If region exists, skip
if (_this._regions.length) return;
var regions = _this._JAWS._meta.projectJson.project.stages[_this._stage].map(function(s) {
return s.region;
});
// Check if stage has regions
if (!regions.length) {
throw new JawsError('This stage has no regions');
}
// If stage only has 1 region, use it and skip prompt
if (regions.length === 1) {
_this._regions = regions;
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',
});
}
}
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.project.stages[_this._stage]);
} else {
_this._regions = [results[0].value];
}
});
});

View File

@ -1,24 +1,129 @@
'use strict';
/**
* JAWS Command: deploy api <stage> <region>
* - Deploys project's API Gateway REST API to the specified stage and region(s)
* JAWS Command: deploy endpoint <stage> <region>
* - Deploys project's API Gateway REST API to the specified stage and one or all regions
*/
// TODO: figure out what specific permissions are needed
// TODO: Add Concurrent API creation across multiple regions, currently consecutive
// TODO: On completion, list API G routes not used within the project (all regions). Offer option to delete them.
var JawsError = require('../jaws-error'),
JawsCli = require('../utils/cli'),
Promise = require('bluebird'),
fs = require('fs'),
os = require('os'),
path = require('path'),
utils = require('../utils/index'),
CMDtag = require('./tag'),
JawsAPIClient = require('jaws-api-gateway-client');
Promise.promisifyAll(fs);
/**
* Api Deployer Class
* Run
* @param {Jaws} JAWS
* @param stage
* @returns Promise
*/
module.exports.run = function(JAWS, stage, regions, allTagged) {
var command = new CMD(JAWS, stage, regions, allTagged);
return command.run();
};
/**
* CMD Class
* @param JAWS
* @param stage
* @param regions
* @param allTagged
* @constructor
*/
function CMD(JAWS, stage, regions, allTagged) {
var _this = this;
_this._stage = stage;
_this._regions = regions.length ? regions : Object.keys(this._JAWS._meta.projectJson.project.stages[this._stage]);
_this._allTagged = allTagged;
_this._JAWS = JAWS;
_this._prjJson = JAWS._meta.projectJson;
_this._prjRootPath = JAWS._meta.projectRootPath;
_this._prjCreds = JAWS._meta.credentials;
}
/**
* CMD: Run
*/
CMD.prototype.run = Promise.method(function() {
var _this = this;
// Flow
return Promise.try(function() {
// If !allTagged, tag current directory
if (!_this._allTagged) {
return CMDtag.tag('api', null, false);
}
})
.bind(_this)
.then(_this._promptStage)
.then(_this._promptRegion)
.then(function() {
return _this._regions;
})
.each(function(region) {
JawsCli.log('Endpoint Deployer: Deploying endpoint(s) to region "' + region + '"...');
var deployer = new ApiDeployer(
_this._stage,
region,
_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 "'
+ 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.resolve(function() {
// If stage, skip
if (_this._stage) return;
var stages = Object.keys(_this._prjJson.project.stages);
if (!stages.length) {
throw new JawsError('You have no stages in this project');
}
var choices = [];
for (var i = 0; i < stages.length; i++) {
choices.push({
key: (i + 1) + ': ',
value: stages[i]
});
}
return JawsCLI.checklist('Select a stage to deploy to: ', choices);
});
/**
* Api Deployer
* @param stage
* @param regions
* @param prjJson
@ -37,8 +142,16 @@ function ApiDeployer(stage, region, prjRootPath, prjJson, prjCreds) {
_this._prjCreds = prjCreds;
_this._endpoints = [];
_this._resources = [];
_this._awsAccountNumber = _this._region.iamRoleArnApiGateway.replace('arn:aws:iam::', '').split(':')[0];
_this._restApiId = _this._region.restApiId ? _this._region.restApiId : null;
// Get Region JSON
for (var i = 0; i < _this._prjJson.project.stages[_this._stage].length; i++) {
if (_this._region === _this._prjJson.project.stages[_this._stage][i].region) {
_this._regionJson = _this._prjJson.project.stages[_this._stage][i];
}
}
_this._awsAccountNumber = _this._regionJson.iamRoleArnApiGateway.replace('arn:aws:iam::', '').split(':')[0];
_this._restApiId = _this._regionJson.restApiId ? _this._regionJson.restApiId : null;
// Instantiate API Gateway Client
this.ApiClient = new JawsAPIClient({
@ -67,7 +180,7 @@ ApiDeployer.prototype.deploy = Promise.method(function() {
return 'https://'
+ _this._restApiId
+ '.execute-api.'
+ _this._region.region
+ _this._region
+ '.amazonaws.com/'
+ _this._stage
+ '/';
@ -95,10 +208,10 @@ ApiDeployer.prototype._findTaggedEndpoints = Promise.method(function() {
JawsError.errorCodes.UNKNOWN);
}
utils.logIfVerbose(
'API Gateway: "'
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage + ' - '
+ _this._region.region
+ _this._region
+ '": found '
+ _this._endpoints.length + ' endpoints to deploy');
});
@ -144,7 +257,7 @@ ApiDeployer.prototype._saveApiId = Promise.method(function() {
// Attach API Gateway REST API ID
for (var i = 0; i < _this._prjJson.project.stages[_this._stage].length; i++) {
if (_this._prjJson.project.stages[_this._stage][i].region === _this._region.region) {
if (_this._prjJson.project.stages[_this._stage][i].region === _this._region) {
_this._prjJson.project.stages[_this._stage][i].restApiId = _this._restApiId;
}
}
@ -167,10 +280,10 @@ ApiDeployer.prototype._findOrCreateApi = Promise.method(function() {
.then(function(response) {
_this._restApiId = response.id;
utils.logIfVerbose(
'API Gateway: "'
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage + ' - '
+ _this._region.region
+ _this._region
+ '": found existing REST API on AWS API Gateway with ID: '
+ response.id);
});
@ -183,10 +296,10 @@ ApiDeployer.prototype._findOrCreateApi = Promise.method(function() {
}).then(function(response) {
_this._restApiId = response.id;
utils.logIfVerbose(
'API Gateway: "'
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage + ' - '
+ _this._region.region
+ _this._region
+ '": created a new REST API on AWS API Gateway with ID: '
+ response.id);
});
@ -215,10 +328,10 @@ ApiDeployer.prototype._listApiResources = Promise.method(function() {
}
}
utils.logIfVerbose(
'API Gateway: "'
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage + ' - '
+ _this._region.region
+ _this._region
+ '": found '
+ _this._resources.length
+ ' existing resources on API Gateway');
@ -293,10 +406,10 @@ ApiDeployer.prototype._createEndpointResources = Promise.method(function(endpoin
// Add resource to _this.resources and callback
_this._resources.push(response);
utils.logIfVerbose(
'API Gateway: "' +
JawsCli.log(
'Endpoint Deployer: "' +
_this._stage + ' - '
+ _this._region.region
+ _this._region
+ ' - ' + endpoint.endpoint.path + '": '
+ 'created resource: '
+ response.pathPart);
@ -368,10 +481,10 @@ ApiDeployer.prototype._createEndpointMethod = Promise.method(function(endpoint)
.delay(250) // API Gateway takes time to delete Methods. Might have to increase this.
.then(function(response) {
utils.logIfVerbose(
'API Gateway: "'
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage + ' - '
+ _this._region.region
+ _this._region
+ ' - ' + endpoint.endpoint.path + '": '
+ 'created method: '
+ endpoint.endpoint.method);
@ -394,18 +507,18 @@ ApiDeployer.prototype._createEndpointIntegration = Promise.method(function(endpo
httpMethod: 'POST', // Must be post for lambda
authorizationType: 'none',
uri: 'arn:aws:apigateway:'
+ _this._region.region
+ ':lambda:path/2015-03-31/functions/arn:aws:lambda:'
+ _this._region.region
+ ':'
+ _this._awsAccountNumber
+ ':function:'
+ [_this._stage,
+ _this._region
+ ':lambda:path/2015-03-31/functions/arn:aws:lambda:'
+ _this._region
+ ':'
+ _this._awsAccountNumber
+ ':function:'
+ [_this._stage,
_this._prjJson.name,
endpoint.lambda.functionName,
].join('_-_').replace(/ /g, '')
+ '/invocations',
credentials: _this._region.iamRoleArnApiGateway,
+ '/invocations',
credentials: _this._regionJson.iamRoleArnApiGateway,
requestParameters: endpoint.endpoint.requestParameters || {},
requestTemplates: endpoint.endpoint.requestTemplates || {},
cacheNamespace: endpoint.endpoint.cacheNamespace || null,
@ -428,10 +541,10 @@ ApiDeployer.prototype._createEndpointIntegration = Promise.method(function(endpo
// Save integration to apig property
endpoint.endpoint.apig.integration = response;
utils.logIfVerbose(
'API Gateway: "'
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage + ' - '
+ _this._region.region
+ _this._region
+ ' - ' + endpoint.endpoint.path + '": '
+ 'created integration with the type: '
+ endpoint.endpoint.type);
@ -476,11 +589,14 @@ ApiDeployer.prototype._createEndpointMethodResponses = Promise.method(function(e
thisResponse.statusCode,
methodResponseBody)
.then(function() {
utils.logIfVerbose(
'API Gateway: "' +
_this._stage + ' - ' +
_this._region.region
+ ' - ' + endpoint.endpoint.path + '": '
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage
+ ' - '
+ _this._region
+ ' - '
+ endpoint.endpoint.path
+ '": '
+ 'created method response');
})
.catch(function(error) {
@ -529,11 +645,14 @@ ApiDeployer.prototype._createEndpointMethodIntegResponses = Promise.method(funct
thisResponse.statusCode,
integrationResponseBody)
.then(function() {
utils.logIfVerbose(
'API Gateway: "'
+ _this._stage + ' - '
+ _this._region.region
+ ' - ' + endpoint.endpoint.path + '": '
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage
+ ' - '
+ _this._region
+ ' - '
+ endpoint.endpoint.path
+ '": '
+ 'created method integration response');
}).catch(function(error) {
throw new JawsError(
@ -575,11 +694,14 @@ ApiDeployer.prototype._createEndpointMethodResponses = Promise.method(function(e
thisResponse.statusCode,
methodResponseBody)
.then(function() {
utils.logIfVerbose(
'API Gateway: "'
+ _this._stage + ' - '
+ _this._region.region
+ ' - ' + endpoint.endpoint.path + '": '
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage
+ ' - '
+ _this._region
+ ' - '
+ endpoint.endpoint.path
+ '": '
+ 'created method response');
})
.catch(function(error) {
@ -628,11 +750,14 @@ ApiDeployer.prototype._createEndpointMethodIntegResponses = Promise.method(funct
thisResponse.statusCode,
integrationResponseBody)
.then(function() {
utils.logIfVerbose(
'API Gateway: "'
+ _this._stage + ' - '
+ _this._region.region
+ ' - ' + endpoint.endpoint.path + '": '
JawsCli.log(
'Endpoint Deployer: "'
+ _this._stage
+ ' - '
+ _this._region
+ ' - '
+ endpoint.endpoint.path
+ '": '
+ 'created method integration response');
}).catch(function(error) {
throw new JawsError(
@ -664,84 +789,4 @@ ApiDeployer.prototype._createDeployment = Promise.method(function() {
error.message,
JawsError.errorCodes.UNKNOWN);
});
});
/**
* Deploy API
*
* @param {Jaws} JAWS
* @param stage
* @returns {bluebird|exports|module.exports}
*/
module.exports.deployApi = function(JAWS, stage, region, allTagged) {
// Check region (required)
if (!region) {
Promise.reject(new JawsError(
'Must specify a region',
JawsError.errorCodes.UNKNOWN));
}
// Check stage exists
stage = stage.toLowerCase().trim();
if (!JAWS._meta.projectJson.project.stages[stage]) {
Promise.reject(new JawsError(
'The stage "' + stage
+ '" does not exist. Please generate this stage if you would like to deploy to it.',
JawsError.errorCodes.UNKNOWN));
}
// Check if stage has regions
if (!JAWS._meta.projectJson.project.stages[stage].length) {
Promise.reject(new JawsError(
'You do not have any regions set for this stage. Add one before deploying.',
JawsError.errorCodes.UNKNOWN));
}
var tagCmd = require('./tag');
// Tag CWD if necessary
return (allTagged ? Promise.resolve() : tagCmd.tag('api', null, false))
.then(function() {
// Validate region. If no region specified, deploy to all regions
if (!region) {
var regions = JAWS._meta.projectJson.project.stages[stage];
} else {
region = region.toLowerCase().trim();
for (var i = 0; i < JAWS._meta.projectJson.project.stages[stage].length; i++) {
var tempRegion = JAWS._meta.projectJson.project.stages[stage][i];
if (region === tempRegion.region) var regions = [tempRegion];
}
// If missing region, throw error
if (!regions) {
throw new JawsError(
'The region "' + region + '" does not exist in this stage.',
JawsError.errorCodes.UNKNOWN);
}
}
return regions;
})
.each(function(region) {
var deployer = new ApiDeployer(
stage,
region,
JAWS._meta.projectRootPath,
JAWS._meta.projectJson,
JAWS._meta.credentials
);
return deployer.deploy()
.then(function(url) {
console.log('API Gateway successfully deployed: ' + url);
// Untag
return allTagged ? tagCmd.tagAll(JAWS, 'api', true) : tagCmd.tag('api', null, true);
});
});
};
});

File diff suppressed because it is too large Load Diff

View File

@ -8,6 +8,7 @@
// Defaults
var JawsError = require('../jaws-error'),
JawsCLI = require('../utils/jaws-cli')
Promise = require('bluebird'),
fs = require('fs'),
path = require('path'),

View File

@ -1,329 +1,409 @@
'use strict';
/**
* JAWS Command: new
* JAWS Command: new project
* - Asks the user for information about their new JAWS project
* - Creates a new project in the current working directory
* - Creates IAM resources via CloudFormation
*/
// TODO: Add region into jaws-cf template using pseudo params via CF
// Defaults
var JawsError = require('../jaws-error'),
JawsCLI = require('../utils/cli'),
Promise = require('bluebird'),
fs = require('fs'),
path = require('path'),
os = require('os'),
AWSUtils = require('../utils/aws'),
utils = require('../utils'),
inquirer = require('bluebird-inquirer'),
chalk = require('chalk'),
shortid = require('shortid'),
extend = require('util')._extend, //OK per Isaacs and http://stackoverflow.com/a/22286375/563420
Spinner = require('cli-spinner').Spinner;
shortid = require('shortid');
Promise.promisifyAll(fs);
// Define Project
var project = {};
/**
* Generate ASCII
* @return string
* Run
* @param name
* @param stage
* @param s3Bucket
* @param notificationEmail
* @param region
* @param profile
* @param noCf
* @returns {*}
*/
function _generateAscii() {
module.exports.run = function(name, stage, s3Bucket, notificationEmail, region, profile, noCf) {
var command = new CMD(
name,
stage,
s3Bucket,
notificationEmail,
region,
profile,
noCf);
return command.run();
};
var art = '';
art = art + ' ____ _____ __ __ _________ ' + os.EOL;
art = art + ' | | / _ \\/ \\ / \\/ _____/ ' + os.EOL;
art = art + ' | |/ /_\\ \\ \\/\\/ /\\_____ \\ ' + os.EOL;
art = art + ' /\\__| / | \\ / / \\ ' + os.EOL;
art = art + ' \\________\\____|__ /\\__/\\__/ /_________/ ' + os.EOL;
art = art + '' + os.EOL;
art = art + ' *** The Server-less Framework *** ' + os.EOL;
/**
* CMD Class
* @param name
* @param stage
* @param s3Bucket
* @param notificationEmail
* @param region
* @param profile
* @param noExeCf
* @constructor
*/
return art;
function CMD(name, stage, s3Bucket, notificationEmail, region, profile, noCf) {
// Defaults
this._name = name ? name : null;
this._stage = stage ? stage.toLowerCase().replace(/\W+/g, '').substring(0, 15) : null;
this._s3Bucket = s3Bucket;
this._notificationEmail = notificationEmail;
this._region = region;
this._profile = profile;
this._noCf = noCf;
this._prompts = {
properties: {},
};
this.Prompter = JawsCLI.prompt();
this.Prompter.override = {};
}
/**
* Get Answers
*
* @returns {Promise}
* @private
* CMD: Run
*/
function _getAnswers(projName, stage, s3Bucket, lambdaRegion, notificationEmail, awsProfile) {
// Greet
console.log(chalk.yellow(_generateAscii()));
// Define CLI prompts
var prompts = [],
overrideAnswers = {};
CMD.prototype.run = Promise.method(function() {
if (!projName) {
prompts.push({
type: 'input',
name: 'name',
message: 'Type a name for your new project (max 20 chars. Aphanumeric and - only):',
default: 'jaws-new',
});
} else {
overrideAnswers.name = projName;
var _this = this;
return Promise.try(function() {
// ASCII Greeting
JawsCLI.ascii();
})
.bind(_this)
.then(_this._prompt)
.then(_this._prepareProjectData)
.then(_this._createS3JawsStructure) //see if bucket is avail first before doing work
.then(_this._createProjectDirectory)
.then(function() {
if (!_this._noCf) {
return _this._createCfStack();
} else {
utils.logIfVerbose('No exec cf specified, updating proj jaws.json only');
JawsCLI.log('Project and env var file in s3 successfully created. CloudFormation file can be run manually');
JawsCLI.log('After creating CF stack, remember to put the IAM role outputs in your project jaws.json');
}
})
.then(_this._createProjectJson);
});
/**
* CMD: Prompt
*/
CMD.prototype._prompt = Promise.method(function() {
var _this = this;
// Prompt: name (project name)
_this.Prompter.override.name = _this._name;
_this._prompts.properties.name = {
description: 'Enter a project name: '.yellow,
default: 'jaws-' + shortid.generate().replace(/\W+/g, '').substring(0, 19),
message: 'Name must be only letters, numbers, underscores or dashes',
conform: function(name) {
var re = /^[a-zA-Z0-9-_]+$/;
return re.test(name);
},
};
// 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: s3 bucket - holds env vars for this project
_this.Prompter.override.s3Bucket = _this._s3Bucket;
_this._prompts.properties.s3Bucket = {
description: 'Enter an AWS S3 Bucket name to store this project\'s env vars: '.yellow,
default: 'jaws.yourapp.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: 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: API Keys - Create an AWS profile by entering API keys
if (!fs.existsSync(path.join(AWSUtils.getConfigDir(), 'credentials'))) {
_this.Prompter.override.awsAdminKeyId = _this._awsAdminKeyId;
_this._prompts.properties.awsAdminKeyId = {
description: 'Enter the ACCESS KEY ID for your Admin AWS IAM User: '.yellow,
required: true,
message: 'Please enter a valid access key ID',
conform: function(key) {
if (!key) return false;
return true;
},
};
_this.Prompter.override.awsAdminSecretKey = _this._awsAdminSecretKey;
_this._prompts.properties.awsAdminSecretKey = {
description: 'Enter the SECRET ACCESS KEY for your Admin AWS IAM User: '.yellow,
required: true,
message: 'Please enter a valid secret access key',
conform: function(key) {
if (!key) return false;
return true;
},
};
}
if (!stage) {
prompts.push({
type: 'input',
name: 'stage',
message: 'Which stage would you like to create? (you can import more later)',
default: 'dev',
// Show Prompts
return new Promise(function(resolve, reject) {
_this.Prompter.get(_this._prompts, function(err, answers) {
if (err) {
reject(new JawsError(err));
}
resolve(answers);
});
} else {
overrideAnswers.stage = stage;
}
})
.then(function(answers) {
if (!s3Bucket) {
prompts.push({
type: 'input',
name: 's3Bucket',
message: 'What bucket should be used to store JAWS env var files for this project? (/JAWS/envVars/<stage> file ' +
'will be created. This bucket should be specific to this project.)',
default: 'jawsproject.yourdomain.com',
});
} else {
overrideAnswers.s3Bucket = s3Bucket;
}
// Set Answers
_this._name = answers.name;
_this._stage = answers.stage.toLowerCase();
_this._s3Bucket = answers.s3Bucket;
_this._notificationEmail = answers.notificationEmail;
_this._awsAdminKeyId = answers.awsAdminKeyId;
_this._awsAdminSecretKey = answers.awsAdminSecretKey;
// Request Region - Only available AWS Lambda regions allowed
if (!lambdaRegion) {
prompts.push({
type: 'rawlist',
name: 'region',
message: 'Which AWS Region would you like to use (can add more/change later)?',
default: 'us-east-1',
choices: [
'us-east-1',
'us-west-1',
'eu-west-1',
'ap-northeast-1',
],
});
} else {
overrideAnswers.region = lambdaRegion;
}
// If region exists, skip select prompt
if (_this._region) return Promise.resolve();
if (!notificationEmail) {
prompts.push({
type: 'input',
name: 'notificationEmail',
message: 'Email would you like to use for AWS alarms:',
default: '',
});
} else {
overrideAnswers.notificationEmail = notificationEmail;
}
// Prompt: region select
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: '4) ', label: 'ap-northeast-1', value: 'ap-northeast-1' },
];
// Use existing or create new AWS CLI profile
if (fs.existsSync(path.join(AWSUtils.getConfigDir(), 'credentials'))) {
return JawsCLI.select('Select a region for your project: ', choices, false)
.then(function(results) {
_this._region = results[0].value;
return Promise.resolve();
});
})
.then(function() {
var profilesList = AWSUtils.profilesMap(),
profiles = Object.keys(profilesList);
// If profile exists, skip select prompt
if (_this._profile) return Promise.resolve();
if (awsProfile && -1 !== profiles.indexOf(awsProfile)) {
overrideAnswers.awsProfile = awsProfile;
} else {
prompts.unshift({
type: 'rawlist',
name: 'awsProfile',
message: 'What AWS profile in ~/.aws/credentials should be used for your admin user?:',
choices: profiles,
default: profiles[0],
// Prompt: profile select
var profilesList = AWSUtils.profilesMap(),
profiles = Object.keys(profilesList),
choices = [];
for (var i = 0; i < profiles.length; i++) {
choices.push({
key: (i + 1) + ') ',
value: profiles[i],
label: profiles[i],
});
}
return JawsCLI.select('Select a profile for your project: ', choices, false)
.then(function(results) {
_this._profile = results[0].value;
return Promise.resolve();
});
});
}
} else {
prompts.unshift({ //need to create aws creds profile (will use 'default')
type: 'input',
name: 'awsAdminKeyId',
message: 'Please enter the ACCESS KEY ID for your ADMIN AWS IAM User:',
}, {
type: 'input',
name: 'awsAdminSecretKey',
message: 'Please enter the SECRET ACCESS KEY for your ADMIN AWS IAM User:',
});
}
if (prompts.length > 0) {
return inquirer.prompt(prompts)
.then(function(answers) {
return extend(answers, overrideAnswers);
});
} else {
return Promise.resolve(overrideAnswers);
}
}
});
/**
* Prepare project data
*
* @param answers
* CMD: Prepare Project Data
* @returns {Promise}
* @private
*/
function _prepareProjectData(answers) {
if (answers.stage.toLowerCase() == 'local') {
Promise.reject(new JawsError(
'Stage ' + answers.stage + ' is reserved',
JawsError.errorCodes.UNKNOWN));
CMD.prototype._prepareProjectData = Promise.method(function() {
var _this = this;
// Validate: Ensure stage isn't "local"
if (_this._stage.toLowerCase() == 'local') {
throw new JawsError('Stage ' + _this._stage + ' is reserved');
}
project.name = answers.name.toLowerCase().trim()
.replace(/[^a-zA-Z-\d\s:]/g, '')
.replace(/\s/g, '-')
.substring(0, 19);
// AWS only allows Alphanumeric and - in name
var nameOk = /^([a-zA-Z0-9-]+)$/.exec(project.name);
// Validate: AWS only allows Alphanumeric and - in name
var nameOk = /^([a-zA-Z0-9-]+)$/.exec(_this._name);
if (!nameOk) {
Promise.reject(new JawsError(
'Project names can only be alphanumeric and -',
JawsError.errorCodes.INVALID_PROJ_NAME));
throw new JawsError('Project names can only be alphanumeric and -');
}
// Append unique id if name is in use
if (fs.existsSync(path.join(process.cwd(), project.name))) {
project.name = project.name + '-' + shortid.generate().replace(/[_-]/g, '');
if (fs.existsSync(path.join(process.cwd(), _this._name))) {
_this._name = _this._name + '-' + shortid.generate().replace(/\W+/g, '').substring(0, 19);
}
// Set or Create Profile
if (answers.awsProfile) {
// Validate: If no profile, ensure access keys, create profile
if (!_this._profile) {
project.awsProfile = answers.awsProfile;
} else {
if (!answers.awsAdminKeyId) {
reject(new JawsError(
if (!_this._awsAdminKeyId) {
throw new JawsError(
'An AWS Access Key ID is required',
JawsError.errorCodes.MISSING_AWS_CREDS));
JawsError.errorCodes.MISSING_AWS_CREDS);
}
if (!answers.awsAdminSecretKey) {
reject(new JawsError(
if (!_this._awsAdminSecretKey) {
throw new JawsError(
'An AWS Secret Key is required',
JawsError.errorCodes.MISSING_AWS_CREDS));
JawsError.errorCodes.MISSING_AWS_CREDS);
}
// Set profile
AWSUtils.profilesSet('default', answers.region, answers.awsAdminKeyId, answers.awsAdminSecretKey);
project.awsProfile = 'default';
AWSUtils.profilesSet('default', _this._region, _this._awsAdminKeyId, _this._awsAdminSecretKey);
_this._profile = 'default';
}
// Set other project data
project.stage = answers.stage;
project.region = answers.region;
project.notificationEmail = answers.notificationEmail.trim();
project.s3Bucket = answers.s3Bucket;
return Promise.resolve();
}
});
/**
*
* CMD: Create Project Directory
* @returns {Promise}
* @private
*/
function _createProjectDirectory() {
// Set Root Path
project.rootPath = path.resolve(path.join(path.dirname('.'), project.name));
CMD.prototype._createProjectDirectory = Promise.method(function() {
var _this = this;
_this._projectRootPath = path.resolve(path.join(path.dirname('.'), _this._name));
// Prepare admin.env
var adminEnv = 'ADMIN_AWS_PROFILE=' + project.awsProfile + os.EOL;
var adminEnv = 'ADMIN_AWS_PROFILE=' + _this._profile + os.EOL;
// Prepare CloudFormation template
var cfTemplate = require('../templates/jaws-cf');
cfTemplate.Parameters.aaProjectName.Default = project.name;
cfTemplate.Parameters.aaProjectName.AllowedValues = [project.name];
cfTemplate.Parameters.aaStage.Default = project.stage;
cfTemplate.Parameters.aaDataModelPrefix.Default = project.stage; //to simplify bootstrap use same stage
cfTemplate.Parameters.aaNotficationEmail.Default = project.notificationEmail;
cfTemplate.Parameters.aaProjectName.Default = _this._name;
cfTemplate.Parameters.aaProjectName.AllowedValues = [_this._name];
cfTemplate.Parameters.aaStage.Default = _this._stage;
cfTemplate.Parameters.aaDataModelPrefix.Default = _this._stage; //to simplify bootstrap use same stage
cfTemplate.Parameters.aaNotficationEmail.Default = _this._notificationEmail;
// Create files
// Create Project Scaffolding
return utils.writeFile(
path.join(project.rootPath, 'back', '.env'),
'JAWS_STAGE=' + project.stage + '\nJAWS_DATA_MODEL_PREFIX=' + project.stage
)
path.join(_this._projectRootPath, 'back', '.env'),
'JAWS_STAGE=' + _this._stage
+ '\nJAWS_DATA_MODEL_PREFIX=' + _this._stage)
.then(function() {
return Promise.all([
fs.mkdirAsync(path.join(project.rootPath, 'front')),
fs.mkdirAsync(path.join(project.rootPath, 'tests')),
fs.mkdirAsync(path.join(project.rootPath, 'back/lambdas')),
fs.mkdirAsync(path.join(project.rootPath, 'back/lib')),
utils.writeFile(path.join(project.rootPath, 'admin.env'), adminEnv),
utils.writeFile(path.join(project.rootPath, 'jaws-cf.json'), JSON.stringify(cfTemplate, null, 2)),
fs.mkdirAsync(path.join(_this._projectRootPath, 'front')),
fs.mkdirAsync(path.join(_this._projectRootPath, 'tests')),
fs.mkdirAsync(path.join(_this._projectRootPath, 'back/lambdas')),
fs.mkdirAsync(path.join(_this._projectRootPath, 'back/lib')),
utils.writeFile(path.join(_this._projectRootPath, 'admin.env'), adminEnv),
utils.writeFile(path.join(_this._projectRootPath, 'jaws-cf.json'), JSON.stringify(cfTemplate, null, 2)),
]);
});
}
});
/**
* Create s3 bucket (if DNE) and upload the 1st stage env var
*
* Format: <bucket>/JAWS/envVars/<projName>/<stage>
* CMD Create S3 Bucket
* (if DNE) and upload the 1st stage env var
* Format: <bucket>/JAWS/envVars/<name>/<stage>
*
* @returns {Promise}
* @private
*/
function _createS3JawsStructure() {
return AWSUtils.createBucket(project.awsProfile, project.region, project.s3Bucket)
CMD.prototype._createS3JawsStructure = Promise.method(function() {
var _this = this;
return AWSUtils.createBucket(_this._profile, _this._region, _this._s3Bucket)
.then(function() {
var envFileContents = 'JAWS_STAGE=' + project.stage + '\nJAWS_DATA_MODEL_PREFIX=' + project.stage;
var envFileContents = 'JAWS_STAGE=' + _this._stage
+ '\nJAWS_DATA_MODEL_PREFIX=' + _this._stage;
return AWSUtils.putEnvFile(
project.awsProfile,
project.region,
project.s3Bucket,
project.name,
project.stage,
_this._profile,
_this._region,
_this._s3Bucket,
_this._name,
_this._stage,
envFileContents);
});
}
});
/**
* Create CloudFormation Stack
* CMD: Create CloudFormation Stack
*/
function _createCfStack() {
CMD.prototype._createCfStack = Promise.method(function() {
var _this = this;
// Show loading messages
var message = 'JAWS is now going to create an AWS CloudFormation Stack for the "' + project.stage +
var message = 'JAWS is now going to create an AWS CloudFormation Stack for the "' + _this._stage +
'" stage of your JAWS project. This doesn\'t cost anything, but takes around 5 minutes to set-up. Sit tight!';
var spinner = new Spinner('%s Creating CloudFormation Stack...');
console.log(message);
spinner.setSpinnerString('|/-\\');
// Start loading icon
var spinner = JawsCLI.spinner('Creating CloudFormation Stack for your new project...');
spinner.start();
// Create CF stack
return AWSUtils.cfCreateStack(
project.awsProfile,
project.region,
project.rootPath,
project.name,
project.stage,
project.notificationEmail
)
_this._profile,
_this._region,
_this._projectRootPath,
_this._name,
_this._stage,
_this._notificationEmail)
.then(function(cfData) {
return AWSUtils.monitorCfCreate(cfData, project.awsProfile, project.region, spinner);
return AWSUtils.monitorCfCreate(cfData, _this._profile, _this._region, spinner);
});
}
});
/**
* Create Project JSON
* CMD: Create Project JSON
*
* @param cfOutputs. Optional
* @returns {Promise} jaws json js obj
* @private
*/
function _createProjectJson(cfOutputs) {
CMD.prototype._createProjectJson = Promise.method(function(cfOutputs) {
var _this = this;
var iamRoleArnLambda,
iamRoleArnApiGateway;
@ -341,64 +421,33 @@ function _createProjectJson(cfOutputs) {
}
var jawsJson = {
name: project.name,
name: _this._name,
version: '0.0.1',
location: '<enter project\'s github repository url here>',
author: 'Vera D. Servers <vera@gmail.com> http://vera.io',
description: project.name + ': An ambitious, server-less application built with the JAWS framework.',
author: 'You <you@yourapp.com>',
description: _this._name + ': An ambitious, server-less application built with the JAWS framework.',
project: {
stages: {},
},
};
jawsJson.project.stages[project.stage] = [];
jawsJson.project.stages[project.stage].push({
region: project.region,
jawsJson.project.stages[_this._stage] = [];
jawsJson.project.stages[_this._stage].push({
region: _this._region,
iamRoleArnLambda: iamRoleArnLambda || '',
iamRoleArnApiGateway: iamRoleArnApiGateway || '',
});
jawsJson.project.envVarBucket = {
name: project.s3Bucket,
region: project.region,
name: _this._s3Bucket,
region: _this._region,
};
fs.writeFileSync(path.join(project.rootPath, 'jaws.json'), JSON.stringify(jawsJson, null, 2));
fs.writeFileSync(path.join(_this._projectRootPath, 'jaws.json'),
JSON.stringify(jawsJson, null, 2));
console.log('Your project "' +
project.name +
'" has been successfully created in the current directory.'
);
JawsCLI.log('Your project "' + _this._name
+ '" has been successfully created in the current directory.');
return Promise.resolve(jawsJson);
}
/**
*
* @param projName
* @param stage
* @param s3Bucket store things like env vars: <bucket>/JAWS/envVars/<proj-name>/<stage>. Create bucket if DNE
* @param lambdaRegion
* @param notificationEmail
* @param awsProfile
* @param noExeCf don't execute CloudFormation at the end
* @returns {*}
*/
module.exports.create = function(projName, stage, s3Bucket, lambdaRegion, notificationEmail, awsProfile, noExeCf) {
return _getAnswers(projName, stage, s3Bucket, lambdaRegion, notificationEmail, awsProfile)
.then(_prepareProjectData)
.then(_createS3JawsStructure) //see if bucket is avail first before doing work
.then(_createProjectDirectory)
.then(function() {
if (noExeCf) {
utils.logIfVerbose('No exec cf specified, updating proj jaws.json only');
console.log('Project and env var file in s3 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 _createProjectJson();
} else {
return _createCfStack()
.then(_createProjectJson);
}
});
};
return jawsJson;
});

View File

@ -17,15 +17,15 @@ Promise.promisifyAll(fs);
/**
* Tag a lambda for deployment (set deploy = true)
*
* @param type api|lambda
* @param type endpoint|lambda
* @param fullPathToJawsJson optional. Uses cwd by default
* @param {boolean} untag. default false
* @returns {Promise} full path to jaws.json that was updated
*/
module.exports.tag = function(type, fullPathToJawsJson, untag) {
untag = !!(untag);
var jawsJsonPath = fullPathToJawsJson || path.join(process.cwd(), 'jaws.json');
untag = !!(untag);
var jawsJsonPath = fullPathToJawsJson ? fullPathToJawsJson : path.join(process.cwd(), 'jaws.json');
return new Promise(function(resolve, reject) {
if (!fs.existsSync(jawsJsonPath)) {
@ -38,7 +38,7 @@ module.exports.tag = function(type, fullPathToJawsJson, untag) {
var jawsJson = require(jawsJsonPath);
if (type === 'lambda' && typeof jawsJson.lambda !== 'undefined') {
jawsJson.lambda.deploy = !untag;
} else if (type === 'api' && typeof jawsJson.endpoint !== 'undefined') {
} else if (type === 'endpoint' && typeof jawsJson.endpoint !== 'undefined') {
jawsJson.endpoint.deploy = !untag;
} else {
reject(new JawsError(
@ -56,7 +56,7 @@ module.exports.tag = function(type, fullPathToJawsJson, untag) {
* Tag or untag all
*
* @param {Jaws} JAWS
* @prams type api|lambda
* @prams type endpoint|lambda
* @param {boolean} untag default false
* @returns {Promise}
*/
@ -66,14 +66,14 @@ module.exports.tagAll = function(JAWS, type, untag) {
findAllFunc = (type == 'lambda') ? 'findAllLambdas' : 'findAllEndpoints';
return utils[findAllFunc](JAWS._meta.projectRootPath)
.then(function(lJawsJsonPaths) {
.then(function(jawsJsonPaths) {
var tagQueue = [];
if (!lJawsJsonPaths) {
if (!jawsJsonPaths) {
throw new JawsError('Could not find any lambdas', JawsError.errorCodes.UNKNOWN);
}
lJawsJsonPaths.forEach(function(ljp) {
tagQueue.push(_this.tag(type, ljp, untag));
jawsJsonPaths.forEach(function(jawsJsonPath) {
tagQueue.push(_this.tag(type, jawsJsonPath, untag));
});
return Promise.all(tagQueue);

View File

@ -329,8 +329,6 @@ exports.getEnvFile = function(awsProfile, awsRegion, bucketName, projectName, st
Key: key,
};
utils.logIfVerbose('Getting env file from ' + bucketName + '/' + key);
return this.getS3Object(awsProfile, awsRegion, params);
};

264
lib/utils/cli.js Normal file
View File

@ -0,0 +1,264 @@
// 'use strict';
/**
* JAWS Services: CLI
*/
var Promise = require('bluebird'),
prompt = require('prompt'),
path = require('path'),
os = require('os'),
JawsError = require('../jaws-error/index'),
utils = require('../utils'),
fs = require('fs'),
chalk = require('chalk'),
Spinner = require('cli-spinner').Spinner,
keypress = require('keypress'),
packageJson = require('../../package.json');
Promise.promisifyAll(fs);
/**
* ASCII
*/
module.exports.ascii = function() {
var art = '';
art = art + ' ____ _____ __ __ _________ ' + os.EOL;
art = art + ' | | / _ \\/ \\ / \\/ _____/ ' + os.EOL;
art = art + ' | |/ /_\\ \\ \\/\\/ /\\_____ \\ ' + os.EOL;
art = art + ' /\\__| / | \\ / / \\ ' + os.EOL;
art = art + ' \\________\\____|__ /\\__/\\__/ /_________/ v' + packageJson.version + os.EOL;
art = art + '' + os.EOL;
art = art + ' *** The Server-Less Framework *** ' + os.EOL;
console.log(chalk.yellow(art));
};
/**
* Spinner
*/
module.exports.spinner = function(message) {
var spinner = new Spinner('JAWS: ' + chalk.yellow('%s ' + message));
spinner.setSpinnerString('|/-\\');
return spinner;
};
/**
* Log
*/
module.exports.log = function(message) {
console.log('JAWS: ' + chalk.yellow(message + ' '));
};
/**
* Prompt
*/
module.exports.prompt = function() {
prompt.start();
prompt.delimiter = '';
prompt.message = 'JAWS: ';
return prompt;
};
/**
* Prompt: Select
*
* Accepts array: {key: '1: ', key2: '(deployed) ', value: 'a great choice!'}
* Or: {spacer: '-----'}
*
* @returns {Promise}
*/
var Select = {
data: null,
};
// Render
Select._render = function() {
var _this = this;
// Clear Rendering
_this._clear();
// Reset line count
_this.state.lines = 1;
// Render Line
for (var i = 0; i < _this.state.choices.length; i++) {
var choice = _this.state.choices[i],
line = '';
// Increment line count
_this.state.lines++;
// Select Arrow
var arrow = i === (_this.state.index - 1) ? ' > ' : ' ';
// Render Choice
if (choice.label) {
// Line - Key
if (choice.key) line = line + choice.key;
// Line - Key2
if (choice.key2) line = line + choice.key2;
// Line - Line
line = line + choice.label;
// Add toggled style
if (choice.toggled) {
line = chalk.yellow(line);
}
// Add line break
line = line + os.EOL;
}
// Render Spacer
if (choice.spacer) {
line = chalk.grey(choice.spacer) + os.EOL;
}
// TODO: Add custom word wrap after measuring terminal width. Re-count lines.
// Render
process.stdout.write(arrow + line);
}
};
// Private: Clear Rendering
Select._clear = function() {
var _this = this;
for (var i = 1; i < Select.state.lines; i++) {
process.stdout.moveCursor(0, -1);
process.stdout.clearLine();
}
};
// Private: Close
Select._close = function(cb) {
var _this = this;
process.stdin.pause();
// Gather Choices
var selected = [];
for (var i = 0; i < _this.state.choices.length; i++) {
if (_this.state.choices[i].toggled) selected.push(_this.state.choices[i]);
}
return Select._promise(selected);
};
/**
* Select
* @param message
* @param choices
* @param multi
* @param spacer
* @param doneLabel
* @returns {Promise}
*/
module.exports.select = function(message, choices, multi, doneLabel) {
// Set keypress listener, if not set
if (!Select.state) {
keypress(process.stdin);
process.stdin.on('keypress', function (ch, key) {
if (key && key.ctrl && key.name == 'c') {
process.stdin.pause();
} else if (key.name == 'up' && Select.state.index > 1) {
if (Select.state.index === 2 && Select.state.choices[0].spacer) {
// If first choice is spacer, do nothing
Select.state.index = 2;
} else if (Select.state.choices[Select.state.index - 2].spacer) {
// If next choice is spacer, move up 2
Select.state.index = Select.state.index - 2;
} else {
// Move up
Select.state.index = Select.state.index - 1;
}
return Select._render();
} else if (key.name == 'down' && Select.state.index < Select.state.choices.length) {
if (Select.state.choices[Select.state.index].spacer) {
// If next choice is spacer, move down 2
Select.state.index = Select.state.index + 2;
} else {
// Move down
Select.state.index = Select.state.index + 1;
}
return Select._render();
} else if (key.name == 'return') {
// Check if "done" option
if (Select.state.choices[Select.state.index - 1].action
&& Select.state.choices[Select.state.index - 1].action.toLowerCase() === 'done') {
return Select._close();
} else {
// Toggle option
Select.state.choices[Select.state.index - 1].toggled = Select.state.choices[Select.state.index - 1].toggled ? false : true;
if (!Select.state.multi) {
Select._close();
} else {
return Select._render();
}
}
}
});
process.stdin.setRawMode(true);
}
return new Promise(function(resolve, reject) {
// Resume stdin
process.stdin.resume();
process.stdin.setEncoding('utf8');
// Update CheckList
Select.state = {
choices: choices,
index: (choices[0] && choices[0].spacer) ? 2 : 1,
lines: 0,
multi: multi,
doneLabel: doneLabel ? doneLabel : 'Done',
};
// Add Done and Cancel to choices
if (Select.state.multi) {
Select.state.choices.push(
{ spacer: '- - - - -' },
{
action: 'Done',
label: Select.state.doneLabel,
});
}
// Log Message
if (message) console.log('JAWS: ' + chalk.yellow(message));
// Assign CheckList Promise
Select._promise = resolve;
// Initial Render
Select._render();
});
};

View File

@ -211,7 +211,7 @@ module.exports.checkForDuplicateLambdaNames = function(projectRootPath) {
return this.findAllLambdas(projectRootPath)
.then(function(lambdaJawsPaths) {
//Verify 2 lambdas dont have same name
// Verify 2 lambdas dont have same name
lambdaJawsPaths.forEach(function(ljp) {
var ljpJson = require(ljp);

View File

@ -46,10 +46,13 @@
"inquirer": "^0.9.0",
"insert-module-globals": "^6.5.2",
"jaws-api-gateway-client": "0.11.0",
"keypress": "^0.2.1",
"minimist": "^1.2.0",
"mkdirp-then": "^1.1.0",
"moment": "^2.10.6",
"node-uuid": "^1.4.2",
"node-zip": "^1.1.0",
"prompt": "^0.2.14",
"readdirp": "^1.4.0",
"shortid": "^2.2.2",
"uglify-js": "^2.4.24",

View File

@ -5,24 +5,25 @@
require('./config'); //init config
describe('AllTests', function() {
before(function(done) {
this.timeout(0); //dont timeout anything
done();
});
after(function() {
});
after(function() {});
//require tests vs inline so we can run sequentially
require('./cli/tag');
require('./cli/install');
require('./cli/env');
require('./cli/generate');
//require('./cli/tag');
//require('./cli/install');
//require('./cli/env');
//require('./cli/generate');
/**
* Tests below create AWS Resources
*/
//require('./cli/dash');
//require('./cli/deploy_lambda');
//require('./cli/deploy_api');
//require('./cli/new'); //Must be run last
});
//require('./cli/deploy_endpoint');
require('./cli/new_project');
});

73
tests/cli/dash.js Normal file
View File

@ -0,0 +1,73 @@
'use strict';
/**
* JAWS Test: Dash Command
*/
var Jaws = require('../../lib/index.js'),
CMDdash = require('../../lib/commands/dash'),
CMDtag = require('../../lib/commands/tag'),
JawsError = require('../../lib/jaws-error'),
testUtils = require('../test_utils'),
Promise = require('bluebird'),
path = require('path'),
assert = require('chai').assert;
var config = require('../config'),
projPath,
JAWS;
describe('Test "dash" 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,
['back']);
process.chdir(path.join(projPath, 'back'));
// Instantiate JAWS
JAWS = new Jaws();
})
.then(function() {
return CMDtag.tagAll(JAWS, 'lambda');
})
.then(function() {
return CMDtag.tagAll(JAWS, 'endpoint');
})
.then(function() {
return done();
});
});
after(function(done) {
done();
});
describe('Positive tests', function() {
it('Dash deployment via tagged resources', function(done) {
this.timeout(0);
CMDdash.run(JAWS, config.stage, [config.region], true)
.then(function() {
done();
})
.catch(JawsError, function(e) {
done(e);
})
.error(function(e) {
done(e);
});
});
});
});

View File

@ -46,7 +46,7 @@ describe('Test deploy api command', function() {
this.timeout(0);
theCmd.deployApi(JAWS, config.stage, config.region, true)
theCmd.run(JAWS, config.stage, config.region, true)
.then(function() {
done();
})

View File

@ -42,9 +42,9 @@ describe('Test "deploy lambda" command', function() {
it('Multi level module deploy', function(done) {
this.timeout(0);
process.chdir(path.join(projPath, 'back/lambdas/users/show'));
process.chdir(path.join(projPath, 'back/lambdas/sessions/show'));
theCmd.deployLambdas(JAWS, config.stage, false, false)
theCmd.run(JAWS, config.stage, false, false)
.then(function(d) {
done();
})
@ -57,7 +57,7 @@ describe('Test "deploy lambda" command', function() {
this.timeout(0);
process.chdir(path.join(projPath, 'back/lambdas/bundle/browserify'));
theCmd.deployLambdas(JAWS, config.stage, false, false)
theCmd.run(JAWS, config.stage, false, false)
.then(function(d) {
done();
})
@ -70,7 +70,7 @@ describe('Test "deploy lambda" command', function() {
this.timeout(0);
process.chdir(path.join(projPath, 'back/lambdas/bundle/nonoptimized'));
theCmd.deployLambdas(JAWS, config.stage, false, false)
theCmd.run(JAWS, config.stage, false, false)
.then(function(d) {
done();
})

View File

@ -6,6 +6,7 @@
* - Deletes the CF stack created by the project
*/
var Jaws = require('../../lib/index.js'),
JawsError = require('../../lib/jaws-error'),
theCmd = require('../../lib/commands/new_project'),
path = require('path'),
os = require('os'),
@ -31,17 +32,24 @@ describe('Test new command', function() {
this.timeout(0);
theCmd.create(
theCmd.run(
config.newName,
config.stage,
config.envBucket,
config.region,
config.notifyEmail,
config.profile
)
config.region,
config.profile)
.then(function() {
var jawsJson = require(path.join(os.tmpdir(), config.newName, 'jaws.json'));
assert.isTrue(!!jawsJson.project.regions['us-east-1'].stages[config.stage].iamRoleArn);
var region = false;
for (var i = 0; i < jawsJson.project.stages[config.stage].length; i++) {
var stage = jawsJson.project.stages[config.stage][i];
if (stage.region === config.region) {
region = stage.region;
}
}
assert.isTrue(region !== false);
done();
})
.catch(JawsError, function(e) {
@ -53,12 +61,6 @@ describe('Test new command', function() {
});
});
describe('Error tests', function() {
it('Create new project', function(done) {
done();
})
});
//it('Delete Cloudformation stack from new project', function(done) {
// this.timeout(0);
// var CF = new config.AWS.CloudFormation();
@ -67,4 +69,4 @@ describe('Test new command', function() {
// done();
// });
//});
});
});

View File

@ -12,7 +12,7 @@ var AWS = require('aws-sdk'),
uuid = require('node-uuid'),
Promise = require('bluebird');
module.exports.run = function(event, context) {
module.exports.handler = function(event, context) {
console.log('about to run');
var s3 = Promise.promisifyAll(new AWS.S3());

View File

@ -8,7 +8,7 @@
"functionName": "browserifytest",
"runtime": "nodejs",
"runtimeVer": "0.10.36",
"handler": "lambdas/bundle/browserify/main.run",
"handler": "lambdas/bundle/browserify/index.handler",
"envVars": [
"MYAPP_SERVICE_KEY",
"MYAPP_SERVICE2_KEY"

View File

@ -13,7 +13,7 @@ var AWS = require('aws-sdk'),
Promise = require('bluebird'),
awsMetadata = require('aws-sdk/package.json');
module.exports.run = function(event, context) {
module.exports.handler = function(event, context) {
console.log('AWS sdk version', awsMetadata.version);
var s3 = Promise.promisifyAll(new AWS.S3());

View File

@ -8,7 +8,7 @@
"functionName": "notoptimizedtest",
"runtime": "nodejs",
"runtimeVer": "0.10.36",
"handler": "lambdas/bundle/nonoptimized/main.run",
"handler": "lambdas/bundle/nonoptimized/index.handler",
"envVars": [
"MYAPP_SERVICE_KEY",
"MYAPP_SERVICE2_KEY"

View File

@ -0,0 +1,7 @@
/**
* API: Sessions: Create
*/
exports.handler = function(event, context) {
context.done(null, { message: 'This test lambda function has run successfully!' });
};

View File

@ -1,14 +1,14 @@
{
"name": "jaws-users-signin",
"name": "jaws-sessions-create",
"version": "0.0.1",
"location": "https://github.com/jaws-stack/jaws-users-crud-ddb-jwt-js",
"author": "JAWS",
"description": "A group of lambda functions for user crud operations using dynamodb, JSON web tokens and javascript",
"lambda": {
"functionName": "usersSignIn",
"functionName": "sessionsCreate",
"runtime": "nodejs",
"runtimeVer": "0.10.36",
"handler": "lambdas/users/signin/index.handler",
"handler": "lambdas/sessions/create/index.handler",
"envVars": [
"MYAPP_SERVICE_KEY",
"MYAPP_SERVICE2_KEY"
@ -28,9 +28,9 @@
}
},
"endpoint": {
"type": "lambda",
"path": "sessions/{sessionId}",
"method": "PUT",
"type": "AWS",
"path": "sessions",
"method": "POST",
"authorizationType": "none",
"apiKeyRequired": false,
"requestTemplates": {

View File

@ -0,0 +1,7 @@
/**
* API: Sessions: Show
*/
exports.handler = function(event, context) {
context.done(null, { message: 'This test lambda function has run successfully!' });
};

View File

@ -5,10 +5,10 @@
"author": "JAWS",
"description": "A lambda function to fetch a user from the database and show them",
"lambda": {
"functionName": "usersShow",
"functionName": "sessionsShow",
"runtime": "nodejs",
"runtimeVer": "0.10.33",
"handler": "lambdas/users/show/index.handler",
"handler": "lambdas/sessions/show/index.handler",
"envVars": [
"MYAPP_SERVICE_KEY",
"MYAPP_SERVICE2_KEY"
@ -28,7 +28,7 @@
}
},
"endpoint": {
"type": "lambda",
"type": "AWS",
"path": "sessions/{sessionId}",
"method": "GET",
"authorizationType": "none",

View File

@ -0,0 +1,7 @@
/**
* API: Users: Create
*/
exports.handler = function(event, context) {
context.done(null, { message: 'This test lambda function has run successfully!' });
};

View File

@ -5,10 +5,10 @@
"author": "JAWS",
"description": "A group of lambda functions for user crud operations using dynamodb, JSON web tokens and javascript",
"lambda": {
"functionName": "usersSignUp",
"functionName": "usersCreate",
"runtime": "nodejs",
"runtimeVer": "0.10.36",
"handler": "lambdas/users/signup/index.handler",
"handler": "lambdas/users/create/index.handler",
"envVars": [
"MYAPP_SERVICE_KEY",
"MYAPP_SERVICE2_KEY"
@ -28,7 +28,7 @@
}
},
"endpoint": {
"type": "lambda",
"type": "AWS",
"path": "users",
"method": "POST",
"authorizationType": "none",

View File

@ -1,9 +0,0 @@
/**
* API: Users: Show
*/
exports.handler = function(event, context) {
context.done(null, { message: 'You\'ve made a successful request to your JAWS API!' });
};

View File

@ -1,5 +0,0 @@
/**
* API: Users: Sign-In
*/
exports.handler = function(event, context) {};

View File

@ -1,5 +0,0 @@
/**
* API: Users: Sign-Up
*/
exports.handler = function(event, context) {};

View File

@ -61,7 +61,7 @@ module.exports.createTestProject = function(projectName,
projectJSON.project.stages[projectStage] = [{
region: projectRegion,
iamRoleArnLambda: projectLambdaIAMRole,
iamRoleArnApiGateway: projectApiGIAMRole
iamRoleArnApiGateway: projectApiGIAMRole,
},];
projectJSON.project.envVarBucket = {
name: projectEnvBucket,