@znemz/cfn-include 2.1.11 → 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 +55 -0
- package/index.js +203 -4
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1201,6 +1201,61 @@ 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
|
+
|
|
1204
1259
|
**CLI Options for Fn::RefNow:**
|
|
1205
1260
|
|
|
1206
1261
|
The `cfn-include` command provides CLI options to control how unresolved references are handled:
|
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,17 @@ 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
|
+
}
|
|
486
655
|
|
|
487
656
|
// Check if this ref name is in the ignores list
|
|
488
657
|
if (opts.refNowIgnores && opts.refNowIgnores.includes(refName)) {
|
|
@@ -502,6 +671,36 @@ async function recurse({ base, scope, cft, ...opts }) {
|
|
|
502
671
|
return allRefs[refName];
|
|
503
672
|
}
|
|
504
673
|
|
|
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
|
+
|
|
505
704
|
// If not found and refNowIgnoreMissing is true, return Ref syntax; otherwise throw error
|
|
506
705
|
if (opts.refNowIgnoreMissing) {
|
|
507
706
|
return { Ref: refName };
|
|
@@ -538,7 +737,7 @@ async function recurse({ base, scope, cft, ...opts }) {
|
|
|
538
737
|
}
|
|
539
738
|
|
|
540
739
|
return Promise.props(
|
|
541
|
-
_.mapValues(cft, (template) => recurse({ base, scope, cft: template, ...opts })),
|
|
740
|
+
_.mapValues(cft, (template, key) => recurse({ base, scope, cft: template, key, ...opts })),
|
|
542
741
|
);
|
|
543
742
|
}
|
|
544
743
|
|