1
1
import * as crypto from 'node:crypto' ;
2
2
import { loadResourceModel } from '@aws-cdk/cloudformation-diff/lib/diff/util' ;
3
- import type { CloudFormationTemplate } from './cloudformation' ;
3
+ import type { CloudFormationResource , CloudFormationStack } from './cloudformation' ;
4
+ import { ResourceGraph } from './graph' ;
4
5
5
6
/**
6
7
* Computes the digest for each resource in the template.
@@ -25,75 +26,28 @@ import type { CloudFormationTemplate } from './cloudformation';
25
26
* CloudFormation template form a directed acyclic graph, this function is
26
27
* well-defined.
27
28
*/
28
- export function computeResourceDigests ( template : CloudFormationTemplate ) : Record < string , string > {
29
- const resources = template . Resources || { } ;
30
- const graph : Record < string , Set < string > > = { } ;
31
- const reverseGraph : Record < string , Set < string > > = { } ;
32
-
33
- // 1. Build adjacency lists
34
- for ( const id of Object . keys ( resources ) ) {
35
- graph [ id ] = new Set ( ) ;
36
- reverseGraph [ id ] = new Set ( ) ;
37
- }
38
-
39
- // 2. Detect dependencies by searching for Ref/Fn::GetAtt
40
- const findDependencies = ( value : any ) : string [ ] => {
41
- if ( ! value || typeof value !== 'object' ) return [ ] ;
42
- if ( Array . isArray ( value ) ) {
43
- return value . flatMap ( findDependencies ) ;
44
- }
45
- if ( 'Ref' in value ) {
46
- return [ value . Ref ] ;
47
- }
48
- if ( 'Fn::GetAtt' in value ) {
49
- const refTarget = Array . isArray ( value [ 'Fn::GetAtt' ] ) ? value [ 'Fn::GetAtt' ] [ 0 ] : value [ 'Fn::GetAtt' ] . split ( '.' ) [ 0 ] ;
50
- return [ refTarget ] ;
51
- }
52
- const result = [ ] ;
53
- if ( 'DependsOn' in value ) {
54
- if ( Array . isArray ( value . DependsOn ) ) {
55
- result . push ( ...value . DependsOn ) ;
56
- } else {
57
- result . push ( value . DependsOn ) ;
58
- }
59
- }
60
- result . push ( ...Object . values ( value ) . flatMap ( findDependencies ) ) ;
61
- return result ;
62
- } ;
63
-
64
- for ( const [ id , res ] of Object . entries ( resources ) ) {
65
- const deps = findDependencies ( res || { } ) ;
66
- for ( const dep of deps ) {
67
- if ( dep in resources && dep !== id ) {
68
- graph [ id ] . add ( dep ) ;
69
- reverseGraph [ dep ] . add ( id ) ;
70
- }
71
- }
72
- }
73
-
74
- // 3. Topological sort
75
- const outDegree = Object . keys ( graph ) . reduce ( ( acc , k ) => {
76
- acc [ k ] = graph [ k ] . size ;
77
- return acc ;
78
- } , { } as Record < string , number > ) ;
79
-
80
- const queue = Object . keys ( outDegree ) . filter ( ( k ) => outDegree [ k ] === 0 ) ;
81
- const order : string [ ] = [ ] ;
82
-
83
- while ( queue . length > 0 ) {
84
- const node = queue . shift ( ) ! ;
85
- order . push ( node ) ;
86
- for ( const nxt of reverseGraph [ node ] ) {
87
- outDegree [ nxt ] -- ;
88
- if ( outDegree [ nxt ] === 0 ) {
89
- queue . push ( nxt ) ;
90
- }
91
- }
92
- }
93
-
29
+ export function computeResourceDigests ( stacks : CloudFormationStack [ ] ) : Record < string , string > {
30
+ const exports : { [ p : string ] : { stackName : string ; value : any } } = Object . fromEntries (
31
+ stacks . flatMap ( ( s ) =>
32
+ Object . values ( s . template . Outputs ?? { } )
33
+ . filter ( ( o ) => o . Export != null && typeof o . Export . Name === 'string' )
34
+ . map ( ( o ) => [ o . Export . Name , { stackName : s . stackName , value : o . Value } ] as [ string , { stackName : string ; value : any } ] ) ,
35
+ ) ,
36
+ ) ;
37
+
38
+ const resources = Object . fromEntries (
39
+ stacks . flatMap ( ( s ) =>
40
+ Object . entries ( s . template . Resources ?? { } ) . map (
41
+ ( [ id , res ] ) => [ `${ s . stackName } .${ id } ` , res ] as [ string , CloudFormationResource ] ,
42
+ ) ,
43
+ ) ,
44
+ ) ;
45
+
46
+ const graph = new ResourceGraph ( stacks ) ;
47
+ const nodes = graph . sortedNodes ;
94
48
// 4. Compute digests in sorted order
95
49
const result : Record < string , string > = { } ;
96
- for ( const id of order ) {
50
+ for ( const id of nodes ) {
97
51
const resource = resources [ id ] ;
98
52
const resourceProperties = resource . Properties ?? { } ;
99
53
const model = loadResourceModel ( resource . Type ) ;
@@ -112,8 +66,8 @@ export function computeResourceDigests(template: CloudFormationTemplate): Record
112
66
} else {
113
67
// The resource does not have a physical ID defined, so we need to
114
68
// compute the digest based on its properties and dependencies.
115
- const depDigests = Array . from ( graph [ id ] ) . map ( ( d ) => result [ d ] ) ;
116
- const propertiesHash = hashObject ( stripReferences ( stripConstructPath ( resource ) ) ) ;
69
+ const depDigests = Array . from ( graph . outNeighbors ( id ) ) . map ( ( d ) => result [ d ] ) ;
70
+ const propertiesHash = hashObject ( stripReferences ( stripConstructPath ( resource ) , exports ) ) ;
117
71
toHash = resource . Type + propertiesHash + depDigests . join ( '' ) ;
118
72
}
119
73
@@ -153,10 +107,10 @@ export function hashObject(obj: any): string {
153
107
* Removes sub-properties containing Ref or Fn::GetAtt to avoid hashing
154
108
* references themselves but keeps the property structure.
155
109
*/
156
- function stripReferences ( value : any ) : any {
110
+ function stripReferences ( value : any , exports : { [ p : string ] : { stackName : string ; value : any } } ) : any {
157
111
if ( ! value || typeof value !== 'object' ) return value ;
158
112
if ( Array . isArray ( value ) ) {
159
- return value . map ( stripReferences ) ;
113
+ return value . map ( x => stripReferences ( x , exports ) ) ;
160
114
}
161
115
if ( 'Ref' in value ) {
162
116
return { __cloud_ref__ : 'Ref' } ;
@@ -167,9 +121,18 @@ function stripReferences(value: any): any {
167
121
if ( 'DependsOn' in value ) {
168
122
return { __cloud_ref__ : 'DependsOn' } ;
169
123
}
124
+ if ( 'Fn::ImportValue' in value ) {
125
+ const v = exports [ value [ 'Fn::ImportValue' ] ] . value ;
126
+ // Treat Fn::ImportValue as if it were a reference with the same stack
127
+ if ( 'Ref' in v ) {
128
+ return { __cloud_ref__ : 'Ref' } ;
129
+ } else if ( 'Fn::GetAtt' in v ) {
130
+ return { __cloud_ref__ : 'Fn::GetAtt' } ;
131
+ }
132
+ }
170
133
const result : any = { } ;
171
134
for ( const [ k , v ] of Object . entries ( value ) ) {
172
- result [ k ] = stripReferences ( v ) ;
135
+ result [ k ] = stripReferences ( v , exports ) ;
173
136
}
174
137
return result ;
175
138
}
0 commit comments