aws-lager
Version:
AWS Lambda / API Gateway / Endpoint Router
239 lines (207 loc) • 8.35 kB
JavaScript
;
const fs = require('fs');
const path = require('path');
const file = require('file');
const _ = require('lodash');
const Promise = require('bluebird');
const AWS = require('aws-sdk');
const ApiSpecification = require('./api_specification');
const EndpointSpecification = require('./endpoint_specification');
const apiGatewayHelperFn = require('./api_gateway_helper');
AWS.config.apiVersions = {
apigateway: '2015-07-09',
iam: '2010-05-08'
};
let apiGateway;
let apiGatewayHelper;
let iam;
/**
* Constructor function
*
* @param Object options
* {
* credentials: Object AWS.Credentials
* region: String,
* environment: String
* }
*/
let specBuilder = function(options) {
apiGateway = new AWS.APIGateway(options);
apiGatewayHelper = apiGatewayHelperFn(apiGateway);
iam = new AWS.IAM();
};
specBuilder.prototype.initAPISpecs = function(pathToApiDir) {
// Retrieve api configuration directories
let apiPaths = _.map(fs.readdirSync(pathToApiDir), function(dirName) {
return pathToApiDir + '/' + dirName;
});
// Run the functions in serie
return Promise.mapSeries(apiPaths, function(apiPath) {
return this.initSpecification(apiPath);
}.bind(this));
};
/**
* Construct and returns an API specification
* @param String pathToApi
*/
specBuilder.prototype.initSpecification = function(pathToApi) {
// @TODO refactor this to delete "config" and only use the "baseDef"
// Implementing EventEmitter should allow to inject customisation programatically
let config = require(process.cwd() + '/' + pathToApi + '/config');
config.identifier = path.basename(pathToApi);
console.log(' * Initializing specification of \x1b[0;36m' + config.name + '\x1b[0m');
// Retrieve the current API in AWS using config.name
return apiGatewayHelper.getApiByName(config.name)
.then(function(api) {
if (!api) {
// If the API was not found, we create it
console.log(' * The API \x1b[0;36m' + config.name + '\x1b[0m will be created');
return Promise.promisify(apiGateway.createRestApi.bind(apiGateway))(config);
} else {
console.log(' * The API \x1b[0;36m' + config.name + '\x1b[0m already exists');
return Promise.resolve(api);
}
})
.then(function(api) {
config.id = api.id;
console.log(' * The API \x1b[0;36m' + api.name + '\x1b[0m has the ID \x1b[0;36m' + api.id + '\x1b[0m');
return Promise.promisify(fs.readFile)(pathToApi + '/base.json', 'utf-8');
})
.then(function(fileContent) {
let baseDef = JSON.parse(fileContent);
let specification = new ApiSpecification(baseDef, config);
// @TODO add only necessary models (AKA when an endpoint is added)
let modelFiles = fs.readdirSync('./swagger/models/');
modelFiles.forEach(function(fileName) {
if (/\.json$/.test(fileName)) {
specification.addModelDefinition(_.capitalize(fileName.substr(0, fileName.length - 5)), require(process.cwd() + '/swagger/models/' + fileName));
}
});
return specification;
});
};
/**
* [function description]
* @param {String} pathToEnpointsDir
* @return Array the list of all endpoints
*/
specBuilder.prototype.initAllEndpointSpecifications = function(pathToEnpointsDir) {
let endpointsSpecifications = [];
file.walkSync(pathToEnpointsDir, function(dirPath, dirs, files) {
let pathParts = dirPath.split('/');
let method = pathParts.pop();
if (['GET', 'POST', 'PUT', 'DELETE'].indexOf(method)) return;
// We remove the first part of the path which is the name of the directory that contains the endpoints
pathParts.shift();
let path = pathParts.join('/');
let swagger = aggregateSpecifications(pathToEnpointsDir, dirPath);
let integrationHook;
if (swagger['x-amazon-apigateway-integration'].type === 'HTTP' && files.indexOf('integration.js')) {
integrationHook = Promise.promisify(require(process.cwd() + '/' + dirPath + '/httpProxy.js'));
}
endpointsSpecifications.push(new EndpointSpecification(
method,
path,
aggregateSpecifications(pathToEnpointsDir, dirPath),
integrationHook
));
});
return endpointsSpecifications;
};
/**
* [function description]
* @param {[type]} lambdaDeployResponses [description]
* @param {[type]} endpointSpecifications [description]
*/
specBuilder.prototype.addLambdaIntegrationHooks = function(lambdaDeployResponses, endpointSpecifications) {
_.forEach(endpointSpecifications, function(endpointSpecification) {
// If the endpoint has a lambda integration, we add an integration hook based on the results of the deployment
let swagger = endpointSpecification.getSpecification();
if (swagger['x-amazon-apigateway-integration'].type === 'aws' && swagger['x-lager'] && swagger['x-lager'].lambda) {
let lambdaDeployResponse = _.find(lambdaDeployResponses, function(response) {
return response.FunctionName === this.environment + '_' + swagger['x-lager'].lambda;
}.bind(this));
if (!lambdaDeployResponse) {
throw new Error(endpointSpecification.getMethod() + ' ' + endpointSpecification.getPath() + ' is associated with a Lambda that is not defined (' + swagger['x-lager'].lambda + ')');
}
endpointSpecification.setIntegrationHook(function(swagger) {
swagger['x-amazon-apigateway-integration'].uri = 'arn:aws:apigateway:' + lambdaDeployResponse.FunctionArn.split(':')[3] + ':lambda:path/2015-03-31/functions/' + lambdaDeployResponse.FunctionArn + '/invocations';
return retrieveRoleArn(swagger['x-lager'].lambdaInvocationRole, this.environment)
.then(function(invocationRoleArn) {
swagger['x-amazon-apigateway-integration'].credentials = invocationRoleArn;
return Promise.resolve(swagger);
});
}.bind(this));
}
}.bind(this));
};
/**
* [function description]
* @param {[type]} apiSpecifications [description]
* @param {[type]} endpointSpecifications [description]
*/
specBuilder.prototype.addEndpointsToApisSpecifications = function(apiSpecifications, endpointSpecifications) {
return Promise.map(endpointSpecifications, function(endpointSpecifications) {
return endpointSpecifications.execIntegrationHook();
})
.then(function() {
_.forEach(apiSpecifications, function(apiSpecification) {
_.forEach(endpointSpecifications, function(endpointSpecification) {
apiSpecification.addEndpointSpecification(endpointSpecification);
});
});
return Promise.resolve(apiSpecifications);
});
};
/**
* Static method that aggregates the definitions found in all swagger.json files in a path
* @param String beginPath - path from which the function will look for swagger.json files
* @param String endPath -path until which the function will look for swagger.json files
* @return Object - aggregation of definitions that have been found
*/
function aggregateSpecifications(beginDir, endDir) {
// Retrieve paths to all swagger.json that have to be aggregated
let embedDirs = endDir.substr(beginDir.length + 1).split('/');
let def = {};
let fileContent = '';
let next = true;
console.log(beginDir, embedDirs);
while (next) {
try {
fileContent = fs.readFileSync(beginDir + '/swagger.json', 'utf8');
} catch (e) {
// If the file does not exists, a exception will be triggered
fileContent = '{}';
}
// If a JSON parsing error occurs, the Exception has to be thrown
def = _.merge(def, JSON.parse(fileContent));
if (embedDirs.length > 0) {
beginDir += '/' + embedDirs.shift();
} else {
next = false;
}
}
return def;
}
/* istanbul ignore next */
/**
* Retrieve a role Arn from a identifier that can be either the ARN or the name
* @param String identifier - The name or the ARN of the role
* @return Promise
*/
function retrieveRoleArn(identifier, environment) {
if (/arn:aws:iam::\d{12}:role\/?[a-zA-Z_0-9+=,.@\-_/]+]/.test(identifier)) {
return Promise.resolve(identifier);
}
return Promise.promisify(iam.getRole.bind(iam))({ RoleName: identifier })
.then(function(data) {
return Promise.resolve(data.Role.Arn);
})
.catch(function(e) {
if (e && environment) {
return retrieveRoleArn(environment + '_' + identifier);
}
return Promise.reject(e);
});
}
module.exports = specBuilder;