From bc072fb925f1301acbbaa5f78011d1dfe993c638 Mon Sep 17 00:00:00 2001 From: doapp-ryanp Date: Tue, 20 Oct 2015 22:17:50 -0500 Subject: [PATCH] new awsm.json format, alias lambda, version lambda --- docs/testing.md | 3 + lib/Jaws.js | 16 +- lib/commands/DeployEndpoint.js | 700 +++++++++--------- lib/commands/LambdaRun.js | 2 +- lib/commands/ModuleCreate.js | 75 +- lib/commands/dash.js | 2 +- lib/defaults/actions/AliasLambda.js | 196 +++++ lib/defaults/actions/DeployLambda.js | 521 ++++++------- lib/defaults/actions/VersionLambda.js | 67 +- lib/templates/action.awsm.json | 81 +- lib/utils/aws.js | 301 +++++--- lib/utils/index.js | 144 ++-- tests/all.js | 17 +- tests/config.js | 24 +- .../ProjectCreate.js} | 26 +- tests/tests/actions/VersionLambda.js | 79 ++ 16 files changed, 1323 insertions(+), 931 deletions(-) create mode 100644 lib/defaults/actions/AliasLambda.js rename tests/tests/{TestActionProjectCreate.js => actions/ProjectCreate.js} (78%) create mode 100644 tests/tests/actions/VersionLambda.js diff --git a/docs/testing.md b/docs/testing.md index fc48a6167..b11ce3b5a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -6,3 +6,6 @@ ## Running test cases +1. Set env vars defined in `tests/config.js`. By default if you do not set `TEST_JAWS_EXE_CF` no AWS resources will be created. +1. Make sure you have run `npm install` from the jaws project root +1. Run the mocha test from the CLI (`mocha tests/all.js`) or setup mocha test from your IDE. WebStorm allows you to run a debugger in the IDE for your test cases which is really handy to track down issues. \ No newline at end of file diff --git a/lib/Jaws.js b/lib/Jaws.js index 47bb3021e..cc4b67f86 100644 --- a/lib/Jaws.js +++ b/lib/Jaws.js @@ -148,14 +148,14 @@ class Jaws { */ _executeQueue(queue) { return Promise.try(() => { - return queue; - }) - .each(function(p) { - return p(); - }) - .catch(function(error) { - throw new JawsError(error); - }); + return queue; + }) + .each(function(p) { + return p(); + }) + .catch(function(error) { + throw new JawsError(error); + }); } diff --git a/lib/commands/DeployEndpoint.js b/lib/commands/DeployEndpoint.js index fe994865e..4b1e7c669 100644 --- a/lib/commands/DeployEndpoint.js +++ b/lib/commands/DeployEndpoint.js @@ -55,23 +55,23 @@ class ApiDeployer { let _this = this; return this._findTaggedEndpoints() - .bind(_this) - .then(_this._validateAndSantizeTaggedEndpoints) - .then(_this._fetchDeployedLambdas) - .then(_this._findOrCreateApi) - .then(_this._saveApiId) - .then(_this._listApiResources) - .then(_this._buildEndpoints) - .then(_this._createDeployment) - .then(function() { - return 'https://' - + _this._restApiId - + '.execute-api.' - + _this._regionJson.region - + '.amazonaws.com/' - + _this._stage - + '/'; - }); + .bind(_this) + .then(_this._validateAndSantizeTaggedEndpoints) + .then(_this._fetchDeployedLambdas) + .then(_this._findOrCreateApi) + .then(_this._saveApiId) + .then(_this._listApiResources) + .then(_this._buildEndpoints) + .then(_this._createDeployment) + .then(function() { + return 'https://' + + _this._restApiId + + '.execute-api.' + + _this._regionJson.region + + '.amazonaws.com/' + + _this._stage + + '/'; + }); } /** @@ -83,26 +83,26 @@ class ApiDeployer { let _this = this; return JawsUtils.findAllEndpoints(_this._prjRootPath) - .each(function(endpoint) { + .each(function(endpoint) { - let eJson = require(endpoint); - if (eJson.apiGateway.deploy) _this._endpoints.push(eJson); + let eJson = require(endpoint); + if (eJson.apiGateway.deploy) _this._endpoints.push(eJson); - }).then(function() { + }).then(function() { - if (!_this._endpoints.length) { - throw new JawsError( - 'You have no tagged endpoints', - JawsError.errorCodes.UNKNOWN); - } + if (!_this._endpoints.length) { + throw new JawsError( + 'You have no tagged endpoints', + JawsError.errorCodes.UNKNOWN); + } - JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage + ' - ' - + _this._regionJson.region - + '": found ' - + _this._endpoints.length + ' endpoints to deploy'); - }); + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + ' - ' + + _this._regionJson.region + + '": found ' + + _this._endpoints.length + ' endpoints to deploy'); + }); } /** @@ -113,14 +113,14 @@ class ApiDeployer { _fetchDeployedLambdas() { let _this = this; - return AWSUtils.cfGetLambdaNames( - _this._JAWS._profile, - _this._regionJson.region, - _this._stage + '-' + _this._JAWS._projectJson.name + '-l' - ) - .then(lambdas => { - this._lambdas = lambdas; - }); + return AWSUtils.cfGetLambdaResourceSummaries( + _this._JAWS._profile, + _this._regionJson.region, + AWSUtils.cfGetLambdasStackName(_this._stage, _this._JAWS._projectJson.name) + ) + .then(lambdas => { + this._lambdas = lambdas; + }); } /** @@ -140,13 +140,13 @@ class ApiDeployer { // Validate attributes if (!e.Type - || !e.Path - || !e.Method - || !e.AuthorizationType - || typeof e.ApiKeyRequired === 'undefined') { + || !e.Path + || !e.Method + || !e.AuthorizationType + || typeof e.ApiKeyRequired === 'undefined') { return Promise.reject(new JawsError( - 'Missing one of many required endpoint attributes: Type, Path, Method, AuthorizationType, ApiKeyRequired', - JawsError.errorCodes.UNKNOWN)); + 'Missing one of many required endpoint attributes: Type, Path, Method, AuthorizationType, ApiKeyRequired', + JawsError.errorCodes.UNKNOWN)); } // Sanitize path @@ -194,16 +194,16 @@ class ApiDeployer { // Show existing REST API return this.ApiClient.showRestApi(_this._restApiId) - .then(function(response) { + .then(function(response) { - _this._restApiId = response.id; - JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage + ' - ' - + _this._regionJson.region - + '": found existing REST API on AWS API Gateway with ID: ' - + response.id); - }); + _this._restApiId = response.id; + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + ' - ' + + _this._regionJson.region + + '": found existing REST API on AWS API Gateway with ID: ' + + response.id); + }); } else { // Create REST API @@ -218,11 +218,11 @@ class ApiDeployer { _this._restApiId = response.id; JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage + ' - ' - + _this._regionJson.region - + '": created a new REST API on AWS API Gateway with ID: ' - + response.id); + 'Endpoint Deployer: "' + + _this._stage + ' - ' + + _this._regionJson.region + + '": created a new REST API on AWS API Gateway with ID: ' + + response.id); }); } } @@ -238,27 +238,27 @@ class ApiDeployer { // List all Resources for this REST API return this.ApiClient.listResources(_this._restApiId) - .then(function(response) { + .then(function(response) { - // Parse API Gateway's HAL response - _this._resources = response._embedded.item; - if (!Array.isArray(_this._resources)) _this._resources = [_this._resources]; + // Parse API Gateway's HAL response + _this._resources = response._embedded.item; + if (!Array.isArray(_this._resources)) _this._resources = [_this._resources]; - // Get Parent Resource ID - for (let i = 0; i < _this._resources.length; i++) { - if (_this._resources[i].path === '/') { - _this._parentResourceId = _this._resources[i].id; + // Get Parent Resource ID + for (let i = 0; i < _this._resources.length; i++) { + if (_this._resources[i].path === '/') { + _this._parentResourceId = _this._resources[i].id; + } } - } - JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage + ' - ' - + _this._regionJson.region - + '": found ' - + _this._resources.length - + ' existing resources on API Gateway'); - }); + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + ' - ' + + _this._regionJson.region + + '": found ' + + _this._resources.length + + ' existing resources on API Gateway'); + }); } /** @@ -275,18 +275,18 @@ class ApiDeployer { }).each(function(endpoint) { return _this._createEndpointResources(endpoint) - .bind(_this) - .then(_this._createEndpointMethod) - .then(_this._createEndpointIntegration) - .then(_this._manageLambdaAccessPolicy) - .then(_this._createEndpointMethodResponses) - .then(_this._createEndpointMethodIntegResponses) - .then(function() { + .bind(_this) + .then(_this._createEndpointMethod) + .then(_this._createEndpointIntegration) + .then(_this._manageLambdaAccessPolicy) + .then(_this._createEndpointMethodResponses) + .then(_this._createEndpointMethodIntegResponses) + .then(function() { - // Clean-up hack - // TODO figure out how "apig" temp property is being written to the awsm's json and remove that - if (endpoint.apiGateway.apig) delete endpoint.apiGateway.apig; - }); + // Clean-up hack + // TODO figure out how "apig" temp property is being written to the awsm's json and remove that + if (endpoint.apiGateway.apig) delete endpoint.apiGateway.apig; + }); }); } @@ -358,21 +358,21 @@ class ApiDeployer { // Create Resource return _this.ApiClient.createResource( - _this._restApiId, - endpoint.apiGateway.apig.parentResourceId, - eResource) - .then(function(response) { + _this._restApiId, + endpoint.apiGateway.apig.parentResourceId, + eResource) + .then(function(response) { - // Add resource to _this.resources and callback - _this._resources.push(response); - JawsCli.log( - 'Endpoint Deployer: "' + - _this._stage + ' - ' - + _this._regionJson.region - + ' - ' + endpoint.apiGateway.cloudFormation.Path + '": ' - + 'created resource: ' - + response.pathPart); - }); + // Add resource to _this.resources and callback + _this._resources.push(response); + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + ' - ' + + _this._regionJson.region + + ' - ' + endpoint.apiGateway.cloudFormation.Path + '": ' + + 'created resource: ' + + response.pathPart); + }); }).then(function() { @@ -411,42 +411,42 @@ class ApiDeployer { } return _this.ApiClient.showMethod( - _this._restApiId, - endpoint.apiGateway.apig.resource.id, - endpoint.apiGateway.cloudFormation.Method) - .then(function() { + _this._restApiId, + endpoint.apiGateway.apig.resource.id, + endpoint.apiGateway.cloudFormation.Method) + .then(function() { - return _this.ApiClient.deleteMethod( - _this._restApiId, - endpoint.apiGateway.apig.resource.id, - endpoint.apiGateway.cloudFormation.Method) - .then(function() { - _this.ApiClient.putMethod( + return _this.ApiClient.deleteMethod( + _this._restApiId, + endpoint.apiGateway.apig.resource.id, + endpoint.apiGateway.cloudFormation.Method) + .then(function() { + _this.ApiClient.putMethod( + _this._restApiId, + endpoint.apiGateway.apig.resource.id, + endpoint.apiGateway.cloudFormation.Method, + methodBody); + }); + }, function() { + + return _this.ApiClient.putMethod( _this._restApiId, endpoint.apiGateway.apig.resource.id, endpoint.apiGateway.cloudFormation.Method, methodBody); - }); - }, function() { + }) + .delay(250) // API Gateway takes time to delete Methods. Might have to increase this. + .then(function(response) { - return _this.ApiClient.putMethod( - _this._restApiId, - endpoint.apiGateway.apig.resource.id, - endpoint.apiGateway.cloudFormation.Method, - methodBody); - }) - .delay(250) // API Gateway takes time to delete Methods. Might have to increase this. - .then(function(response) { - - JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage + ' - ' - + _this._regionJson.region - + ' - ' + endpoint.apiGateway.cloudFormation.Path + '": ' - + 'created method: ' - + endpoint.apiGateway.cloudFormation.Method); - return endpoint; - }); + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + ' - ' + + _this._regionJson.region + + ' - ' + endpoint.apiGateway.cloudFormation.Path + '": ' + + 'created method: ' + + endpoint.apiGateway.cloudFormation.Method); + return endpoint; + }); } /** @@ -464,7 +464,7 @@ class ApiDeployer { if (endpoint.type === 'lambda' || typeof endpoint.lambda !== 'undefined') { // Find Deployed Lambda and its function name - let cfLogicalResourceId = JawsUtils.generateLambdaName(endpoint); + let cfLogicalResourceId = JawsUtils.getLambdaName(endpoint); let lambda = null; for (let i = 0; i < _this._lambdas.length; i++) { @@ -476,7 +476,7 @@ class ApiDeployer { // If no deployed lambda found, throw error if (!lambda) { return Promise.reject(new JawsError('Could not find a lambda deployed in this stage/region with this function name: ' - + cfLogicalResourceId)); + + cfLogicalResourceId)); } endpoint.apiGateway.apig.lambda = lambda; @@ -513,28 +513,28 @@ class ApiDeployer { // Create Integration return _this.ApiClient.putIntegration( - _this._restApiId, - endpoint.apiGateway.apig.resource.id, - endpoint.apiGateway.cloudFormation.Method, - integrationBody) - .then(function(response) { + _this._restApiId, + endpoint.apiGateway.apig.resource.id, + endpoint.apiGateway.cloudFormation.Method, + integrationBody) + .then(function(response) { - // Save integration to apig property - endpoint.apiGateway.apig.integration = response; - JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage + ' - ' - + _this._regionJson.region - + ' - ' + endpoint.apiGateway.cloudFormation.Path + '": ' - + 'created integration with the type: ' - + endpoint.apiGateway.cloudFormation.Type); - return endpoint; - }) - .catch(function(error) { - throw new JawsError( - error.message, - JawsError.errorCodes.UNKNOWN); - }); + // Save integration to apig property + endpoint.apiGateway.apig.integration = response; + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + ' - ' + + _this._regionJson.region + + ' - ' + endpoint.apiGateway.cloudFormation.Path + '": ' + + 'created integration with the type: ' + + endpoint.apiGateway.cloudFormation.Type); + return endpoint; + }) + .catch(function(error) { + throw new JawsError( + error.message, + JawsError.errorCodes.UNKNOWN); + }); } /** @@ -549,65 +549,65 @@ class ApiDeployer { return Promise.try(function() { - // Collect Response Keys - if (endpoint.apiGateway.cloudFormation.Responses) return Object.keys(endpoint.apiGateway.cloudFormation.Responses); - else return []; - }) - .each(function(responseKey) { + // Collect Response Keys + if (endpoint.apiGateway.cloudFormation.Responses) return Object.keys(endpoint.apiGateway.cloudFormation.Responses); + else return []; + }) + .each(function(responseKey) { - let thisResponse = endpoint.apiGateway.cloudFormation.Responses[responseKey]; - let methodResponseBody = {}; + let thisResponse = endpoint.apiGateway.cloudFormation.Responses[responseKey]; + let methodResponseBody = {}; - // If Request Params, add them - if (thisResponse.responseParameters) { + // If Request Params, add them + if (thisResponse.responseParameters) { - methodResponseBody.responseParameters = {}; + methodResponseBody.responseParameters = {}; - // Format Response Parameters per APIG API's Expectations - for (let prop in thisResponse.responseParameters) { - methodResponseBody.responseParameters[prop] = true; + // Format Response Parameters per APIG API's Expectations + for (let prop in thisResponse.responseParameters) { + methodResponseBody.responseParameters[prop] = true; + } } - } - // If Request models, add them - if (thisResponse.responseModels) { + // If Request models, add them + if (thisResponse.responseModels) { - methodResponseBody.responseModels = {}; + methodResponseBody.responseModels = {}; - // Format Response Models per APIG API's Expectations - for (let name in thisResponse.responseModels) { - let value = thisResponse.responseModels[name]; - methodResponseBody.responseModels[name] = value; + // Format Response Models per APIG API's Expectations + for (let name in thisResponse.responseModels) { + let value = thisResponse.responseModels[name]; + methodResponseBody.responseModels[name] = value; + } } - } - // Create Method Response - return _this.ApiClient.putMethodResponse( - _this._restApiId, - endpoint.apiGateway.apig.resource.id, - endpoint.apiGateway.cloudFormation.Method, - thisResponse.statusCode, - methodResponseBody) - .then(function() { - JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage - + ' - ' - + _this._regionJson.region - + ' - ' - + endpoint.apiGateway.cloudFormation.Path - + '": ' - + 'created method response'); - }) - .catch(function(error) { - throw new JawsError( - error.message, - JawsError.errorCodes.UNKNOWN); - }); - }) - .then(function() { - return endpoint; - }); + // Create Method Response + return _this.ApiClient.putMethodResponse( + _this._restApiId, + endpoint.apiGateway.apig.resource.id, + endpoint.apiGateway.cloudFormation.Method, + thisResponse.statusCode, + methodResponseBody) + .then(function() { + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + + ' - ' + + _this._regionJson.region + + ' - ' + + endpoint.apiGateway.cloudFormation.Path + + '": ' + + 'created method response'); + }) + .catch(function(error) { + throw new JawsError( + error.message, + JawsError.errorCodes.UNKNOWN); + }); + }) + .then(function() { + return endpoint; + }); } /** @@ -622,47 +622,47 @@ class ApiDeployer { return Promise.try(function() { - // Collect Response Keys - if (endpoint.apiGateway.cloudFormation.Responses) return Object.keys(endpoint.apiGateway.cloudFormation.Responses); - else return []; - }) - .each(function(responseKey) { + // Collect Response Keys + if (endpoint.apiGateway.cloudFormation.Responses) return Object.keys(endpoint.apiGateway.cloudFormation.Responses); + else return []; + }) + .each(function(responseKey) { - let thisResponse = endpoint.apiGateway.cloudFormation.Responses[responseKey]; - let integrationResponseBody = {}; + let thisResponse = endpoint.apiGateway.cloudFormation.Responses[responseKey]; + let integrationResponseBody = {}; - // Add Response Parameters - integrationResponseBody.responseParameters = thisResponse.responseParameters || {}; + // Add Response Parameters + integrationResponseBody.responseParameters = thisResponse.responseParameters || {}; - // Add Response Templates - integrationResponseBody.responseTemplates = thisResponse.responseTemplates || {}; + // Add Response Templates + integrationResponseBody.responseTemplates = thisResponse.responseTemplates || {}; - // Add SelectionPattern - integrationResponseBody.selectionPattern = thisResponse.selectionPattern || (responseKey === 'default' ? null : responseKey); + // Add SelectionPattern + integrationResponseBody.selectionPattern = thisResponse.selectionPattern || (responseKey === 'default' ? null : responseKey); - // Create Integration Response - return _this.ApiClient.putIntegrationResponse( - _this._restApiId, - endpoint.apiGateway.apig.resource.id, - endpoint.apiGateway.cloudFormation.Method, - thisResponse.statusCode, - integrationResponseBody) - .then(function() { - JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage - + ' - ' - + _this._regionJson.region - + ' - ' - + endpoint.apiGateway.cloudFormation.Path - + '": ' - + 'created method integration response'); - }).catch(function(error) { - throw new JawsError( - error.message, - JawsError.errorCodes.UNKNOWN); - }); - }); + // Create Integration Response + return _this.ApiClient.putIntegrationResponse( + _this._restApiId, + endpoint.apiGateway.apig.resource.id, + endpoint.apiGateway.cloudFormation.Method, + thisResponse.statusCode, + integrationResponseBody) + .then(function() { + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + + ' - ' + + _this._regionJson.region + + ' - ' + + endpoint.apiGateway.cloudFormation.Path + + '": ' + + 'created method integration response'); + }).catch(function(error) { + throw new JawsError( + error.message, + JawsError.errorCodes.UNKNOWN); + }); + }); } /** @@ -679,9 +679,9 @@ class ApiDeployer { if (!endpoint.apiGateway.apig.lambda) return Promise.resolve(endpoint); return this._getLambdaAccessPolicy(endpoint) - .bind(_this) - .then(_this._removeLambdaAccessPolicy) - .then(_this._updateLambdaAccessPolicy); + .bind(_this) + .then(_this._removeLambdaAccessPolicy) + .then(_this._updateLambdaAccessPolicy); } /** @@ -701,16 +701,16 @@ class ApiDeployer { let _this = this; return AWSUtils.lambdaGetPolicy( - _this._JAWS._meta.profile, - _this._regionJson.region, - endpoint.apiGateway.apig.lambda.PhysicalResourceId) - .then(function(data) { - endpoint.apiGateway.apig.lambda.Policy = JSON.parse(data.Policy); - return endpoint; - }) - .catch(function(error) { - return endpoint; - }); + _this._JAWS._meta.profile, + _this._regionJson.region, + endpoint.apiGateway.apig.lambda.PhysicalResourceId) + .then(function(data) { + endpoint.apiGateway.apig.lambda.Policy = JSON.parse(data.Policy); + return endpoint; + }) + .catch(function(error) { + return endpoint; + }); } /** @@ -737,27 +737,27 @@ class ApiDeployer { if (!statement) return Promise.resolve(endpoint); return AWSUtils.lambdaRemovePermission( - _this._JAWS._meta.profile, - _this._regionJson.region, - endpoint.apiGateway.apig.lambda.PhysicalResourceId, - 'jaws-apigateway-access') - .then(function(data) { + _this._JAWS._meta.profile, + _this._regionJson.region, + endpoint.apiGateway.apig.lambda.PhysicalResourceId, + 'jaws-apigateway-access') + .then(function(data) { - JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage - + ' - ' - + _this._regionJson.region - + ' - ' - + endpoint.apiGateway.cloudFormation.Path - + '": removed existing lambda access policy statement'); + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + + ' - ' + + _this._regionJson.region + + ' - ' + + endpoint.apiGateway.cloudFormation.Path + + '": removed existing lambda access policy statement'); - return endpoint; - }) - .catch(function(error) { - console.log(error); - return endpoint; - }); + return endpoint; + }) + .catch(function(error) { + console.log(error); + return endpoint; + }); } /** @@ -781,35 +781,35 @@ class ApiDeployer { statement.Principal = 'apigateway.amazonaws.com'; statement.StatementId = 'jaws-apigateway-access'; statement.SourceArn = 'arn:aws:execute-api:' - + _this._regionJson.region - + ':' - + _this._awsAccountNumber - + ':' - + _this._restApiId - + '/*/' - + endpoint.apiGateway.cloudFormation.Method - + '/' - + endpoint.apiGateway.cloudFormation.Path; + + _this._regionJson.region + + ':' + + _this._awsAccountNumber + + ':' + + _this._restApiId + + '/*/' + + endpoint.apiGateway.cloudFormation.Method + + '/' + + endpoint.apiGateway.cloudFormation.Path; return AWSUtils.lambdaAddPermission( - _this._JAWS._meta.profile, - _this._regionJson.region, - statement) - .then(function(data) { - JawsCli.log( - 'Endpoint Deployer: "' - + _this._stage - + ' - ' - + _this._regionJson.region - + ' - ' - + endpoint.apiGateway.cloudFormation.Path - + '": created new lambda access policy statement'); - return endpoint; - }) - .catch(function(error) { - console.log(error); - return endpoint; - }); + _this._JAWS._meta.profile, + _this._regionJson.region, + statement) + .then(function(data) { + JawsCli.log( + 'Endpoint Deployer: "' + + _this._stage + + ' - ' + + _this._regionJson.region + + ' - ' + + endpoint.apiGateway.cloudFormation.Path + + '": created new lambda access policy statement'); + return endpoint; + }) + .catch(function(error) { + console.log(error); + return endpoint; + }); } /** @@ -828,14 +828,14 @@ class ApiDeployer { }; return _this.ApiClient.createDeployment(_this._restApiId, deployment) - .then(function(response) { - return response; - }) - .catch(function(error) { - throw new JawsError( - error.message, - JawsError.errorCodes.UNKNOWN); - }); + .then(function(response) { + return response; + }) + .catch(function(error) { + throw new JawsError( + error.message, + JawsError.errorCodes.UNKNOWN); + }); } } @@ -863,46 +863,46 @@ const CMD = class DeployEndpoint extends ProjectCmd { let _this = this; return this._JAWS.validateProject() - .bind(_this) - .then(function() { - // If !allTagged, tag current directory - if (!_this._allTagged) { - return Tag.tag('endpoint', null, false); - } - }) - .then(_this._promptStage) - .then(_this._promptRegions) - .then(function() { - return _this._regions; - }) - .each(function(regionJson) { + .bind(_this) + .then(function() { + // If !allTagged, tag current directory + if (!_this._allTagged) { + return Tag.tag('endpoint', null, false); + } + }) + .then(_this._promptStage) + .then(_this._promptRegions) + .then(function() { + return _this._regions; + }) + .each(function(regionJson) { - JawsCli.log(`Endpoint Deployer: Deploying endpoint(s) to region "${regionJson.region}"...`); + JawsCli.log(`Endpoint Deployer: Deploying endpoint(s) to region "${regionJson.region}"...`); - let deployer = new ApiDeployer( - _this._JAWS, - _this._stage, - regionJson, - _this._prjRootPath, - _this._prjJson, - _this._prjCreds - ); + let deployer = new ApiDeployer( + _this._JAWS, + _this._stage, + regionJson, + _this._prjRootPath, + _this._prjJson, + _this._prjCreds + ); - return deployer.deploy() - .then(function(url) { - JawsCli.log('Endpoint Deployer: Endpoints for stage "' - + _this._stage - + '" successfully deployed to API Gateway in the region "' - + regionJson.region - + '". Access them @ ' - + url); - }); - }) - .then(function() { - // Untag All tagged endpoints - let CmdTag = new Tag(_this._JAWS, 'endpoint'); - return _this._allTagged ? CmdTag.tagAll(true) : Tag.tag('endpoint', null, true); - }); + return deployer.deploy() + .then(function(url) { + JawsCli.log('Endpoint Deployer: Endpoints for stage "' + + _this._stage + + '" successfully deployed to API Gateway in the region "' + + regionJson.region + + '". Access them @ ' + + url); + }); + }) + .then(function() { + // Untag All tagged endpoints + let CmdTag = new Tag(_this._JAWS, 'endpoint'); + return _this._allTagged ? CmdTag.tagAll(true) : Tag.tag('endpoint', null, true); + }); } _promptStage() { @@ -933,11 +933,11 @@ const CMD = class DeployEndpoint extends ProjectCmd { } return JawsCli.select('Select a stage to deploy to: ', choices, false) - .then(function(selectedStages) { - if (selectedStages && (selectedStages.length > 0)) { - _this._stage = selectedStages[0].value; - } - }); + .then(function(selectedStages) { + if (selectedStages && (selectedStages.length > 0)) { + _this._stage = selectedStages[0].value; + } + }); } _promptRegions() { diff --git a/lib/commands/LambdaRun.js b/lib/commands/LambdaRun.js index 40709766f..a4b33e618 100644 --- a/lib/commands/LambdaRun.js +++ b/lib/commands/LambdaRun.js @@ -15,7 +15,7 @@ const GlobalCmd = require('./GlobalCmd'), function simulateNodeJs(awsmJson, handler, event) { return new Promise(function(resolve, reject) { - let lambdaName = utils.generateLambdaName(awsmJson); + let lambdaName = utils.getLambdaName(awsmJson); utils.jawsDebug('Testing', lambdaName); handler(event, context(lambdaName, function(err, result) { diff --git a/lib/commands/ModuleCreate.js b/lib/commands/ModuleCreate.js index 04841d689..973874a26 100644 --- a/lib/commands/ModuleCreate.js +++ b/lib/commands/ModuleCreate.js @@ -1,24 +1,23 @@ 'use strict'; -//TODO: make this extend GlobalCmd but it will require some decent sized refactoring in bin/jaws -//plus need to handle if CWD is in a jaws project, to intelligently create it in aws_modules +//TODO: account for changes in new awsm.json structure /** * Create jaws module * */ const ProjectCmd = require('./ProjectCmd.js'), - JawsError = require('../jaws-error'), - JawsCLI = require('../utils/cli'), - Promise = require('bluebird'), - fs = require('fs'), - path = require('path'), - utils = require('../utils'); + JawsError = require('../jaws-error'), + JawsCLI = require('../utils/cli'), + Promise = require('bluebird'), + fs = require('fs'), + path = require('path'), + utils = require('../utils'); let supportedRuntimes = { nodejs: { defaultPkgMgr: 'npm', - validPkgMgrs: ['npm'] + validPkgMgrs: ['npm'] } }; @@ -38,20 +37,20 @@ const CMD = class ModuleCreate extends ProjectCmd { throw new JawsError(`Unsupported runtime "${runtime}"`, JawsError.errorCodes.UNKNOWN); } - let _this = this, + let _this = this, supportedRuntimeObj = supportedRuntimes[runtime]; - this._module = { - name: name, + this._module = { + name: name, runtime: runtime, - action: action, - pkgMgr: pkgMgr, + action: action, + pkgMgr: pkgMgr, modType: modType, }; - this._prompts = { + this._prompts = { properties: {}, }; - this.Prompter = JawsCLI.prompt(); + this.Prompter = JawsCLI.prompt(); this.Prompter.override = {}; if (pkgMgr && supportedRuntimeObj.validPkgMgrs.indexOf(_this._module.pkgMgr) == -1) { @@ -81,14 +80,14 @@ const CMD = class ModuleCreate extends ProjectCmd { * @private */ _createPackageMgrSkeleton() { - let _this = this, + let _this = this, deferredWrites = []; switch (_this._module.runtime) { case 'nodejs': if (_this._module.pkgMgr == 'npm') { - let modulePath = path.join( + let modulePath = path.join( _this._JAWS._meta.projectRootPath, //TOOD: make this CWD if not in a JAWS project 'node_modules', _this._module.name); @@ -101,9 +100,9 @@ const CMD = class ModuleCreate extends ProjectCmd { // Create module package.json if DNE in node_module if (!utils.fileExistsSync(path.join(modulePath, 'package.json'))) { - let packageJsonTemplate = utils.readAndParseJsonSync(path.join(templatesPath, 'nodejs', 'package.json')); - packageJsonTemplate.name = _this._name; - packageJsonTemplate.description = 'An aws-module'; + let packageJsonTemplate = utils.readAndParseJsonSync(path.join(templatesPath, 'nodejs', 'package.json')); + packageJsonTemplate.name = _this._name; + packageJsonTemplate.description = 'An aws-module'; packageJsonTemplate.dependencies = {}; if (packageJsonTemplate.private) delete packageJsonTemplate.private; deferredWrites.push( @@ -116,7 +115,7 @@ const CMD = class ModuleCreate extends ProjectCmd { // Create module awsm.json if DNE in node_module if (!utils.fileExistsSync(path.join(modulePath, 'awsm.json'))) { - let moduleTemplateJson = utils.readAndParseJsonSync(path.join(templatesPath, 'module.awsm.json')); + let moduleTemplateJson = utils.readAndParseJsonSync(path.join(templatesPath, 'module.awsm.json')); moduleTemplateJson.name = _this._module.name; deferredWrites.push( utils.writeFile( @@ -145,11 +144,11 @@ const CMD = class ModuleCreate extends ProjectCmd { 'action.awsm.json')); // Create action awsm.json - actionTemplateJson.apiGateway.cloudFormation.Path = _this._module.name + '/' + _this._module.action; + actionTemplateJson.apiGateway.cloudFormation.Path = _this._module.name + '/' + _this._module.action; actionTemplateJson.apiGateway.cloudFormation.Method = 'GET'; - actionTemplateJson.apiGateway.cloudFormation.Type = 'AWS'; - actionTemplateJson.lambda.cloudFormation.Runtime = 'nodejs'; - actionTemplateJson.lambda.cloudFormation.Handler = path.join( + actionTemplateJson.apiGateway.cloudFormation.Type = 'AWS'; + actionTemplateJson.lambda.cloudFormation.Runtime = 'nodejs'; + actionTemplateJson.lambda.cloudFormation.Handler = path.join( 'aws_modules', _this._module.name, _this._module.action, @@ -157,7 +156,7 @@ const CMD = class ModuleCreate extends ProjectCmd { // Create handler.js, index.js, event.json, package.json let handlerJs = fs.readFileSync(path.join(templatesPath, 'nodejs', 'handler.js')); - let indexJs = fs.readFileSync(path.join(templatesPath, 'nodejs', 'index.js')); + let indexJs = fs.readFileSync(path.join(templatesPath, 'nodejs', 'index.js')); deferredWrites.push( utils.writeFile( @@ -201,17 +200,17 @@ const CMD = class ModuleCreate extends ProjectCmd { * @private */ _createSkeleton() { - let _this = this, + let _this = this, writeFilesDeferred = []; // Fetch skeleton resources - let templatesPath = path.join(__dirname, '..', 'templates'); + let templatesPath = path.join(__dirname, '..', 'templates'); let actionTemplateJson = utils.readAndParseJsonSync(path.join(templatesPath, 'action.awsm.json')); - let modulePath = path.join( + let modulePath = path.join( _this._JAWS._meta.projectRootPath, //TOOD: make this CWD if not in a JAWS project 'aws_modules', _this._module.name); - let actionPath = path.join(modulePath, _this._module.action); + let actionPath = path.join(modulePath, _this._module.action); // If module/action already exists, throw error if (utils.dirExistsSync(actionPath)) { @@ -225,7 +224,7 @@ const CMD = class ModuleCreate extends ProjectCmd { // If module awsm.json doesn't exist, create it if (!utils.fileExistsSync(path.join(modulePath, 'awsm.json'))) { - let moduleTemplateJson = utils.readAndParseJsonSync(path.join(templatesPath, 'module.awsm.json')); + let moduleTemplateJson = utils.readAndParseJsonSync(path.join(templatesPath, 'module.awsm.json')); moduleTemplateJson.name = _this._module.name; writeFilesDeferred.push( utils.writeFile( @@ -237,11 +236,17 @@ const CMD = class ModuleCreate extends ProjectCmd { writeFilesDeferred.push(actionPath); // Create action awsm.json - actionTemplateJson.apiGateway.cloudFormation.Path = _this._module.name + '/' + _this._module.action; + actionTemplateJson.apiGateway.cloudFormation.Path = _this._module.name + '/' + _this._module.action; actionTemplateJson.apiGateway.cloudFormation.Method = 'GET'; - actionTemplateJson.apiGateway.cloudFormation.Type = 'AWS'; + actionTemplateJson.apiGateway.cloudFormation.Type = 'AWS'; if (['lambda', 'both'].indexOf(_this._module.modType) != -1) { + //We prefix with an l to make sure the CloudFormation resource map index is unique + //we will probably have API gateway resources in their own CF JSON but we dont want to + //make that decsision until CF has API gateway support + actionTemplateJson.lambda.name = 'l' + _this._module.name.charAt(0).toUpperCase() + _this._module.name.slice(1) + _this._module.action.charAt(0).toUpperCase() + _this._module.action.slice(1); + actionTemplateJson.lambda.cloudFormation.Runtime = _this._module.runtime; + // Create files for lambda actions switch (_this._module.runtime) { case 'nodejs': @@ -256,7 +261,7 @@ const CMD = class ModuleCreate extends ProjectCmd { // Create handler.js, index.js, event.json, package.json let handlerJs = fs.readFileSync(path.join(templatesPath, 'nodejs', 'handler.js')); - let indexJs = fs.readFileSync(path.join(templatesPath, 'nodejs', 'index.js')); + let indexJs = fs.readFileSync(path.join(templatesPath, 'nodejs', 'index.js')); writeFilesDeferred.push( utils.writeFile(path.join(actionPath, 'handler.js'), handlerJs), diff --git a/lib/commands/dash.js b/lib/commands/dash.js index 32a2f015e..f33e16f7c 100644 --- a/lib/commands/dash.js +++ b/lib/commands/dash.js @@ -164,7 +164,7 @@ CMD.prototype._prepareResources = Promise.method(function() { key: ' L) ', value: jsonPaths[i], type: 'lambda', - label: JawsUtils.generateLambdaName(json), + label: JawsUtils.getLambdaName(json), }; // Create path diff --git a/lib/defaults/actions/AliasLambda.js b/lib/defaults/actions/AliasLambda.js new file mode 100644 index 000000000..07ff40d78 --- /dev/null +++ b/lib/defaults/actions/AliasLambda.js @@ -0,0 +1,196 @@ +'use strict'; + +/** + * Action: AliasLambda + */ + +const JawsPlugin = require('../../JawsPlugin'), + JawsError = require('../../jaws-error'), + JawsCLI = require('../../utils/cli'), + BbPromise = require('bluebird'), + path = require('path'), + os = require('os'), + AWSUtils = require('../../utils/aws'), + JawsUtils = require('../../utils/index'); + +let fs = require('fs'); +BbPromise.promisifyAll(fs); + +class AliasLambda extends JawsPlugin { + + /** + * @param Jaws class object + * @param config object + */ + + constructor(Jaws, config) { + super(Jaws, config); + this._stage = null; + this._region = null; + this._lambdaLogicalIdsToAlias = []; + } + + /** + * Define your plugins name + * + * @returns {string} + */ + static getName() { + return 'jaws.core.' + AliasLambda.name; + } + + /** + * @returns {Promise} upon completion of all registrations + */ + + registerActions() { + this.Jaws.action(this.lambdaCreateAlias.bind(this), { + handler: 'lambdaCreateAlias', + description: `Version lambda at CWD +usage: jaws lambda alias `, + context: 'lambda', + contextAction: 'alias', + options: [ + { + option: 'stage', + shortcut: 's', + description: 'Optional if only one stage is defined in project' + }, { + option: 'region', + shortcut: 'r', + description: 'Optional. Default is to version lambda in all regions defined in stage' + } + ], + }); + return Promise.resolve(); + } + + /** + * + * @param stage Optional if only one stage is defined in project + * @param region Optional. Default is to version lambda in all regions defined in stage + * @param verAndAliasName version, aliasName + * @returns {Promise.} + */ + lambdaCreateAlias(stage, region) { + let _this = this, + verAliasName = Array.prototype.slice.call(arguments, 3); + + if (!verAliasNameAndPaths || verAliasName.length !== 2) { + return Promise.reject(new JawsError('Must specify a lambda version and alias name')); + } + + this._stage = stage; + this._region = region; //may not be set + + return this._JAWS.validateProject() + .bind(_this) + .then(_this._promptStage) + .then(() => { + JawsUtils.jawsDebug('publishing version for stage:', _this._stage); + return _this._setLambdaLogicalIds(lambdaPaths); + }) + .then(_this._getRegions) + .each(region => { + //1) For each region, get all the lambdas for stack + let lStackName = AWSUtils.cfGetLambdasStackName(_this._stage, _this._JAWS._projectJson.name); + return AWSUtils.cfGetLambdaResourceSummaries(_this.Jaws._profile, region, lStackName) + .then(lambdaSummaries => { + //2) identify physical function names from logical + return AWSUtils.cfGetLambdaPhysicalsFromLogicals(_this._lambdaLogicalIdsToAlias, lambdaSummaries); + }) + .then(lambdaNamesToVersion => { + //3) publishVersions + return AWSUtils.lambdaCreateAliass(_this.Jaws._profile, region, lambdaNamesToVersion); + }); + }) + .then(versionedLambdas => { + JawsCLI.log('Lambda AliasLambda: Successfully published following lambda versions to the requested regions:'); + JawsCLI.log(versionedLambdas); + return versionedLambdas; + }); + } + + /** + * + * @returns {Promise} + * @private + */ + _promptStage() { + let stages = [], + _this = this; + + // If stage exists, skip + if (!this._stage) { + stages = Object.keys(_this.JAWS._projectJson.stages); + + // If project only has 1 stage, skip prompt + if (stages.length === 1) { + this._stage = stages[0]; + } + } + + if (this._stage) { //User specified stage or only one stage + return Promise.resolve(); + } + + // Create Choices + let choices = []; + for (let i = 0; i < stages.length; i++) { + choices.push({ + key: '', + value: stages[i], + label: stages[i], + }); + } + + return this.selectInput('AliasLambda: Choose a stage: ', choices, false) + .then(results => { + _this._stage = results[0].value; + }); + } + + /** + * this._stage must be set before calling this method + * @returns {Promise} list of regions + * @private + */ + _getRegions() { + if (this._region) { //user specified specific region to deploy to + JawsUtils.jawsDebug('Deploying to region: ' + this._region); + return BbPromise.resolve([region]); + } + + //Deploy to all regions in stage + + let stage = this._stage, + projJson = this.JAWS._projectJson; + + let regionConfigs = projJson.stages[stage], + regions = regionConfigs.map(rCfg => { + return rCfg.region; + }); + + JawsUtils.jawsDebug('Publishing version to regions:', regions); + return BbPromise.resolve(regions); + } + + /** + * + * @param lambdaPaths [] optional abs or rel (to cwd) paths to lambda dirs. If ommitted deploys lambda @ cwd + * @return {Promise} + * @private + */ + _setLambdaLogicalIds(lambdaPaths) { + let _this = this; + return JawsUtils.getFullLambdaPaths(process.cwd(), lambdaPaths) + .then(fullAwsmJsonPaths => { + _this._lambdaLogicalIdsToAlias = fullAwsmJsonPaths.map(alp => { + let awsmJson = JawsUtils.readAndParseJsonSync(alp); + return JawsUtils.getLambdaName(awsmJson); + }); + }); + } +} + +module.exports = AliasLambda; \ No newline at end of file diff --git a/lib/defaults/actions/DeployLambda.js b/lib/defaults/actions/DeployLambda.js index 3860e5118..be0a158ce 100644 --- a/lib/defaults/actions/DeployLambda.js +++ b/lib/defaults/actions/DeployLambda.js @@ -41,91 +41,91 @@ class Deployer { awsmLambdas = []; return BbPromise.try(function() { - }) - .bind(_this) - .then(function() { - return _this._lambdaAwsmPaths; - }) - .each(function(lambdaAwsmPath) { - let packager = new Packager( - _this._JAWS, - _this._stage, - _this._region, - lambdaAwsmPath - ); - - return BbPromise.try(function() { - }) - .bind(_this) - .then(function() { - return packager.run(); - }) - .then(function(packagedLambda) { - let jawsBucket = _this._JAWS.getJawsBucket(_this._region, _this._stage); - JawsCLI.log('Lambda Deployer: Uploading ' + packagedLambda.lambdaName + ` to ${jawsBucket}`); - - return AWSUtils.putLambdaZip( - _this._JAWS._profile, - _this._region, - jawsBucket, - projName, + }) + .bind(_this) + .then(function() { + return _this._lambdaAwsmPaths; + }) + .each(function(lambdaAwsmPath) { + let packager = new Packager( + _this._JAWS, _this._stage, - packagedLambda.lambdaName, - packagedLambda.zipBuffer - ) - .then(function(s3Key) { - awsmLambdas.push({ - awsmPath: lambdaAwsmPath, - Code: { - S3Bucket: jawsBucket, - S3Key: s3Key, - }, - lambdaName: packagedLambda.lambdaName, - }); + _this._region, + lambdaAwsmPath + ); + + return BbPromise.try(function() { + }) + .bind(_this) + .then(function() { + return packager.run(); + }) + .then(function(packagedLambda) { + let jawsBucket = _this._JAWS.getJawsBucket(_this._region, _this._stage); + JawsCLI.log('Lambda Deployer: Uploading ' + packagedLambda.lambdaName + ` to ${jawsBucket}`); + + return AWSUtils.putLambdaZip( + _this._JAWS._profile, + _this._region, + jawsBucket, + projName, + _this._stage, + packagedLambda.lambdaName, + packagedLambda.zipBuffer + ) + .then(function(s3Key) { + awsmLambdas.push({ + awsmPath: lambdaAwsmPath, + Code: { + S3Bucket: jawsBucket, + S3Key: s3Key, + }, + lambdaName: packagedLambda.lambdaName, + }); + }); }); - }); - }) - .then(function() { - //At this point all packages have been created and uploaded to s3 - let lambdaRoleArn = JawsUtils - .getProjRegionConfigForStage(_this._JAWS._projectJson, _this._stage, _this._region).iamRoleArnLambda; - return [lambdaRoleArn, _this._generateLambdaCf(awsmLambdas, lambdaRoleArn)]; - }) - .spread(function(lambdaRoleArn, existingStack) { - if (_this._noExeCf) { - JawsCLI.log(`Lambda Deployer: not executing CloudFormation. Remember to set aaLambdaRoleArn parameter to ${lambdaRoleArn}`); - return false; - } else { - let createOrUpdate, - cfDeferred; - - JawsUtils.jawsDebug(`Deploying with lambda role arn ${lambdaRoleArn}`); - - if (existingStack) { - cfDeferred = AWSUtils.cfUpdateLambdasStack(_this._JAWS, _this._stage, _this._region, lambdaRoleArn); - createOrUpdate = 'update'; + }) + .then(function() { + //At this point all packages have been created and uploaded to s3 + let lambdaRoleArn = JawsUtils + .getProjRegionConfigForStage(_this._JAWS._projectJson, _this._stage, _this._region).iamRoleArnLambda; + return [lambdaRoleArn, _this._generateLambdaCf(awsmLambdas, lambdaRoleArn)]; + }) + .spread(function(lambdaRoleArn, existingStack) { + if (_this._noExeCf) { + JawsCLI.log(`Lambda Deployer: not executing CloudFormation. Remember to set aaLambdaRoleArn parameter to ${lambdaRoleArn}`); + return false; } else { - cfDeferred = AWSUtils.cfCreateLambdasStack(_this._JAWS, _this._stage, _this._region, lambdaRoleArn); - createOrUpdate = 'create'; + let createOrUpdate, + cfDeferred; + + JawsUtils.jawsDebug(`Deploying with lambda role arn ${lambdaRoleArn}`); + + if (existingStack) { + cfDeferred = AWSUtils.cfUpdateLambdasStack(_this._JAWS, _this._stage, _this._region, lambdaRoleArn); + createOrUpdate = 'update'; + } else { + cfDeferred = AWSUtils.cfCreateLambdasStack(_this._JAWS, _this._stage, _this._region, lambdaRoleArn); + createOrUpdate = 'create'; + } + + JawsCLI.log('Running CloudFormation lambda deploy...'); + let spinner = JawsCLI.spinner(); + spinner.start(); + + return cfDeferred + .then(function(cfData) { + return AWSUtils.monitorCf(cfData, _this._JAWS._profile, _this._region, createOrUpdate); + }) + .then(function() { + spinner.stop(true); + }); } - - JawsCLI.log('Running CloudFormation lambda deploy...'); - let spinner = JawsCLI.spinner(); - spinner.start(); - - return cfDeferred - .then(function(cfData) { - return AWSUtils.monitorCf(cfData, _this._JAWS._profile, _this._region, createOrUpdate); - }) - .then(function() { - spinner.stop(true); - }); - } - }) - .then(function() { - JawsCLI.log('Lambda Deployer: Done deploying lambdas in ' + _this._region); - } - ); + }) + .then(function() { + JawsCLI.log('Lambda Deployer: Done deploying lambdas in ' + _this._region); + } + ); } /** @@ -151,83 +151,83 @@ class Deployer { projName = this._JAWS._projectJson.name; return AWSUtils.cfGetLambdasStackTemplate(_this._JAWS._profile, _this._region, _this._stage, projName) - .error(e => { - if (e && ['ValidationError', 'ResourceNotFoundException'].indexOf(e.code) == -1) { //ValidationError if DNE - console.error( - 'Error trying to fetch existing lambda cf stack for region', _this._region, 'stage', _this._stage, e + .error(e => { + if (e && ['ValidationError', 'ResourceNotFoundException'].indexOf(e.code) == -1) { //ValidationError if DNE + console.error( + 'Error trying to fetch existing lambda cf stack for region', _this._region, 'stage', _this._stage, e + ); + throw new JawsError(e.message, JawsError.errorCodes.UNKNOWN); + } + + JawsUtils.jawsDebug('no exsting lambda stack'); + existingStack = false; + return false; + }) + .then(cfTemplateBody => { + let templatesPath = path.join(__dirname, '..', 'templates'), + lambdaCf = JawsUtils.readAndParseJsonSync(path.join(templatesPath, 'lambdas-cf.json')); + + delete lambdaCf.Resources.lTemplate; + lambdaCf.Description = projName + " lambdas"; + lambdaCf.Parameters.aaLambdaRoleArn.Default = lambdaRoleArn; + + //Always add lambdas tagged for deployment + taggedLambdaPkgs.forEach(function(pkg) { + let lResource = { + Type: "AWS::Lambda::Function", + Properties: {} + }, + awsm = JawsUtils.readAndParseJsonSync(pkg.awsmPath); + + lResource.Properties = awsm.lambda.cloudFormation; + lResource.Properties.Code = pkg.Code; + lResource.Properties.Role = { + Ref: "aaLambdaRoleArn" + }; + + JawsUtils.jawsDebug('adding Resource ' + pkg.lambdaName + ': '); + JawsUtils.jawsDebug(lResource); + + lambdaCf.Resources[pkg.lambdaName] = lResource; + }); + + // If existing lambdas CF template + if (cfTemplateBody) { + JawsUtils.jawsDebug('existing stack detected'); + + // Find all lambdas in project, and copy ones that are in existing lambda-cf + let existingTemplate = JSON.parse(cfTemplateBody); + + return JawsUtils.getAllLambdaNames(_this._JAWS._projectRootPath) + .then(allLambdaNames => { + Object.keys(existingTemplate.Resources).forEach(resource => { + + if (!lambdaCf.Resources[resource] && allLambdaNames.indexOf(resource) != -1) { + JawsUtils.jawsDebug(`Adding exsiting lambda ${resource}`); + lambdaCf.Resources[resource] = existingTemplate.Resources[resource]; + } + }); + + return lambdaCf; + }); + } else { + return lambdaCf; + } + }) + .then(lambdaCfTemplate => { + let lambdasCfPath = path.join( + _this._JAWS._projectRootPath, + 'cloudformation', + _this._stage, + _this._region, + 'lambdas-cf.json' ); - throw new JawsError(e.message, JawsError.errorCodes.UNKNOWN); - } - JawsUtils.jawsDebug('no exsting lambda stack'); - existingStack = false; - return false; - }) - .then(cfTemplateBody => { - let templatesPath = path.join(__dirname, '..', 'templates'), - lambdaCf = JawsUtils.readAndParseJsonSync(path.join(templatesPath, 'lambdas-cf.json')); + JawsUtils.jawsDebug(`Wrting to ${lambdasCfPath}`); - delete lambdaCf.Resources.lTemplate; - lambdaCf.Description = projName + " lambdas"; - lambdaCf.Parameters.aaLambdaRoleArn.Default = lambdaRoleArn; - - //Always add lambdas tagged for deployment - taggedLambdaPkgs.forEach(function(pkg) { - let lResource = { - Type: "AWS::Lambda::Function", - Properties: {} - }, - awsm = JawsUtils.readAndParseJsonSync(pkg.awsmPath); - - lResource.Properties = awsm.lambda.cloudFormation; - lResource.Properties.Code = pkg.Code; - lResource.Properties.Role = { - Ref: "aaLambdaRoleArn" - }; - - JawsUtils.jawsDebug('adding Resource ' + pkg.lambdaName + ': '); - JawsUtils.jawsDebug(lResource); - - lambdaCf.Resources[pkg.lambdaName] = lResource; + return JawsUtils.writeFile(lambdasCfPath, JSON.stringify(lambdaCfTemplate, null, 2)) + .then(() => existingStack); }); - - // If existing lambdas CF template - if (cfTemplateBody) { - JawsUtils.jawsDebug('existing stack detected'); - - // Find all lambdas in project, and copy ones that are in existing lambda-cf - let existingTemplate = JSON.parse(cfTemplateBody); - - return JawsUtils.getAllLambdaNames(_this._JAWS._projectRootPath) - .then(allLambdaNames => { - Object.keys(existingTemplate.Resources).forEach(resource => { - - if (!lambdaCf.Resources[resource] && allLambdaNames.indexOf(resource) != -1) { - JawsUtils.jawsDebug(`Adding exsiting lambda ${resource}`); - lambdaCf.Resources[resource] = existingTemplate.Resources[resource]; - } - }); - - return lambdaCf; - }); - } else { - return lambdaCf; - } - }) - .then(lambdaCfTemplate => { - let lambdasCfPath = path.join( - _this._JAWS._projectRootPath, - 'cloudformation', - _this._stage, - _this._region, - 'lambdas-cf.json' - ); - - JawsUtils.jawsDebug(`Wrting to ${lambdasCfPath}`); - - return JawsUtils.writeFile(lambdasCfPath, JSON.stringify(lambdaCfTemplate, null, 2)) - .then(() => existingStack); - }); } } @@ -251,22 +251,22 @@ class Packager { this._lambdaName = this.createLambdaName(); return this._createDistFolder() - .then(function() { + .then(function() { - // Package by runtime - switch (_this._awsmJson.lambda.cloudFormation.Runtime) { - case 'nodejs': - return _this._packageNodeJs() - .then(function(packageData) { - packageData.lambdaName = _this._lambdaName; - return packageData; - }); - break; - default: - return Promise.reject(new JawsError('Unsupported lambda runtime ' + _this._awsmJson.lambda.cloudFormation.Runtime)); - break; - } - }); + // Package by runtime + switch (_this._awsmJson.lambda.cloudFormation.Runtime) { + case 'nodejs': + return _this._packageNodeJs() + .then(function(packageData) { + packageData.lambdaName = _this._lambdaName; + return packageData; + }); + break; + default: + return Promise.reject(new JawsError('Unsupported lambda runtime ' + _this._awsmJson.lambda.cloudFormation.Runtime)); + break; + } + }); } /** @@ -277,7 +277,7 @@ class Packager { */ createLambdaName() { let _this = this, - name = JawsUtils.generateLambdaName(_this._awsmJson); + name = JawsUtils.getLambdaName(_this._awsmJson); JawsUtils.jawsDebug(`computed lambdaName: ${name}`); return name; @@ -306,42 +306,42 @@ class Packager { // Copy entire test project to temp folder _this._excludePatterns = _this._awsmJson.lambda.package.excludePatterns || []; wrench.copyDirSyncRecursive( - _this._JAWS._projectRootPath, - _this._distDir, - { - exclude: function(name, prefix) { - if (!_this._excludePatterns.length) { - return false; - } - - let relPath = path.join( - prefix.replace(_this._distDir, ''), name); - - return _this._excludePatterns.some(sRegex => { - relPath = (relPath.charAt(0) == path.sep) ? relPath.substr(1) : relPath; - - let re = new RegExp(sRegex), - matches = re.exec(relPath); - - let willExclude = (matches && matches.length > 0); - - if (willExclude) { - JawsCLI.log(`Lambda Deployer: Excluding ${relPath}`); + _this._JAWS._projectRootPath, + _this._distDir, + { + exclude: function(name, prefix) { + if (!_this._excludePatterns.length) { + return false; } - return willExclude; - }); - }, - } + let relPath = path.join( + prefix.replace(_this._distDir, ''), name); + + return _this._excludePatterns.some(sRegex => { + relPath = (relPath.charAt(0) == path.sep) ? relPath.substr(1) : relPath; + + let re = new RegExp(sRegex), + matches = re.exec(relPath); + + let willExclude = (matches && matches.length > 0); + + if (willExclude) { + JawsCLI.log(`Lambda Deployer: Excluding ${relPath}`); + } + + return willExclude; + }); + }, + } ); JawsUtils.jawsDebug('Packaging stage & region:', _this._stage, _this._region); // Get ENV file from S3 return _this._JAWS.getEnvFile(_this._region, _this._stage) - .then(function(s3ObjData) { - fs.writeFileSync(path.join(_this._distDir, '.env'), s3ObjData.Body); - }); + .then(function(s3ObjData) { + fs.writeFileSync(path.join(_this._distDir, '.env'), s3ObjData.Body); + }); } /** @@ -355,30 +355,30 @@ class Packager { deferred = null; if (_this._awsmJson.lambda.package - && _this._awsmJson.lambda.package.optimize - && _this._awsmJson.lambda.package.optimize.builder) { + && _this._awsmJson.lambda.package.optimize + && _this._awsmJson.lambda.package.optimize.builder) { deferred = _this._optimizeNodeJs() - .then(optimizedCodeBuffer => { + .then(optimizedCodeBuffer => { - // Lambda freaks out if code doesnt end in newline - let ocbWithNewline = optimizedCodeBuffer.concat(new Buffer('\n')); - let envData = fs.readFileSync(path.join(_this._distDir, '.env')); + // Lambda freaks out if code doesnt end in newline + let ocbWithNewline = optimizedCodeBuffer.concat(new Buffer('\n')); + let envData = fs.readFileSync(path.join(_this._distDir, '.env')); - let handlerFileName = _this._awsmJson.lambda.cloudFormation.Handler.split('.')[0], - compressPaths = [ + let handlerFileName = _this._awsmJson.lambda.cloudFormation.Handler.split('.')[0], + compressPaths = [ - // handlerFileName is the full path lambda file including dir rel to back - {fileName: handlerFileName + '.js', data: ocbWithNewline}, - {fileName: '.env', data: envData}, - ]; + // handlerFileName is the full path lambda file including dir rel to back + {fileName: handlerFileName + '.js', data: ocbWithNewline}, + {fileName: '.env', data: envData}, + ]; - if (_this._awsmJson.lambda.package.optimize.includePaths.length) { - compressPaths = compressPaths.concat(_this._generateIncludePaths()); - } + if (_this._awsmJson.lambda.package.optimize.includePaths.length) { + compressPaths = compressPaths.concat(_this._generateIncludePaths()); + } - return _this._compressCode(compressPaths); - }); + return _this._compressCode(compressPaths); + }); } else { // User chose not to optimize, zip up whatever is in back _this._awsmJson.lambda.package.optimize.includePaths = ['.']; @@ -387,14 +387,14 @@ class Packager { } return deferred - .then(function(compressedCodeBuffer) { - let zippedFilePath = path.join(_this._distDir, 'package.zip'); // Save for auditing; - fs.writeFileSync(zippedFilePath, compressedCodeBuffer); + .then(function(compressedCodeBuffer) { + let zippedFilePath = path.join(_this._distDir, 'package.zip'); // Save for auditing; + fs.writeFileSync(zippedFilePath, compressedCodeBuffer); - JawsCLI.log(`Lambda Deployer: Compressed lambda written to ${zippedFilePath}`); + JawsCLI.log(`Lambda Deployer: Compressed lambda written to ${zippedFilePath}`); - return Promise.resolve({awsmFilePath: _this._lambdaPath, zipBuffer: compressedCodeBuffer}); - }); + return Promise.resolve({awsmFilePath: _this._lambdaPath, zipBuffer: compressedCodeBuffer}); + }); } /** @@ -407,7 +407,7 @@ class Packager { let _this = this; if (!_this._awsmJson.lambda.package.optimize - || !_this._awsmJson.lambda.package.optimize.builder) { + || !_this._awsmJson.lambda.package.optimize.builder) { return Promise.reject(new JawsError('Cant optimize for nodejs. lambda jaws.json does not have optimize.builder set')); } @@ -524,20 +524,20 @@ class Packager { let dirname = path.basename(p); wrench - .readdirSyncRecursive(fullPath) - .forEach(file => { - // Ignore certain files - for (let i = 0; i < ignore.length; i++) { - if (file.toLowerCase().indexOf(ignore[i]) > -1) return; - } + .readdirSyncRecursive(fullPath) + .forEach(file => { + // Ignore certain files + for (let i = 0; i < ignore.length; i++) { + if (file.toLowerCase().indexOf(ignore[i]) > -1) return; + } - let filePath = [fullPath, file].join('/'); - if (fs.lstatSync(filePath).isFile()) { - let pathInZip = path.join(dirname, file); - JawsUtils.jawsDebug('Adding', pathInZip); - compressPaths.push({fileName: pathInZip, data: fs.readFileSync(filePath)}); - } - }); + let filePath = [fullPath, file].join('/'); + if (fs.lstatSync(filePath).isFile()) { + let pathInZip = path.join(dirname, file); + JawsUtils.jawsDebug('Adding', pathInZip); + compressPaths.push({fileName: pathInZip, data: fs.readFileSync(filePath)}); + } + }); } }); @@ -564,8 +564,8 @@ class Packager { if (zippedData.length > 52428800) { Promise.reject(new JawsError( - 'Zip file is > the 50MB Lambda deploy limit (' + zippedData.length + ' bytes)', - JawsError.errorCodes.ZIP_TOO_BIG) + 'Zip file is > the 50MB Lambda deploy limit (' + zippedData.length + ' bytes)', + JawsError.errorCodes.ZIP_TOO_BIG) ); } @@ -604,7 +604,8 @@ class DeployLambda extends JawsPlugin { registerActions() { this.Jaws.action(this.deployLambda.bind(this), { handler: 'lambdaDeploy', - description: 'Deploy lambda at CWD or lambdas at specified paths', + description: `Deploy lambda at CWD or lambdas at specified paths +usage: jaws lambda deploy [rel or abs path to lambda dirs. default is cwd]`, context: 'lambda', contextAction: 'deploy', options: [ @@ -643,22 +644,22 @@ class DeployLambda extends JawsPlugin { this._noExeCf = (noExeCf == true || noExeCf == 'true'); return this._JAWS.validateProject() - .bind(_this) - .then(_this._promptStage) - .then(_this._validate) - .then(() => { - JawsUtils.jawsDebug('Deploying to stage:', _this._stage); - return _this._setLambdaAwsmPaths(lambdaPaths); - }) - .then(_this._getRegions) - .each(region => { - let d = new Deployer(_this.JAWS, _this._lambdaAwsmPathsToDeploy, _this._stage, region, _this._noExeCf); - return d.run(); - }) - .then(lambdaAwsmPkgs => { - JawsCLI.log('Lambda Deployer: Successfully deployed lambdas to the requested regions!'); - return lambdaAwsmPkgs; - }); + .bind(_this) + .then(_this._promptStage) + .then(_this._validate) + .then(() => { + JawsUtils.jawsDebug('Deploying to stage:', _this._stage); + return _this._setLambdaAwsmPaths(lambdaPaths); + }) + .then(_this._getRegions) + .each(region => { + let d = new Deployer(_this.JAWS, _this._lambdaAwsmPathsToDeploy, _this._stage, region, _this._noExeCf); + return d.run(); + }) + .then(lambdaAwsmPkgs => { + JawsCLI.log('Lambda Deployer: Successfully deployed lambdas to the requested regions!'); + return lambdaAwsmPkgs; + }); } /** @@ -695,9 +696,9 @@ class DeployLambda extends JawsPlugin { } return this.selectInput('Lambda Deployer: Choose a stage: ', choices, false) - .then(results => { - _this._stage = results[0].value; - }); + .then(results => { + _this._stage = results[0].value; + }); } /** @@ -749,9 +750,9 @@ class DeployLambda extends JawsPlugin { _setLambdaAwsmPaths(lambdaPaths) { let _this = this; return JawsUtils.getFullLambdaPaths(process.cwd(), lambdaPaths) - .then(fullAwsmJsonPaths => { - _this._lambdaAwsmPathsToDeploy = fullAwsmJsonPaths; - }); + .then(fullAwsmJsonPaths => { + _this._lambdaAwsmPathsToDeploy = fullAwsmJsonPaths; + }); } } diff --git a/lib/defaults/actions/VersionLambda.js b/lib/defaults/actions/VersionLambda.js index 541965ee3..26f3675cc 100644 --- a/lib/defaults/actions/VersionLambda.js +++ b/lib/defaults/actions/VersionLambda.js @@ -10,7 +10,6 @@ const JawsPlugin = require('../../JawsPlugin'), BbPromise = require('bluebird'), path = require('path'), os = require('os'), - AWS = require('aws-sdk'), AWSUtils = require('../../utils/aws'), JawsUtils = require('../../utils/index'); @@ -26,10 +25,9 @@ class VersionLambda extends JawsPlugin { constructor(Jaws, config) { super(Jaws, config); - this._stage = null; - this._region = null; - this._noExeCf = false; - this._lambdaAwsmPathsToVersion = []; + this._stage = null; + this._region = null; + this._lambdaLogicalIdsToVersion = []; } /** @@ -71,7 +69,7 @@ class VersionLambda extends JawsPlugin { * @param stage Optional if only one stage is defined in project * @param region Optional. Default is to version lambda in all regions defined in stage * @param lambdaPaths [] optional abs or rel (to cwd) paths to lambda dirs. If ommitted versions lambda @ cwd - * @returns {Promise.} + * @returns {Promise.} */ lambdaPublishVersion(stage, region) { let _this = this, @@ -81,21 +79,31 @@ class VersionLambda extends JawsPlugin { this._region = region; //may not be set return this._JAWS.validateProject() - .bind(_this) - .then(_this._promptStage) - .then(() => { - JawsUtils.jawsDebug('publishing version for stage:', _this._stage); - return _this._setLambdaAwsmPaths(lambdaPaths); - }) - .then(_this._getRegions) - .each(region => { - - }) - .then(versionedLambdas => { - JawsCLI.log('Lambda Deployer: Successfully published lambda versions to the requested regions!'); - JawsCLI.log(versionedLambdas); - return versionedLambdas; - }); + .bind(_this) + .then(_this._promptStage) + .then(() => { + JawsUtils.jawsDebug('publishing version for stage:', _this._stage); + return _this._setLambdaLogicalIds(lambdaPaths); + }) + .then(_this._getRegions) + .each(region => { + //1) For each region, get all the lambdas for stack + let lStackName = AWSUtils.cfGetLambdasStackName(_this._stage, _this._JAWS._projectJson.name); + return AWSUtils.cfGetLambdaResourceSummaries(_this.Jaws._profile, region, lStackName) + .then(lambdaSummaries => { + //2) identify physical function names from logical + return AWSUtils.cfGetLambdaPhysicalsFromLogicals(_this._lambdaLogicalIdsToVersion, lambdaSummaries); + }) + .then(lambdaNamesToVersion => { + //3) publishVersions + return AWSUtils.lambdaPublishVersions(_this.Jaws._profile, region, lambdaNamesToVersion); + }); + }) + .then(versionedLambdas => { + JawsCLI.log('Lambda VersionLambda: Successfully published following lambda versions to the requested regions:'); + JawsCLI.log(versionedLambdas); + return versionedLambdas; + }); } /** @@ -132,9 +140,9 @@ class VersionLambda extends JawsPlugin { } return this.selectInput('VersionLambda: Choose a stage: ', choices, false) - .then(results => { - _this._stage = results[0].value; - }); + .then(results => { + _this._stage = results[0].value; + }); } /** @@ -168,12 +176,15 @@ class VersionLambda extends JawsPlugin { * @return {Promise} * @private */ - _setLambdaAwsmPaths(lambdaPaths) { + _setLambdaLogicalIds(lambdaPaths) { let _this = this; return JawsUtils.getFullLambdaPaths(process.cwd(), lambdaPaths) - .then(fullAwsmJsonPaths => { - _this._lambdaAwsmPathsToVersion = fullAwsmJsonPaths; - }); + .then(fullAwsmJsonPaths => { + _this._lambdaLogicalIdsToVersion = fullAwsmJsonPaths.map(alp => { + let awsmJson = JawsUtils.readAndParseJsonSync(alp); + return JawsUtils.getLambdaName(awsmJson); + }); + }); } } diff --git a/lib/templates/action.awsm.json b/lib/templates/action.awsm.json index e459c00ce..5fce6cf7f 100644 --- a/lib/templates/action.awsm.json +++ b/lib/templates/action.awsm.json @@ -1,31 +1,55 @@ { - "lambda": { - "envVars": [], - "deploy": false, - "package": { - "optimize": { - "builder": "browserify", - "minify": true, - "ignore": [], - "exclude": [ - "aws-sdk" - ], - "includePaths": [] - }, - "excludePatterns": [] + "name": "", + "envVars": [], + "package": { + "optimize": { + "builder": "browserify", + "minify": true, + "ignore": [], + "exclude": [ + "aws-sdk" + ], + "includePaths": [] }, - "cloudFormation": { - "Description": "", - "Handler": "", - "MemorySize": 1024, - "Runtime": "nodejs", - "Timeout": 6 - } + "excludePatterns": [] }, - "apiGateway": { - "deploy": false, - "cloudFormation": { - "Type": "", + "plugins": [], + "cloudFormation": { + "Lambda": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Handler": "", + "Role": { + "Ref": "aaLambdaRoleArn" + }, + "Code": { + "S3Bucket": "", + "S3Key": "" + }, + "Runtime": "nodejs", + "Timeout": 6, + "MemorySize": 1024 + } + }, + "LambdaEventSourceMapping": { + "Type": "AWS::Lambda::EventSourceMapping", + "Properties": { + "EventSourceArn": {}, + "FunctionName": "", + "StartingPosition": "" + } + }, + "LambdaAccessPolicyX": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "FunctionName": "", + "Action": "", + "Principal": "", + "SourceAccount": "" + } + }, + "APIGatewayEndpoint": { + "Type": "AWS::ApiGateway::Endpoint", "Path": "", "Method": "GET", "AuthorizationType": "none", @@ -33,16 +57,15 @@ "RequestTemplates": {}, "RequestParameters": {}, "Responses": { + "400": { + "statusCode": "400" + }, "default": { "statusCode": "200", "responseParameters": {}, - "responseModels": {}, "responseTemplates": { "application/json": "" } - }, - "400": { - "statusCode": "400" } } } diff --git a/lib/utils/aws.js b/lib/utils/aws.js index a8e6d9617..e684b75d9 100644 --- a/lib/utils/aws.js +++ b/lib/utils/aws.js @@ -53,8 +53,8 @@ module.exports.getConfigDir = function() { let env = process.env; let home = env.HOME || - env.USERPROFILE || - (env.HOMEPATH ? ((env.HOMEDRIVE || 'C:/') + env.HOMEPATH) : null); + env.USERPROFILE || + (env.HOMEPATH ? ((env.HOMEDRIVE || 'C:/') + env.HOMEPATH) : null); if (!home) { throw new JawsError('Cant find homedir', JawsError.errorCodes.MISSING_HOMEDIR); @@ -94,16 +94,16 @@ module.exports.profilesSet = function(awsProfile, awsRegion, accessKeyId, secret } fs.appendFileSync( - credsPath, - `[${awsProfile}] + credsPath, + `[${awsProfile}] aws_access_key_id = ${accessKeyId.trim()} aws_secret_access_key = ${secretKey.trim()}\n`); let profileNameForConfig = (awsProfile == 'default') ? 'default' : 'profile ' + awsProfile; fs.appendFileSync( - configPath, - `[${profileNameForConfig}] + configPath, + `[${profileNameForConfig}] region = ${awsRegion}\n`); }; @@ -152,8 +152,8 @@ exports.iamGetRole = function(awsProfile, awsRegion, roleName) { if (error) { return reject(new JawsError( - error.message, - JawsError.errorCodes.UNKNOWN)); + error.message, + JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } @@ -190,8 +190,8 @@ exports.cfDescribeStacks = function(awsProfile, awsRegion, stackName) { if (error) { return reject(new JawsError( - error.message, - JawsError.errorCodes.UNKNOWN)); + error.message, + JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } @@ -231,8 +231,8 @@ exports.cfDescribeStackResource = function(awsProfile, awsRegion, stackId, cfRes if (error) { return reject(new JawsError( - error.message, - JawsError.errorCodes.UNKNOWN)); + error.message, + JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } @@ -264,48 +264,131 @@ exports.cfListStackResources = function(awsProfile, awsRegion, stackName, nextTo return CF.listStackResourcesAsync(params); }; -exports.cfGetLambdaNames = function(awsProfile, awsRegion, stackName) { +/** + * Returns data like: + * [ + { + "LogicalResourceId": "lChannelWxLatlng", + "PhysicalResourceId": "prod-pushChannelSearch-l-lChannelWxLatlng-AS845QCZ8J1L", + "ResourceType": "AWS::Lambda::Function", + "LastUpdatedTimestamp": "2015-10-15T19:06:55.134Z", + "ResourceStatus": "UPDATE_COMPLETE" + }, + { + "LogicalResourceId": "lChannelWxTypeahead", + "PhysicalResourceId": "prod-pushChannelSearch-l-lChannelWxTypeahead-15NUNJF0O22HA", + "ResourceType": "AWS::Lambda::Function", + "LastUpdatedTimestamp": "2015-10-19T21:35:24.357Z", + "ResourceStatus": "UPDATE_COMPLETE" + } + ] + + * @param awsProfile + * @param awsRegion + * @param stackName + * @returns {Promise.} + */ +exports.cfGetLambdaResourceSummaries = function(awsProfile, awsRegion, stackName) { let _this = this, moreResources = true, nextStackToken, lambdas = []; - async.whilst( - function() { - return moreResources === true; - }, - function(callback) { - _this.cfListStackResources(awsProfile, awsRegion, stackName, nextStackToken) - .then(function(lambdaCfResources) { - if (lambdaCfResources.StackResourceSummaries) { - lambdas = lambdas.concat(lambdaCfResources.StackResourceSummaries); - } + return new Promise((resolve, reject)=> { + async.whilst( + function() { + return moreResources === true; + }, + function(callback) { + _this.cfListStackResources(awsProfile, awsRegion, stackName, nextStackToken) + .then(function(lambdaCfResources) { + if (lambdaCfResources.StackResourceSummaries) { + lambdas = lambdas.concat(lambdaCfResources.StackResourceSummaries); + } - // Check if more resources are available - if (!lambdaCfResources.NextToken) { - moreResources = false; + // Check if more resources are available + if (!lambdaCfResources.NextToken) { + moreResources = false; + } else { + nextStackToken = lambdaCfResources.NextToken; + } + + callback(); + }) + .catch(function(error) { + JawsCli.log('Warning: JAWS could not find a deployed Cloudformation ' + + 'template containing lambda functions.'); + console.log(error); + moreResources = false; + callback(error); + }); + }, + function(err) { + if (err) { + reject(err); } else { - nextStackToken = lambdaCfResources.NextToken; + resolve(lambdas); } + } + ); + }); +}; - callback(); - }) - .catch(function(error) { - JawsCli.log('Warning: JAWS could not find a deployed Cloudformation ' - + 'template containing lambda functions.'); - console.log(error); - moreResources = false; - callback(error); - }); - }, - function(err) { - if (err) { - return Promise.reject(err); - } else { - return Promise.resolve(lambdas); - } +/** + * Given a list of lambda LogicalResourceId's and a list of lambdaResourceSummaries + * return a corresponding list of lambda PhysicalResourceId's + * + * @param logicalIds + * @param lambdaResourceSummaries + */ +exports.cfGetLambdaPhysicalsFromLogicals = function(logicalIds, lambdaResourceSummaries) { + let lambdaPhysicalIds = []; + for (let lid of logicalIds) { + let foundLambda = lambdaResourceSummaries.find(element=> { + return element.LogicalResourceId == lid; + }); + + if (!foundLambda) { + throw new JawsError(`unable to find lambda with logical id ${lid}`, JawsError.errorCodes.UNKNOWN); } - ); + + lambdaPhysicalIds.push(foundLambda.PhysicalResourceId); + } +}; + +/** + * Retrns [{FunctionName: d.FunctionName, Version: d.Version},...] + * @param awsProfile + * @param awsRegion + * @param functionNames + * @returns {Promise.} + */ +exports.lambdaPublishVersions = function(awsProfile, awsRegion, functionNames) { + this.configAWS(awsProfile, awsRegion); + + let L = Promise.promisifyAll(new AWS.Lambda({ + apiVersion: '2015-03-31', + })); + + let d = new Date(), + ds = `versioned at ${d}`, + deferreds = []; + + functionNames.forEach(fn => { + let params = { + FunctionName: stackName, + Description: ds + }; + + deferreds.push(L.publishVersionAsync(params)); + }); + + return Promise.all(deferreds) + .then(data => { + return data.map(d => { + return {FunctionName: d.FunctionName, Version: d.Version}; + }); + }); }; /** @@ -350,11 +433,11 @@ exports.cfGetLambdasStackTemplate = function(awsProfile, awsRegion, stage, projN })); return CF.getTemplateAsync({ - StackName: _this.cfGetLambdasStackName(stage, projName) - }) - .then(function(data) { - return data.TemplateBody; - }); + StackName: _this.cfGetLambdasStackName(stage, projName) + }) + .then(function(data) { + return data.TemplateBody; + }); }; /** @@ -386,13 +469,13 @@ exports.putCfFile = function(awsProfile, projRootPath, awsRegion, bucketName, pr }; return this.putS3Object(awsProfile, awsRegion, params) - .then(function() { - //Really AWS - TemplateURL is an https:// URL. You force us to lookup endpt vs bucket/key attrs!?!? wtf not cool - let s3 = new AWS.S3(); + .then(function() { + //Really AWS - TemplateURL is an https:// URL. You force us to lookup endpt vs bucket/key attrs!?!? wtf not cool + let s3 = new AWS.S3(); - //Seriously, not cool... - return 'https://' + s3.endpoint.hostname + `/${bucketName}/${key}`; - }) + //Seriously, not cool... + return 'https://' + s3.endpoint.hostname + `/${bucketName}/${key}`; + }) }; /** @@ -434,10 +517,10 @@ exports.cfCreateLambdasStack = function(JAWS, stage, region, lambdaRoleArn) { }; return _this.putCfFile(awsProfile, projRootPath, region, bucketName, projName, stage, 'lambdas') - .then(function(templateUrl) { - params.TemplateURL = templateUrl; - return CF.createStackAsync(params); - }); + .then(function(templateUrl) { + params.TemplateURL = templateUrl; + return CF.createStackAsync(params); + }); }; /** @@ -475,10 +558,10 @@ exports.cfUpdateLambdasStack = function(JAWS, stage, region, lambdaRoleArn) { }; return _this.putCfFile(awsProfile, projRootPath, region, bucketName, projName, stage, 'lambdas') - .then(function(templateUrl) { - params.TemplateURL = templateUrl; - return CF.updateStackAsync(params); - }); + .then(function(templateUrl) { + params.TemplateURL = templateUrl; + return CF.updateStackAsync(params); + }); }; /** @@ -591,10 +674,10 @@ exports.cfUpdateResourcesStack = function(JAWS, stage, region) { }; return _this.putCfFile(awsProfile, projRootPath, region, bucketName, projName, stage, 'resources') - .then(function(templateUrl) { - params.TemplateURL = templateUrl; - return CF.updateStackAsync(params); - }); + .then(function(templateUrl) { + params.TemplateURL = templateUrl; + return CF.updateStackAsync(params); + }); }; /** @@ -630,31 +713,31 @@ exports.monitorCf = function(cfData, awsProfile, region, createOrUpdate, checkFr stackData = null; async.whilst( - function() { - return stackStatus !== stackStatusComplete; - }, + function() { + return stackStatus !== stackStatusComplete; + }, - function(callback) { - setTimeout(function() { - _this.cfDescribeStacks(awsProfile, region, cfData.StackId) - .then(function(data) { - stackData = data; - stackStatus = stackData.Stacks[0].StackStatus; + function(callback) { + setTimeout(function() { + _this.cfDescribeStacks(awsProfile, region, cfData.StackId) + .then(function(data) { + stackData = data; + stackStatus = stackData.Stacks[0].StackStatus; - if (!stackStatus || validStatuses.indexOf(stackStatus) === -1) { - console.log((data.Stacks && data.Stacks.length ? data.Stacks[0] : data)); - return reject(new JawsError( - `Something went wrong while ${createOrUpdate}ing your cloudformation`)); - } else { - return callback(); - } - }); - }, checkFreq); - }, + if (!stackStatus || validStatuses.indexOf(stackStatus) === -1) { + console.log((data.Stacks && data.Stacks.length ? data.Stacks[0] : data)); + return reject(new JawsError( + `Something went wrong while ${createOrUpdate}ing your cloudformation`)); + } else { + return callback(); + } + }); + }, checkFreq); + }, - function() { - return resolve(stackData.Stacks[0]); - } + function() { + return resolve(stackData.Stacks[0]); + } ); }); }; @@ -673,21 +756,21 @@ exports.createBucket = function(awsProfile, awsRegion, bucketName) { let s3 = Promise.promisifyAll(new AWS.S3()); return s3.getBucketAclAsync({Bucket: bucketName}) - .then(function() { - }) - .error(function(err) { - if (err.code == 'AccessDenied') { - throw new JawsError( - `Bucket ${bucketName} already exists and you do not have permissions to use it`, - JawsError.errorCodes.ACCESS_DENIED - ); - } + .then(function() { + }) + .error(function(err) { + if (err.code == 'AccessDenied') { + throw new JawsError( + `Bucket ${bucketName} already exists and you do not have permissions to use it`, + JawsError.errorCodes.ACCESS_DENIED + ); + } - return s3.createBucketAsync({ - Bucket: bucketName, - ACL: 'private', + return s3.createBucketAsync({ + Bucket: bucketName, + ACL: 'private', + }); }); - }); }; /** @@ -784,9 +867,9 @@ exports.putLambdaZip = function(awsProfile, awsRegion, bucketName, projectName, utils.jawsDebug(`lambda zip s3 key: ${key}`); return this.putS3Object(awsProfile, awsRegion, params) - .then(function() { - return key; - }); + .then(function() { + return key; + }); }; /** @@ -815,8 +898,8 @@ exports.cwGetLogStreams = function(logGroupName, limit) { if (error) { return reject(new JawsError( - error.message, - JawsError.errorCodes.UNKNOWN)); + error.message, + JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } @@ -847,8 +930,8 @@ exports.cwGetStreamEvents = function(logGroupName, logStreamName) { cwLogs.getLogEvents(params, function(err, data) { if (error) { return reject(new JawsError( - error.message, - JawsError.errorCodes.UNKNOWN)); + error.message, + JawsError.errorCodes.UNKNOWN)); } else { return resolve(data); } diff --git a/lib/utils/index.js b/lib/utils/index.js index fd0178753..586cd8189 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -37,26 +37,26 @@ exports.findAllAwsmPathsOfType = function(startPath, type) { } return _this.readRecursively(startPath, '*awsm.json') - .then(function(jsonPaths) { - return new Promise(function(resolve, reject) { + .then(function(jsonPaths) { + return new Promise(function(resolve, reject) { - let jawsPathsOfType = []; + let jawsPathsOfType = []; - // Check each file to ensure it is a lambda - async.eachLimit(jsonPaths, 10, function(jsonPath, cb) { - let lambdaJawsPath = path.join(startPath, jsonPath), - json = require(lambdaJawsPath); + // Check each file to ensure it is a lambda + async.eachLimit(jsonPaths, 10, function(jsonPath, cb) { + let lambdaJawsPath = path.join(startPath, jsonPath), + json = require(lambdaJawsPath); - if (typeof json[jawsJsonAttr] !== 'undefined') jawsPathsOfType.push(lambdaJawsPath); - return cb(); - }, + if (typeof json[jawsJsonAttr] !== 'undefined') jawsPathsOfType.push(lambdaJawsPath); + return cb(); + }, - function(error) { - if (error) reject(error); - resolve(jawsPathsOfType); - }); + function(error) { + if (error) reject(error); + resolve(jawsPathsOfType); + }); + }); }); - }); }; /** @@ -102,15 +102,15 @@ exports.findProjectRootPath = function(startDir) { exports.execute = function(promise) { promise - .catch(JawsError, function(e) { - console.error(e); - process.exit(e.messageId); - }) - .error(function(e) { - console.error(e); - process.exit(1); - }) - .done(); + .catch(JawsError, function(e) { + console.error(e); + process.exit(e.messageId); + }) + .error(function(e) { + console.error(e); + process.exit(1); + }) + .done(); }; /** @@ -128,15 +128,15 @@ exports.readRecursively = function(path, filter) { root: path, fileFilter: filter, }) - .on('data', function(entry) { - files.push(entry.path); - }) - .on('error', function(error) { - reject(error); - }) - .on('end', function() { - resolve(files); - }); + .on('data', function(entry) { + files.push(entry.path); + }) + .on('error', function(error) { + reject(error); + }) + .on('end', function() { + resolve(files); + }); }); }; @@ -171,24 +171,24 @@ exports.findAllEnvletsForAwsm = function(projectRootPath, modName) { let _this = this; return this.findAllAwsmPathsOfType(path.join(projectRootPath, 'aws_modules', modName), 'lambda') - .then(function(awsmJsonPaths) { - let envletKeys = []; + .then(function(awsmJsonPaths) { + let envletKeys = []; - awsmJsonPaths.forEach(function(awsmJsonPath) { - let awsmJson = _this.readAndParseJsonSync(awsmJsonPath); + awsmJsonPaths.forEach(function(awsmJsonPath) { + let awsmJson = _this.readAndParseJsonSync(awsmJsonPath); - //TODO: change to es6 set... - if (awsmJson.lambda && awsmJson.lambda.envlets) { - awsmJson.lambda.envlets.forEach(function(envlet) { - if (envletKeys.indexOf(envlet) == -1) { - envletKeys.push(envlet); - } - }); - } + //TODO: change to es6 set... + if (awsmJson.lambda && awsmJson.lambda.envlets) { + awsmJson.lambda.envlets.forEach(function(envlet) { + if (envletKeys.indexOf(envlet) == -1) { + envletKeys.push(envlet); + } + }); + } + }); + + return envletKeys; }); - - return envletKeys; - }); }; /** @@ -199,11 +199,11 @@ exports.findAllEnvletsForAwsm = function(projectRootPath, modName) { */ exports.findAllAwsmJsons = function(startPath) { return this.readRecursively(startPath, '*awsm.json') - .then(function(jsonPaths) { - return jsonPaths.map(function(jsonPath) { - return path.resolve(path.join(startPath, jsonPath)); + .then(function(jsonPaths) { + return jsonPaths.map(function(jsonPath) { + return path.resolve(path.join(startPath, jsonPath)); + }); }); - }); }; /** @@ -250,27 +250,19 @@ exports.writeFile = function(filePath, contents) { } return mkdirpAsync(path.dirname(filePath)) - .then(function() { - return fs.writeFileAsync(filePath, contents); - }); + .then(function() { + return fs.writeFileAsync(filePath, contents); + }); }; /** - * Generate Lambda Name + * Get Lambda Name * @param awsmJson * @returns {string} */ -exports.generateLambdaName = function(awsmJson) { - let handlerName = awsmJson.lambda.cloudFormation.Handler.replace('aws_modules', ''), - resourceAction = handlerName.substr(0, handlerName.lastIndexOf('/')); - - //We prefix with an l to make sure the CloudFormation resource map index is unique - //we will probably have API gateway resources in their own CF JSON but we dont want to - //make that decsision until CF has API gateway support - return 'l' + resourceAction.replace(/(\/|-|_)([a-z])/g, function(g) { - return g[1].toUpperCase(); - }); +exports.getLambdaName = function(awsmJson) { + return awsmJson.name; }; /** @@ -302,16 +294,16 @@ exports.getAllLambdaNames = function(projectRootPath) { lambdaNames = []; return this.findAllLambdas(projectRootPath) - .then(function(lambdaAwsmPaths) { - lambdaAwsmPaths.forEach(function(ljp) { - let awsm = _this.readAndParseJsonSync(ljp), - lambdaName = _this.generateLambdaName(awsm); + .then(function(lambdaAwsmPaths) { + lambdaAwsmPaths.forEach(function(ljp) { + let awsm = _this.readAndParseJsonSync(ljp), + lambdaName = _this.getLambdaName(awsm); - lambdaNames.push(lambdaName); + lambdaNames.push(lambdaName); + }); + + return lambdaNames; }); - - return lambdaNames; - }); }; /** @@ -392,8 +384,8 @@ exports.generateResourcesCf = function(projRootPath, projName, projDomain, stage cfTemplate.Description = projName + " resources"; return this.writeFile( - path.join(projRootPath, 'cloudformation', stage, region, 'resources-cf.json'), - JSON.stringify(cfTemplate, null, 2) + path.join(projRootPath, 'cloudformation', stage, region, 'resources-cf.json'), + JSON.stringify(cfTemplate, null, 2) ); }; diff --git a/tests/all.js b/tests/all.js index f1b9b7b1a..f648cc5bd 100644 --- a/tests/all.js +++ b/tests/all.js @@ -1,7 +1,5 @@ 'use strict'; -//TODO: doc on needing to set env vars -//TODO: must setup an env var file for unittest require('./config'); //init config describe('All Tests', function() { @@ -15,7 +13,7 @@ describe('All Tests', function() { }); //require tests vs inline so we can run sequentially - require('./tests/TestPluginCustom'); + //require('./tests/TestPluginCustom'); //require('./cli/tag'); //require('./cli/env'); //require('./cli/module_create'); @@ -24,10 +22,11 @@ describe('All Tests', function() { /** * Tests below create AWS Resources */ - //require('./cli/dash'); - //require('./cli/deploy_lambda'); - //require('./cli/deploy_resources'); - //require('./cli/deploy_endpoint'); - //require('./cli/new_stage_region'); - require('./tests/TestActionProjectCreate'); + //require('./cli/dash'); + //require('./cli/deploy_lambda'); + //require('./cli/deploy_resources'); + //require('./cli/deploy_endpoint'); + //require('./cli/new_stage_region'); + //require('./tests/actions/ProjectCreate'); + require('./tests/actions/VersionLambda'); }); \ No newline at end of file diff --git a/tests/config.js b/tests/config.js index ba9e13612..3acb40158 100644 --- a/tests/config.js +++ b/tests/config.js @@ -1,25 +1,25 @@ 'use strict'; const path = require('path'), - AWS = require('aws-sdk'); + AWS = require('aws-sdk'); // Require ENV lets, can also set ENV lets in your IDE require('dotenv').config({path: path.join(__dirname, '.env'), silent: true}); -process.env.JAWS_VERBOSE = true; +process.env.DEBUG = '*'; let config = { - name: 'test-prj', - domain: 'test-prj.com', - notifyEmail: 'tester@jawsstack.com', - stage: 'unittest', - region: 'us-east-1', - stage2: 'unittest2', - region2: 'us-west-2', + name: 'test-prj', + domain: process.env.TEST_JAWS_DOMAIN, + notifyEmail: 'tester@jawsstack.com', + stage: 'unittest', + region: 'us-east-1', + stage2: 'unittest2', + region2: 'us-west-2', iamRoleArnApiGateway: process.env.TEST_JAWS_APIGATEWAY_ROLE, - iamRoleArnLambda: process.env.TEST_JAWS_LAMBDA_ROLE, - profile: process.env.TEST_JAWS_PROFILE, - noExecuteCf: (process.env.TEST_JAWS_NO_EXE_CF != "false"), + iamRoleArnLambda: process.env.TEST_JAWS_LAMBDA_ROLE, + profile: process.env.TEST_JAWS_AWS_PROFILE, + noExecuteCf: process.env.TEST_JAWS_EXE_CF != "true", }; AWS.config.credentials = new AWS.SharedIniFileCredentials({ diff --git a/tests/tests/TestActionProjectCreate.js b/tests/tests/actions/ProjectCreate.js similarity index 78% rename from tests/tests/TestActionProjectCreate.js rename to tests/tests/actions/ProjectCreate.js index 14ddcbc33..d13e8bf17 100644 --- a/tests/tests/TestActionProjectCreate.js +++ b/tests/tests/actions/ProjectCreate.js @@ -6,20 +6,20 @@ * - Deletes the CF stack created by the project */ -let JAWS = require('../../lib/jaws.js'), - JawsError = require('../../lib/jaws-error'), +let JAWS = require('../../../lib/Jaws.js'), + JawsError = require('../../../lib/jaws-error/index'), path = require('path'), os = require('os'), - utils = require('../../lib/utils'), + utils = require('../../../lib/utils/index'), assert = require('chai').assert, shortid = require('shortid'), - config = require('../config'); + config = require('../../config'); // Instantiate JAWS let Jaws = new JAWS({ - awsAdminKeyId: '123', + awsAdminKeyId: '123', awsAdminSecretKey: '123', - interactive: false, + interactive: false, }); describe('Test Plugin: Project Create', function() { @@ -38,17 +38,17 @@ describe('Test Plugin: Project Create', function() { this.timeout(0); - let name = 'jaws-test-' + shortid.generate().replace('_', ''); + let name = config.name + shortid.generate().replace('_', ''); Jaws.actions.projectCreate( name, - name + '.com', - 'test', - 'us-east-1', - 'test@test.com', + config.domain, + config.stage, + config.region, + config.notifyEmail, 'nodejs', - true - ) + config.noExecuteCf + ) .then(function() { let jawsJson = utils.readAndParseJsonSync(path.join(os.tmpdir(), name, 'jaws.json')); diff --git a/tests/tests/actions/VersionLambda.js b/tests/tests/actions/VersionLambda.js new file mode 100644 index 000000000..f739958ac --- /dev/null +++ b/tests/tests/actions/VersionLambda.js @@ -0,0 +1,79 @@ +'use strict'; + +/** + * Test: Project Create Action + * - Creates a new project in your system's temp directory + * - Deletes the CF stack created by the project + */ + +//let JAWS = require('../../../lib/Jaws.js'), +// JawsError = require('../../../lib/jaws-error/index'), +// path = require('path'), +// os = require('os'), +// utils = require('../../../lib/utils/index'), +// assert = require('chai').assert, +// shortid = require('shortid'), +// config = require('../../config'); + +//let Jaws = new JAWS(); + +let AwsUtils = require('../../../lib/utils/aws'); + +describe('Test Plugin: Version Lambda', function() { + + before(function(done) { + this.timeout(0); + done(); + }); + + after(function(done) { + done(); + }); + + describe('Test Plugin: Version lambda', function() { + it('should create a new project in temp directory', function(done) { + this.timeout(0); + + //let name = config.name + shortid.generate().replace('_', ''); + // + //Jaws.actions.projectCreate( + // name, + // config.domain, + // config.stage, + // config.region, + // config.notifyEmail, + // 'nodejs', + // config.noExecuteCf + // ) + // .then(function() { + // let jawsJson = utils.readAndParseJsonSync(path.join(os.tmpdir(), name, 'jaws.json')); + // + // let region = false; + // + // for (let i = 0; i < jawsJson.stages[config.stage].length; i++) { + // let stage = jawsJson.stages[config.stage][i]; + // if (stage.region === config.region) { + // region = stage.region; + // } + // } + // assert.isTrue(region !== false); + // done(); + // }) + // .catch(JawsError, function(e) { + // done(e); + // }) + // .error(function(e) { + // done(e); + // }); + }); + }); + + //it('Delete Cloudformation stack from new project', function(done) { + // this.timeout(0); + // let CF = new config.AWS.CloudFormation(); + // CF.deleteStack({ StackName: config.stage + '-' + config.name }, function(err, data) { + // if (err) console.log(err, err.stack); + // done(); + // }); + //}); +});