@znemz/cfn-include 2.1.11 → 2.1.13

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/README.md CHANGED
@@ -93,7 +93,9 @@ Options:
93
93
  * `--enable` different options / toggles: ['env','eval'] [string] [choices: 'env','eval','env.eval' etc...]
94
94
  * `env` pre-process env vars and inject into templates as they are processed looks for $KEY or ${KEY} matches
95
95
  * `-i, --inject` JSON string payload to use for template injection. (Takes precedence over process.env (if enabled) injection and will be merged on top of process.env)
96
- * `--doLog` console log out include options in recurse step.
96
+ * `--doLog` console log out include options in recurse step. Shows caller parameter to aid debugging nested function calls.
97
+ * `--ref-now-ignore-missing` do not fail if `Fn::RefNow` reference cannot be resolved; instead return in standard CloudFormation `Ref` syntax
98
+ * `--ref-now-ignores <names>` comma-separated list of reference names to ignore if not found (e.g., `OptionalRef1,OptionalRef2`)
97
99
  `cfn-include` also accepts a template passed from stdin
98
100
 
99
101
  ```
@@ -1201,6 +1203,61 @@ If an AWS pseudo-parameter is not set via environment variables, it falls back t
1201
1203
  **Error handling:**
1202
1204
  If a reference cannot be resolved, `Fn::RefNow` will throw an error. Ensure all referenced parameters and variables are available via inject, scope, or environment variables.
1203
1205
 
1206
+ **Resolving LogicalResourceIds:**
1207
+
1208
+ `Fn::RefNow` can also resolve references to CloudFormation Resource LogicalResourceIds, enabling you to construct ARNs or other resource-specific values during template preprocessing. When a reference matches a LogicalResourceId in the Resources section, `Fn::RefNow` will automatically generate the appropriate ARN based on the resource type and properties.
1209
+
1210
+ **Supported Resource Types for ARN/Name Resolution:**
1211
+
1212
+ - `AWS::IAM::ManagedPolicy` - Returns policy ARN (supports Path)
1213
+ - `AWS::IAM::Role` - Returns role ARN (supports Path)
1214
+ - `AWS::IAM::InstanceProfile` - Returns instance profile ARN (supports Path)
1215
+ - `AWS::S3::Bucket` - Returns bucket ARN
1216
+ - `AWS::Lambda::Function` - Returns function ARN
1217
+ - `AWS::SQS::Queue` - Returns queue ARN
1218
+ - `AWS::SNS::Topic` - Returns topic ARN
1219
+ - `AWS::DynamoDB::Table` - Returns table ARN
1220
+ - `AWS::RDS::DBInstance` - Returns DB instance ARN
1221
+ - `AWS::SecretsManager::Secret` - Returns secret ARN
1222
+ - `AWS::KMS::Key` - Returns key ARN
1223
+
1224
+ Example with AWS::IAM::ManagedPolicy:
1225
+
1226
+ ```yaml
1227
+ Resources:
1228
+ ObjPolicy:
1229
+ Type: AWS::IAM::ManagedPolicy
1230
+ Properties:
1231
+ ManagedPolicyName: teststack-CreateTestDBPolicy-16M23YE3CS700
1232
+ Path: /CRAP/
1233
+
1234
+ IAMRole:
1235
+ Type: AWS::IAM::Role
1236
+ Properties:
1237
+ ManagedPolicyArns:
1238
+ - Fn::RefNow: ObjPolicy # Resolves to: arn:aws:iam::${AWS_ACCOUNT_ID}:policy/CRAP/teststack-CreateTestDBPolicy-16M23YE3CS700
1239
+ ```
1240
+
1241
+ **Returning Resource Names Instead of ARNs:**
1242
+
1243
+ By default, `Fn::RefNow` returns the ARN for supported resource types. However, if the key name ends with `Name` (e.g., `RoleName`, `BucketName`, `FunctionName`), it automatically returns the resource name/identifier instead:
1244
+
1245
+ ```yaml
1246
+ Resources:
1247
+ MyRole:
1248
+ Type: AWS::IAM::Role
1249
+ Properties:
1250
+ RoleName: MyExecutionRole
1251
+
1252
+ RoleArn:
1253
+ Fn::RefNow: MyRole # Returns: arn:aws:iam::${AWS_ACCOUNT_ID}:role/MyExecutionRole
1254
+
1255
+ RoleName:
1256
+ Fn::RefNow: MyRole # Returns: MyExecutionRole (because key ends with "Name")
1257
+ ```
1258
+
1259
+ This intuitive approach makes templates more readable and follows natural CloudFormation naming conventions.
1260
+
1204
1261
  **CLI Options for Fn::RefNow:**
1205
1262
 
1206
1263
  The `cfn-include` command provides CLI options to control how unresolved references are handled:
@@ -1315,6 +1372,122 @@ Options are query parameters.
1315
1372
 
1316
1373
  - `validate=false` do not validate template [true]
1317
1374
 
1375
+ ## Developer Documentation
1376
+
1377
+ ### Debug Logging with doLog
1378
+
1379
+ The `cfn-include` preprocessor supports comprehensive debug logging for tracing template processing. When enabled, the `doLog` option logs all arguments at each recursion level in the template processing pipeline.
1380
+
1381
+ #### Enabling doLog
1382
+
1383
+ **CLI:**
1384
+ ```bash
1385
+ cfn-include template.yaml --doLog
1386
+ ```
1387
+
1388
+ **Programmatic:**
1389
+ ```javascript
1390
+ include({
1391
+ template: myTemplate,
1392
+ url: 'file:///path/to/template.yaml',
1393
+ doLog: true
1394
+ })
1395
+ ```
1396
+
1397
+ #### Understanding Caller Parameter
1398
+
1399
+ The `recurse` function now includes a `caller` parameter to help identify which function triggered each recursion step. This is invaluable for debugging complex templates with nested function calls. The caller parameter provides a trace path like:
1400
+
1401
+ - `recurse:isArray` - Processing an array
1402
+ - `Fn::Map` - Inside an `Fn::Map` function
1403
+ - `Fn::Include` - Inside an `Fn::Include` function
1404
+ - `recurse:isPlainObject:end` - Final plain object processing
1405
+ - `handleIncludeBody:json` - JSON body being processed
1406
+
1407
+ When `doLog` is enabled, the console output will show the caller for each recursion:
1408
+ ```javascript
1409
+ {
1410
+ base: {...},
1411
+ scope: {...},
1412
+ cft: {...},
1413
+ rootTemplate: {...},
1414
+ caller: "Fn::Map",
1415
+ doEnv: false,
1416
+ doEval: false,
1417
+ ...
1418
+ }
1419
+ ```
1420
+
1421
+ This makes it easy to trace execution flow through nested `Fn::Include`, `Fn::Map`, `Fn::RefNow`, and other functions.
1422
+
1423
+ #### Example Debug Output
1424
+
1425
+ ```bash
1426
+ $ cfn-include examples/base.template --doLog | head -50
1427
+ {
1428
+ base: {
1429
+ protocol: 'file',
1430
+ host: '/Users/SOME_USER/code',
1431
+ path: '/examples/base.template'
1432
+ },
1433
+ scope: {},
1434
+ cft: { AWSTemplateFormatVersion: '2010-09-09', ... },
1435
+ rootTemplate: { AWSTemplateFormatVersion: '2010-09-09', ... },
1436
+ caller: 'recurse:isPlainObject:end',
1437
+ doEnv: false,
1438
+ doEval: false,
1439
+ inject: undefined,
1440
+ doLog: true
1441
+ }
1442
+ ```
1443
+
1444
+ ### Fn::RefNow Improvements
1445
+
1446
+ #### CLI Options for Reference Resolution
1447
+
1448
+ Two new CLI options control how unresolved `Fn::RefNow` references are handled:
1449
+
1450
+ - `--ref-now-ignore-missing`: Do not fail if a reference cannot be resolved. Instead, return the reference in CloudFormation's standard `Ref` syntax, allowing CloudFormation to resolve it at stack creation time.
1451
+
1452
+ - `--ref-now-ignores <names>`: Comma-separated list of specific reference names to ignore if not found. Useful for optional references.
1453
+
1454
+ **Example usage:**
1455
+ ```bash
1456
+ # Ignore all unresolved references
1457
+ cfn-include template.yaml --ref-now-ignore-missing
1458
+
1459
+ # Ignore specific references
1460
+ cfn-include template.yaml --ref-now-ignores "OptionalParam,CustomRef"
1461
+
1462
+ # Combine both
1463
+ cfn-include template.yaml --ref-now-ignore-missing --ref-now-ignores "SpecificRef"
1464
+ ```
1465
+
1466
+ #### rootTemplate Parameter
1467
+
1468
+ The `recurse` function now receives the complete `rootTemplate` for all recursion calls. This enables `Fn::RefNow` to resolve references to CloudFormation resources defined anywhere in the template, even when processing deeply nested includes or function results.
1469
+
1470
+ ### Fn::SubNow and Fn::JoinNow
1471
+
1472
+ New intrinsic functions for immediate string substitution and joining:
1473
+
1474
+ - `Fn::SubNow` - Performs immediate string substitution similar to `Fn::Sub`, but evaluates at template processing time
1475
+ - `Fn::JoinNow` - Joins array elements into a string at template processing time
1476
+
1477
+ See the main documentation sections above for detailed usage.
1478
+
1479
+ ### Template Processing Pipeline
1480
+
1481
+ The template processing follows this call chain for better debugging:
1482
+
1483
+ 1. Entry point calls `recurse()` with `caller: undefined`
1484
+ 2. Array elements call `recurse()` with `caller: 'recurse:isArray'`
1485
+ 3. Each `Fn::*` function calls `recurse()` with `caller: 'Fn::FunctionName'`
1486
+ 4. Final plain object recursion uses `caller: 'recurse:isPlainObject:end'`
1487
+ 5. Include body processing uses `caller: 'handleIncludeBody:json'`
1488
+
1489
+ When combined with `--doLog`, this provides complete visibility into how `cfn-include` processes your templates.
1490
+
1318
1491
  To compile the synopsis run the following command.
1319
1492
 
1320
1493
  ```
package/index.js CHANGED
@@ -25,6 +25,7 @@ const replaceEnv = require('./lib/replaceEnv');
25
25
 
26
26
  const { lowerCamelCase, upperCamelCase } = require('./lib/utils');
27
27
  const { isOurExplicitFunction } = require('./lib/schema');
28
+ const { getAwsPseudoParameters, buildResourceArn } = require('./lib/internals');
28
29
 
29
30
  /**
30
31
  * @param {object} options
@@ -37,6 +38,9 @@ const { isOurExplicitFunction } = require('./lib/schema');
37
38
  * inject { KEY: Value } from where ${KEY}
38
39
  * is substituted with Value
39
40
  * @param {boolean} [options.doLog] log all arguments at the include recurse level
41
+ * @param {Array<string>} [options.refNowIgnores] array of logical resource IDs to ignore when resolving Fn::RefNow references
42
+ * @param {boolean} [options.refNowIgnoreMissing] if true, return Ref syntax for unresolvable references; if false, throw error
43
+ * @param {object} [options.rootTemplate] the root template object for resource lookups in Fn::RefNow
40
44
  *
41
45
  * Example: Load off off file system
42
46
  * include({
@@ -59,7 +63,14 @@ module.exports = async function (options) {
59
63
  template = _.isUndefined(template)
60
64
  ? fnInclude({ base, scope, cft: options.url, ...options })
61
65
  : template;
62
- return recurse({ base, scope, cft: template, ...options });
66
+ // Resolve template if it's a promise to extract the root template for reference lookups
67
+ const resolvedTemplate = await Promise.resolve(template);
68
+ return recurse({
69
+ base, scope,
70
+ cft: resolvedTemplate,
71
+ rootTemplate: resolvedTemplate,
72
+ ...options,
73
+ });
63
74
  };
64
75
 
65
76
  /**
@@ -89,27 +100,18 @@ module.exports = async function (options) {
89
100
  * @param {Object} opts.inject object to inject { KEY: Value } from where ${KEY}
90
101
  * is subtituted with Value
91
102
  * @param {boolean} opts.doLog log all arguments at the include recurse level
103
+ * @param {Array<string>} [opts.refNowIgnores] array of logical resource IDs to ignore when resolving Fn::RefNow references
104
+ * @param {boolean} [opts.refNowIgnoreMissing] if true, return Ref syntax for unresolvable references; if false, throw error
105
+ * @param {object} [rootTemplate] the root template object for resource lookups in Fn::RefNow
106
+ * @param {String} [caller] internal use only for logging, to aid in who called recurse Fn::Include, Fn::RefNow, etc..
92
107
  */
93
- function getAwsPseudoParameters() {
94
- return {
95
- 'AWS::AccountId': process.env.AWS_ACCOUNT_ID || process.env.AWS_ACCOUNT_NUM || '${AWS::AccountId}',
96
- 'AWS::Partition': process.env.AWS_PARTITION || 'aws',
97
- 'AWS::Region': process.env.AWS_REGION || '${AWS::Region}',
98
- 'AWS::StackId': process.env.AWS_STACK_ID || '${AWS::StackId}',
99
- 'AWS::StackName': process.env.AWS_STACK_NAME || '${AWS::StackName}',
100
- 'AWS::URLSuffix': process.env.AWS_URL_SUFFIX || 'amazonaws.com',
101
- 'AWS::NotificationARNs': process.env.AWS_NOTIFICATION_ARNS || '${AWS::NotificationARNs}',
102
- };
103
- }
104
-
105
- async function recurse({ base, scope, cft, ...opts }) {
108
+ async function recurse({ base, scope, cft, rootTemplate, caller, ...opts }) {
106
109
  if (opts.doLog) {
107
-
108
- console.log({ base, scope, cft, ...opts });
110
+ console.log({ base, scope, cft, rootTemplate, caller, ...opts });
109
111
  }
110
112
  scope = _.clone(scope);
111
113
  if (_.isArray(cft)) {
112
- return Promise.all(cft.map((o) => recurse({ base, scope, cft: o, ...opts })));
114
+ return Promise.all(cft.map((o) => recurse({ base, scope, cft: o, rootTemplate, caller: 'recurse:isArray', ...opts })));
113
115
  }
114
116
  if (_.isPlainObject(cft)) {
115
117
  if (cft['Fn::Map']) {
@@ -134,26 +136,26 @@ async function recurse({ base, scope, cft, ...opts }) {
134
136
  if (args.length === 2) {
135
137
  placeholder = '_';
136
138
  }
137
- return PromiseExt.mapX(recurse({ base, scope, cft: list, ...opts }), (replace, key) => {
139
+ return PromiseExt.mapX(recurse({ base, scope, cft: list, rootTemplate, caller: 'Fn::Map', ...opts }), (replace, key) => {
138
140
  scope = _.clone(scope);
139
141
  scope[placeholder] = replace;
140
142
  if (hasindex) {
141
143
  scope[idx] = key;
142
144
  }
143
145
  const replaced = findAndReplace(scope, _.cloneDeep(body));
144
- return recurse({ base, scope, cft: replaced, ...opts });
146
+ return recurse({ base, scope, cft: replaced, rootTemplate, caller: 'Fn::Map', ...opts });
145
147
  }).then((_cft) => {
146
148
  if (hassize) {
147
149
  _cft = findAndReplace({ [sz]: _cft.length }, _cft);
148
150
  }
149
- return recurse({ base, scope, cft: _cft, ...opts });
151
+ return recurse({ base, scope, cft: _cft, rootTemplate, caller: 'Fn::Map', ...opts });
150
152
  });
151
153
  }
152
154
  if (cft['Fn::Length']) {
153
155
  if (Array.isArray(cft['Fn::Length'])) {
154
156
  return cft['Fn::Length'].length;
155
157
  }
156
- return recurse({ base, scope, cft: cft['Fn::Length'], ...opts }).then((x) => {
158
+ return recurse({ base, scope, cft: cft['Fn::Length'], rootTemplate, caller: 'Fn::Length', ...opts }).then((x) => {
157
159
  if (Array.isArray(x)) {
158
160
  return x.length;
159
161
  }
@@ -169,49 +171,49 @@ async function recurse({ base, scope, cft, ...opts }) {
169
171
  return cft;
170
172
  })
171
173
  .then((_cft) => findAndReplace(scope, _cft))
172
- .then((t) => recurse({ base, scope, cft: t, ...opts }));
174
+ .then((t) => recurse({ base, scope, cft: t, rootTemplate, caller: 'Fn::Include', ...opts }));
173
175
  }
174
176
  if (cft['Fn::Flatten']) {
175
- return recurse({ base, scope, cft: cft['Fn::Flatten'], ...opts }).then(function (json) {
177
+ return recurse({ base, scope, cft: cft['Fn::Flatten'], rootTemplate, caller: 'Fn::Flatten', ...opts }).then(function (json) {
176
178
  return _.flatten(json);
177
179
  });
178
180
  }
179
181
  if (cft['Fn::FlattenDeep']) {
180
- return recurse({ base, scope, cft: cft['Fn::FlattenDeep'], ...opts }).then(
182
+ return recurse({ base, scope, cft: cft['Fn::FlattenDeep'], rootTemplate, caller: 'Fn::FlattenDeep', ...opts }).then(
181
183
  function (json) {
182
184
  return _.flattenDeep(json);
183
185
  },
184
186
  );
185
187
  }
186
188
  if (cft['Fn::Uniq']) {
187
- return recurse({ base, scope, cft: cft['Fn::Uniq'], ...opts }).then(function (json) {
189
+ return recurse({ base, scope, cft: cft['Fn::Uniq'], rootTemplate, caller: 'Fn::Uniq', ...opts }).then(function (json) {
188
190
  return _.uniq(json);
189
191
  });
190
192
  }
191
193
  if (cft['Fn::Compact']) {
192
- return recurse({ base, scope, cft: cft['Fn::Compact'], ...opts }).then(function (json) {
194
+ return recurse({ base, scope, cft: cft['Fn::Compact'], rootTemplate, caller: 'Fn::Compact', ...opts }).then(function (json) {
193
195
  return _.compact(json);
194
196
  });
195
197
  }
196
198
  if (cft['Fn::Concat']) {
197
- return recurse({ base, scope, cft: cft['Fn::Concat'], ...opts }).then(function (json) {
199
+ return recurse({ base, scope, cft: cft['Fn::Concat'], rootTemplate, caller: 'Fn::Concat', ...opts }).then(function (json) {
198
200
  return _.concat(...json);
199
201
  });
200
202
  }
201
203
  if (cft['Fn::Sort']) {
202
- return recurse({ base, scope, cft: cft['Fn::Sort'], ...opts }).then(function (array) {
204
+ return recurse({ base, scope, cft: cft['Fn::Sort'], rootTemplate, caller: 'Fn::Sort', ...opts }).then(function (array) {
203
205
  return array.sort();
204
206
  });
205
207
  }
206
208
  if (cft['Fn::SortedUniq']) {
207
- return recurse({ base, scope, cft: cft['Fn::SortedUniq'], ...opts }).then(
209
+ return recurse({ base, scope, cft: cft['Fn::SortedUniq'], rootTemplate, caller: 'Fn::SortedUniq', ...opts }).then(
208
210
  function (array) {
209
211
  return _.sortedUniq(array.sort());
210
212
  },
211
213
  );
212
214
  }
213
215
  if (cft['Fn::SortBy']) {
214
- return recurse({ base, scope, cft: cft['Fn::SortBy'], ...opts }).then(function ({
216
+ return recurse({ base, scope, cft: cft['Fn::SortBy'], rootTemplate, caller: 'Fn::SortBy', ...opts }).then(function ({
215
217
  list,
216
218
  iteratees,
217
219
  }) {
@@ -219,7 +221,7 @@ async function recurse({ base, scope, cft, ...opts }) {
219
221
  });
220
222
  }
221
223
  if (cft['Fn::SortObject']) {
222
- return recurse({ base, scope, cft: cft['Fn::SortObject'], ...opts }).then(function ({
224
+ return recurse({ base, scope, cft: cft['Fn::SortObject'], rootTemplate, caller: 'Fn::SortObject', ...opts }).then(function ({
223
225
 
224
226
  object,
225
227
  options,
@@ -229,19 +231,19 @@ async function recurse({ base, scope, cft, ...opts }) {
229
231
  });
230
232
  }
231
233
  if (cft['Fn::Without']) {
232
- return recurse({ base, scope, cft: cft['Fn::Without'], ...opts }).then(function (json) {
234
+ return recurse({ base, scope, cft: cft['Fn::Without'], rootTemplate, caller: 'Fn::Without', ...opts }).then(function (json) {
233
235
  json = Array.isArray(json) ? { list: json[0], withouts: json[1] } : json;
234
236
  return _.without(json.list, ...json.withouts);
235
237
  });
236
238
  }
237
239
  if (cft['Fn::Omit']) {
238
- return recurse({ base, scope, cft: cft['Fn::Omit'], ...opts }).then(function (json) {
240
+ return recurse({ base, scope, cft: cft['Fn::Omit'], rootTemplate, caller: 'Fn::Omit', ...opts }).then(function (json) {
239
241
  json = Array.isArray(json) ? { object: json[0], omits: json[1] } : json;
240
242
  return _.omit(json.object, json.omits);
241
243
  });
242
244
  }
243
245
  if (cft['Fn::OmitEmpty']) {
244
- return recurse({ base, scope, cft: cft['Fn::OmitEmpty'], ...opts }).then(
246
+ return recurse({ base, scope, cft: cft['Fn::OmitEmpty'], rootTemplate, caller: 'Fn::OmitEmpty', ...opts }).then(
245
247
  function (json) {
246
248
  // omit falsy values except false, and 0
247
249
  return _.omitBy(json, (v) => !v && v !== false && v !== 0);
@@ -252,7 +254,7 @@ async function recurse({ base, scope, cft, ...opts }) {
252
254
  if (!opts.doEval) {
253
255
  return Promise.reject(new Error('Fn::Eval is not allowed doEval is falsy'));
254
256
  }
255
- return recurse({ base, scope, cft: cft['Fn::Eval'], ...opts }).then(function (json) {
257
+ return recurse({ base, scope, cft: cft['Fn::Eval'], rootTemplate, caller: 'Fn::Eval', ...opts }).then(function (json) {
256
258
  // **WARNING** you have now enabled god mode
257
259
 
258
260
  let { state, script, inject, doLog } = json;
@@ -266,7 +268,7 @@ async function recurse({ base, scope, cft, ...opts }) {
266
268
  });
267
269
  }
268
270
  if (cft['Fn::Filenames']) {
269
- return recurse({ base, scope, cft: cft['Fn::Filenames'], ...opts }).then(
271
+ return recurse({ base, scope, cft: cft['Fn::Filenames'], rootTemplate, caller: 'Fn::Filenames', ...opts }).then(
270
272
  function (json) {
271
273
  json = _.isPlainObject(json) ? { ...json } : { location: json };
272
274
  if (json.doLog) {
@@ -293,14 +295,14 @@ async function recurse({ base, scope, cft, ...opts }) {
293
295
  );
294
296
  }
295
297
  if (cft['Fn::Merge']) {
296
- return recurse({ base, scope, cft: cft['Fn::Merge'], ...opts }).then(function (json) {
298
+ return recurse({ base, scope, cft: cft['Fn::Merge'], rootTemplate, caller: 'Fn::Merge', ...opts }).then(function (json) {
297
299
  delete cft['Fn::Merge'];
298
300
 
299
- return recurse({ base, scope, cft: _.defaults(cft, _.merge.apply(_, json)), ...opts });
301
+ return recurse({ base, scope, cft: _.defaults(cft, _.merge.apply(_, json)), rootTemplate, caller: 'Fn::Merge', ...opts });
300
302
  });
301
303
  }
302
304
  if (cft['Fn::DeepMerge']) {
303
- return recurse({ base, scope, cft: cft['Fn::DeepMerge'], ...opts }).then(
305
+ return recurse({ base, scope, cft: cft['Fn::DeepMerge'], rootTemplate, caller: 'Fn::DeepMerge', ...opts }).then(
304
306
  function (json) {
305
307
  delete cft['Fn::DeepMerge'];
306
308
  let mergedObj = {};
@@ -309,29 +311,29 @@ async function recurse({ base, scope, cft, ...opts }) {
309
311
  mergedObj = deepMerge(mergedObj, j);
310
312
  });
311
313
  }
312
- return recurse({ base, scope, cft: _.defaults(cft, mergedObj), ...opts });
314
+ return recurse({ base, scope, cft: _.defaults(cft, mergedObj), rootTemplate, caller: 'Fn::DeepMerge', ...opts });
313
315
  },
314
316
  );
315
317
  }
316
318
  if (cft['Fn::ObjectKeys']) {
317
- return recurse({ base, scope, cft: cft['Fn::ObjectKeys'], ...opts }).then((json) =>
319
+ return recurse({ base, scope, cft: cft['Fn::ObjectKeys'], rootTemplate, caller: 'Fn::ObjectKeys', ...opts }).then((json) =>
318
320
  Object.keys(json),
319
321
  );
320
322
  }
321
323
  if (cft['Fn::ObjectValues']) {
322
- return recurse({ base, scope, cft: cft['Fn::ObjectValues'], ...opts }).then((json) =>
324
+ return recurse({ base, scope, cft: cft['Fn::ObjectValues'], rootTemplate, caller: 'Fn::ObjectValues', ...opts }).then((json) =>
323
325
  Object.values(json),
324
326
  );
325
327
  }
326
328
  if (cft['Fn::Stringify']) {
327
- return recurse({ base, scope, cft: cft['Fn::Stringify'], ...opts }).then(
329
+ return recurse({ base, scope, cft: cft['Fn::Stringify'], rootTemplate, caller: 'Fn::Stringify', ...opts }).then(
328
330
  function (json) {
329
331
  return JSON.stringify(json);
330
332
  },
331
333
  );
332
334
  }
333
335
  if (cft['Fn::StringSplit']) {
334
- return recurse({ base, scope, cft: cft['Fn::StringSplit'], ...opts }).then(
336
+ return recurse({ base, scope, cft: cft['Fn::StringSplit'], rootTemplate, caller: 'Fn::StringSplit', ...opts }).then(
335
337
  ({ string, separator, doLog }) => {
336
338
  if (!string) {
337
339
  string = '';
@@ -366,7 +368,7 @@ async function recurse({ base, scope, cft, ...opts }) {
366
368
  }
367
369
 
368
370
  if (cft['Fn::Outputs']) {
369
- const outputs = await recurse({ base, scope, cft: cft['Fn::Outputs'], ...opts });
371
+ const outputs = await recurse({ base, scope, cft: cft['Fn::Outputs'], caller: 'Fn::Outputs', ...opts });
370
372
  const result = {};
371
373
 
372
374
  for (const output in outputs) {
@@ -391,7 +393,7 @@ async function recurse({ base, scope, cft, ...opts }) {
391
393
  }
392
394
 
393
395
  if (cft['Fn::Sequence']) {
394
- const outputs = await recurse({ base, scope, cft: cft['Fn::Sequence'], ...opts });
396
+ const outputs = await recurse({ base, scope, cft: cft['Fn::Sequence'], caller: 'Fn::Sequence', ...opts });
395
397
 
396
398
  let [start, stop, step = 1] = outputs;
397
399
  const isString = typeof start === 'string';
@@ -410,7 +412,7 @@ async function recurse({ base, scope, cft, ...opts }) {
410
412
  if (!opts.doEval) {
411
413
  return Promise.reject(new Error('Fn::IfEval is not allowed doEval is falsy'));
412
414
  }
413
- return recurse({ base, scope, cft: cft['Fn::IfEval'], ...opts }).then(function (json) {
415
+ return recurse({ base, scope, cft: cft['Fn::IfEval'], rootTemplate, caller: 'Fn::IfEval', ...opts }).then(function (json) {
414
416
 
415
417
  let { truthy, falsy, evalCond, inject, doLog } = json;
416
418
  if (!evalCond) {
@@ -439,13 +441,13 @@ async function recurse({ base, scope, cft, ...opts }) {
439
441
  }
440
442
 
441
443
  if (condResult) {
442
- return recurse({ base, scope, cft: truthy, ...opts });
444
+ return recurse({ base, scope, cft: truthy, rootTemplate, caller: 'Fn::IfEval', ...opts });
443
445
  }
444
- return recurse({ base, scope, cft: falsy, ...opts });
446
+ return recurse({ base, scope, cft: falsy, rootTemplate, caller: 'Fn::IfEval', ...opts });
445
447
  });
446
448
  }
447
449
  if (cft['Fn::JoinNow']) {
448
- return recurse({ base, scope, cft: cft['Fn::JoinNow'], ...opts }).then((array) => {
450
+ return recurse({ base, scope, cft: cft['Fn::JoinNow'], rootTemplate, caller: 'Fn::JoinNow', ...opts }).then((array) => {
449
451
  // keeps with same format as Fn::Join ~ more complex
450
452
  // vs let [delimitter, ...toJoinArray] = array;
451
453
  let [delimitter, toJoinArray] = array;
@@ -454,7 +456,7 @@ async function recurse({ base, scope, cft, ...opts }) {
454
456
  });
455
457
  }
456
458
  if (cft['Fn::SubNow']) {
457
- return recurse({ base, scope, cft: cft['Fn::SubNow'], ...opts }).then((input) => {
459
+ return recurse({ base, scope, cft: cft['Fn::SubNow'], rootTemplate, caller: 'Fn::SubNow', ...opts }).then((input) => {
458
460
  let template = input;
459
461
  let variables = {};
460
462
 
@@ -481,8 +483,21 @@ async function recurse({ base, scope, cft, ...opts }) {
481
483
  });
482
484
  }
483
485
  if (cft['Fn::RefNow']) {
484
- return recurse({ base, scope, cft: cft['Fn::RefNow'], ...opts }).then((logicalName) => {
485
- let refName = logicalName;
486
+ // console.log("outter");
487
+ // console.log(opts);
488
+ // Handle both simple string and object format for Fn::RefNow
489
+ // e.g., Fn::RefNow: "MyRole" or Fn::RefNow: { Ref: "MyRole" }
490
+ return recurse({ base, scope, cft: cft['Fn::RefNow'], rootTemplate, caller: 'Fn::RefNow', ...opts }).then((refInput) => {
491
+ // console.log("inner");
492
+ // console.log(opts);
493
+ let refName = refInput;
494
+ let refOptions = {};
495
+
496
+ // Parse the input - could be string or object
497
+ if (_.isPlainObject(refInput)) {
498
+ refName = refInput.Ref || refInput.ref;
499
+ refOptions = _.omit(refInput, ['Ref', 'ref']);
500
+ }
486
501
 
487
502
  // Check if this ref name is in the ignores list
488
503
  if (opts.refNowIgnores && opts.refNowIgnores.includes(refName)) {
@@ -502,6 +517,36 @@ async function recurse({ base, scope, cft, ...opts }) {
502
517
  return allRefs[refName];
503
518
  }
504
519
 
520
+ // Check if this is a LogicalResourceId in the Resources section
521
+ if (rootTemplate && rootTemplate.Resources) {
522
+ const resources = rootTemplate.Resources;
523
+ if (refName in resources) {
524
+ const resource = resources[refName];
525
+ const resourceType = resource.Type;
526
+ const properties = resource.Properties || {};
527
+
528
+ // Determine return type based on key name
529
+ // If the key ends with "Name" (e.g., RoleName, BucketName), return name/identifier
530
+ // Otherwise return ARN (default)
531
+ let returnType = 'arn';
532
+ if (opts.key && opts.key.endsWith('Name')) {
533
+ returnType = 'name';
534
+ }
535
+
536
+ // Try to build an ARN or name for this resource
537
+ // Merge ref options with global options (ref options take precedence)
538
+ const resourceOptions = {
539
+ returnType,
540
+ ...opts.refNowReturnType ? { returnType: opts.refNowReturnType } : {},
541
+ ...refOptions,
542
+ };
543
+ const result = buildResourceArn(resourceType, properties, allRefs, resourceOptions);
544
+ if (result) {
545
+ return result;
546
+ }
547
+ }
548
+ }
549
+
505
550
  // If not found and refNowIgnoreMissing is true, return Ref syntax; otherwise throw error
506
551
  if (opts.refNowIgnoreMissing) {
507
552
  return { Ref: refName };
@@ -512,7 +557,7 @@ async function recurse({ base, scope, cft, ...opts }) {
512
557
  });
513
558
  }
514
559
  if (cft['Fn::ApplyTags']) {
515
- return recurse({ base, scope, cft: cft['Fn::ApplyTags'], ...opts }).then((json) => {
560
+ return recurse({ base, scope, cft: cft['Fn::ApplyTags'], rootTemplate, caller: 'Fn::ApplyTags', ...opts }).then((json) => {
516
561
  let { tags, Tags, resources } = json;
517
562
  tags = tags || Tags; // allow for both caseing
518
563
  const promises = [];
@@ -538,7 +583,7 @@ async function recurse({ base, scope, cft, ...opts }) {
538
583
  }
539
584
 
540
585
  return Promise.props(
541
- _.mapValues(cft, (template) => recurse({ base, scope, cft: template, ...opts })),
586
+ _.mapValues(cft, (template, key) => recurse({ base, scope, cft: template, key, rootTemplate, caller: 'recurse:isPlainObject:end', ...opts })),
542
587
  );
543
588
  }
544
589
 
@@ -632,6 +677,21 @@ function fnIncludeOpts(cft, opts) {
632
677
  return cft;
633
678
  }
634
679
 
680
+ /**
681
+ * Process includes for template documents
682
+ * @param {object} base file options (protocol, host, path, relative, raw)
683
+ * @param {object} scope variable scope for Fn::Map substitutions
684
+ * @param {Document|String|Array} cft include specification (location, query, parser, type, etc.)
685
+ * @param {Object} opts processing options
686
+ * @param {boolean} [opts.doEnv] inject environment from process.env
687
+ * @param {boolean} [opts.doEval] allow Fn::Eval to be used
688
+ * @param {Object} [opts.inject] object to inject { KEY: Value }
689
+ * @param {boolean} [opts.doLog] log all arguments at the include recurse level
690
+ * @param {Array<string>} [opts.refNowIgnores] array of logical resource IDs to ignore when resolving Fn::RefNow references
691
+ * @param {boolean} [opts.refNowIgnoreMissing] if true, return Ref syntax for unresolvable references; if false, throw error
692
+ * @param {object} [opts.rootTemplate] the root template object for resource lookups in Fn::RefNow
693
+ * @returns {Promise} resolved include content
694
+ */
635
695
  async function fnInclude({ base, scope, cft, ...opts }) {
636
696
  let procTemplate = async (template, inject = cft.inject, doEnv = opts.doEnv) =>
637
697
  replaceEnv(template, inject, doEnv);
@@ -653,7 +713,6 @@ async function fnInclude({ base, scope, cft, ...opts }) {
653
713
  cft = fnIncludeOpts(cft, opts);
654
714
 
655
715
  if (cft.doLog) {
656
-
657
716
  console.log({ base, scope, args: cft, ...opts });
658
717
  }
659
718
  // console.log(args)
@@ -673,14 +732,14 @@ async function fnInclude({ base, scope, cft, ...opts }) {
673
732
  if (isGlob(cft, absolute)) {
674
733
  const paths = globSync(absolute).sort();
675
734
  const template = yaml.load(paths.map((_p) => `- Fn::Include: file://${_p}`).join('\n'));
676
- return recurse({ base, scope, cft: template, ...opts });
735
+ return recurse({ base, scope, cft: template, rootTemplate: template, ...opts });
677
736
  }
678
737
  body = readFile(absolute).then(String).then(procTemplate);
679
738
  absolute = `${location.protocol}://${absolute}`;
680
739
  } else if (location.protocol === 's3') {
681
740
  const basedir = pathParse(base.path).dir;
682
741
  const bucket = location.relative ? base.host : location.host;
683
-
742
+
684
743
  let key = location.relative ? url.resolve(`${basedir}/`, location.raw) : location.path;
685
744
  key = key.replace(/^\//, '');
686
745
  absolute = `${location.protocol}://${[bucket, key].join('/')}`;
@@ -695,11 +754,11 @@ async function fnInclude({ base, scope, cft, ...opts }) {
695
754
  .then(procTemplate);
696
755
  } else if (location.protocol && location.protocol.match(/^https?$/)) {
697
756
  const basepath = `${pathParse(base.path).dir}/`;
698
-
757
+
699
758
  absolute = location.relative
700
759
  ? url.resolve(`${location.protocol}://${base.host}${basepath}`, location.raw)
701
760
  : location.raw;
702
-
761
+
703
762
  body = request(absolute).then(procTemplate);
704
763
  }
705
764
  return handleIncludeBody({ scope, args: cft, body, absolute });
@@ -709,6 +768,27 @@ function isGlob(args, str) {
709
768
  return args.isGlob || /.*\*/.test(str);
710
769
  }
711
770
 
771
+ /**
772
+ * Process the resolved include body content
773
+ * @param {object} config configuration object
774
+ * @param {object} config.scope variable scope for substitutions
775
+ * @param {Object} config.args include arguments (type, query, parser, inject, doEnv, doEval, doLog, refNowIgnores, refNowIgnoreMissing, rootTemplate)
776
+ * @param {Promise} config.body promise resolving to the file content
777
+ * @param {string} config.absolute absolute path/URL to the included resource
778
+ * @param {string} [config.args.type='json'] type of content to process (json, string, literal)
779
+ * @param {Object|string|Array} [config.args.query] query to apply to the processed template
780
+ * @param {string} [config.args.parser='lodash'] parser to use for querying (lodash, jsonpath)
781
+ * @param {Object} [config.args.context] context object for literal interpolation
782
+ * @param {Object} [config.args.inject] object to inject { KEY: Value }
783
+ * @param {boolean} [config.args.doEnv] inject environment from process.env
784
+ * @param {boolean} [config.args.doEval] allow Fn::Eval to be used
785
+ * @param {boolean} [config.args.doLog] log all arguments at the include recurse level
786
+ * @param {Array<string>} [config.args.refNowIgnores] array of logical resource IDs to ignore when resolving Fn::RefNow references
787
+ * @param {boolean} [config.args.refNowIgnoreMissing] if true, return Ref syntax for unresolvable references; if false, throw error
788
+ *
789
+ * rootTeamplate is not an argument here cause it's loaded via arg.type
790
+ * @returns {Promise} processed template content
791
+ */
712
792
  async function handleIncludeBody({ scope, args, body, absolute }) {
713
793
  const procTemplate = (temp) => replaceEnv(temp, args.inject, args.doEnv);
714
794
  try {
@@ -716,18 +796,22 @@ async function handleIncludeBody({ scope, args, body, absolute }) {
716
796
  case 'json': {
717
797
  let b = await body;
718
798
  b = procTemplate(b);
719
- const template = yaml.load(b);
720
-
799
+ const rootTemplate = yaml.load(b);
800
+ const caller = 'handleIncludeBody:json';
721
801
  // recurse all the way down and work your way out
722
802
  const loopTemplate = (temp) => {
723
803
  return recurse({
724
804
  base: parseLocation(absolute),
725
805
  scope,
726
806
  cft: temp,
807
+ caller,
808
+ rootTemplate,
727
809
  doEnv: args.doEnv,
728
810
  doEval: args.doEval,
729
811
  doLog: args.doLog,
730
812
  inject: args.inject,
813
+ refNowIgnores: args.refNowIgnores,
814
+ refNowIgnoreMissing: args.refNowIgnoreMissing, // note we can't use ...args here because of circular ref
731
815
  }).then((_temp) => {
732
816
  if (!_temp || !Object.keys(_temp).length) {
733
817
  return _temp;
@@ -740,7 +824,7 @@ async function handleIncludeBody({ scope, args, body, absolute }) {
740
824
  });
741
825
  };
742
826
 
743
- return loopTemplate(template).then(async (temp) => {
827
+ return loopTemplate(rootTemplate).then(async (temp) => {
744
828
  if (!args.query) {
745
829
  return temp;
746
830
  }
@@ -751,9 +835,13 @@ async function handleIncludeBody({ scope, args, body, absolute }) {
751
835
  base: parseLocation(absolute),
752
836
  scope,
753
837
  cft: args.query,
838
+ caller,
839
+ rootTemplate,
754
840
  doEnv: args.doEnv,
755
841
  doLog: args.doLog,
756
842
  inject: args.inject,
843
+ refNowIgnores: args.refNowIgnores,
844
+ refNowIgnoreMissing: args.refNowIgnoreMissing,
757
845
  });
758
846
  return getParser(args.parser)(temp, query);
759
847
  });
@@ -0,0 +1,174 @@
1
+ function getAwsPseudoParameters() {
2
+ return {
3
+ 'AWS::AccountId': process.env.AWS_ACCOUNT_ID || process.env.AWS_ACCOUNT_NUM || '${AWS::AccountId}',
4
+ 'AWS::Partition': process.env.AWS_PARTITION || 'aws',
5
+ 'AWS::Region': process.env.AWS_REGION || '${AWS::Region}',
6
+ 'AWS::StackId': process.env.AWS_STACK_ID || '${AWS::StackId}',
7
+ 'AWS::StackName': process.env.AWS_STACK_NAME || '${AWS::StackName}',
8
+ 'AWS::URLSuffix': process.env.AWS_URL_SUFFIX || 'amazonaws.com',
9
+ 'AWS::NotificationARNs': process.env.AWS_NOTIFICATION_ARNS || '${AWS::NotificationARNs}',
10
+ };
11
+ }
12
+
13
+ /**
14
+ * Build an ARN for a CloudFormation resource based on its Type and Properties
15
+ * Supports both ARN construction and property name resolution
16
+ * @param {string} resourceType - The CloudFormation resource type (e.g., 'AWS::IAM::ManagedPolicy')
17
+ * @param {object} properties - The resource properties
18
+ * @param {object} pseudoParams - AWS pseudo-parameters including AccountId, Region
19
+ * @param {object} options - Optional configuration
20
+ * @param {string} options.returnType - 'arn' (default) or 'name' to return the resource name/identifier
21
+ * @returns {string|null} - The ARN, name, or null if not applicable
22
+ */
23
+ function buildResourceArn(resourceType, properties = {}, pseudoParams = {}, options = {}) {
24
+ const accountId = pseudoParams['AWS::AccountId'] || '${AWS::AccountId}';
25
+ const region = pseudoParams['AWS::Region'] || '${AWS::Region}';
26
+ const partition = pseudoParams['AWS::Partition'] || 'aws';
27
+ const returnType = options.returnType || 'arn';
28
+
29
+ // Handle AWS::IAM::ManagedPolicy
30
+ if (resourceType === 'AWS::IAM::ManagedPolicy') {
31
+ const { ManagedPolicyName, Path } = properties;
32
+ if (ManagedPolicyName) {
33
+ if (returnType === 'name') {
34
+ return ManagedPolicyName;
35
+ }
36
+ const path = Path || '/';
37
+ return `arn:${partition}:iam::${accountId}:policy${path}${ManagedPolicyName}`;
38
+ }
39
+ }
40
+
41
+ // Handle AWS::IAM::Role
42
+ if (resourceType === 'AWS::IAM::Role') {
43
+ const { RoleName, Path } = properties;
44
+ if (RoleName) {
45
+ if (returnType === 'name') {
46
+ return RoleName;
47
+ }
48
+ const path = Path || '/';
49
+ return `arn:${partition}:iam::${accountId}:role${path}${RoleName}`;
50
+ }
51
+ }
52
+
53
+ // Handle AWS::S3::Bucket
54
+ if (resourceType === 'AWS::S3::Bucket') {
55
+ const { BucketName } = properties;
56
+ if (BucketName) {
57
+ if (returnType === 'name') {
58
+ return BucketName;
59
+ }
60
+ return `arn:${partition}:s3:::${BucketName}`;
61
+ }
62
+ }
63
+
64
+ // Handle AWS::Lambda::Function
65
+ if (resourceType === 'AWS::Lambda::Function') {
66
+ const { FunctionName } = properties;
67
+ if (FunctionName) {
68
+ if (returnType === 'name') {
69
+ return FunctionName;
70
+ }
71
+ return `arn:${partition}:lambda:${region}:${accountId}:function:${FunctionName}`;
72
+ }
73
+ }
74
+
75
+ // Handle AWS::SQS::Queue
76
+ if (resourceType === 'AWS::SQS::Queue') {
77
+ const { QueueName } = properties;
78
+ if (QueueName) {
79
+ if (returnType === 'name') {
80
+ return QueueName;
81
+ }
82
+ return `arn:${partition}:sqs:${region}:${accountId}:${QueueName}`;
83
+ }
84
+ }
85
+
86
+ // Handle AWS::SNS::Topic
87
+ if (resourceType === 'AWS::SNS::Topic') {
88
+ const { TopicName } = properties;
89
+ if (TopicName) {
90
+ if (returnType === 'name') {
91
+ return TopicName;
92
+ }
93
+ return `arn:${partition}:sns:${region}:${accountId}:${TopicName}`;
94
+ }
95
+ }
96
+
97
+ // Handle AWS::DynamoDB::Table
98
+ if (resourceType === 'AWS::DynamoDB::Table') {
99
+ const { TableName } = properties;
100
+ if (TableName) {
101
+ if (returnType === 'name') {
102
+ return TableName;
103
+ }
104
+ return `arn:${partition}:dynamodb:${region}:${accountId}:table/${TableName}`;
105
+ }
106
+ }
107
+
108
+ // Handle AWS::RDS::DBInstance
109
+ if (resourceType === 'AWS::RDS::DBInstance') {
110
+ const { DBInstanceIdentifier } = properties;
111
+ if (DBInstanceIdentifier) {
112
+ if (returnType === 'name') {
113
+ return DBInstanceIdentifier;
114
+ }
115
+ return `arn:${partition}:rds:${region}:${accountId}:db:${DBInstanceIdentifier}`;
116
+ }
117
+ }
118
+
119
+ // Handle AWS::EC2::SecurityGroup
120
+ if (resourceType === 'AWS::EC2::SecurityGroup') {
121
+ const { GroupName } = properties;
122
+ if (GroupName) {
123
+ if (returnType === 'name') {
124
+ return GroupName;
125
+ }
126
+ // Security groups need to be referenced by ID in most cases, not ARN
127
+ // This returns the name for reference purposes
128
+ return GroupName;
129
+ }
130
+ }
131
+
132
+ // Handle AWS::IAM::InstanceProfile
133
+ if (resourceType === 'AWS::IAM::InstanceProfile') {
134
+ const { InstanceProfileName, Path } = properties;
135
+ if (InstanceProfileName) {
136
+ if (returnType === 'name') {
137
+ return InstanceProfileName;
138
+ }
139
+ const path = Path || '/';
140
+ return `arn:${partition}:iam::${accountId}:instance-profile${path}${InstanceProfileName}`;
141
+ }
142
+ }
143
+
144
+ // Handle AWS::KMS::Key
145
+ if (resourceType === 'AWS::KMS::Key') {
146
+ // KMS keys are referenced by KeyId or KeyArn in properties
147
+ const { KeyId } = properties;
148
+ if (KeyId) {
149
+ if (returnType === 'name') {
150
+ return KeyId;
151
+ }
152
+ return `arn:${partition}:kms:${region}:${accountId}:key/${KeyId}`;
153
+ }
154
+ }
155
+
156
+ // Handle AWS::SecretsManager::Secret
157
+ if (resourceType === 'AWS::SecretsManager::Secret') {
158
+ const { Name } = properties;
159
+ if (Name) {
160
+ if (returnType === 'name') {
161
+ return Name;
162
+ }
163
+ return `arn:${partition}:secretsmanager:${region}:${accountId}:secret:${Name}`;
164
+ }
165
+ }
166
+
167
+ // Add more resource types as needed
168
+ return null;
169
+ }
170
+
171
+ module.exports = {
172
+ getAwsPseudoParameters,
173
+ buildResourceArn,
174
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@znemz/cfn-include",
3
- "version": "2.1.11",
3
+ "version": "2.1.13",
4
4
  "description": "Preprocessor for CloudFormation templates with support for loops and flexible include statements",
5
5
  "keywords": [
6
6
  "aws",