UNPKG

11.1 kBJavaScriptView Raw
1'use strict';
2
3const fs = require('fs');
4const path = require('path');
5const get = require('lodash.get');
6const has = require('lodash.has');
7const values = require('lodash.values');
8const startsWith = require('lodash.startswith');
9const trim = require('lodash.trim');
10const replace = require('lodash.replace');
11const upperFirst = require('lodash.upperfirst');
12const capitalize = require('lodash.capitalize');
13const merge = require('lodash.merge');
14const yaml = require('js-yaml');
15const yamlfiles = require('yaml-files');
16const Mustache = require('mustache');
17const utils = require('./utils');
18
19/**
20 * This class handles reading and parsing configuration files.
21 * It primarily reads `config.yml` and `.env` files
22 *
23 * @example
24 * const config = new Config('mystack', 'dev', '.kes/config.yml', '.kes/.env');
25 *
26 * @param {String} stack Stack name
27 * @param {String} deployment Deployment name
28 * @param {String} configFile path to the config.yml file
29 * @param {String} envFile path to the .env file (optional)
30 *
31 *
32 * @param {Object} options a js object that includes required options.
33 * @param {String} [options.stack] the stack name
34 * @param {String} [options.deployment=null] the deployment name
35 * @param {String} [options.region='us-east-1'] the aws region
36 * @param {String} [options.profile=null] the profile name
37 * @param {String} [options.kesFolder='.kes'] the path to the kes folder
38 * @param {String} [options.configFile='config.yml'] the path to the config.yml
39 * @param {String} [options.envFile='.env'] the path to the .env file
40 * @param {String} [options.cfFile='cloudformation.template.yml'] the path to the CF template
41
42 * @class Config
43 */
44class Config {
45 //constructor(stack, deployment, configFile, envFile) {
46 constructor(options) {
47 this.region = get(options, 'region');
48 this.profile = get(options, 'profile', null);
49 this.deployment = get(options, 'deployment');
50 this.role = get(options, 'role', process.env.AWS_DEPLOYMENT_ROLE);
51 this.stack = get(options, 'stack', null);
52
53 // use template if provided
54 if (has(options, 'template')) {
55 const templatePath = get(options, 'template');
56 fs.lstatSync(templatePath);
57 this.template = {
58 kesFolder: templatePath,
59 configFile: path.join(templatePath, 'config.yml'),
60 cfFile: path.join(templatePath, 'cloudformation.template.yml')
61 };
62 }
63 else {
64 this.template = null;
65 }
66
67 this.kesFolder = get(options, 'kesFolder', path.join(process.cwd(), '.kes'));
68 this.configFile = get(options, 'configFile', path.join(this.kesFolder, 'config.yml'));
69 this.envFile = get(options, 'envFile', path.join(this.kesFolder, '.env'));
70 this.cfFile = get(options, 'cfFile', path.join(this.kesFolder, 'cloudformation.template.yml'));
71
72 this.envs = utils.loadLocalEnvs(this.envFile);
73 this.parse();
74 }
75
76 /**
77 * Generates configuration arrays for ApiGateway portion of
78 * the CloudFormation
79 *
80 * @private
81 * @static
82 * @param {Object} config The configuration object
83 * @return {Object} Returns the updated configuration object
84 */
85 static configureApiGateway(config) {
86 if (config.apis) {
87 // APIGateway name used in AWS APIGateway Definition
88 const apiMethods = [];
89 const apiMethodsOptions = {};
90 const apiDependencies = {};
91
92 config.apis.forEach((api) => {
93 apiDependencies[api.name] = [];
94 });
95
96 // The array containing all the info
97 // needed to define each APIGateway resource
98 const apiResources = {};
99
100 // We loop through all the lambdas in config.yml
101 // To construct the API resources and methods
102 let lambdas = config.lambdas;
103 if (!Array.isArray(config.lambdas)) {
104 lambdas = Object.keys(config.lambdas).map(name => {
105 const lambda = config.lambdas[name];
106 lambda.name = name;
107 return lambda;
108 });
109 }
110
111 for (const lambda of lambdas) {
112 // We only care about lambdas that have apigateway config
113 if (lambda.hasOwnProperty('apiGateway')) {
114 //loop the apiGateway definition
115 for (const api of lambda.apiGateway) {
116 // Because each segment of the URL path gets its own
117 // resource and paths with the same segment shares that resource
118 // we start by dividing the path segments into an array.
119 // For example. /foo, /foo/bar and /foo/column create 3 resources:
120 // 1. FooResource 2.FooBarResource 3.FooColumnResource
121 // where FooBar and FooColumn are dependents of Foo
122 const segments = api.path.split('/');
123
124 // this array is used to keep track of names
125 // within a given array of segments
126 const segmentNames = [];
127
128 segments.forEach((segment, index) => {
129 let name = segment;
130 let parents = [];
131
132 // when a segment includes a variable, e.g. {short_name}
133 // we remove the curly braces and underscores and add Var to the name
134 if (startsWith(segment, '{')) {
135 name = `${replace(trim(segment, '{}'), '_', '')}Var`;
136 }
137
138 name = upperFirst(name);
139 segmentNames.push(name);
140
141 // the first segment is always have rootresourceid as parent
142 if (index === 0) {
143 parents = [
144 'Fn::GetAtt:',
145 `- ${api.api}RestApi`,
146 '- RootResourceId'
147 ];
148 }
149 else {
150 // This logic finds the parents of other segments
151 parents = [
152 `Ref: ApiGateWayResource${segmentNames.slice(0, index).join('')}`
153 ];
154
155 name = segmentNames.map(x => x).join('');
156 }
157
158 // We use an object here to catch duplicate resources
159 // This ensures if to paths shares a segment, they also
160 // share a parent
161 apiResources[name] = {
162 name: `ApiGateWayResource${name}`,
163 pathPart: segment,
164 parents: parents,
165 api: api.api
166 };
167 });
168
169 const method = capitalize(api.method);
170 const name = segmentNames.map(x => x).join('');
171
172 const methodName = `ApiGatewayMethod${name}${capitalize(method)}`;
173
174 // Build the ApiMethod array
175 apiMethods.push({
176 name: methodName,
177 method: method.toUpperCase(),
178 cors: api.cors || false,
179 resource: `ApiGateWayResource${name}`,
180 lambda: lambda.name,
181 api: api.api
182 });
183
184 // populate api dependency list
185 try {
186 apiDependencies[api.api].push({
187 name: methodName
188 });
189 }
190 catch (e) {
191 console.error(`${api.api} is not defined`);
192 throw e;
193 }
194
195 // Build the ApiMethod Options array. Only needed for resources
196 // with cors set to true
197 if (api.cors) {
198 apiMethodsOptions[name] = {
199 name: `ApiGatewayMethod${name}Options`,
200 resource: `ApiGateWayResource${name}`,
201 api: api.api
202 };
203 }
204 }
205 }
206 }
207
208 return Object.assign(Config, {
209 apiMethods,
210 apiResources: values(apiResources),
211 apiMethodsOptions: values(apiMethodsOptions),
212 apiDependencies: Object.keys(apiDependencies).map(k => ({
213 name: k,
214 methods: apiDependencies[k]
215 }))
216 });
217 }
218
219 return config;
220 }
221
222 /**
223 * Sets default values for the lambda function.
224 * if the lambda function includes source path, it does copy, zip and upload
225 * the functions to Amazon S3
226 *
227 * @private
228 * @static
229 * @param {Object} config The configuration object
230 * @return {Object} Returns the updated configruation object
231 */
232 static configureLambda(config) {
233 if (config.lambdas) {
234 // Add default memory and timeout to all lambdas
235 let lambdas = config.lambdas;
236 if (!Array.isArray(config.lambdas)) {
237 lambdas = Object.keys(config.lambdas).map(name => {
238 const lambda = config.lambdas[name];
239 lambda.name = name;
240 return lambda;
241 });
242 }
243
244 for (const lambda of lambdas) {
245 if (!has(lambda, 'memory')) {
246 lambda.memory = 1024;
247 }
248
249 if (!has(lambda, 'timeout')) {
250 lambda.timeout = 300;
251 }
252
253 // add lambda name to services if any
254 if (lambda.hasOwnProperty('services')) {
255 for (const service of lambda.services) {
256 service.lambdaName = lambda.name;
257 }
258 }
259
260 if (!has(lambda, 'envs')) {
261 lambda.envs = {};
262 }
263
264 // lambda fullName
265 lambda.fullName = `${config.stackName}-${lambda.name}`;
266 }
267 }
268
269 return config;
270 }
271
272 mustacheRender(obj, values) {
273 const tmp = JSON.stringify(obj);
274 const rendered = Mustache.render(tmp, values);
275 return JSON.parse(rendered);
276 }
277
278 readConfigFile() {
279 if (this.template) {
280 return utils.mergeYamls(this.template.configFile, this.configFile);
281 }
282
283 return fs.readFileSync(this.configFile, 'utf8');
284 }
285
286 /**
287 * Parses the config.yml
288 * It uses the default environment values under config.yml and overrides them with values of
289 * the select environment.
290 *
291 * @private
292 * @return {Object} returns configuration object
293 */
294 parseConfig() {
295 const configText = this.readConfigFile();
296 Mustache.escape = (text) => text;
297
298 // load, dump, then load to make sure all yaml included files pass through mustach render
299 const parsedConfig = yaml.safeLoad(configText.toString(), { schema: yamlfiles.YAML_FILES_SCHEMA });
300
301 let config = parsedConfig.default;
302 if (this.deployment && parsedConfig[this.deployment]) {
303 config = merge(config, parsedConfig[this.deployment]);
304 }
305
306 // doing this twice to ensure variables in child yml files are also parsed and replaced
307 config = this.mustacheRender(config, merge({}, config, this.envs));
308 config = this.mustacheRender(config, merge({}, config, this.envs));
309
310 if (this.stack) {
311 config.stackName = this.stack;
312 }
313 else {
314 this.stack = config.stackName;
315 }
316
317 config = this.constructor.configureLambda(config);
318 return merge(config, this.constructor.configureApiGateway(config));
319 }
320
321 /**
322 * Main method of the class. It parses a configuration and returns it
323 * as a JS object.
324 *
325 * @example
326 * const configInstance = new Config(null, null, 'path/to/config.yml', 'path/to/.env');
327 * config = configInstance.parse();
328 *
329 * @return {Object} the configuration object
330 */
331 parse() {
332 const config = this.parseConfig();
333 this.bucket = get(config, 'buckets.internal');
334
335 // merge with the instnace
336 merge(this, config);
337 }
338}
339
340module.exports = Config;