UNPKG

kes

Version:

Making deployment to AWS using CloudFormation easier and fun

305 lines (263 loc) 9.46 kB
'use strict'; const fs = require('fs'); const has = require('lodash.has'); const values = require('lodash.values'); const startsWith = require('lodash.startswith'); const trim = require('lodash.trim'); const replace = require('lodash.replace'); const upperFirst = require('lodash.upperfirst'); const capitalize = require('lodash.capitalize'); const yaml = require('js-yaml'); const yamlinc = require('yaml-include'); const Mustache = require('mustache'); const utils = require('./utils'); /** * This class handles reading and parsing configuration files. * It primarily reads `stage.yml`, `config.yml` and `.env` files * * @example * const configurator = new Config('mystack', 'dev', '.kes/config.yml', '.kes/stage.yml', '.kes/.env'); * const config = configurator.parse(); * * @param {String} stack Stack name * @param {String} stage Stage name * @param {String} configFile path to the config.yml file * @param {String} stageFile path to the stage.yml file (optional) * @param {String} envFile path to the .env file (optional) * @class Config */ class Config { constructor(stack, stage, configFile, stageFile, envFile) { this.stack = stack; this.stage = stage; this.configFile = configFile; this.stageFile = stageFile; this.envs = utils.loadLocalEnvs(envFile); } /** * Generates configuration arrays for ApiGateway portion of * the CloudFormation * * @private * @static * @param {Object} config The configuration object * @return {Object} Returns the updated configuration object */ static configureApiGateway(config) { if (config.apis) { // APIGateway name used in AWS APIGateway Definition const apiMethods = []; const apiMethodsOptions = {}; const apiDependencies = {}; config.apis.forEach((api) => { apiDependencies[api.name] = []; }); // The array containing all the info // needed to define each APIGateway resource const apiResources = {}; // We loop through all the lambdas in config.yml // To construct the API resources and methods for (const lambda of config.lambdas) { // We only care about lambdas that have apigateway config if (lambda.hasOwnProperty('apiGateway')) { //loop the apiGateway definition for (const api of lambda.apiGateway) { // Because each segment of the URL path gets its own // resource and paths with the same segment shares that resource // we start by dividing the path segments into an array. // For example. /foo, /foo/bar and /foo/column create 3 resources: // 1. FooResource 2.FooBarResource 3.FooColumnResource // where FooBar and FooColumn are dependents of Foo const segments = api.path.split('/'); // this array is used to keep track of names // within a given array of segments const segmentNames = []; segments.forEach((segment, index) => { let name = segment; let parents = []; // when a segment includes a variable, e.g. {short_name} // we remove the curly braces and underscores and add Var to the name if (startsWith(segment, '{')) { name = `${replace(trim(segment, '{}'), '_', '')}Var`; } name = upperFirst(name); segmentNames.push(name); // the first segment is always have rootresourceid as parent if (index === 0) { parents = [ 'Fn::GetAtt:', `- ${api.api}RestApi`, '- RootResourceId' ]; } else { // This logic finds the parents of other segments parents = [ `Ref: ApiGateWayResource${segmentNames.slice(0, index).join('')}` ]; name = segmentNames.map(x => x).join(''); } // We use an object here to catch duplicate resources // This ensures if to paths shares a segment, they also // share a parent apiResources[name] = { name: `ApiGateWayResource${name}`, pathPart: segment, parents: parents, api: api.api }; }); const method = capitalize(api.method); const name = segmentNames.map(x => x).join(''); const methodName = `ApiGatewayMethod${name}${capitalize(method)}`; // Build the ApiMethod array apiMethods.push({ name: methodName, method: method.toUpperCase(), cors: api.cors || false, resource: `ApiGateWayResource${name}`, lambda: lambda.name, api: api.api }); // populate api dependency list try { apiDependencies[api.api].push({ name: methodName }); } catch (e) { console.error(`${api.api} is not defined`); throw e; } // Build the ApiMethod Options array. Only needed for resources // with cors set to true if (api.cors) { apiMethodsOptions[name] = { name: `ApiGatewayMethod${name}Options`, resource: `ApiGateWayResource${name}`, api: api.api }; } } } } return Object.assign(Config, { apiMethods, apiResources: values(apiResources), apiMethodsOptions: values(apiMethodsOptions), apiDependencies: Object.keys(apiDependencies).map(k => ({ name: k, methods: apiDependencies[k] })) }); } return config; } /** * Sets default values for the lambda function. * if the lambda function includes source path, it does copy, zip and upload * the functions to Amazon S3 * * @private * @static * @param {Object} config The configuration object * @return {Object} Returns the updated configruation object */ static configureLambda(config) { if (config.lambdas) { // Add default memory and timeout to all lambdas for (const lambda of config.lambdas) { if (!has(lambda, 'memory')) { lambda.memory = 1024; } if (!has(lambda, 'timeout')) { lambda.timeout = 300; } // add lambda name to services if any if (lambda.hasOwnProperty('services')) { for (const service of lambda.services) { service.lambdaName = lambda.name; } } if (!has(lambda, 'envs')) { lambda.envs = []; } // lambda fullName lambda.fullName = `${config.stackName}-${config.stage}-${lambda.name}`; } } return config; } /** * reads and parses stage.yml, merges the variables under default with * the specified stage and returns the it as a js object * * @private * @return {Object} returns the content of config.yml as a js object */ parseStage() { let t; try { t = fs.readFileSync(this.stageFile); } catch (e) { if (e.message.includes('no such file or directory')) { console.log(`${this.stageFile} was not found. Skipping stage`); return {}; } throw e; } Mustache.escape = (text) => text; const rendered = Mustache.render(t.toString(), this.envs); // convert to object from yaml const stageConfig = yaml.safeLoad(rendered, { schema: yamlinc.YAML_INCLUDE_SCHEMA }); if (this.stage) { Object.assign(stageConfig.default, stageConfig[this.stage]); } return stageConfig.default; } /** * Parses the config.yml to js Object after passing it through variables set * in stage.yml * * @private * @param {Object} stageConfig the stage.yml object * @return {Object} returns configuration object */ parseConfig(stageConfig) { const configText = fs.readFileSync(this.configFile, 'utf8'); Object.assign(stageConfig, this.envs); Mustache.escape = (text) => text; let rendered = Mustache.render(configText.toString(), stageConfig); // load, dump, then load to make sure all yaml included files pass through mustach render const tmp = yaml.safeLoad(rendered, { schema: yamlinc.YAML_INCLUDE_SCHEMA }); const tmp2 = yaml.dump(tmp); rendered = Mustache.render(tmp2, stageConfig); let config = yaml.safeLoad(rendered); if (this.stack) { config.stackName = this.stack; } if (this.stage) { config.stage = this.stage; } Object.assign(config, stageConfig); config = this.constructor.configureLambda(config); Object.assign(config, this.constructor.configureApiGateway(config)); return config; } /** * Main method of the class. It parses a configuration and returns it * as a JS object. * * @example * const configInstance = new Config(null, null, 'path/to/config.yml', 'path/to/stage.yml', 'path/to/.env'); * config = configInstance.parse(); * * @return {Object} the configuration object */ parse() { const stageConfig = this.parseStage(); return this.parseConfig(stageConfig); } } module.exports = Config;