diff --git a/bin/jaws b/bin/jaws index 4eba04ade..21a1df72e 100755 --- a/bin/jaws +++ b/bin/jaws @@ -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 [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 ', 'AWS profile to use (as defined in ~/.aws/credentials). Only valid for new project.') - .option('-n, --proj-name ', 'Project name. Only valid for new project.') - .option('-s, --stage ', 'Stage to create. all lowercase Only valid for new project.') - .option('-r, --region ', 'Region lambda(s)&API(s) will be created in (Can add more later). Only valid for new project.') - .option('-3, --s3-bucket ', 'S3 bucket to store stage env files. Key is JAWS/envVars//. Bucket will be created if it DNE. Only valid for new project.') - .option('-e, --notification-email ', 'Email to be notified with stack alerts. Only valid for new project.') - .action(function(type, regionName, stageName, options) { - type = type.toLowerCase(); + .command('new ') + .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 [region]') - .description('Deploy a lambda function (type lambda), a REST API (api), or provision AWS resources (resources) for the specified stage.' + + .command('deploy [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 ') .description('Get logs for the lambda function in the specified stage in your current working directory.') diff --git a/lib/commands/dash.js b/lib/commands/dash.js new file mode 100644 index 000000000..26cc2b8bb --- /dev/null +++ b/lib/commands/dash.js @@ -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]; + } + }); +}); \ No newline at end of file diff --git a/lib/commands/deploy_api.js b/lib/commands/deploy_endpoint.js similarity index 77% rename from lib/commands/deploy_api.js rename to lib/commands/deploy_endpoint.js index 9d0d9ca69..4ce72c859 100644 --- a/lib/commands/deploy_api.js +++ b/lib/commands/deploy_endpoint.js @@ -1,24 +1,129 @@ 'use strict'; /** - * JAWS Command: deploy api - * - Deploys project's API Gateway REST API to the specified stage and region(s) + * JAWS Command: deploy endpoint + * - 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); - }); - }); -}; +}); \ No newline at end of file diff --git a/lib/commands/deploy_lambda.js b/lib/commands/deploy_lambda.js index 4f89a5cb4..773052035 100644 --- a/lib/commands/deploy_lambda.js +++ b/lib/commands/deploy_lambda.js @@ -1,11 +1,12 @@ 'use strict'; /** - * JAWS Command: deploy lambda + * JAWS Command: deploy lambda * - Deploys project's lambda(s) to the specified stage */ var JawsError = require('../jaws-error'), + JawsCLI = require('../utils/cli'), Promise = require('bluebird'), fs = require('fs'), path = require('path'), @@ -17,362 +18,595 @@ var JawsError = require('../jaws-error'), browserify = require('browserify'), UglifyJS = require('uglify-js'), wrench = require('wrench'), + CMDtag = require('./tag'), Zip = require('node-zip'); Promise.promisifyAll(fs); /** - * I know this is a long func name.. - * + * Run * @param JAWS - * @param region * @param stage - * @private + * @param regions - Defaults to all regions + * @param allTagged + * @param allAtOnce + * @returns {*} */ -function _validateJawsProjAttrsForLambdaDeploy(JAWS, region, stage) { - if (!JAWS._meta.projectJson.project.stages[stage]) { - throw new JawsError( - stage + ' not setup in project jaws.json', - JawsError.errorCodes.INVALID_PROJECT_JAWS - ); - } - var regionObj = utils.getProjRegionConfig(JAWS._meta.projectJson.project.stages[stage], region); +module.exports.run = function(JAWS, stage, regions, allTagged, allAtOnce) { + var command = new CMD(JAWS, stage, regions, allTagged, allAtOnce); + return command.run(); +}; - if (!regionObj.iamRoleArnLambda) { - throw new JawsError( - 'iamRoleArnLambda stage ' + stage + ' in region ' + region + ' not setup in project jaws.json', - JawsError.errorCodes.INVALID_PROJECT_JAWS - ); - } +/** + * Command Class + * @param JAWS + * @param stage + * @param regions - Defaults to all regions + * @param allTagged + * @param allAtOnce + * @constructor + */ + +function CMD(JAWS, stage, regions, allTagged, allAtOnce) { + this._JAWS = JAWS; + this._stage = stage; + this._regions = regions.length ? regions : Object.keys(this._JAWS._meta.projectJson.project.stages[this._stage]); + this._allTagged = allTagged; + this._allAtOnce = allAtOnce; + this._lambdaPaths = []; } /** - * Copy source back dir to temp dir, excluding paths - * - * @param srcBackDir - * @param targetBackDir - * @param excludePatterns list of regular expressions + * CMD: Run */ -function copyBackDirToTmp(srcBackDir, targetBackDir, excludePatterns) { - wrench.copyDirSyncRecursive(srcBackDir, targetBackDir, { - exclude: function(name, prefix) { - if (!excludePatterns.length) { - return false; - } - var relPath = path.join(prefix.replace(srcBackDir, ''), name); +CMD.prototype.run = Promise.method(function() { - return excludePatterns.some(function(sRegex) { - relPath = (relPath.charAt(0) == path.sep) ? relPath.substr(1) : relPath; + var _this = this; - var re = new RegExp(sRegex), - matches = re.exec(relPath); + // Flow + return _this._promptStage() + .bind(_this) + .then(_this._validate) + .then(_this._getTaggedLambdaPaths) + .then(function() { + return _this._regions; + }) + .each(function(region) { + var deployer = new Deployer(_this._JAWS, _this._lambdaPaths, _this._stage, region); + return deployer.deploy(); + }).then(function() { + JawsCLI.log('Lambda Deployer: Successfully deployed lambdas to the requested regions!'); + return Promise.resolve(); + }); +}); - var willExclude = (matches && matches.length > 0); +/** + * CMD: Prompt: Stage + */ - if (willExclude) { - utils.logIfVerbose('Excluding ' + relPath); +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('Lambda Deployer: Choose a stage: ', choices, false) + .then(function(results) { + _this._stage = results[0].value; + }); +}); + +/** + * CMD: Validate + */ + +CMD.prototype._validate = Promise.method(function() { + + var _this = this; + + // Validate: Check stage exists within project + if (!_this._JAWS._meta.projectJson.project.stages[_this._stage]) { + throw new JawsError('Invalid stage ' + stage); + } +}); + +/** + * CMD: Get Tagged Lambda Paths + */ + +CMD.prototype._getTaggedLambdaPaths = Promise.method(function() { + + var _this = this; + + if (_this._allTagged) { + return CMDtag.listAll(_this._JAWS, 'lambda') + .then(function(lambdaPaths) { + + if (!lambdaPaths.length) { + throw new JawsError('No tagged lambdas found'); + } + + _this._lambdaPaths = lambdaPaths; + }); + } else { + return CMDtag.tag('lambda') + .then(function(lambdaPath) { + + if (!lambdaPath) { + throw new JawsError('No tagged lambdas found'); + } + + _this._lambdaPaths = [lambdaPath]; + }); + } +}); + +/** + * Deployer Class + */ + +function Deployer(JAWS, lambdaPaths, stage, region) { + this._JAWS = JAWS; + this._lambdaPaths = lambdaPaths; + this._stage = stage; + this._region = region; +} + +/** + * Deployer: Deploy + */ + +Deployer.prototype.deploy = Promise.method(function() { + + var _this = this; + var arns = {}; + arns[_this._region] = {}; + + return Promise.try(function() {}) + .bind(_this) + .then(function() { + return _this._lambdaPaths; + }) + .each(function(lambdaPath) { + var packager = new Packager( + _this._JAWS, + _this._stage, + _this._region, + lambdaPath); + return packager.package() + .bind(_this) + .then(_this._createOrUpdateLambda) + .then(function(result) { + arns[_this._region][result.lambdaName] = result.arn; + }); + }) + .then(function() { + JawsCLI.log('Lambda Deployer: Done deploying lambdas in ' + _this._region); + //utils.logIfVerbose(arns); + }); +}); + +/** + * Deployer: Create Or Update Lambda (On AWS) + */ + +Deployer.prototype._createOrUpdateLambda = Promise.method(function(packagedLambda) { + + var _this = this; + var lambdaJawsJson = require(path.join(process.cwd(), packagedLambda.jawsFilePath)); + var iamRole = utils.getProjRegionConfig( + _this._JAWS._meta.projectJson.project.stages[_this._stage], + _this._region).iamRoleArnLambda; + var params = { + FunctionName: packagedLambda.fullLambdaName, + Handler: lambdaJawsJson.lambda.handler, + Role: iamRole, + Runtime: lambdaJawsJson.lambda.runtime, + Description: lambdaJawsJson.description, + MemorySize: lambdaJawsJson.lambda.memorySize, + Timeout: lambdaJawsJson.lambda.timeout, + }; + + // Instantiate Lambda + var l = new AWS.Lambda({ // Don't put into AWSUtils because we may want to use diff apiVersion + apiVersion: '2015-03-31', + }); + + // Promisify lambda functions + var lGetFunctionAsync = Promise.promisify(l.getFunction, l); + var lCreateFunctionAsync = Promise.promisify(l.createFunction, l); + var lUpdateFunctionCodeAsync = Promise.promisify(l.updateFunctionCode, l); + var lUpdateFunctionConfigurationAsync = Promise.promisify(l.updateFunctionConfiguration, l); + + // Check if Lambda exists + return lGetFunctionAsync({ FunctionName: packagedLambda.fullLambdaName }) + .then(function() { + + // Lambda exists, update it + JawsCLI.log('Lambda Deployer: Updating "' + + packagedLambda.fullLambdaName + + '" on AWS Lambda in region "' + + _this._region + + '" (Bytes: ' + + packagedLambda.zipBuffer.length + + ')'); + + return lUpdateFunctionCodeAsync({ + FunctionName: packagedLambda.fullLambdaName, + ZipFile: packagedLambda.zipBuffer, + }) + .then(function() { + return lUpdateFunctionConfigurationAsync({ + FunctionName: params.FunctionName, + Description: params.Description, + Handler: params.Handler, + MemorySize: params.MemorySize, + Role: params.Role, + Timeout: params.Timeout, + }); + }); + }) + .error(function(e) { + if (e && e.code !== 'ResourceNotFoundException') { + console.error('Error trying to create/update', packagedLambda.fullLambdaName, e); + throw new JawsError(e.message, JawsError.errorCodes.UNKNOWN); } - return willExclude; - }); - }, - }); -} + // Lambda doesn't exist, create it + JawsCLI.log('Lambda Deployer: Creating "' + + packagedLambda.fullLambdaName + + '" on AWS Lambda in region "' + + _this._region + + '" (Bytes: ' + + packagedLambda.zipBuffer.length + + ')'); -function systemJsBundle(baseDir, entries, tmpDistDir, minify, mangle, excludeFiles, ignoreFiles) { - return Promise.reject(new JawsError('Systemjs not yet supported', JawsError.errorCodes.UNKNOWN)); + params.Code = { ZipFile: packagedLambda.zipBuffer }; + return lCreateFunctionAsync(params); + }) + .then(function(data) { + return { + lambdaName: packagedLambda.fullLambdaName, + arn: data.FunctionArn, + }; + }); +}); + +/** + * Packager Class + */ + +function Packager(JAWS, stage, region, lambdaPath) { + this._JAWS = JAWS; + this._lambdaPath = lambdaPath; + this._stage = stage; + this._region = region; + this._srcDir = os.tmpdir(); + this._lambdaJson = require(path.join(process.cwd(), this._lambdaPath)); } /** - * Complie and optionally minify - * - * @param baseDir - * @param entries - * @param tmpDistDir - * @param minify - * @param mangle - * @param excludes see https://github.com/substack/browserify-handbook#ignoring-and-excluding - * @param ignores see https://github.com/substack/browserify-handbook#ignoring-and-excluding - * @returns {Promise} NodeBuffer of bundled code + * Packager: Package */ -function browserifyBundle(baseDir, entries, tmpDistDir, minify, mangle, excludes, ignores) { - var bundledFilePath = path.join(tmpDistDir, 'bundled.js'), //save for auditing - minifiedFilePath = path.join(tmpDistDir, 'minified.js'), //save for auditing - uglyOptions = { - mangle: mangle, - compress: {}, //@see http://lisperator.net/uglifyjs/compress - }, - b = browserify({ - basedir: baseDir, - entries: entries, - standalone: 'lambda', - //setup for node app (copy logic of --node in bin/args.js) - browserField: false, - builtins: false, - commondir: false, - detectGlobals: true, //default for bare in cli is true, but we dont care if its slower +Packager.prototype.package = Promise.method(function() { - //handle process https://github.com/substack/node-browserify/issues/1277 - insertGlobalVars: { - //__filename: insertGlobals.vars.__filename, - //__dirname: insertGlobals.vars.__dirname, - //process: insertGlobals.vars.process, - process: function() { - return; - }, - }, + var _this = this; + + // Set Lambda Name + _this._lambdaName = [ + _this._stage, + _this._JAWS._meta.projectJson.name, + _this._lambdaJson.lambda.functionName, + ].join('_-_'); + + // Package + return _this._createDistFolder() + .then(function() { + + // Package by runtime + switch (_this._lambdaJson.lambda.runtime) { + case 'nodejs': + return _this._packageNodeJs() + .then(function(packageData) { + packageData.fullLambdaName = _this._lambdaName; + return packageData; + }); + break; + default: + return Promise.reject(new JawsError('Unsupported lambda runtime ' + jawsJson.lambda.runtime)); + break; + } }); +}); - excludes.forEach(function(file) { +/** + * Packager: Create Dist Folder (for an individual lambda) + */ + +Packager.prototype._createDistFolder = Promise.method(function() { + + var _this = this; + + // Create dist folder + var d = new Date(); + _this._distDir = path.join(os.tmpdir(), _this._lambdaName + '@' + d.getTime()); + fs.mkdirSync(_this._distDir); + + // Status + JawsCLI.log('Lambda Deployer: Packaging "' + _this._lambdaName + '"...'); + JawsCLI.log('Lambda Deployer: Saving in dist dir ' + _this._distDir); + + // Copy entire test project to temp folder + _this._excludePatterns = _this._lambdaJson.lambda.package.excludePatterns || []; + wrench.copyDirSyncRecursive( + path.join(_this._JAWS._meta.projectRootPath, 'back'), + path.join(_this._distDir, 'back'), + { + exclude: function(name, prefix) { + if (!_this._excludePatterns.length) { + return false; + } + + var relPath = path.join( + prefix.replace(path.join(_this._distDir, 'back'), ''), + name); + + return _this._excludePatterns.some(function (sRegex) { + relPath = (relPath.charAt(0) == path.sep) ? relPath.substr(1) : relPath; + + var re = new RegExp(sRegex), + matches = re.exec(relPath); + + var willExclude = (matches && matches.length > 0); + + if (willExclude) { + JawsCLI.log('Lambda Deployer: Excluding ' + relPath); + } + + return willExclude; + }); + }, + } + ); + + // Get ENV file from S3 + JawsCLI.log('Lambda Deployer: Getting ENV file from S3 bucket: ' + + _this._JAWS._meta.projectJson.project.envVarBucket.name); + return AWSUtils.getEnvFile( + _this._JAWS._meta.profile, + _this._JAWS._meta.projectJson.project.envVarBucket.region, + _this._JAWS._meta.projectJson.project.envVarBucket.name, + _this._JAWS._meta.projectJson.name, + _this._stage) + .then(function(s3ObjData) { + + // Always add env file at root of back + var targetBackDir = path.join(_this._JAWS._meta.projectRootPath, 'back'); + fs.writeFileSync(path.join(targetBackDir, '.env'), s3ObjData.Body); + return Promise.resolve(); + }); +}); + +/** + * Packager: Package NodeJs + */ + +Packager.prototype._packageNodeJs = Promise.method(function() { + + var _this = this, + deferred = null; + + if (_this._lambdaJson.lambda.package + && _this._lambdaJson.lambda.package.optimize + && _this._lambdaJson.lambda.package.optimize.builder) { + + deferred = _this._optimizeNodeJs() + .then(function(optimizedCodeBuffer) { + + // Lambda freaks out if code doesnt end in newline + var ocbWithNewline = optimizedCodeBuffer.concat(new Buffer('\n')); + + var handlerFileName = _this._lambdaJson.lambda.handler.split('.')[0], + compressPaths = [ + + // handlerFileName is the full path lambda file including dir rel to back + { fileName: handlerFileName + '.js', data: ocbWithNewline }, + { fileName: '.env', data: path.join(_this._distDir, 'back', '.env') }, + ]; + + if (_this._lambdaJson.lambda.package.optimize.includePaths.length) { + compressPaths = compressPaths.concat(_this._generateIncludePaths()); + } + + return _this._compressCode(compressPaths); + }); + } else { + + // User chose not to optimize, zip up whatever is in back + _this._lambdaJson.lambda.package.optimize.includePaths = ['.']; + var compressPaths = _this._generateIncludePaths(); + deferred = _this._compressCode(compressPaths); + } + + return deferred + .then(function(compressedCodeBuffer) { + var zippedFilePath = path.join(_this._distDir, 'package.zip'); // Save for auditing; + fs.writeFileSync(zippedFilePath, compressedCodeBuffer); + + JawsCLI.log('Lambda Deployer: Compressed lambda written to ' + zippedFilePath); + + return Promise.resolve({ jawsFilePath: _this._lambdaPath, zipBuffer: compressedCodeBuffer }); + }); +}); + +/** + * Packager: Optimize NodeJs + */ + +Packager.prototype._optimizeNodeJs = Promise.method(function() { + + var _this = this; + + if (!_this._lambdaJson.lambda.package.optimize + || !_this._lambdaJson.lambda.package.optimize.builder) { + throw new JawsError('Cant optimize for nodejs. lambda jaws.json does not have optimize.builder set'); + } + + if (_this._lambdaJson.lambda.package.optimize.builder.toLowerCase() == 'browserify') { + return _this._browserifyBundle(); + } else { + throw new JawsError('Unsupported builder ' + builder); + } +}); + +/** + * Packager: Browserify Bundle + */ + +Packager.prototype._browserifyBundle = Promise.method(function() { + + var _this = this; + var uglyOptions = { + mangle: true, // @see http://lisperator.net/uglifyjs/compress + compress: {}, + }; + var b = browserify({ + basedir: path.join(_this._distDir, 'back'), + entries: [_this._lambdaJson.lambda.handler.split('.')[0] + '.js'], + standalone: 'lambda', + browserField: false, // Setup for node app (copy logic of --node in bin/args.js) + builtins: false, + commondir: false, + detectGlobals: true, // Default for bare in cli is true, but we don't care if its slower + insertGlobalVars: { // Handle process https://github.com/substack/node-browserify/issues/1277 + //__filename: insertGlobals.vars.__filename, + //__dirname: insertGlobals.vars.__dirname, + process: function() { + return; + }, + }, + }); + + // optimize.exclude + _this._lambdaJson.lambda.package.optimize.exclude.forEach(function(file) { b.exclude(file); }); - ignores.forEach(function(file) { + // optimize.ignore + _this._lambdaJson.lambda.package.optimize.ignore.forEach(function(file) { b.ignore(file); }); - return new Promise(function(resolve, reject) { - b - .bundle(function(err, bundledBuf) { - if (err) { - console.error('Error running browserify bundle'); - reject(err); - } else { - fs.writeFileSync(bundledFilePath, bundledBuf); - utils.logIfVerbose('bundled file wrote to ' + bundledFilePath); - if (minify) { - var result = UglifyJS.minify(bundledFilePath, uglyOptions); - if (!result || !result.code) { - reject(new JawsError('Problem uglifying code'), JawsError.errorCodes.UNKNOWN); - } + // Perform Bundle + var bundledFilePath = path.join(_this._distDir, 'bundled.js'); // Save for auditing + var minifiedFilePath = path.join(_this._distDir, 'minified.js'); // Save for auditing - fs.writeFileSync(minifiedFilePath, result.code); - utils.logIfVerbose('minified file wrote to ' + minifiedFilePath); - resolve(result.code); - } else { - resolve(bundledBuf); - } + return new Promise(function(resolve, reject) { + b.bundle(function(err, bundledBuf) { + if (err) { + console.error('Error running browserify bundle'); + reject(err); + } else { + fs.writeFileSync(bundledFilePath, bundledBuf); + JawsCLI.log('Lambda Deployer: Bundled file written to ' + bundledFilePath); + + if (_this._lambdaJson.lambda.package.optimize.exclude) { + var result = UglifyJS.minify(bundledFilePath, uglyOptions); + + if (!result || !result.code) { + reject(new JawsError('Problem uglifying code')); } - }); + + fs.writeFileSync(minifiedFilePath, result.code); + + JawsCLI.log('Lambda Deployer: Minified file written to ' + minifiedFilePath); + resolve(result.code); + } else { + resolve(bundledBuf); + } + } + }); }); -} +}); /** - * - * @param tmpDistDir - * @param includePaths relative to back dir - * @returns {[]} of {fileName: '', data: fullPath} + * Packager: Generate Include Paths */ -function generateIncludePaths(tmpDistDir, includePaths) { - var compressPaths = [], - backDirPath = path.join(tmpDistDir, 'back'); - includePaths.forEach(function(p) { +Packager.prototype._generateIncludePaths = function() { + + var _this = this; + var compressPaths = []; + var ignore = ['.ds_store']; + + _this._lambdaJson.lambda.package.optimize.includePaths.forEach(function(p) { try { - var fullPath = path.resolve(path.join(backDirPath, p)), - stats = fs.lstatSync(fullPath); + var fullPath = path.resolve(path.join(_this._distDir, 'back', p)); + var stats = fs.lstatSync(fullPath); } catch (e) { console.error('Cant find includePath ', p, e); throw e; } if (stats.isFile()) { - compressPaths.push({fileName: p, data: fullPath}); + compressPaths.push({ fileName: p, data: fullPath }); } else if (stats.isDirectory()) { wrench .readdirSyncRecursive(fullPath) .forEach(function(file) { + + // Ignore certain files + for (var i = 0; i < ignore.length; i++) { + if (file.toLowerCase().indexOf(ignore[i]) > -1) return; + } + var filePath = [fullPath, file].join('/'); if (fs.lstatSync(filePath).isFile()) { - compressPaths.push({fileName: file, data: fs.readFileSync(filePath)}); + compressPaths.push({ fileName: file, data: fs.readFileSync(filePath) }); } }); } }); return compressPaths; -} - -/** - * For each region, deploy all lambda packages - * - * @param {Jaws} JAWS - * @param {[]} packagedLambdas [{jawsFilePath:'/path/to',zipBuffer:zip,fullLambdaName:'stage_-_proj-name_-_lambdaName'}] - * @param stage - * @param {boolean} allAtOnce deploy all at once. default one at a time - * @param {string} region. Optional. If specified will only deploy to one region - * @returns {Promise} map of regions to lambda arns deployed {'us-east-1':['arn1','arn2']} - * @private - */ -module.exports._deployLambasInAllRegions = function(JAWS, packagedLambdas, stage, allAtOnce, region) { - var _this = this, - regions = (region) ? [region] : Object.keys(JAWS._meta.projectJson.project.regions), - deployedArnsByRegion = {}; - - return new Promise(function(resolve, reject) { - async.each(regions, function(region, regionCB) { //Loop over each region - _validateJawsProjAttrsForLambdaDeploy(JAWS, region, stage); - - deployedArnsByRegion[region] = []; - - AWSUtils.configAWS(JAWS._meta.profile, region); - - //Concurrent queue to deploy each lambda - var concurrentDeploys = (allAtOnce) ? 10 : 1;//fake deploy all at once, imagine 100 25meg uploads... - - var q = async.queue(function(task, cb) { - _this.createOrUpdateLambda( - task.jawsFilePath, - task.zipBuffer, - task.fullLambdaName, - utils.getProjRegionConfig(JAWS._meta.projectJson.project.stages[stage], region).iamRoleArnLambda - ) - .then(function() { - var tagCmd = require('./tag'); - - deployedArnsByRegion[region].push(task.fullLambdaName); - tagCmd.tag('lambda', task.jawsFilePath, true); - cb(); - }) - .error(function(createErr) { - console.error('Error creating/updating', task.fullLambdaName, 'in', region, createErr); - cb(createErr); - }); - }, concurrentDeploys); - - q.drain = function() { //done with all the lambdas in this region - regionCB(); - }; - - packagedLambdas.forEach(function(lambdaPackage) { - q.push({ - jawsFilePath: lambdaPackage.jawsFilePath, - zipBuffer: lambdaPackage.zipBuffer, - fullLambdaName: lambdaPackage.fullLambdaName, - }, - function(deployError) { //if deploy error for any individual, dont deploy the rest - if (deployError) { - q.kill(); - regionCB(deployError); - } - }); - }); - }, - - function(err) { //end of all regions, success or fail - if (err) { - console.error('Problem deploying to region(s)', err); - reject(new JawsError( - 'Problem deploying to region(s)', - JawsError.errorCodes.INVALID_PROJECT_JAWS - )); - } else { //Done deploying all lambdas to all regions - resolve(deployedArnsByRegion); - } - }); - }); -} - -/** - * make zips for each lambda tagged as deployable - * - * @param JAWS - * @param lambdaJawsPaths - * @param stage - * @returns {Promise} [{jawsFilePath:'/path/to',zipBuffer:zippedData,fullLambdaName:'stage_-_proj-name_-_lambdaName'}] - * @private - */ -module.exports._makeLambdaPackages = function(JAWS, lambdaJawsPaths, stage) { - var _this = this, - deployableJawsFiles = _this.getDeployableLambdas(lambdaJawsPaths), - builderQueue = []; - - deployableJawsFiles.forEach(function(jawsFile) { - builderQueue.push(_this.bundleLambda(JAWS, jawsFile, stage)); - }); - - if (!builderQueue.length) { - throw new JawsError( - 'No lambdas tagged as needing to be deployed', - JawsError.errorCodes.NO_LAMBDAS_TAGGED_DEPLOYABLE - ); - } - - return Promise.all(builderQueue); }; /** - * Filter lambda dirs down to those marked as deployable - * - * @param lambdaJawsPaths list of full paths to lambda jaws.json files - * @returns {[]} of full paths to jaws.json files - * @private + * Packager: Compress Code */ -module.exports.getDeployableLambdas = function(lambdaJawsPaths) { - return lambdaJawsPaths.filter(function(jawsPath) { - var jawsJson = require(jawsPath); - return (jawsJson.lambda.deploy === true); - }); -}; +Packager.prototype._compressCode = Promise.method(function(compressPaths) { -/** - * Optmize code. Assumes entire back directory was already copied to tmpDistDir - * - * @param tmpDistDir - * @param jawsFilePath - * @returns {Promise} Node Buffer of optimized code - */ -module.exports.optimizeNodeJs = function(tmpDistDir, jawsFilePath) { - var backDir = path.join(tmpDistDir, 'back'), - lambdaJson = require(jawsFilePath), - optimizeData = lambdaJson.lambda.package.optimize; - - if (!optimizeData || !optimizeData.builder) { - return Promise.reject( - new JawsError('Cant optimize for nodejs. lambda jaws.json does not have optimize.builder set'), - JawsError.errorCodes.UNKNOWN - ); - } - - var exclude = optimizeData.exclude || [], - ignore = optimizeData.ignore || [], - handlerFileName = lambdaJson.lambda.handler.split('.')[0], - builder = optimizeData.builder || 'browserify', - minify = (optimizeData.minify !== false), - entries = [handlerFileName + '.js'], //rel to back dir - mangle = true; - - builder = builder.toLowerCase(); - - if (builder == 'systemjs') { - return systemJsBundle(backDir, entries, tmpDistDir, minify, mangle, exclude, ignore); - } else if (builder == 'browserify') { - return browserifyBundle(backDir, entries, tmpDistDir, minify, mangle, exclude, ignore); - } else { - return Promise.reject( - new JawsError('Unsupported builder ' + builder), - JawsError.errorCodes.UNKNOWN - ); - } -}; - -/** - * compress and save as zip node buffer - * - * will always include projects env var - * - * @param stage - * @param {[]} fileNameData [{filename:'blah.js',data:String/ArrayBuffer/Uint8Array/Buffer}] - * @returns {Promise} Buffer of compressed package - */ -module.exports.compressCode = function(stage, fileNameData) { + var _this = this; var zip = new Zip(); - fileNameData.forEach(function(nc) { + compressPaths.forEach(function(nc) { zip.file(nc.fileName, nc.data); }); @@ -389,229 +623,4 @@ module.exports.compressCode = function(stage, fileNameData) { } return Promise.resolve(zippedData); -}; - -/** - * Package up nodejs lambda - * - * @param tmpDistDir - * @param lambdaJawsFilePath path to lambda specific jaws.json file - * @param stage - * @returns {Promise} {jawsFilePath: jawsFilePath,zipBuffer:zippedData} - * @private - */ -module.exports.packageNodeJs = function(tmpDistDir, lambdaJawsFilePath, stage) { - var _this = this, - jawsJson = require(lambdaJawsFilePath), - includePaths = jawsJson.lambda.package.includePaths || [], - deferred = null; - - if (jawsJson.lambda.package && jawsJson.lambda.package.optimize && jawsJson.lambda.package.optimize.builder) { - deferred = _this.optimizeNodeJs(tmpDistDir, lambdaJawsFilePath) - .then(function(optimizedCodeBuffer) { - //Lambda freaks out if code doesnt end in newline - var ocbWithNewline = optimizedCodeBuffer.concat(new Buffer('\n')); - - var handlerFileName = jawsJson.lambda.handler.split('.')[0], - compressPaths = [ - - //handlerFileName is the full path lambda file including dir rel to back - {fileName: handlerFileName + '.js', data: ocbWithNewline}, - {fileName: '.env', data: path.join(tmpDistDir, 'back', '.env')}, - ]; - - if (includePaths.length) { - compressPaths = compressPaths.concat(generateIncludePaths(tmpDistDir, includePaths)); - } - - return _this.compressCode(stage, compressPaths); - }); - } else { //user chose not to optimize, zip up whatever is in back - var compressPaths = generateIncludePaths(tmpDistDir, ['.']); - deferred = _this.compressCode(stage, compressPaths); - } - - return deferred - .then(function(compressedCodeBuffer) { - var zippedFilePath = path.join(tmpDistDir, 'package.zip'); //save for auditing; - fs.writeFileSync(zippedFilePath, compressedCodeBuffer); - - utils.logIfVerbose('compressed file wrote to ' + zippedFilePath); - - return {jawsFilePath: lambdaJawsFilePath, zipBuffer: compressedCodeBuffer}; - }); -}; - -/** - * Create lambda package for deployment - * - * @param {Jaws} JAWS - * @param jawsFilePath lambda jaws file path - * @param stage - * @returns {Promise} {jawsFilePath:jawsFilePath,zipBuffer:zippedData,fullLambdaName:'stage_-_proj-name_-_lambdaName'} - * @private - */ -module.exports.bundleLambda = function(JAWS, jawsFilePath, stage) { - var _this = this, - jawsJson = require(jawsFilePath), - projName = JAWS._meta.projectJson.name, - fullLambdaName = [stage, projName, jawsJson.lambda.functionName].join('_-_'), - d = new Date(), - tmpDistDir = path.join(os.tmpdir(), fullLambdaName + '@' + d.getTime()), - srcBackDir = path.join(JAWS._meta.projectRootPath, 'back'), - targetBackDir = path.join(tmpDistDir, 'back'), - excludePatterns = jawsJson.lambda.package.excludePatterns || []; - - console.log('Packaging', fullLambdaName, 'in dist dir', tmpDistDir); - - fs.mkdirSync(tmpDistDir); - - //Copy back dir omitting excludePatterns - copyBackDirToTmp(srcBackDir, targetBackDir, excludePatterns); - - return AWSUtils.getEnvFile( - JAWS._meta.profile, - JAWS._meta.projectJson.project.envVarBucket.region, - JAWS._meta.projectJson.project.envVarBucket.name, - projName, - stage - ) - .then(function(s3ObjData) { - //always add env file at root of back - fs.writeFileSync(path.join(targetBackDir, '.env'), s3ObjData.Body); - - switch (jawsJson.lambda.runtime) { - case 'nodejs': - return _this.packageNodeJs( - tmpDistDir, - jawsFilePath, - stage - ) - .then(function(packageData) { - packageData.fullLambdaName = fullLambdaName; - return packageData; - }); - - break; - default: - return Promise.reject(new JawsError( - 'Unsupported lambda runtime ' + jawsJson.lambda.runtime, - JawsError.errorCodes.UNKNOWN)); - break; - } - }); -}; - -/** - * Create or update lambda if it exists - * - * @param lambdaJawsFilePath - * @param zipBuffer - * @param fullLambdaName - * @param iamRole - * @returns {Promise} lambda function arn - * @private - */ -module.exports.createOrUpdateLambda = function(lambdaJawsFilePath, zipBuffer, fullLambdaName, iamRole) { - var lambdaJawsJson = require(lambdaJawsFilePath), - l = new AWS.Lambda({ //don't put into AWSUtils because we may want to use diff apiVersion - apiVersion: '2015-03-31', - }), - lambdaGetFunctionAsync = Promise.promisify(l.getFunction, l), - lUpdateFunctionCodeAsync = Promise.promisify(l.updateFunctionCode, l), - lUpdateFunctionConfigurationAsync = Promise.promisify(l.updateFunctionConfiguration, l); - - var params = { - FunctionName: fullLambdaName, - Handler: lambdaJawsJson.lambda.handler, - Role: iamRole, - Runtime: lambdaJawsJson.lambda.runtime, - Description: lambdaJawsJson.description, - MemorySize: lambdaJawsJson.lambda.memorySize, - Timeout: lambdaJawsJson.lambda.timeout, - }; - - return lambdaGetFunctionAsync({FunctionName: fullLambdaName}) - .then(function() { //Function already exists, so update :) - console.log('updating', fullLambdaName); - - return lUpdateFunctionCodeAsync({ - FunctionName: fullLambdaName, - ZipFile: zipBuffer, - }) - .then(function() { - return lUpdateFunctionConfigurationAsync({ - FunctionName: params.FunctionName, - Description: params.Description, - Handler: params.Handler, - MemorySize: params.MemorySize, - Role: params.Role, - Timeout: params.Timeout, - }); - }); - }) - .error(function(e) { - if (e && e.code !== 'ResourceNotFoundException') { - console.error('Error trying to create/update', fullLambdaName, e); - throw new JawsError(e.message, JawsError.errorCodes.UNKNOWN); - } - - //create new lambda - console.log('creating', fullLambdaName); - var lambdaCreateFunctionAsync = Promise.promisify(l.createFunction, l); - - params.Code = {ZipFile: zipBuffer}; - - return lambdaCreateFunctionAsync(params); - }) - .then(function(data) { - return data.FunctionArn; - }); -}; - -/** - * Deploy lambda at cwd or if deployAll is true does all tag'd lambdas under back dir - * - * @param {Jaws} JAWS - * @param stage - * @param {boolean} deployAllTagged optional. by default deploys cwd - * @param {boolean} allAtOnce by default one lambda will be deployed at a time - * @param {string} region. optional. Only deploy to this region. if only 1 region defined for stage will use it. - * @returns {Promise} map of region to list of lambda names deployed - */ -module.exports.deployLambdas = function(JAWS, stage, deployAllTagged, allAtOnce, region) { - var _this = this; - - if (!JAWS._meta.projectJson.project.stages[stage]) { - return Promise.reject(new JawsError('Invalid stage ' + stage, JawsError.errorCodes.UNKNOWN)); - } - - if (region) { - utils.getProjRegionConfig(JAWS._meta.projectJson.project.stages[stage], region); //make sure region defined - } else { - if (JAWS._meta.projectJson.project.stages[stage].length == 1) { //config only has 1 region - region = JAWS._meta.projectJson.project.stages[stage][0].region; - console.log('Only one region', region, 'defined for stage, so using it'); - } - } - - return utils.checkForDuplicateLambdaNames(JAWS._meta.projectRootPath) - .then(function(allLambdaJawsPaths) { - if (deployAllTagged) { - return allLambdaJawsPaths; - } else { - var tagCmd = require('./tag'); - return tagCmd.tag('lambda') - .then(function() { - return Promise.resolve([path.join(process.cwd(), 'jaws.json')]); - }); - } - }) - .then(function(lambdaJawsPaths) { //Step 1: make zips for each lambda tagged as deployable - return _this._makeLambdaPackages(JAWS, lambdaJawsPaths, stage); - }) - .then(function(packagedLambdas) { //Step 2: For each region, deploy all lambda packages - return _this._deployLambasInAllRegions(JAWS, packagedLambdas, stage, allAtOnce, region); - }); -}; - +}); diff --git a/lib/commands/generate.js b/lib/commands/generate.js index fb08b5432..25bc88fa3 100644 --- a/lib/commands/generate.js +++ b/lib/commands/generate.js @@ -8,6 +8,7 @@ // Defaults var JawsError = require('../jaws-error'), + JawsCLI = require('../utils/jaws-cli') Promise = require('bluebird'), fs = require('fs'), path = require('path'), diff --git a/lib/commands/new_project.js b/lib/commands/new_project.js index 7bd2e4957..b1c309d5d 100644 --- a/lib/commands/new_project.js +++ b/lib/commands/new_project.js @@ -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/ 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: /JAWS/envVars// + * CMD Create S3 Bucket + * (if DNE) and upload the 1st stage env var + * Format: /JAWS/envVars// * * @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: '', - author: 'Vera D. Servers http://vera.io', - description: project.name + ': An ambitious, server-less application built with the JAWS framework.', + author: 'You ', + 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: /JAWS/envVars//. 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; +}); \ No newline at end of file diff --git a/lib/commands/tag.js b/lib/commands/tag.js index b5d91b794..166d80a96 100644 --- a/lib/commands/tag.js +++ b/lib/commands/tag.js @@ -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); diff --git a/lib/utils/aws.js b/lib/utils/aws.js index ca3ca7553..6ec1aa16d 100644 --- a/lib/utils/aws.js +++ b/lib/utils/aws.js @@ -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); }; diff --git a/lib/utils/cli.js b/lib/utils/cli.js new file mode 100644 index 000000000..5a73b606d --- /dev/null +++ b/lib/utils/cli.js @@ -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(); + }); +}; diff --git a/lib/utils/index.js b/lib/utils/index.js index 66c3ffb3b..24bf7f51c 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -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); diff --git a/package.json b/package.json index ecb18055e..50c447767 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/tests/all.js b/tests/all.js index 2ec03cd6c..6298cfad2 100644 --- a/tests/all.js +++ b/tests/all.js @@ -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'); +}); \ No newline at end of file diff --git a/tests/cli/dash.js b/tests/cli/dash.js new file mode 100644 index 000000000..a0e78109c --- /dev/null +++ b/tests/cli/dash.js @@ -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); + }); + }); + }); +}); \ No newline at end of file diff --git a/tests/cli/deploy_api.js b/tests/cli/deploy_endpoint.js similarity index 97% rename from tests/cli/deploy_api.js rename to tests/cli/deploy_endpoint.js index b00374467..c7557f9ad 100644 --- a/tests/cli/deploy_api.js +++ b/tests/cli/deploy_endpoint.js @@ -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(); }) diff --git a/tests/cli/deploy_lambda.js b/tests/cli/deploy_lambda.js index 11cf571c2..546133c4a 100644 --- a/tests/cli/deploy_lambda.js +++ b/tests/cli/deploy_lambda.js @@ -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(); }) diff --git a/tests/cli/new.js b/tests/cli/new_project.js similarity index 77% rename from tests/cli/new.js rename to tests/cli/new_project.js index 2d872359a..590aff05a 100644 --- a/tests/cli/new.js +++ b/tests/cli/new_project.js @@ -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(); // }); //}); -}); \ No newline at end of file +}); diff --git a/tests/test-prj/back/lambdas/bundle/browserify/main.js b/tests/test-prj/back/lambdas/bundle/browserify/index.js similarity index 97% rename from tests/test-prj/back/lambdas/bundle/browserify/main.js rename to tests/test-prj/back/lambdas/bundle/browserify/index.js index a4b0f2dec..9f1540e03 100644 --- a/tests/test-prj/back/lambdas/bundle/browserify/main.js +++ b/tests/test-prj/back/lambdas/bundle/browserify/index.js @@ -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()); diff --git a/tests/test-prj/back/lambdas/bundle/browserify/jaws.json b/tests/test-prj/back/lambdas/bundle/browserify/jaws.json index 42f5c7344..50d09fd02 100644 --- a/tests/test-prj/back/lambdas/bundle/browserify/jaws.json +++ b/tests/test-prj/back/lambdas/bundle/browserify/jaws.json @@ -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" diff --git a/tests/test-prj/back/lambdas/bundle/nonoptimized/main.js b/tests/test-prj/back/lambdas/bundle/nonoptimized/index.js similarity index 97% rename from tests/test-prj/back/lambdas/bundle/nonoptimized/main.js rename to tests/test-prj/back/lambdas/bundle/nonoptimized/index.js index 9d429f8cf..379d3cf53 100644 --- a/tests/test-prj/back/lambdas/bundle/nonoptimized/main.js +++ b/tests/test-prj/back/lambdas/bundle/nonoptimized/index.js @@ -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()); diff --git a/tests/test-prj/back/lambdas/bundle/nonoptimized/jaws.json b/tests/test-prj/back/lambdas/bundle/nonoptimized/jaws.json index 14d2c307f..f66eab9dd 100644 --- a/tests/test-prj/back/lambdas/bundle/nonoptimized/jaws.json +++ b/tests/test-prj/back/lambdas/bundle/nonoptimized/jaws.json @@ -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" diff --git a/tests/test-prj/back/lambdas/users/signin/event.json b/tests/test-prj/back/lambdas/sessions/create/event.json similarity index 100% rename from tests/test-prj/back/lambdas/users/signin/event.json rename to tests/test-prj/back/lambdas/sessions/create/event.json diff --git a/tests/test-prj/back/lambdas/sessions/create/index.js b/tests/test-prj/back/lambdas/sessions/create/index.js new file mode 100644 index 000000000..1a9b5dbd3 --- /dev/null +++ b/tests/test-prj/back/lambdas/sessions/create/index.js @@ -0,0 +1,7 @@ +/** + * API: Sessions: Create + */ + +exports.handler = function(event, context) { + context.done(null, { message: 'This test lambda function has run successfully!' }); +}; \ No newline at end of file diff --git a/tests/test-prj/back/lambdas/users/signin/jaws.json b/tests/test-prj/back/lambdas/sessions/create/jaws.json similarity index 86% rename from tests/test-prj/back/lambdas/users/signin/jaws.json rename to tests/test-prj/back/lambdas/sessions/create/jaws.json index 038a8813a..7502851d5 100644 --- a/tests/test-prj/back/lambdas/users/signin/jaws.json +++ b/tests/test-prj/back/lambdas/sessions/create/jaws.json @@ -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": { diff --git a/tests/test-prj/back/lambdas/users/show/event.json b/tests/test-prj/back/lambdas/sessions/show/event.json similarity index 100% rename from tests/test-prj/back/lambdas/users/show/event.json rename to tests/test-prj/back/lambdas/sessions/show/event.json diff --git a/tests/test-prj/back/lambdas/sessions/show/index.js b/tests/test-prj/back/lambdas/sessions/show/index.js new file mode 100644 index 000000000..b8841d8c6 --- /dev/null +++ b/tests/test-prj/back/lambdas/sessions/show/index.js @@ -0,0 +1,7 @@ +/** + * API: Sessions: Show + */ + +exports.handler = function(event, context) { + context.done(null, { message: 'This test lambda function has run successfully!' }); +}; \ No newline at end of file diff --git a/tests/test-prj/back/lambdas/users/show/jaws.json b/tests/test-prj/back/lambdas/sessions/show/jaws.json similarity index 92% rename from tests/test-prj/back/lambdas/users/show/jaws.json rename to tests/test-prj/back/lambdas/sessions/show/jaws.json index c0ffa3f23..b30db200f 100644 --- a/tests/test-prj/back/lambdas/users/show/jaws.json +++ b/tests/test-prj/back/lambdas/sessions/show/jaws.json @@ -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", diff --git a/tests/test-prj/back/lambdas/users/signup/event.json b/tests/test-prj/back/lambdas/users/create/event.json similarity index 100% rename from tests/test-prj/back/lambdas/users/signup/event.json rename to tests/test-prj/back/lambdas/users/create/event.json diff --git a/tests/test-prj/back/lambdas/users/create/index.js b/tests/test-prj/back/lambdas/users/create/index.js new file mode 100644 index 000000000..7ea855166 --- /dev/null +++ b/tests/test-prj/back/lambdas/users/create/index.js @@ -0,0 +1,7 @@ +/** + * API: Users: Create + */ + +exports.handler = function(event, context) { + context.done(null, { message: 'This test lambda function has run successfully!' }); +}; \ No newline at end of file diff --git a/tests/test-prj/back/lambdas/users/signup/jaws.json b/tests/test-prj/back/lambdas/users/create/jaws.json similarity index 92% rename from tests/test-prj/back/lambdas/users/signup/jaws.json rename to tests/test-prj/back/lambdas/users/create/jaws.json index 7864a901c..3caec1254 100644 --- a/tests/test-prj/back/lambdas/users/signup/jaws.json +++ b/tests/test-prj/back/lambdas/users/create/jaws.json @@ -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", diff --git a/tests/test-prj/back/lambdas/users/show/index.js b/tests/test-prj/back/lambdas/users/show/index.js deleted file mode 100644 index 7b0fbaa0a..000000000 --- a/tests/test-prj/back/lambdas/users/show/index.js +++ /dev/null @@ -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!' }); - -}; \ No newline at end of file diff --git a/tests/test-prj/back/lambdas/users/signin/index.js b/tests/test-prj/back/lambdas/users/signin/index.js deleted file mode 100644 index b825fa8de..000000000 --- a/tests/test-prj/back/lambdas/users/signin/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * API: Users: Sign-In - */ - -exports.handler = function(event, context) {}; \ No newline at end of file diff --git a/tests/test-prj/back/lambdas/users/signup/index.js b/tests/test-prj/back/lambdas/users/signup/index.js deleted file mode 100644 index ffce20e64..000000000 --- a/tests/test-prj/back/lambdas/users/signup/index.js +++ /dev/null @@ -1,5 +0,0 @@ -/** - * API: Users: Sign-Up - */ - -exports.handler = function(event, context) {}; \ No newline at end of file diff --git a/tests/test_utils.js b/tests/test_utils.js index a9999cce9..f9673705c 100644 --- a/tests/test_utils.js +++ b/tests/test_utils.js @@ -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,