@znemz/cfn-include 1.6.3 → 2.0.0
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/LICENSE +1 -1
- package/README.md +602 -21
- package/bin/cli.js +163 -110
- package/index.js +597 -190
- package/lib/cfnclient.js +42 -19
- package/lib/include/api.js +2 -3
- package/lib/include/query.js +5 -4
- package/lib/parselocation.js +6 -2
- package/lib/promise.js +19 -24
- package/lib/replaceEnv.js +17 -11
- package/lib/schema.js +58 -9
- package/lib/yaml.js +11 -15
- package/package.json +62 -38
package/index.js
CHANGED
|
@@ -1,124 +1,340 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
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(
|
|
35
|
-
return Promise.all(
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
50
|
-
|
|
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
|
|
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] =
|
|
125
|
+
scope[idx] = key;
|
|
62
126
|
}
|
|
63
|
-
|
|
64
|
-
return recurse(base, scope, replaced,
|
|
65
|
-
}).then(
|
|
127
|
+
const replaced = findAndReplace(scope, _.cloneDeep(body));
|
|
128
|
+
return recurse({ base, scope, cft: replaced, ...opts });
|
|
129
|
+
}).then((_cft) => {
|
|
66
130
|
if (hassize) {
|
|
67
|
-
|
|
131
|
+
_cft = findAndReplace({ [sz]: _cft.length }, _cft);
|
|
68
132
|
}
|
|
69
|
-
return recurse(base, scope,
|
|
133
|
+
return recurse({ base, scope, cft: _cft, ...opts });
|
|
70
134
|
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
|
|
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,
|
|
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
|
-
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
}
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
json.
|
|
107
|
-
|
|
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
|
-
|
|
111
|
-
|
|
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
|
-
}
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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
|
-
}
|
|
132
|
-
|
|
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
|
|
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
|
|
142
|
-
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
|
|
366
|
+
Value: { 'Fn::Sub': val },
|
|
148
367
|
...exp,
|
|
149
|
-
}
|
|
368
|
+
};
|
|
150
369
|
}
|
|
151
370
|
}
|
|
152
371
|
return result;
|
|
153
|
-
}
|
|
154
|
-
|
|
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(
|
|
162
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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,214 @@ function findAndReplace(scope, object) {
|
|
|
205
500
|
|
|
206
501
|
function interpolate(lines, context) {
|
|
207
502
|
return lines.map(function (line) {
|
|
208
|
-
|
|
209
|
-
line
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
return
|
|
215
|
-
|
|
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
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
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
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
|
242
|
-
|
|
243
|
-
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
250
|
-
body = s3
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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
|
|
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 =
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
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
|
+
doLog: args.doLog,
|
|
649
|
+
inject: args.inject,
|
|
650
|
+
}).then((_temp) => {
|
|
651
|
+
if (!_temp || !Object.keys(_temp).length) {
|
|
652
|
+
return _temp;
|
|
653
|
+
}
|
|
654
|
+
// ONLY RECURSE IF it is one of our funcs.
|
|
655
|
+
if (isOurExplicitFunction(Object.keys(_temp)[0])) {
|
|
656
|
+
return loopTemplate(_temp);
|
|
657
|
+
}
|
|
658
|
+
return _temp;
|
|
659
|
+
});
|
|
295
660
|
};
|
|
296
|
-
|
|
661
|
+
|
|
662
|
+
return loopTemplate(template).then(async (temp) => {
|
|
663
|
+
if (!args.query) {
|
|
664
|
+
return temp;
|
|
665
|
+
}
|
|
666
|
+
// once fully recursed we can query the resultant template
|
|
667
|
+
const query = _.isString(args.query)
|
|
668
|
+
? replaceEnv(args.query, args.inject, args.doEnv)
|
|
669
|
+
: await recurse({
|
|
670
|
+
base: parseLocation(absolute),
|
|
671
|
+
scope,
|
|
672
|
+
cft: args.query,
|
|
673
|
+
doEnv: args.doEnv,
|
|
674
|
+
doLog: args.doLog,
|
|
675
|
+
inject: args.inject,
|
|
676
|
+
});
|
|
677
|
+
return getParser(args.parser)(temp, query);
|
|
678
|
+
});
|
|
679
|
+
}
|
|
680
|
+
case 'string': {
|
|
681
|
+
const template = await body;
|
|
682
|
+
return procTemplate(template);
|
|
683
|
+
}
|
|
684
|
+
case 'literal': {
|
|
685
|
+
return body.then(function (template) {
|
|
686
|
+
template = procTemplate(template);
|
|
687
|
+
let lines = JSONifyString(template);
|
|
688
|
+
if (_.isPlainObject(args.context)) {
|
|
689
|
+
lines = interpolate(lines, args.context);
|
|
690
|
+
}
|
|
691
|
+
return {
|
|
692
|
+
'Fn::Join': ['', _.flatten(lines)],
|
|
693
|
+
};
|
|
694
|
+
});
|
|
695
|
+
}
|
|
696
|
+
default:
|
|
697
|
+
throw new Error(`Unknown template type to process type: ${args.type}.`);
|
|
698
|
+
}
|
|
699
|
+
} catch (e) {
|
|
700
|
+
// if the location matches replaceEnv.IsRegExVar then swallow error and retun ''
|
|
701
|
+
if ((replaceEnv.IsRegExVar(absolute) && args.ignoreMissingVar) || args.ignoreMissingFile) {
|
|
702
|
+
return '';
|
|
297
703
|
}
|
|
704
|
+
throw e;
|
|
298
705
|
}
|
|
299
706
|
}
|
|
300
707
|
|
|
301
708
|
function JSONifyString(string) {
|
|
302
|
-
|
|
303
|
-
|
|
709
|
+
const lines = [];
|
|
710
|
+
const split = string.toString().split(/(\r?\n)/);
|
|
304
711
|
split.forEach(function (line, idx) {
|
|
305
712
|
if (idx % 2) {
|
|
306
713
|
lines[(idx - 1) / 2] = lines[(idx - 1) / 2] + line;
|