@znemz/cfn-include 1.6.4 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/index.js CHANGED
@@ -1,124 +1,340 @@
1
- var _ = require('lodash'),
2
- url = require('url'),
3
- path = require('path'),
4
- readFile = path => new Promise((resolve, reject) => require('fs').readFile(path, (err, data) => {
5
- if (err) reject(err);
6
- else resolve(data.toString());
7
- })),
8
- pathParse = require('path-parse'),
9
- request = require('./lib/request'),
10
- p = require('./lib/promise'),
11
- AWS = require('aws-sdk-proxy'),
12
- s3 = new AWS.S3(),
13
- yaml = require('./lib/yaml'),
14
- { getParser } = require('./lib/include/query'),
15
- deepMerge = require('deepmerge'),
16
- parseLocation = require('./lib/parselocation'),
17
- replaceEnv = require('./lib/replaceEnv');
1
+ const url = require('url');
2
+ const path = require('path');
3
+ const { readFile } = require('fs/promises');
4
+ const _ = require('lodash');
5
+ const { globSync } = require('glob');
6
+ const Promise = require('bluebird');
7
+ const sortObject = require('@znemz/sort-object');
8
+ const { S3Client, GetObjectCommand } = require('@aws-sdk/client-s3');
9
+ const { addProxyToClient } = require('aws-sdk-v3-proxy');
10
+
11
+ const pathParse = require('path-parse');
12
+ const deepMerge = require('deepmerge');
13
+ const { isTaggableResource } = require('@znemz/cft-utils/src/resources/taggable');
14
+
15
+ const request = require('./lib/request');
16
+ const PromiseExt = require('./lib/promise');
17
+
18
+ const S3 = (opts = {}) => addProxyToClient(new S3Client(opts), { throwOnNoProxy: false });
19
+
20
+ const s3 = S3();
21
+ const yaml = require('./lib/yaml');
22
+ const { getParser } = require('./lib/include/query');
23
+ const parseLocation = require('./lib/parselocation');
24
+ const replaceEnv = require('./lib/replaceEnv');
18
25
 
19
26
  const { lowerCamelCase, upperCamelCase } = require('./lib/utils');
27
+ const { isOurExplicitFunction } = require('./lib/schema');
20
28
 
21
- module.exports = function (options) {
22
- var template = options.template,
23
- base = parseLocation(options.url),
24
- scope = options.scope || {};
25
- if (base.relative) throw "url cannot be relative";
26
- template = _.isUndefined(template) ? include(base, scope, options.url, options.doEnv) : template;
27
- return Promise.resolve(template).then(function (template) {
28
- return recurse(base, scope, template, options.doEnv);
29
- });
30
- }
29
+ /**
30
+ * @param {object} options
31
+ * @param {object} [options.template] JSON|Yaml Object Document
32
+ * optional and can be derived from url
33
+ * @param {string} [options.url] '(file|s3):///SOME_FILE_PATH.(json|yaml)'
34
+ * @param {boolean} options.doEnv inject environment from process.env as well
35
+ * @param {Object.<string, string>} [options.inject] object to
36
+ * inject { KEY: Value } from where ${KEY}
37
+ * is subtituted with Value
38
+ * @param {boolean} [options.doLog] log all arguments at the include recurse level
39
+ *
40
+ * Example: Load off off file system
41
+ * include({
42
+ * template: yaml.load(template),
43
+ * url: `file://${location}`,
44
+ * doEnv: opts.enable === 'env',
45
+ * inject: opts.inject,
46
+ * doLog: opts.doLog,
47
+ * doEval: opts.doEval, -- allow Fn::Eval to be used
48
+ * })
49
+ */
50
+ module.exports = async function (options) {
51
+ let { template } = options;
52
+ const base = parseLocation(options.url);
53
+ const scope = options.scope || {};
54
+ if (base.relative) throw new Error('url cannot be relative');
55
+ template = _.isUndefined(template)
56
+ ? fnInclude({ base, scope, cft: options.url, ...options })
57
+ : template;
58
+ return recurse({ base, scope, cft: template, ...options });
59
+ };
31
60
 
32
- async function recurse(base, scope, object, doEnv) {
61
+ /**
62
+ * @param {object} base file options
63
+ * @param {string} base.protocol 'file' | 's3
64
+ * @param {string} [base.host] url to download file from
65
+ * @param {string} base.path 'SOME_FILE_PATH.(json|yaml)',
66
+ * @param {boolean} [base.relative] is the file path relative?
67
+ * @param {string} [base.raw] '(file|s3):///SOME_FILE_PATH.(json|yaml)'
68
+ * @param {object} [scope] initally {} and optional eventually defined
69
+ * derrived in Fn::Map for template find and replace
70
+ * IE:
71
+ * TEMPLATE:
72
+ * {
73
+ * "Fn::Map": [
74
+ * [1, 2], {
75
+ * "Value": "_"
76
+ * }
77
+ * ]
78
+ *}
79
+ * yields:
80
+ * { scope: { _: 1 } }
81
+ * @param {Document|String|Promise<DocumentString>} cft
82
+ * Document object is recursed at every Field level and eventually reduced to a string
83
+ * @param {Object} opts
84
+ * @param {boolean} opts.doEnv inject environment from process.env as well
85
+ * @param {Object} opts.inject object to inject { KEY: Value } from where ${KEY}
86
+ * is subtituted with Value
87
+ * @param {boolean} opts.doLog log all arguments at the include recurse level
88
+ */
89
+ async function recurse({ base, scope, cft, ...opts }) {
90
+ if (opts.doLog) {
91
+ // eslint-disable-next-line no-console
92
+ console.log({ base, scope, cft, ...opts });
93
+ }
33
94
  scope = _.clone(scope);
34
- if (_.isArray(object)) {
35
- return Promise.all(object.map((o) => recurse(base, scope, o, doEnv)))
36
- } else if (_.isPlainObject(object)) {
37
- if (object["Fn::Map"]) {
38
- var args = object["Fn::Map"],
39
- list = args[0],
40
- placeholder = args[1],
41
- body = args[args.length - 1],
42
- idx, sz, i = 0,
43
- hasindex = false,
44
- hassize = false;
45
- if (Array.isArray(placeholder)) { // multiple placeholders
95
+ if (_.isArray(cft)) {
96
+ return Promise.all(cft.map((o) => recurse({ base, scope, cft: o, ...opts })));
97
+ }
98
+ if (_.isPlainObject(cft)) {
99
+ if (cft['Fn::Map']) {
100
+ const args = cft['Fn::Map'];
101
+ const [list] = args;
102
+ const body = args[args.length - 1];
103
+ let placeholder = args[1],
104
+ idx,
105
+ sz,
106
+ hasindex = false,
107
+ hassize = false;
108
+ if (Array.isArray(placeholder)) {
109
+ // multiple placeholders
46
110
  idx = placeholder[1];
47
111
  hasindex = true;
48
112
  if (placeholder.length > 2) {
49
- sz = placeholder[2];
50
- hassize = true;
113
+ sz = placeholder[2];
114
+ hassize = true;
51
115
  }
52
116
  placeholder = placeholder[0];
53
117
  }
54
118
  if (args.length === 2) {
55
119
  placeholder = '_';
56
120
  }
57
- return p.map(recurse(base, scope, list, doEnv), function(replace) {
121
+ return PromiseExt.mapX(recurse({ base, scope, cft: list, ...opts }), (replace, key) => {
58
122
  scope = _.clone(scope);
59
123
  scope[placeholder] = replace;
60
124
  if (hasindex) {
61
- scope[idx] = i++;
125
+ scope[idx] = key;
62
126
  }
63
- var replaced = findAndReplace(scope, _.cloneDeep(body));
64
- return recurse(base, scope, replaced, doEnv);
65
- }).then(function(obj) {
127
+ const replaced = findAndReplace(scope, _.cloneDeep(body));
128
+ return recurse({ base, scope, cft: replaced, ...opts });
129
+ }).then((_cft) => {
66
130
  if (hassize) {
67
- obj = findAndReplace({[sz] :obj.length}, obj);
131
+ _cft = findAndReplace({ [sz]: _cft.length }, _cft);
68
132
  }
69
- return recurse(base, scope, obj, doEnv);
133
+ return recurse({ base, scope, cft: _cft, ...opts });
70
134
  });
71
- } else if (object["Fn::Length"]) {
72
- if (Array.isArray(object["Fn::Length"])) {
73
- return object["Fn::Length"].length;
135
+ }
136
+ if (cft['Fn::Length']) {
137
+ if (Array.isArray(cft['Fn::Length'])) {
138
+ return cft['Fn::Length'].length;
74
139
  }
75
- return recurse(base, scope, object["Fn::Length"], doEnv).then((x) => {
140
+ return recurse({ base, scope, cft: cft['Fn::Length'], ...opts }).then((x) => {
76
141
  if (Array.isArray(x)) {
77
142
  return x.length;
78
143
  }
79
144
  return 0;
80
145
  });
81
- } else if (object["Fn::Include"]) {
82
- return include(base, scope, object["Fn::Include"], doEnv)
83
- .then(function(json) {
84
- if (!_.isPlainObject(json))
85
- return json;
86
- delete object["Fn::Include"];
87
- _.defaults(object, json);
88
- return object;
89
- })
90
- .then(_.bind(findAndReplace, this, scope))
91
- .then((t) => recurse(base, scope, t, doEnv));
92
- } else if (object["Fn::Flatten"]) {
93
- return recurse(base, scope, object["Fn::Flatten"], doEnv)
94
- .then(function(json) { return _.flatten(json); });
95
- } else if (object["Fn::Merge"]) {
96
- return recurse(base, scope, object["Fn::Merge"], doEnv).then(function(json) {
97
- delete object["Fn::Merge"];
98
- _.defaults(object, _.merge.apply(_, json));
99
- return object;
146
+ }
147
+ if (cft['Fn::Include']) {
148
+ return fnInclude({ base, scope, cft: cft['Fn::Include'], ...opts })
149
+ .then(function (json) {
150
+ if (!_.isPlainObject(json)) return json;
151
+ delete cft['Fn::Include'];
152
+ _.defaults(cft, json);
153
+ return cft;
154
+ })
155
+ .then((_cft) => findAndReplace(scope, _cft))
156
+ .then((t) => recurse({ base, scope, cft: t, ...opts }));
157
+ }
158
+ if (cft['Fn::Flatten']) {
159
+ return recurse({ base, scope, cft: cft['Fn::Flatten'], ...opts }).then(function (json) {
160
+ return _.flatten(json);
161
+ });
162
+ }
163
+ if (cft['Fn::FlattenDeep']) {
164
+ return recurse({ base, scope, cft: cft['Fn::FlattenDeep'], ...opts }).then(
165
+ function (json) {
166
+ return _.flattenDeep(json);
167
+ }
168
+ );
169
+ }
170
+ if (cft['Fn::Uniq']) {
171
+ return recurse({ base, scope, cft: cft['Fn::Uniq'], ...opts }).then(function (json) {
172
+ return _.uniq(json);
173
+ });
174
+ }
175
+ if (cft['Fn::Compact']) {
176
+ return recurse({ base, scope, cft: cft['Fn::Compact'], ...opts }).then(function (json) {
177
+ return _.compact(json);
178
+ });
179
+ }
180
+ if (cft['Fn::Concat']) {
181
+ return recurse({ base, scope, cft: cft['Fn::Concat'], ...opts }).then(function (json) {
182
+ return _.concat(...json);
183
+ });
184
+ }
185
+ if (cft['Fn::Sort']) {
186
+ return recurse({ base, scope, cft: cft['Fn::Sort'], ...opts }).then(function (array) {
187
+ return array.sort();
188
+ });
189
+ }
190
+ if (cft['Fn::SortedUniq']) {
191
+ return recurse({ base, scope, cft: cft['Fn::SortedUniq'], ...opts }).then(
192
+ function (array) {
193
+ return _.sortedUniq(array.sort());
194
+ }
195
+ );
196
+ }
197
+ if (cft['Fn::SortBy']) {
198
+ return recurse({ base, scope, cft: cft['Fn::SortBy'], ...opts }).then(function ({
199
+ list,
200
+ iteratees,
201
+ }) {
202
+ return _.sortBy(list, iteratees);
203
+ });
204
+ }
205
+ if (cft['Fn::SortObject']) {
206
+ return recurse({ base, scope, cft: cft['Fn::SortObject'], ...opts }).then(function ({
207
+ // eslint-disable-next-line no-shadow
208
+ object,
209
+ options,
210
+ ...rest // allow object to be optional (implied)
211
+ }) {
212
+ return sortObject(object || rest, options);
213
+ });
214
+ }
215
+ if (cft['Fn::Without']) {
216
+ return recurse({ base, scope, cft: cft['Fn::Without'], ...opts }).then(function (json) {
217
+ json = Array.isArray(json) ? { list: json[0], withouts: json[1] } : json;
218
+ return _.without(json.list, ...json.withouts);
219
+ });
220
+ }
221
+ if (cft['Fn::Omit']) {
222
+ return recurse({ base, scope, cft: cft['Fn::Omit'], ...opts }).then(function (json) {
223
+ json = Array.isArray(json) ? { object: json[0], omits: json[1] } : json;
224
+ return _.omit(json.object, json.omits);
225
+ });
226
+ }
227
+ if (cft['Fn::OmitEmpty']) {
228
+ return recurse({ base, scope, cft: cft['Fn::OmitEmpty'], ...opts }).then(
229
+ function (json) {
230
+ // omit falsy values except false, and 0
231
+ return _.omitBy(json, (v) => !v && v !== false && v !== 0);
232
+ }
233
+ );
234
+ }
235
+ if (cft['Fn::Eval'] && opts.doEval) {
236
+ return recurse({ base, scope, cft: cft['Fn::Eval'], ...opts }).then(function (json) {
237
+ // **WARNING** you have now enabled god mode
238
+ // eslint-disable-next-line no-unused-vars, prefer-const
239
+ let { state, script, inject, doLog } = json;
240
+ script = replaceEnv(script, _.merge(_.cloneDeep(opts.inject), inject), opts.doEnv);
241
+ if (doLog) {
242
+ // eslint-disable-next-line no-console
243
+ console.log({ state, script, inject });
244
+ }
245
+ // eslint-disable-next-line no-eval
246
+ return eval(script);
100
247
  });
101
- } else if (object["Fn::DeepMerge"]) {
102
- return recurse(base, scope, object["Fn::DeepMerge"], doEnv).then(function (json) {
103
- delete object["Fn::DeepMerge"];
104
- let mergedObj = {};
105
- if (json && json.length) {
106
- json.forEach((j) => {
107
- mergedObj = deepMerge(mergedObj, j)
108
- })
248
+ }
249
+ if (cft['Fn::Filenames']) {
250
+ return recurse({ base, scope, cft: cft['Fn::Filenames'], ...opts }).then(
251
+ function (json) {
252
+ json = _.isPlainObject(json) ? { ...json } : { location: json };
253
+ if (json.doLog) {
254
+ // eslint-disable-next-line no-console
255
+ console.log(json);
256
+ }
257
+ const location = parseLocation(json.location);
258
+
259
+ if (!_.isEmpty(location) && !location.protocol) {
260
+ location.protocol = base.protocol;
261
+ }
262
+ if (location.protocol === 'file') {
263
+ const absolute = location.relative
264
+ ? path.join(path.dirname(base.path), location.host, location.path || '')
265
+ : [location.host, location.path].join('');
266
+ const globs = globSync(absolute).sort();
267
+ if (json.omitExtension) {
268
+ return globs.map((f) => path.basename(f, path.extname(f)));
269
+ }
270
+ return globs;
271
+ }
272
+ return 'Unsupported File Type';
109
273
  }
110
- _.defaults(object, mergedObj);
111
- return object;
274
+ );
275
+ }
276
+ if (cft['Fn::Merge']) {
277
+ return recurse({ base, scope, cft: cft['Fn::Merge'], ...opts }).then(function (json) {
278
+ delete cft['Fn::Merge'];
279
+ // eslint-disable-next-line prefer-spread
280
+ return recurse({ base, scope, cft: _.defaults(cft, _.merge.apply(_, json)), ...opts });
112
281
  });
113
- } else if (object["Fn::Stringify"]) {
114
- return recurse(base, scope, object["Fn::Stringify"], doEnv)
115
- .then(function(json) { return JSON.stringify(json); });
116
- } else if (object["Fn::UpperCamelCase"]) {
117
- return upperCamelCase(object["Fn::UpperCamelCase"]);
118
- } else if (object["Fn::LowerCamelCase"]) {
119
- return lowerCamelCase(object["Fn::LowerCamelCase"]);
120
- } else if (object["Fn::GetEnv"]) {
121
- const args = object["Fn::GetEnv"];
282
+ }
283
+ if (cft['Fn::DeepMerge']) {
284
+ return recurse({ base, scope, cft: cft['Fn::DeepMerge'], ...opts }).then(
285
+ function (json) {
286
+ delete cft['Fn::DeepMerge'];
287
+ let mergedObj = {};
288
+ if (json && json.length) {
289
+ json.forEach((j) => {
290
+ mergedObj = deepMerge(mergedObj, j);
291
+ });
292
+ }
293
+ return recurse({ base, scope, cft: _.defaults(cft, mergedObj), ...opts });
294
+ }
295
+ );
296
+ }
297
+ if (cft['Fn::ObjectKeys']) {
298
+ return recurse({ base, scope, cft: cft['Fn::ObjectKeys'], ...opts }).then((json) =>
299
+ Object.keys(json)
300
+ );
301
+ }
302
+ if (cft['Fn::ObjectValues']) {
303
+ return recurse({ base, scope, cft: cft['Fn::ObjectValues'], ...opts }).then((json) =>
304
+ Object.values(json)
305
+ );
306
+ }
307
+ if (cft['Fn::Stringify']) {
308
+ return recurse({ base, scope, cft: cft['Fn::Stringify'], ...opts }).then(
309
+ function (json) {
310
+ return JSON.stringify(json);
311
+ }
312
+ );
313
+ }
314
+ if (cft['Fn::StringSplit']) {
315
+ return recurse({ base, scope, cft: cft['Fn::StringSplit'], ...opts }).then(
316
+ ({ string, separator, doLog }) => {
317
+ if (!string) {
318
+ string = '';
319
+ }
320
+ // eslint-disable-next-line no-console
321
+ if (doLog) console.log({ string, separator });
322
+ if (!separator) {
323
+ separator = ',';
324
+ }
325
+ return string.split(separator);
326
+ }
327
+ );
328
+ }
329
+ if (cft['Fn::UpperCamelCase']) {
330
+ return upperCamelCase(cft['Fn::UpperCamelCase']);
331
+ }
332
+ if (cft['Fn::LowerCamelCase']) {
333
+ return lowerCamelCase(cft['Fn::LowerCamelCase']);
334
+ }
335
+
336
+ if (cft['Fn::GetEnv']) {
337
+ const args = cft['Fn::GetEnv'];
122
338
  if (Array.isArray(args)) {
123
339
  const val = process.env[args[0]];
124
340
  return val === undefined ? args[1] : val;
@@ -128,62 +344,141 @@ async function recurse(base, scope, object, doEnv) {
128
344
  throw new Error(`environmental variable ${args} is undefined`);
129
345
  }
130
346
  return val;
131
- } else if (object["Fn::Outputs"]) {
132
- const outputs = await recurse(base, scope, object["Fn::Outputs"], doEnv);
347
+ }
348
+
349
+ if (cft['Fn::Outputs']) {
350
+ const outputs = await recurse({ base, scope, cft: cft['Fn::Outputs'], ...opts });
133
351
  const result = {};
352
+ // eslint-disable-next-line no-restricted-syntax, guard-for-in
134
353
  for (const output in outputs) {
135
354
  const val = outputs[output];
136
355
  const exp = {
137
- Export : {Name : {'Fn::Sub' : '${AWS::StackName}:' + output}}
356
+ Export: { Name: { 'Fn::Sub': '${AWS::StackName}:' + output } },
138
357
  };
139
358
  if (!Array.isArray(val) && typeof val === 'object') {
140
359
  result[output] = {
141
- Value : {'Fn::Sub' : val.Value},
142
- Condition : val.Condition,
360
+ Value: { 'Fn::Sub': val.Value },
361
+ Condition: val.Condition,
143
362
  ...exp,
144
- }
363
+ };
145
364
  } else {
146
365
  result[output] = {
147
- Value : {'Fn::Sub' : val},
366
+ Value: { 'Fn::Sub': val },
148
367
  ...exp,
149
- }
368
+ };
150
369
  }
151
370
  }
152
371
  return result;
153
- } else if (object["Fn::Sequence"]) {
154
- const outputs = await recurse(base, scope, object["Fn::Sequence"], doEnv);
372
+ }
373
+
374
+ if (cft['Fn::Sequence']) {
375
+ const outputs = await recurse({ base, scope, cft: cft['Fn::Sequence'], ...opts });
376
+ // eslint-disable-next-line prefer-const
155
377
  let [start, stop, step = 1] = outputs;
156
378
  const isString = typeof start === 'string';
157
379
  if (isString) {
158
380
  start = start.charCodeAt(0);
159
381
  stop = stop.charCodeAt(0);
160
382
  }
161
- const seq = Array.from({length : Math.floor((stop - start) / step) + 1},
162
- (_, i) => start + i * step);
383
+ const seq = Array.from(
384
+ { length: Math.floor((stop - start) / step) + 1 },
385
+ (__, i) => start + i * step
386
+ );
163
387
  return isString ? seq.map((i) => String.fromCharCode(i)) : seq;
164
- } else {
165
- return p.props(_.mapValues(object, (template) => recurse(base, scope, template, doEnv)))
166
388
  }
167
- } else if (_.isUndefined(object)) {
389
+
390
+ if (cft['Fn::IfEval'] && opts.doEval) {
391
+ return recurse({ base, scope, cft: cft['Fn::IfEval'], ...opts }).then(function (json) {
392
+ // eslint-disable-next-line prefer-const
393
+ let { truthy, falsy, evalCond, inject, doLog } = json;
394
+ if (!evalCond) {
395
+ return Promise.reject(new Error('Fn::IfEval evalCond is required'));
396
+ }
397
+ evalCond = `(${evalCond})`;
398
+ if (!falsy) {
399
+ falsy = '';
400
+ }
401
+ if (!truthy) {
402
+ truthy = '';
403
+ }
404
+
405
+ evalCond = replaceEnv(evalCond, _.merge(_.cloneDeep(opts.inject), inject), opts.doEnv);
406
+ truthy = replaceEnv(truthy, _.merge(_.cloneDeep(opts.inject), inject), opts.doEnv);
407
+ if (falsy) {
408
+ falsy = replaceEnv(falsy, _.merge(_.cloneDeep(opts.inject), inject), opts.doEnv);
409
+ }
410
+
411
+ // eslint-disable-next-line no-eval
412
+ const condResult = eval(evalCond);
413
+
414
+ if (doLog) {
415
+ // eslint-disable-next-line no-console
416
+ console.log({ truthy, falsy, inject, evalCond, condResult });
417
+ }
418
+
419
+ if (condResult) {
420
+ return recurse({ base, scope, cft: truthy, ...opts });
421
+ }
422
+ return recurse({ base, scope, cft: falsy, ...opts });
423
+ });
424
+ }
425
+ if (cft['Fn::JoinNow']) {
426
+ return recurse({ base, scope, cft: cft['Fn::JoinNow'], ...opts }).then((array) => {
427
+ // keeps with same format as Fn::Join ~ more complex
428
+ // vs let [delimitter, ...toJoinArray] = array;
429
+ let [delimitter, toJoinArray] = array;
430
+ delimitter = replaceEnv(delimitter, opts.inject, opts.doEnv);
431
+ return toJoinArray.join(delimitter);
432
+ });
433
+ }
434
+ if (cft['Fn::ApplyTags']) {
435
+ return recurse({ base, scope, cft: cft['Fn::ApplyTags'], ...opts }).then((json) => {
436
+ let { tags, Tags, resources } = json;
437
+ tags = tags || Tags; // allow for both caseing
438
+ const promises = [];
439
+ _.each(resources, (val, id) => {
440
+ promises.push(
441
+ isTaggableResource(val.Type).then((isTaggable) => {
442
+ if (isTaggable) {
443
+ resources[id] = deepMerge(
444
+ {
445
+ Properties: {
446
+ Tags: tags,
447
+ },
448
+ },
449
+ val
450
+ );
451
+ }
452
+ return resources[id];
453
+ })
454
+ );
455
+ });
456
+ return Promise.all(promises).then(() => resources);
457
+ });
458
+ }
459
+
460
+ return Promise.props(
461
+ _.mapValues(cft, (template) => recurse({ base, scope, cft: template, ...opts }))
462
+ );
463
+ }
464
+
465
+ if (_.isUndefined(cft)) {
168
466
  return null;
169
- } else {
170
- return object;
171
467
  }
468
+ return replaceEnv(cft, opts.inject, opts.doEnv);
172
469
  }
173
470
 
174
-
175
471
  function findAndReplace(scope, object) {
176
472
  if (_.isString(object)) {
177
- _.forEach(scope, function(replace, find) {
473
+ _.forEach(scope, function (replace, find) {
178
474
  if (object === find) {
179
475
  object = replace;
180
476
  }
181
477
  });
182
478
  }
183
479
  if (_.isString(object)) {
184
- _.forEach(scope, function(replace, find) {
185
- let regex = new RegExp('\\${' + find + '}', 'g');
186
- let found = false;
480
+ _.forEach(scope, function (replace, find) {
481
+ const regex = new RegExp(`\\\${${find}}`, 'g');
187
482
  if (find !== '_' && object.match(regex)) {
188
483
  object = object.replace(regex, replace);
189
484
  }
@@ -192,11 +487,11 @@ function findAndReplace(scope, object) {
192
487
  if (_.isArray(object)) {
193
488
  object = object.map(_.bind(findAndReplace, this, scope));
194
489
  } else if (_.isPlainObject(object)) {
195
- object = _.mapKeys(
196
- object, function(value, key) { return findAndReplace(scope, key); });
197
- _.keys(object).forEach(function(key) {
198
- if (key === 'Fn::Map')
199
- return;
490
+ object = _.mapKeys(object, function (value, key) {
491
+ return findAndReplace(scope, key);
492
+ });
493
+ _.keys(object).forEach(function (key) {
494
+ if (key === 'Fn::Map') return;
200
495
  object[key] = findAndReplace(scope, object[key]);
201
496
  });
202
497
  }
@@ -205,102 +500,215 @@ function findAndReplace(scope, object) {
205
500
 
206
501
  function interpolate(lines, context) {
207
502
  return lines.map(function (line) {
208
- var parts = [];
209
- line.split(/({{\w+?}})/g).map(function (line) {
210
- var match = line.match(/^{{(\w+)}}$/),
211
- value = match ? context[match[1]] : undefined;
212
- if (!match) return line;
213
- else if (_.isUndefined(value)) {
214
- return ''
215
- } else {
503
+ const parts = [];
504
+ line
505
+ .split(/({{\w+?}})/g)
506
+ .map(function (_line) {
507
+ const match = _line.match(/^{{(\w+)}}$/);
508
+ const value = match ? context[match[1]] : undefined;
509
+ if (!match) return _line;
510
+ if (_.isUndefined(value)) {
511
+ return '';
512
+ }
216
513
  return value;
217
- }
218
- }).forEach(function (part) {
219
- var last = parts[parts.length - 1];
220
- if (_.isPlainObject(part) || _.isPlainObject(last) || !parts.length) {
221
- parts.push(part);
222
- } else if (parts.length) {
223
- parts[parts.length - 1] = last + part;
224
- }
225
- });
514
+ })
515
+ .forEach(function (part) {
516
+ const last = parts[parts.length - 1];
517
+ if (_.isPlainObject(part) || _.isPlainObject(last) || !parts.length) {
518
+ parts.push(part);
519
+ } else if (parts.length) {
520
+ parts[parts.length - 1] = last + part;
521
+ }
522
+ });
226
523
  return parts.filter(function (part) {
227
524
  return part !== '';
228
525
  });
229
526
  });
230
527
  }
231
528
 
232
- async function include(base, scope, args, doEnv) {
233
- const procTemplate = args.doEnv? replaceEnv : passThrough;
234
- args = _.defaults(_.isPlainObject(args) ? { ...args, doEnv } : {
235
- location: args,
236
- doEnv,
237
- }, { type: 'json' });
238
- var body, absolute, location = parseLocation(args.location);
529
+ function fnIncludeOptsFromArray(cft, opts) {
530
+ // Array 1, location, Array 2 Query, Array 3 Optional Parser (default lodash)
531
+ const [location, query, parser = 'lodash'] = cft;
532
+ cft = { location, query, parser, ...opts };
533
+ return cft;
534
+ }
535
+
536
+ function fnIncludeOpts(cft, opts) {
537
+ if (_.isPlainObject(cft)) {
538
+ cft = _.merge(cft, _.cloneDeep(opts));
539
+ } else if (_.isArray(cft)) {
540
+ cft = fnIncludeOptsFromArray(cft, opts);
541
+ } else {
542
+ // should be string{
543
+ const splits = cft.split('|');
544
+ if (splits.length > 1) {
545
+ cft = fnIncludeOptsFromArray(splits, opts);
546
+ } else {
547
+ cft = { location: cft, ...opts };
548
+ }
549
+ }
550
+
551
+ cft = _.defaults(cft, { type: 'json' });
552
+ return cft;
553
+ }
554
+
555
+ async function fnInclude({ base, scope, cft, ...opts }) {
556
+ let procTemplate = async (template, inject = cft.inject, doEnv = opts.doEnv) =>
557
+ replaceEnv(template, inject, doEnv);
558
+ const handleInjectSetup = () => {
559
+ if (cft.inject) {
560
+ const origProcTemplate = procTemplate;
561
+ procTemplate = async (template) => {
562
+ try {
563
+ const inject = await recurse({ base, scope, cft: cft.inject, ...opts });
564
+ const processed = await origProcTemplate(template, inject, opts.doEnv);
565
+ return replaceEnv(processed, inject, opts.doEnv);
566
+ } catch (e) {
567
+ return '';
568
+ }
569
+ };
570
+ }
571
+ };
572
+ handleInjectSetup();
573
+ cft = fnIncludeOpts(cft, opts);
574
+
575
+ if (cft.doLog) {
576
+ // eslint-disable-next-line no-console
577
+ console.log({ base, scope, args: cft, ...opts });
578
+ }
579
+ // console.log(args)
580
+ let body;
581
+ let absolute;
582
+ const location = parseLocation(cft.location);
239
583
  if (!_.isEmpty(location) && !location.protocol) location.protocol = base.protocol;
240
584
  if (location.protocol === 'file') {
241
- absolute = location.relative ? path.join(path.dirname(base.path), location.host, location.path || '') : [location.host, location.path].join('');
242
- body = readFile(absolute).then(procTemplate);
243
- absolute = location.protocol + '://' + absolute;
585
+ absolute = location.relative
586
+ ? path.join(path.dirname(base.path), location.host, location.path || '')
587
+ : [location.host, location.path].join('');
588
+
589
+ // allow script to resolve their own, __dirname via ${CFN_INCLUDE_DIRNAME}
590
+ cft.inject = { CFN_INCLUDE_DIRNAME: path.dirname(absolute), ...cft.inject };
591
+
592
+ handleInjectSetup();
593
+ if (isGlob(cft, absolute)) {
594
+ const paths = globSync(absolute).sort();
595
+ const template = yaml.load(paths.map((_p) => `- Fn::Include: file://${_p}`).join('\n'));
596
+ return recurse({ base, scope, cft: template, ...opts });
597
+ }
598
+ body = readFile(absolute).then(String).then(procTemplate);
599
+ absolute = `${location.protocol}://${absolute}`;
244
600
  } else if (location.protocol === 's3') {
245
- var basedir = pathParse(base.path).dir;
246
- var bucket = location.relative ? base.host : location.host,
247
- key = location.relative ? url.resolve(basedir + '/', location.raw) : location.path;
601
+ const basedir = pathParse(base.path).dir;
602
+ const bucket = location.relative ? base.host : location.host;
603
+ // eslint-disable-next-line n/no-deprecated-api
604
+ let key = location.relative ? url.resolve(`${basedir}/`, location.raw) : location.path;
248
605
  key = key.replace(/^\//, '');
249
- absolute = location.protocol + '://' + [bucket, key].join('/');
250
- body = s3.getObject({
251
- Bucket: bucket,
252
- Key: key,
253
- }).promise().then(res => res['Body'].toString()).then(procTemplate);
606
+ absolute = `${location.protocol}://${[bucket, key].join('/')}`;
607
+ body = s3
608
+ .send(
609
+ new GetObjectCommand({
610
+ Bucket: bucket,
611
+ Key: key,
612
+ })
613
+ )
614
+ .then((res) => res.Body.toString())
615
+ .then(procTemplate);
254
616
  } else if (location.protocol && location.protocol.match(/^https?$/)) {
255
- var basepath = pathParse(base.path).dir + '/';
256
- absolute = location.relative ? url.resolve(location.protocol + '://' + base.host + basepath, location.raw) : location.raw;
617
+ const basepath = `${pathParse(base.path).dir}/`;
618
+ /* eslint-disable n/no-deprecated-api */
619
+ absolute = location.relative
620
+ ? url.resolve(`${location.protocol}://${base.host}${basepath}`, location.raw)
621
+ : location.raw;
622
+ /* eslint-enable n/no-deprecated-api */
257
623
  body = request(absolute).then(procTemplate);
258
624
  }
259
- return handleIncludeBody({scope, args, body, absolute });
625
+ return handleIncludeBody({ scope, args: cft, body, absolute });
260
626
  }
261
627
 
262
- function passThrough(template) { return template; }
628
+ function isGlob(args, str) {
629
+ return args.isGlob || /.*\*/.test(str);
630
+ }
263
631
 
264
632
  async function handleIncludeBody({ scope, args, body, absolute }) {
265
- const procTemplate = args.doEnv? replaceEnv : passThrough;
266
- switch (args.type) {
267
- case 'json': {
268
- let b = await body;
269
- b = procTemplate(b);
270
- let template = yaml.load(b)
271
- if (args.query) {
272
- const query = _.isString(args.query) ? args.query : await recurse(parseLocation(absolute), scope, args.query, args.doEnv);
273
- template = getParser(args.parser)(template, query);
274
- }
275
- return recurse(parseLocation(absolute), scope, template, args.doEnv);
276
- }
277
- case 'api': {
278
- var handler = require('./lib/include/api');
279
- let template = await handler(args);
280
- return procTemplate(template);
281
- }
282
- case 'string': {
283
- let template = await body;
284
- return procTemplate(template)
285
- }
286
- case 'literal': {
287
- return body.then(function (template) {
288
- template = procTemplate(template);
289
- var lines = JSONifyString(template);
290
- if (_.isPlainObject(args.context)) {
291
- lines = interpolate(lines, args.context);
292
- }
293
- return {
294
- 'Fn::Join': ['', _.flatten(lines)]
633
+ const procTemplate = (temp) => replaceEnv(temp, args.inject, args.doEnv);
634
+ try {
635
+ switch (args.type) {
636
+ case 'json': {
637
+ let b = await body;
638
+ b = procTemplate(b);
639
+ const template = yaml.load(b);
640
+
641
+ // recurse all the way down and work your way out
642
+ const loopTemplate = (temp) => {
643
+ return recurse({
644
+ base: parseLocation(absolute),
645
+ scope,
646
+ cft: temp,
647
+ doEnv: args.doEnv,
648
+ doEval: args.doEval,
649
+ doLog: args.doLog,
650
+ inject: args.inject,
651
+ }).then((_temp) => {
652
+ if (!_temp || !Object.keys(_temp).length) {
653
+ return _temp;
654
+ }
655
+ // ONLY RECURSE IF it is one of our funcs.
656
+ if (isOurExplicitFunction(Object.keys(_temp)[0])) {
657
+ return loopTemplate(_temp);
658
+ }
659
+ return _temp;
660
+ });
295
661
  };
296
- });
662
+
663
+ return loopTemplate(template).then(async (temp) => {
664
+ if (!args.query) {
665
+ return temp;
666
+ }
667
+ // once fully recursed we can query the resultant template
668
+ const query = _.isString(args.query)
669
+ ? replaceEnv(args.query, args.inject, args.doEnv)
670
+ : await recurse({
671
+ base: parseLocation(absolute),
672
+ scope,
673
+ cft: args.query,
674
+ doEnv: args.doEnv,
675
+ doLog: args.doLog,
676
+ inject: args.inject,
677
+ });
678
+ return getParser(args.parser)(temp, query);
679
+ });
680
+ }
681
+ case 'string': {
682
+ const template = await body;
683
+ return procTemplate(template);
684
+ }
685
+ case 'literal': {
686
+ return body.then(function (template) {
687
+ template = procTemplate(template);
688
+ let lines = JSONifyString(template);
689
+ if (_.isPlainObject(args.context)) {
690
+ lines = interpolate(lines, args.context);
691
+ }
692
+ return {
693
+ 'Fn::Join': ['', _.flatten(lines)],
694
+ };
695
+ });
696
+ }
697
+ default:
698
+ throw new Error(`Unknown template type to process type: ${args.type}.`);
699
+ }
700
+ } catch (e) {
701
+ // if the location matches replaceEnv.IsRegExVar then swallow error and retun ''
702
+ if ((replaceEnv.IsRegExVar(absolute) && args.ignoreMissingVar) || args.ignoreMissingFile) {
703
+ return '';
297
704
  }
705
+ throw e;
298
706
  }
299
707
  }
300
708
 
301
709
  function JSONifyString(string) {
302
- var lines = [],
303
- split = string.toString().split(/(\r?\n)/);
710
+ const lines = [];
711
+ const split = string.toString().split(/(\r?\n)/);
304
712
  split.forEach(function (line, idx) {
305
713
  if (idx % 2) {
306
714
  lines[(idx - 1) / 2] = lines[(idx - 1) / 2] + line;