'use strict'; /** * JAWS Command: deploy api * - Deploys project's API Gateway REST API to the specified stage */ var JawsError = require('../jaws-error'), Promise = require('bluebird'), fs = require('fs'), path = require('path'), os = require('os'), wrench = require('wrench'), async = require('async'), AWSUtils = require('../utils/aws'), inquirer = require('bluebird-inquirer'), chalk = require('chalk'), utils = require('../utils/index'), shortid = require('shortid'), extend = require('util')._extend, Spinner = require('cli-spinner').Spinner, JawsAPIClient = require('jaws-api-gateway-client'); Promise.promisifyAll(fs); module.exports = function(JAWS) { var client = null; /** * Find Or Create Rest Api * @returns {bluebird|exports|module.exports} * @private */ JAWS._dapiFindOrCreateApi = function(state) { return new Promise(function(resolve, reject) { // Set Region state.region = Object.keys(JAWS._meta.projectJson.project.regions)[0]; state.restApiId = JAWS._meta.projectJson.project.regions[state.region].restApiId || null; // Validate Stage if (!JAWS._meta.projectJson.project.regions[state.region].stages[state.stage]) { reject(new JawsError( 'This stage doesn\'t exist. Please add it to the jaws.json file at the root of your project', JawsError.errorCodes.UNKNOWN)); } // Get AWS Account Number state.awsAccountNumber = JAWS._meta.projectJson.project.regions[state.region].stages[state.stage].iamRoleArn state.awsAccountNumber = state.awsAccountNumber.replace('arn:aws:iam::','').split(':')[0]; // Instantiate JawsApiGatewayClient client = new JawsAPIClient({ accessKeyId: JAWS._meta.credentials.aws_access_key_id, secretAccessKey: JAWS._meta.credentials.aws_secret_access_key, region: state.region, }); // Start Spinner state.spinner = new Spinner('%s Creating your REST API for the state "' + state.stage + '"...'); state.spinner.setSpinnerString('|/-\\'); state.spinner.start(); // Check Project's jaws.json for restApiId, otherwise create an api if (state.restApiId) { // Show existing REST API client.showRestApi(state.restApiId).then(function(response) { resolve(state); }).catch(function(error) { reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); }); } else { // Create REST API client.createRestApi({ name: JAWS._meta.projectJson.name, description: JAWS._meta.projectJson.description, }).then(function(response) { // Update Project's jaws.json JAWS._meta.projectJson.project.regions[state.region].restApiId = response.id; var newJson = JSON.stringify(JAWS._meta.projectJson, null, 2); fs.writeFileSync(path.join(JAWS._meta.projectRootPath, 'jaws.json'), newJson); state.restApiId = response.id; resolve(state); }); } }); }; /** * Delete All API Resources * @param state * @returns {*} * @private */ JAWS._dapiDeleteAllResources = function(state) { // List all Resources for this REST API return client.listResources(state.restApiId).then(function(response) { return new Promise(function(resolve, reject) { // Parse API Gateway's HAL response var apiResources = response._embedded.item; if (!Array.isArray(apiResources)) apiResources = [apiResources]; // Delete Every Resource async.eachSeries(apiResources, function(resource, cb) { // If Parent Resource ('/'), save its ID and skip if (resource.path === '/') { state.resourceParentId = resource.id; return cb(); } // Delete Resource client.deleteResource(state.restApiId, resource.id) .then(function(response) { return cb(); }) .catch(function(error) { return cb(); }); }, function(error) {resolve(state);}); }); }); }; /** * Find All Endpoint Modules * @private */ JAWS._dapiFindAllEndpoints = function(state) { return utils.findAllEndpoints(JAWS._meta.projectRootPath) .then(function(endpoints) { return new Promise(function(resolve, reject) { // Check each lambda has a 'path', otherwise it's not an API endpoint async.eachSeries(endpoints, function(endpoint, cb) { // Load Endpoint JSON var endpointJson = require(path.join(JAWS._meta.projectRootPath, endpoint)); if (endpointJson.endpoint) { // Remove CloudFormation Snippet if (endpointJson.cfExtension) delete endpointJson.cfExtension; // Push to state's endpoints state.endpoints.push(endpointJson); } return cb(); }, function(error) { resolve(state); }); }); }); }; /** * Create Resources * @param state * @returns {*} * @private */ JAWS._dapiCreateResources = function(state) { return new Promise(function(resolve, reject) { // Store created resources var createdResources = []; // Loop through each Endpoint and check if its required resources have been created async.eachSeries(state.endpoints, function(endpoint, endpointCb) { // Each part of the path is a resource, turn this into an array var endpointResources = endpoint.endpoint.path.split('/'); // Add "apig" property endpoint.endpoint.apig = {}; // Loop through each resource required by the Endpoint's Path async.eachSeries(endpointResources, function(endpointResource, resourceCb) { // Remove any slashes endpointResource = endpointResource.replace(/\//g, ''); // If already created, skip for (var i = 0; i < createdResources.length; i++) { if (endpointResource === createdResources[i].pathPart) { return resourceCb(); } } // Get This Resource's Parent ID var parentIndex = endpointResources.indexOf(endpointResource) - 1; if (parentIndex === -1) { endpoint.endpoint.apig.parentResourceId = state.resourceParentId; } else if (parentIndex > -1) { // Get Parent Resource ID for (var i = 0; i < createdResources.length; i++) { if (createdResources[i].pathPart === endpointResources[parentIndex]) { endpoint.endpoint.apig.parentResourceId = createdResources[i].id; } } } // Throw error if no parentId is found if (!endpoint.endpoint.apig.parentResourceId) reject(new JawsError( 'Couldn\'t find a parent resource for ' + endpointResource, JawsError.errorCodes.UNKNOWN)); // Create Resource client.createResource(state.restApiId, endpoint.endpoint.apig.parentResourceId, endpointResource) .then(function(response) { // Add resource to state.resources and callback createdResources.push(response); return resourceCb(); }).catch(function(error) { reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); }); }, function(error) { // Attach the resource to the state's endpoint for later use var endpointResource = endpoint.endpoint.path.split('/').pop().replace(/\//g, ''); for (var i = 0; i < createdResources.length; i++) { if (createdResources[i].pathPart === endpointResource) { endpoint.endpoint.apig.resource = createdResources[i]; } } return endpointCb(); }); }, function(error) { resolve(state); }); }); }; /** * Create Methods * @param state * @returns {*} * @private */ JAWS._dapiCreateMethods = function(state) { return new Promise(function(resolve, reject) { // Loop through Endpoints async.eachSeries(state.endpoints, function(endpoint, cb) { // Create Method var methodBody = { authorizationType: endpoint.endpoint.authorizationType, }; // If Request Params, add them if (endpoint.endpoint.requestParameters) { methodBody.requestParameters = {}; // Format them per APIG's API's Expectations for (var prop in endpoint.endpoint.requestParameters) { var requestParam = endpoint.endpoint.requestParameters[prop]; methodBody.requestParameters[requestParam] = true; } } // Create Method client.putMethod(state.restApiId, endpoint.endpoint.apig.resource.id, endpoint.endpoint.method, methodBody) .then(function(response) { // Save method to Lambda apig property endpoint.endpoint.apig.method = response; return cb(); }).catch(function(error) { return reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); }); }, function(error) { resolve(state); }); }); }; /** * Create Integrations * @param state * @returns {*} * @private */ JAWS._dapiCreateIntegrations = function(state) { return new Promise(function(resolve, reject) { // Loop through Endpoints async.eachSeries(state.endpoints, function(endpoint, cb) { // Create Integration if (typeof endpoint.lambda !== 'undefined') { var integrationBody = { type: 'AWS', httpMethod: 'POST', authorizationType: 'none', uri: 'arn:aws:apigateway:' + state.region + ':lambda:path/2015-03-31/functions/arn:aws:lambda:' + state.region + ':' + state.awsAccountNumber + ':function:' + [state.stage, JAWS._meta.projectJson.name, endpoint.lambda.functionName, ].join('_-_').replace(/ /g, '') + '/invocations', credentials: JAWS._meta.projectJson.project.regions[state.region].stages[state.stage].iamRoleArn, requestParameters: endpoint.endpoint.requestParameters || {}, requestTemplates: endpoint.endpoint.requestTemplates || {}, cacheNamespace: endpoint.endpoint.cacheNamespace || null, cacheKeyParameters: endpoint.endpoint.cacheKeyParameters || [], }; } else { reject(new JawsError( 'JAWS API Gateway integration currently supports lambda only', JawsError.errorCodes.UNKNOWN)); } // Create Integration client.putIntegration(state.restApiId, endpoint.endpoint.apig.resource.id, endpoint.endpoint.method, integrationBody) .then(function(response) { // Save integration to apig property endpoint.endpoint.apig.integration = response; return cb(); }).catch(function(error) { return reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); }); }, function(error) { resolve(state); }); }); }; /** * Create Method Responses * @param state * @returns {*} * @private */ JAWS._dapiCreateMethodResponses = function(state) { return new Promise(function(resolve, reject) { // Loop through Endpoints async.eachSeries(state.endpoints, function(endpoint, endpointCb) { // Collect response keys to an array if (endpoint.endpoint.responses) var responses = Object.keys(endpoint.endpoint.responses); else var responses = []; // Loop through Responses async.eachSeries(responses, function(responseKey, responseCb) { var thisResponse = endpoint.endpoint.responses[responseKey]; var methodResponseBody = {}; // Format Response Parameters per APIG API's Expectations for (prop in thisResponse.responseParameters) { var param = endpoint.endpoint.responseParameters[prop]; methodResponseBody.responseParameters[param[prop]] = true; } // Create Method Response client.putMethodResponse(state.restApiId, endpoint.endpoint.apig.resource.id, endpoint.endpoint.method, thisResponse.statusCode, methodResponseBody) .then(function(response) { return responseCb(); }).catch(function(error) { reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); }); }, function(error) { return endpointCb(); }); }, function(error) { resolve(state); }); }); }; /** * Create Integration Responses * @param state * @returns {*} * @private */ JAWS._dapiCreateIntegrationResponses = function(state) { return new Promise(function(resolve, reject) { // Loop through Endpoints async.eachSeries(state.endpoints, function(endpoint, endpointCb) { // Collect response keys to an array if (endpoint.endpoint.responses) var responses = Object.keys(endpoint.endpoint.responses); else var responses = []; // Loop through Responses async.eachSeries(responses, function(responseKey, responseCb) { var thisResponse = endpoint.endpoint.responses[responseKey]; var integrationResponseBody = {}; // Add Response Parameters integrationResponseBody.responseParameters = thisResponse.responseParameters; // Add Response Templates integrationResponseBody.responseTemplates = thisResponse.responseTemplates; // Add SelectionPattern integrationResponseBody.selectionPattern = responseKey === 'default' ? null : responseKey;// null = default // Create Integration Response client.putIntegrationResponse(state.restApiId, endpoint.endpoint.apig.resource.id, endpoint.endpoint.method, thisResponse.statusCode, integrationResponseBody) .then(function(response) { return responseCb(); }).catch(function(error) { reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); }); }, function(error) { return endpointCb(); }); }, function(error) { resolve(state); }); }); }; /** * Create Deployment * @param state * @returns {*} * @private */ JAWS._dapiCreateDeployment = function(state) { return new Promise(function(resolve, reject) { var deployment = { stageName: state.stage, stageDescription: state.stage, description: 'JAWS deployment', }; client.createDeployment(state.restApiId, deployment) .then(function(response) { // Add deployment to state state.deployment = response; resolve(state); }) .catch(function(error) { reject(new JawsError( error.message, JawsError.errorCodes.UNKNOWN)); }); }); }; /** * Deploy API * @param stage * @returns {bluebird|exports|module.exports} */ JAWS.deployApi = function(stage) { return new Promise(function(resolve, reject) { // Create state object for all functions return resolve({ stage: stage.toLowerCase().trim(), region: null, restApiId: null, resourceParentId: null, endpoints: [], }); }) .then(this._dapiFindOrCreateApi) .then(this._dapiDeleteAllResources) .then(this._dapiFindAllEndpoints) .then(this._dapiCreateResources) .then(this._dapiCreateMethods) .then(this._dapiCreateIntegrations) .then(this._dapiCreateMethodResponses) .then(this._dapiCreateIntegrationResponses) .then(this._dapiCreateDeployment) .then(function(state) { state.spinner.stop(true); console.log('API successfully deployed: https://' + state.restApiId + '.execute-api.' + state.region + '.amazonaws.com/' + state.stage + '/'); return Promise.resolve(); }); }; };