UNPKG

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