@znemz/cfn-include 2.1.10 → 2.1.12
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 +77 -0
- package/bin/cli.js +15 -0
- package/index.js +214 -5
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1201,6 +1201,83 @@ If an AWS pseudo-parameter is not set via environment variables, it falls back t
|
|
|
1201
1201
|
**Error handling:**
|
|
1202
1202
|
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
1203
|
|
|
1204
|
+
**Resolving LogicalResourceIds:**
|
|
1205
|
+
|
|
1206
|
+
`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.
|
|
1207
|
+
|
|
1208
|
+
**Supported Resource Types for ARN/Name Resolution:**
|
|
1209
|
+
|
|
1210
|
+
- `AWS::IAM::ManagedPolicy` - Returns policy ARN (supports Path)
|
|
1211
|
+
- `AWS::IAM::Role` - Returns role ARN (supports Path)
|
|
1212
|
+
- `AWS::IAM::InstanceProfile` - Returns instance profile ARN (supports Path)
|
|
1213
|
+
- `AWS::S3::Bucket` - Returns bucket ARN
|
|
1214
|
+
- `AWS::Lambda::Function` - Returns function ARN
|
|
1215
|
+
- `AWS::SQS::Queue` - Returns queue ARN
|
|
1216
|
+
- `AWS::SNS::Topic` - Returns topic ARN
|
|
1217
|
+
- `AWS::DynamoDB::Table` - Returns table ARN
|
|
1218
|
+
- `AWS::RDS::DBInstance` - Returns DB instance ARN
|
|
1219
|
+
- `AWS::SecretsManager::Secret` - Returns secret ARN
|
|
1220
|
+
- `AWS::KMS::Key` - Returns key ARN
|
|
1221
|
+
|
|
1222
|
+
Example with AWS::IAM::ManagedPolicy:
|
|
1223
|
+
|
|
1224
|
+
```yaml
|
|
1225
|
+
Resources:
|
|
1226
|
+
ObjPolicy:
|
|
1227
|
+
Type: AWS::IAM::ManagedPolicy
|
|
1228
|
+
Properties:
|
|
1229
|
+
ManagedPolicyName: teststack-CreateTestDBPolicy-16M23YE3CS700
|
|
1230
|
+
Path: /CRAP/
|
|
1231
|
+
|
|
1232
|
+
IAMRole:
|
|
1233
|
+
Type: AWS::IAM::Role
|
|
1234
|
+
Properties:
|
|
1235
|
+
ManagedPolicyArns:
|
|
1236
|
+
- Fn::RefNow: ObjPolicy # Resolves to: arn:aws:iam::${AWS_ACCOUNT_ID}:policy/CRAP/teststack-CreateTestDBPolicy-16M23YE3CS700
|
|
1237
|
+
```
|
|
1238
|
+
|
|
1239
|
+
**Returning Resource Names Instead of ARNs:**
|
|
1240
|
+
|
|
1241
|
+
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:
|
|
1242
|
+
|
|
1243
|
+
```yaml
|
|
1244
|
+
Resources:
|
|
1245
|
+
MyRole:
|
|
1246
|
+
Type: AWS::IAM::Role
|
|
1247
|
+
Properties:
|
|
1248
|
+
RoleName: MyExecutionRole
|
|
1249
|
+
|
|
1250
|
+
RoleArn:
|
|
1251
|
+
Fn::RefNow: MyRole # Returns: arn:aws:iam::${AWS_ACCOUNT_ID}:role/MyExecutionRole
|
|
1252
|
+
|
|
1253
|
+
RoleName:
|
|
1254
|
+
Fn::RefNow: MyRole # Returns: MyExecutionRole (because key ends with "Name")
|
|
1255
|
+
```
|
|
1256
|
+
|
|
1257
|
+
This intuitive approach makes templates more readable and follows natural CloudFormation naming conventions.
|
|
1258
|
+
|
|
1259
|
+
**CLI Options for Fn::RefNow:**
|
|
1260
|
+
|
|
1261
|
+
The `cfn-include` command provides CLI options to control how unresolved references are handled:
|
|
1262
|
+
|
|
1263
|
+
- `--ref-now-ignore-missing`: Do not fail if a `Fn::RefNow` reference cannot be resolved. Instead, the reference will be returned in AWS CloudFormation's standard `Ref` syntax (e.g., `{ Ref: 'UnresolvedRef' }`), allowing CloudFormation to resolve it at stack creation time.
|
|
1264
|
+
- `--ref-now-ignores <names>`: Comma-separated list of reference names to ignore if not found. These references will be returned in `Ref` syntax instead of throwing an error.
|
|
1265
|
+
|
|
1266
|
+
**Example usage:**
|
|
1267
|
+
|
|
1268
|
+
```bash
|
|
1269
|
+
# Ignore all unresolved references
|
|
1270
|
+
cfn-include template.yaml --ref-now-ignore-missing
|
|
1271
|
+
|
|
1272
|
+
# Ignore specific reference names
|
|
1273
|
+
cfn-include template.yaml --ref-now-ignores "OptionalRef1,OptionalRef2"
|
|
1274
|
+
|
|
1275
|
+
# Combine both options
|
|
1276
|
+
cfn-include template.yaml --ref-now-ignore-missing --ref-now-ignores "SpecificRef"
|
|
1277
|
+
```
|
|
1278
|
+
|
|
1279
|
+
This is useful for templates that reference CloudFormation parameters or other resources that may not be available at template processing time but will be available at stack creation time.
|
|
1280
|
+
|
|
1204
1281
|
## Fn::ApplyTags
|
|
1205
1282
|
|
|
1206
1283
|
See [ApplyTags test file](t/tests/applyTags.yml).
|
package/bin/cli.js
CHANGED
|
@@ -88,6 +88,14 @@ const { env } = process;
|
|
|
88
88
|
|
|
89
89
|
desc: `console log out include options in recurse step`,
|
|
90
90
|
},
|
|
91
|
+
'ref-now-ignore-missing': {
|
|
92
|
+
boolean: true,
|
|
93
|
+
desc: 'do not fail if Fn::RefNow reference cannot be resolved',
|
|
94
|
+
},
|
|
95
|
+
'ref-now-ignores': {
|
|
96
|
+
string: true,
|
|
97
|
+
desc: 'comma-separated list of reference names to ignore if not found',
|
|
98
|
+
},
|
|
91
99
|
version: {
|
|
92
100
|
boolean: true,
|
|
93
101
|
desc: 'print version and exit',
|
|
@@ -102,6 +110,9 @@ const { env } = process;
|
|
|
102
110
|
// make enable an array
|
|
103
111
|
opts.enable = opts.enable.split(',');
|
|
104
112
|
|
|
113
|
+
// Parse ref-now-ignores into an array
|
|
114
|
+
const refNowIgnores = opts['ref-now-ignores'] ? opts['ref-now-ignores'].split(',').map(s => s.trim()) : [];
|
|
115
|
+
|
|
105
116
|
let promise;
|
|
106
117
|
if (opts.path) {
|
|
107
118
|
let location;
|
|
@@ -115,6 +126,8 @@ if (opts.path) {
|
|
|
115
126
|
doEval: opts.enable.includes('eval'),
|
|
116
127
|
inject: opts.inject,
|
|
117
128
|
doLog: opts.doLog,
|
|
129
|
+
refNowIgnoreMissing: opts['ref-now-ignore-missing'],
|
|
130
|
+
refNowIgnores: refNowIgnores,
|
|
118
131
|
});
|
|
119
132
|
} else {
|
|
120
133
|
promise = new Promise((resolve, reject) => {
|
|
@@ -142,6 +155,8 @@ if (opts.path) {
|
|
|
142
155
|
doEval: opts.enable.includes('eval'),
|
|
143
156
|
inject: opts.inject,
|
|
144
157
|
doLog: opts.doLog,
|
|
158
|
+
refNowIgnoreMissing: opts['ref-now-ignore-missing'],
|
|
159
|
+
refNowIgnores: refNowIgnores,
|
|
145
160
|
}).catch((err) => console.error(err));
|
|
146
161
|
});
|
|
147
162
|
}
|
package/index.js
CHANGED
|
@@ -59,7 +59,9 @@ module.exports = async function (options) {
|
|
|
59
59
|
template = _.isUndefined(template)
|
|
60
60
|
? fnInclude({ base, scope, cft: options.url, ...options })
|
|
61
61
|
: template;
|
|
62
|
-
|
|
62
|
+
// Resolve template if it's a promise to extract the root template for reference lookups
|
|
63
|
+
const resolvedTemplate = await Promise.resolve(template);
|
|
64
|
+
return recurse({ base, scope, cft: resolvedTemplate, rootTemplate: resolvedTemplate, ...options });
|
|
63
65
|
};
|
|
64
66
|
|
|
65
67
|
/**
|
|
@@ -102,6 +104,164 @@ function getAwsPseudoParameters() {
|
|
|
102
104
|
};
|
|
103
105
|
}
|
|
104
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Build an ARN for a CloudFormation resource based on its Type and Properties
|
|
109
|
+
* Supports both ARN construction and property name resolution
|
|
110
|
+
* @param {string} resourceType - The CloudFormation resource type (e.g., 'AWS::IAM::ManagedPolicy')
|
|
111
|
+
* @param {object} properties - The resource properties
|
|
112
|
+
* @param {object} pseudoParams - AWS pseudo-parameters including AccountId, Region
|
|
113
|
+
* @param {object} options - Optional configuration
|
|
114
|
+
* @param {string} options.returnType - 'arn' (default) or 'name' to return the resource name/identifier
|
|
115
|
+
* @returns {string|null} - The ARN, name, or null if not applicable
|
|
116
|
+
*/
|
|
117
|
+
function buildResourceArn(resourceType, properties = {}, pseudoParams = {}, options = {}) {
|
|
118
|
+
const accountId = pseudoParams['AWS::AccountId'] || '${AWS::AccountId}';
|
|
119
|
+
const region = pseudoParams['AWS::Region'] || '${AWS::Region}';
|
|
120
|
+
const partition = pseudoParams['AWS::Partition'] || 'aws';
|
|
121
|
+
const returnType = options.returnType || 'arn';
|
|
122
|
+
|
|
123
|
+
// Handle AWS::IAM::ManagedPolicy
|
|
124
|
+
if (resourceType === 'AWS::IAM::ManagedPolicy') {
|
|
125
|
+
const { ManagedPolicyName, Path } = properties;
|
|
126
|
+
if (ManagedPolicyName) {
|
|
127
|
+
if (returnType === 'name') {
|
|
128
|
+
return ManagedPolicyName;
|
|
129
|
+
}
|
|
130
|
+
const path = Path || '/';
|
|
131
|
+
return `arn:${partition}:iam::${accountId}:policy${path}${ManagedPolicyName}`;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Handle AWS::IAM::Role
|
|
136
|
+
if (resourceType === 'AWS::IAM::Role') {
|
|
137
|
+
const { RoleName, Path } = properties;
|
|
138
|
+
if (RoleName) {
|
|
139
|
+
if (returnType === 'name') {
|
|
140
|
+
return RoleName;
|
|
141
|
+
}
|
|
142
|
+
const path = Path || '/';
|
|
143
|
+
return `arn:${partition}:iam::${accountId}:role${path}${RoleName}`;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// Handle AWS::S3::Bucket
|
|
148
|
+
if (resourceType === 'AWS::S3::Bucket') {
|
|
149
|
+
const { BucketName } = properties;
|
|
150
|
+
if (BucketName) {
|
|
151
|
+
if (returnType === 'name') {
|
|
152
|
+
return BucketName;
|
|
153
|
+
}
|
|
154
|
+
return `arn:${partition}:s3:::${BucketName}`;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Handle AWS::Lambda::Function
|
|
159
|
+
if (resourceType === 'AWS::Lambda::Function') {
|
|
160
|
+
const { FunctionName } = properties;
|
|
161
|
+
if (FunctionName) {
|
|
162
|
+
if (returnType === 'name') {
|
|
163
|
+
return FunctionName;
|
|
164
|
+
}
|
|
165
|
+
return `arn:${partition}:lambda:${region}:${accountId}:function:${FunctionName}`;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Handle AWS::SQS::Queue
|
|
170
|
+
if (resourceType === 'AWS::SQS::Queue') {
|
|
171
|
+
const { QueueName } = properties;
|
|
172
|
+
if (QueueName) {
|
|
173
|
+
if (returnType === 'name') {
|
|
174
|
+
return QueueName;
|
|
175
|
+
}
|
|
176
|
+
return `arn:${partition}:sqs:${region}:${accountId}:${QueueName}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Handle AWS::SNS::Topic
|
|
181
|
+
if (resourceType === 'AWS::SNS::Topic') {
|
|
182
|
+
const { TopicName } = properties;
|
|
183
|
+
if (TopicName) {
|
|
184
|
+
if (returnType === 'name') {
|
|
185
|
+
return TopicName;
|
|
186
|
+
}
|
|
187
|
+
return `arn:${partition}:sns:${region}:${accountId}:${TopicName}`;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Handle AWS::DynamoDB::Table
|
|
192
|
+
if (resourceType === 'AWS::DynamoDB::Table') {
|
|
193
|
+
const { TableName } = properties;
|
|
194
|
+
if (TableName) {
|
|
195
|
+
if (returnType === 'name') {
|
|
196
|
+
return TableName;
|
|
197
|
+
}
|
|
198
|
+
return `arn:${partition}:dynamodb:${region}:${accountId}:table/${TableName}`;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Handle AWS::RDS::DBInstance
|
|
203
|
+
if (resourceType === 'AWS::RDS::DBInstance') {
|
|
204
|
+
const { DBInstanceIdentifier } = properties;
|
|
205
|
+
if (DBInstanceIdentifier) {
|
|
206
|
+
if (returnType === 'name') {
|
|
207
|
+
return DBInstanceIdentifier;
|
|
208
|
+
}
|
|
209
|
+
return `arn:${partition}:rds:${region}:${accountId}:db:${DBInstanceIdentifier}`;
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// Handle AWS::EC2::SecurityGroup
|
|
214
|
+
if (resourceType === 'AWS::EC2::SecurityGroup') {
|
|
215
|
+
const { GroupName } = properties;
|
|
216
|
+
if (GroupName) {
|
|
217
|
+
if (returnType === 'name') {
|
|
218
|
+
return GroupName;
|
|
219
|
+
}
|
|
220
|
+
// Security groups need to be referenced by ID in most cases, not ARN
|
|
221
|
+
// This returns the name for reference purposes
|
|
222
|
+
return GroupName;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// Handle AWS::IAM::InstanceProfile
|
|
227
|
+
if (resourceType === 'AWS::IAM::InstanceProfile') {
|
|
228
|
+
const { InstanceProfileName, Path } = properties;
|
|
229
|
+
if (InstanceProfileName) {
|
|
230
|
+
if (returnType === 'name') {
|
|
231
|
+
return InstanceProfileName;
|
|
232
|
+
}
|
|
233
|
+
const path = Path || '/';
|
|
234
|
+
return `arn:${partition}:iam::${accountId}:instance-profile${path}${InstanceProfileName}`;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// Handle AWS::KMS::Key
|
|
239
|
+
if (resourceType === 'AWS::KMS::Key') {
|
|
240
|
+
// KMS keys are referenced by KeyId or KeyArn in properties
|
|
241
|
+
const { KeyId } = properties;
|
|
242
|
+
if (KeyId) {
|
|
243
|
+
if (returnType === 'name') {
|
|
244
|
+
return KeyId;
|
|
245
|
+
}
|
|
246
|
+
return `arn:${partition}:kms:${region}:${accountId}:key/${KeyId}`;
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// Handle AWS::SecretsManager::Secret
|
|
251
|
+
if (resourceType === 'AWS::SecretsManager::Secret') {
|
|
252
|
+
const { Name } = properties;
|
|
253
|
+
if (Name) {
|
|
254
|
+
if (returnType === 'name') {
|
|
255
|
+
return Name;
|
|
256
|
+
}
|
|
257
|
+
return `arn:${partition}:secretsmanager:${region}:${accountId}:secret:${Name}`;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// Add more resource types as needed
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
|
|
105
265
|
async function recurse({ base, scope, cft, ...opts }) {
|
|
106
266
|
if (opts.doLog) {
|
|
107
267
|
|
|
@@ -481,8 +641,22 @@ async function recurse({ base, scope, cft, ...opts }) {
|
|
|
481
641
|
});
|
|
482
642
|
}
|
|
483
643
|
if (cft['Fn::RefNow']) {
|
|
484
|
-
|
|
485
|
-
|
|
644
|
+
// Handle both simple string and object format for Fn::RefNow
|
|
645
|
+
// e.g., Fn::RefNow: "MyRole" or Fn::RefNow: { Ref: "MyRole" }
|
|
646
|
+
return recurse({ base, scope, cft: cft['Fn::RefNow'], ...opts }).then((refInput) => {
|
|
647
|
+
let refName = refInput;
|
|
648
|
+
let refOptions = {};
|
|
649
|
+
|
|
650
|
+
// Parse the input - could be string or object
|
|
651
|
+
if (_.isPlainObject(refInput)) {
|
|
652
|
+
refName = refInput.Ref || refInput.ref;
|
|
653
|
+
refOptions = _.omit(refInput, ['Ref', 'ref']);
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Check if this ref name is in the ignores list
|
|
657
|
+
if (opts.refNowIgnores && opts.refNowIgnores.includes(refName)) {
|
|
658
|
+
return { Ref: refName };
|
|
659
|
+
}
|
|
486
660
|
|
|
487
661
|
// Merge AWS pseudo-parameters with inject and scope variables
|
|
488
662
|
const allRefs = {
|
|
@@ -497,7 +671,42 @@ async function recurse({ base, scope, cft, ...opts }) {
|
|
|
497
671
|
return allRefs[refName];
|
|
498
672
|
}
|
|
499
673
|
|
|
500
|
-
//
|
|
674
|
+
// Check if this is a LogicalResourceId in the Resources section
|
|
675
|
+
if (opts.rootTemplate && _.isPlainObject(opts.rootTemplate)) {
|
|
676
|
+
const resources = opts.rootTemplate.Resources || {};
|
|
677
|
+
if (refName in resources) {
|
|
678
|
+
const resource = resources[refName];
|
|
679
|
+
const resourceType = resource.Type;
|
|
680
|
+
const properties = resource.Properties || {};
|
|
681
|
+
|
|
682
|
+
// Determine return type based on key name
|
|
683
|
+
// If the key ends with "Name" (e.g., RoleName, BucketName), return name/identifier
|
|
684
|
+
// Otherwise return ARN (default)
|
|
685
|
+
let returnType = 'arn';
|
|
686
|
+
if (opts.key && opts.key.endsWith('Name')) {
|
|
687
|
+
returnType = 'name';
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Try to build an ARN or name for this resource
|
|
691
|
+
// Merge ref options with global options (ref options take precedence)
|
|
692
|
+
const resourceOptions = {
|
|
693
|
+
returnType,
|
|
694
|
+
...opts.refNowReturnType ? { returnType: opts.refNowReturnType } : {},
|
|
695
|
+
...refOptions,
|
|
696
|
+
};
|
|
697
|
+
const result = buildResourceArn(resourceType, properties, allRefs, resourceOptions);
|
|
698
|
+
if (result) {
|
|
699
|
+
return result;
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
// If not found and refNowIgnoreMissing is true, return Ref syntax; otherwise throw error
|
|
705
|
+
if (opts.refNowIgnoreMissing) {
|
|
706
|
+
return { Ref: refName };
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
// Default behavior: throw an error (backward compatible)
|
|
501
710
|
throw new Error(`Unable to resolve Ref for logical name: ${refName}`);
|
|
502
711
|
});
|
|
503
712
|
}
|
|
@@ -528,7 +737,7 @@ async function recurse({ base, scope, cft, ...opts }) {
|
|
|
528
737
|
}
|
|
529
738
|
|
|
530
739
|
return Promise.props(
|
|
531
|
-
_.mapValues(cft, (template) => recurse({ base, scope, cft: template, ...opts })),
|
|
740
|
+
_.mapValues(cft, (template, key) => recurse({ base, scope, cft: template, key, ...opts })),
|
|
532
741
|
);
|
|
533
742
|
}
|
|
534
743
|
|