1
1
import * as AWS from 'aws-sdk' ;
2
2
import { ISDK } from './aws-auth' ;
3
+ import { NestedStackNames } from './nested-stack-helpers' ;
3
4
4
5
export interface ListStackResources {
5
6
listStackResources ( ) : Promise < AWS . CloudFormation . StackResourceSummary [ ] > ;
@@ -42,27 +43,33 @@ export interface ResourceDefinition {
42
43
}
43
44
44
45
export interface EvaluateCloudFormationTemplateProps {
46
+ readonly stackName : string ;
45
47
readonly template : Template ;
46
48
readonly parameters : { [ parameterName : string ] : string } ;
47
49
readonly account : string ;
48
50
readonly region : string ;
49
51
readonly partition : string ;
50
52
readonly urlSuffix : ( region : string ) => string ;
51
- readonly listStackResources : ListStackResources ;
53
+ readonly sdk : ISDK ;
54
+ readonly nestedStackNames ?: { [ nestedStackLogicalId : string ] : NestedStackNames } ;
52
55
}
53
56
54
57
export class EvaluateCloudFormationTemplate {
55
- private readonly stackResources : ListStackResources ;
58
+ private readonly stackName : string ;
56
59
private readonly template : Template ;
57
60
private readonly context : { [ k : string ] : any } ;
58
61
private readonly account : string ;
59
62
private readonly region : string ;
60
63
private readonly partition : string ;
61
64
private readonly urlSuffix : ( region : string ) => string ;
65
+ private readonly sdk : ISDK ;
66
+ private readonly nestedStackNames : { [ nestedStackLogicalId : string ] : NestedStackNames } ;
67
+ private readonly stackResources : LazyListStackResources ;
68
+
62
69
private cachedUrlSuffix : string | undefined ;
63
70
64
71
constructor ( props : EvaluateCloudFormationTemplateProps ) {
65
- this . stackResources = props . listStackResources ;
72
+ this . stackName = props . stackName ;
66
73
this . template = props . template ;
67
74
this . context = {
68
75
'AWS::AccountId' : props . account ,
@@ -74,22 +81,34 @@ export class EvaluateCloudFormationTemplate {
74
81
this . region = props . region ;
75
82
this . partition = props . partition ;
76
83
this . urlSuffix = props . urlSuffix ;
84
+ this . sdk = props . sdk ;
85
+
86
+ // We need names of nested stack so we can evaluate cross stack references
87
+ this . nestedStackNames = props . nestedStackNames ?? { } ;
88
+
89
+ // The current resources of the Stack.
90
+ // We need them to figure out the physical name of a resource in case it wasn't specified by the user.
91
+ // We fetch it lazily, to save a service call, in case all hotswapped resources have their physical names set.
92
+ this . stackResources = new LazyListStackResources ( this . sdk , this . stackName ) ;
77
93
}
78
94
79
95
// clones current EvaluateCloudFormationTemplate object, but updates the stack name
80
- public createNestedEvaluateCloudFormationTemplate (
81
- listNestedStackResources : ListStackResources ,
96
+ public async createNestedEvaluateCloudFormationTemplate (
97
+ stackName : string ,
82
98
nestedTemplate : Template ,
83
99
nestedStackParameters : { [ parameterName : string ] : any } ,
84
100
) {
101
+ const evaluatedParams = await this . evaluateCfnExpression ( nestedStackParameters ) ;
85
102
return new EvaluateCloudFormationTemplate ( {
103
+ stackName,
86
104
template : nestedTemplate ,
87
- parameters : nestedStackParameters ,
105
+ parameters : evaluatedParams ,
88
106
account : this . account ,
89
107
region : this . region ,
90
108
partition : this . partition ,
91
109
urlSuffix : this . urlSuffix ,
92
- listStackResources : listNestedStackResources ,
110
+ sdk : this . sdk ,
111
+ nestedStackNames : this . nestedStackNames ,
93
112
} ) ;
94
113
}
95
114
@@ -262,20 +281,52 @@ export class EvaluateCloudFormationTemplate {
262
281
return this . cachedUrlSuffix ;
263
282
}
264
283
284
+ // Try finding the ref in the passed in parameters
265
285
const parameterTarget = this . context [ logicalId ] ;
266
286
if ( parameterTarget ) {
267
287
return parameterTarget ;
268
288
}
289
+
290
+ // If not in the passed in parameters, see if there is a default value in the template parameter that was not passed in
291
+ const defaultParameterValue = this . template . Parameters ?. [ logicalId ] ?. Default ;
292
+ if ( defaultParameterValue ) {
293
+ return defaultParameterValue ;
294
+ }
295
+
269
296
// if it's not a Parameter, we need to search in the current Stack resources
270
297
return this . findGetAttTarget ( logicalId ) ;
271
298
}
272
299
273
300
private async findGetAttTarget ( logicalId : string , attribute ?: string ) : Promise < string | undefined > {
301
+
302
+ // Handle case where the attribute is referencing a stack output (used in nested stacks to share parameters)
303
+ // See https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/quickref-cloudformation.html#w2ab1c17c23c19b5
304
+ if ( logicalId === 'Outputs' && attribute ) {
305
+ return this . evaluateCfnExpression ( this . template . Outputs [ attribute ] ?. Value ) ;
306
+ }
307
+
274
308
const stackResources = await this . stackResources . listStackResources ( ) ;
275
309
const foundResource = stackResources . find ( sr => sr . LogicalResourceId === logicalId ) ;
276
310
if ( ! foundResource ) {
277
311
return undefined ;
278
312
}
313
+
314
+ if ( foundResource . ResourceType == 'AWS::CloudFormation::Stack' && attribute ?. startsWith ( 'Outputs.' ) ) {
315
+ // need to resolve attributes from another stack's Output section
316
+ const dependantStackName = this . nestedStackNames [ logicalId ] ?. nestedStackPhysicalName ;
317
+ if ( ! dependantStackName ) {
318
+ //this is a newly created nested stack and cannot be hotswapped
319
+ return undefined ;
320
+ }
321
+ const dependantStackTemplate = this . template . Resources [ logicalId ] ;
322
+ const evaluateCfnTemplate = await this . createNestedEvaluateCloudFormationTemplate (
323
+ dependantStackName ,
324
+ dependantStackTemplate ?. Properties ?. NestedTemplate ,
325
+ dependantStackTemplate . newValue ?. Properties ?. Parameters ) ;
326
+
327
+ // Split Outputs.<refName> into 'Outputs' and '<refName>' and recursively call evaluate
328
+ return evaluateCfnTemplate . evaluateCfnExpression ( { 'Fn::GetAtt' : attribute . split ( / \. ( .* ) / s) } ) ;
329
+ }
279
330
// now, we need to format the appropriate identifier depending on the resource type,
280
331
// and the requested attribute name
281
332
return this . formatResourceAttribute ( foundResource , attribute ) ;
0 commit comments