1
+ import { Resource , Template } from './template' ;
2
+
3
+ /**
4
+ * Check a template for cyclic dependencies
5
+ *
6
+ * This will make sure that we don't happily validate templates
7
+ * in unit tests that wouldn't deploy to CloudFormation anyway.
8
+ */
9
+ export function checkTemplateForCyclicDependencies ( template : Template ) : void {
10
+ const logicalIds = new Set ( Object . keys ( template . Resources ?? { } ) ) ;
11
+
12
+ const dependencies = new Map < string , Set < string > > ( ) ;
13
+ for ( const [ logicalId , resource ] of Object . entries ( template . Resources ?? { } ) ) {
14
+ dependencies . set ( logicalId , intersect ( findResourceDependencies ( resource ) , logicalIds ) ) ;
15
+ }
16
+
17
+ // We will now progressively remove entries from the map of 'dependencies' that have
18
+ // 0 elements in them. If we can't do that anymore and the map isn't empty, we
19
+ // have a cyclic dependency.
20
+ while ( dependencies . size > 0 ) {
21
+ const free = Array . from ( dependencies . entries ( ) ) . filter ( ( [ _ , deps ] ) => deps . size === 0 ) ;
22
+ if ( free . length === 0 ) {
23
+ // Oops!
24
+ const cycle = findCycle ( dependencies ) ;
25
+
26
+ const cycleResources : any = { } ;
27
+ for ( const logicalId of cycle ) {
28
+ cycleResources [ logicalId ] = template . Resources ?. [ logicalId ] ;
29
+ }
30
+
31
+ throw new Error ( `Template is undeployable, these resources have a dependency cycle: ${ cycle . join ( ' -> ' ) } :\n\n${ JSON . stringify ( cycleResources , undefined , 2 ) } ` ) ;
32
+ }
33
+
34
+ for ( const [ logicalId , _ ] of free ) {
35
+ for ( const deps of dependencies . values ( ) ) {
36
+ deps . delete ( logicalId ) ;
37
+ }
38
+ dependencies . delete ( logicalId ) ;
39
+ }
40
+ }
41
+ }
42
+
43
+ function findResourceDependencies ( res : Resource ) : Set < string > {
44
+ return new Set ( [
45
+ ...toArray ( res . DependsOn ?? [ ] ) ,
46
+ ...findExpressionDependencies ( res . Properties ) ,
47
+ ] ) ;
48
+ }
49
+
50
+ function toArray < A > ( x : A | A [ ] ) : A [ ] {
51
+ return Array . isArray ( x ) ? x : [ x ] ;
52
+ }
53
+
54
+ function findExpressionDependencies ( obj : any ) : Set < string > {
55
+ const ret = new Set < string > ( ) ;
56
+ recurse ( obj ) ;
57
+ return ret ;
58
+
59
+ function recurse ( x : any ) : void {
60
+ if ( ! x ) { return ; }
61
+ if ( Array . isArray ( x ) ) {
62
+ x . forEach ( recurse ) ;
63
+ }
64
+ if ( typeof x === 'object' ) {
65
+ const keys = Object . keys ( x ) ;
66
+ if ( keys . length === 1 && keys [ 0 ] === 'Ref' ) {
67
+ ret . add ( x [ keys [ 0 ] ] ) ;
68
+ } else if ( keys . length === 1 && keys [ 0 ] === 'Fn::GetAtt' ) {
69
+ ret . add ( x [ keys [ 0 ] ] [ 0 ] ) ;
70
+ } else if ( keys . length === 1 && keys [ 0 ] === 'Fn::Sub' ) {
71
+ const argument = x [ keys [ 0 ] ] ;
72
+ const pattern = Array . isArray ( argument ) ? argument [ 0 ] : argument ;
73
+ for ( const logId of logicalIdsInSubString ( pattern ) ) {
74
+ ret . add ( logId ) ;
75
+ }
76
+ const contextDict = Array . isArray ( argument ) ? argument [ 1 ] : undefined ;
77
+ if ( contextDict ) {
78
+ Object . values ( contextDict ) . forEach ( recurse ) ;
79
+ }
80
+ } else {
81
+ Object . values ( x ) . forEach ( recurse ) ;
82
+ }
83
+ }
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Return the logical IDs found in a {Fn::Sub} format string
89
+ */
90
+ function logicalIdsInSubString ( x : string ) : string [ ] {
91
+ return analyzeSubPattern ( x ) . flatMap ( ( fragment ) => {
92
+ switch ( fragment . type ) {
93
+ case 'getatt' :
94
+ case 'ref' :
95
+ return [ fragment . logicalId ] ;
96
+ case 'literal' :
97
+ return [ ] ;
98
+ }
99
+ } ) ;
100
+ }
101
+
102
+
103
+ function analyzeSubPattern ( pattern : string ) : SubFragment [ ] {
104
+ const ret : SubFragment [ ] = [ ] ;
105
+ let start = 0 ;
106
+
107
+ let ph0 = pattern . indexOf ( '${' , start ) ;
108
+ while ( ph0 > - 1 ) {
109
+ if ( pattern [ ph0 + 2 ] === '!' ) {
110
+ // "${!" means "don't actually substitute"
111
+ start = ph0 + 3 ;
112
+ ph0 = pattern . indexOf ( '${' , start ) ;
113
+ continue ;
114
+ }
115
+
116
+ const ph1 = pattern . indexOf ( '}' , ph0 + 2 ) ;
117
+ if ( ph1 === - 1 ) {
118
+ break ;
119
+ }
120
+ const placeholder = pattern . substring ( ph0 + 2 , ph1 ) ;
121
+
122
+ if ( ph0 > start ) {
123
+ ret . push ( { type : 'literal' , content : pattern . substring ( start , ph0 ) } ) ;
124
+ }
125
+ if ( placeholder . includes ( '.' ) ) {
126
+ const [ logicalId , attr ] = placeholder . split ( '.' ) ;
127
+ ret . push ( { type : 'getatt' , logicalId : logicalId ! , attr : attr ! } ) ;
128
+ } else {
129
+ ret . push ( { type : 'ref' , logicalId : placeholder } ) ;
130
+ }
131
+
132
+ start = ph1 + 1 ;
133
+ ph0 = pattern . indexOf ( '${' , start ) ;
134
+ }
135
+
136
+ if ( start < pattern . length - 1 ) {
137
+ ret . push ( { type : 'literal' , content : pattern . substr ( start ) } ) ;
138
+ }
139
+
140
+ return ret ;
141
+ }
142
+
143
+ type SubFragment =
144
+ | { readonly type : 'literal' ; readonly content : string }
145
+ | { readonly type : 'ref' ; readonly logicalId : string }
146
+ | { readonly type : 'getatt' ; readonly logicalId : string ; readonly attr : string } ;
147
+
148
+
149
+ function intersect < A > ( xs : Set < A > , ys : Set < A > ) : Set < A > {
150
+ return new Set < A > ( Array . from ( xs ) . filter ( x => ys . has ( x ) ) ) ;
151
+ }
152
+
153
+ /**
154
+ * Find cycles in a graph
155
+ *
156
+ * Not the fastest, but effective and should be rare
157
+ */
158
+ function findCycle ( deps : ReadonlyMap < string , ReadonlySet < string > > ) : string [ ] {
159
+ for ( const node of deps . keys ( ) ) {
160
+ const cycle = recurse ( node , [ node ] ) ;
161
+ if ( cycle ) { return cycle ; }
162
+ }
163
+ throw new Error ( 'No cycle found. Assertion failure!' ) ;
164
+
165
+ function recurse ( node : string , path : string [ ] ) : string [ ] | undefined {
166
+ for ( const dep of deps . get ( node ) ?? [ ] ) {
167
+ if ( dep === path [ 0 ] ) { return [ ...path , dep ] ; }
168
+
169
+ const cycle = recurse ( dep , [ ...path , dep ] ) ;
170
+ if ( cycle ) { return cycle ; }
171
+ }
172
+
173
+ return undefined ;
174
+ }
175
+ }
0 commit comments