@@ -55,7 +55,7 @@ export function fullDiff(
55
55
normalize ( newTemplate ) ;
56
56
const theDiff = diffTemplate ( currentTemplate , newTemplate ) ;
57
57
if ( changeSet ) {
58
- refineDiffWithChangeSet ( theDiff , changeSet , newTemplate . Resources ) ;
58
+ filterFalsePositives ( theDiff , changeSet ) ;
59
59
addImportInformation ( theDiff , changeSet ) ;
60
60
} else if ( isImport ) {
61
61
makeAllResourceChangesImports ( theDiff ) ;
@@ -143,6 +143,13 @@ function calculateTemplateDiff(currentTemplate: { [key: string]: any }, newTempl
143
143
return new types . TemplateDiff ( differences ) ;
144
144
}
145
145
146
+ /**
147
+ * Compare two CloudFormation resources and return semantic differences between them
148
+ */
149
+ export function diffResource ( oldValue : types . Resource , newValue : types . Resource ) : types . ResourceDifference {
150
+ return impl . diffResource ( oldValue , newValue ) ;
151
+ }
152
+
146
153
/**
147
154
* Replace all references to the given logicalID on the given template, in-place
148
155
*
@@ -222,103 +229,45 @@ function makeAllResourceChangesImports(diff: types.TemplateDiff) {
222
229
} ) ;
223
230
}
224
231
225
- function refineDiffWithChangeSet ( diff : types . TemplateDiff , changeSet : DescribeChangeSetOutput , newTemplateResources : { [ logicalId : string ] : any } ) {
226
- const replacements = _findResourceReplacements ( changeSet ) ;
227
-
228
- _addChangeSetResourcesToDiff ( replacements , newTemplateResources ) ;
229
- _enhanceChangeImpacts ( replacements ) ;
230
- return ;
231
-
232
- function _findResourceReplacements ( _changeSet : DescribeChangeSetOutput ) : types . ResourceReplacements {
233
- const _replacements : types . ResourceReplacements = { } ;
234
- for ( const resourceChange of _changeSet . Changes ?? [ ] ) {
235
- const propertiesReplaced : { [ propName : string ] : types . ChangeSetReplacement } = { } ;
236
- for ( const propertyChange of resourceChange . ResourceChange ?. Details ?? [ ] ) {
237
- if ( propertyChange . Target ?. Attribute === 'Properties' ) {
238
- const requiresReplacement = propertyChange . Target . RequiresRecreation === 'Always' ;
239
- if ( requiresReplacement && propertyChange . Evaluation === 'Static' ) {
240
- propertiesReplaced [ propertyChange . Target . Name ! ] = 'Always' ;
241
- } else if ( requiresReplacement && propertyChange . Evaluation === 'Dynamic' ) {
242
- // If Evaluation is 'Dynamic', then this may cause replacement, or it may not.
243
- // see 'Replacement': https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_ResourceChange.html
244
- propertiesReplaced [ propertyChange . Target . Name ! ] = 'Conditionally' ;
245
- } else {
246
- propertiesReplaced [ propertyChange . Target . Name ! ] = propertyChange . Target . RequiresRecreation as types . ChangeSetReplacement ;
247
- }
248
- }
249
- }
250
- _replacements [ resourceChange . ResourceChange ?. LogicalResourceId ! ] = {
251
- resourceReplaced : resourceChange . ResourceChange ?. Replacement === 'True' ,
252
- propertiesReplaced,
253
- } ;
232
+ function filterFalsePositives ( diff : types . TemplateDiff , changeSet : DescribeChangeSetOutput ) {
233
+ const replacements = findResourceReplacements ( changeSet ) ;
234
+ diff . resources . forEachDifference ( ( logicalId : string , change : types . ResourceDifference ) => {
235
+ if ( change . resourceType . includes ( 'AWS::Serverless' ) ) {
236
+ // CFN applies the SAM transform before creating the changeset, so the changeset contains no information about SAM resources
237
+ return ;
254
238
}
255
-
256
- return _replacements ;
257
- }
258
-
259
- function _addChangeSetResourcesToDiff ( _replacements : types . ResourceReplacements , _newTemplateResources : { [ logicalId : string ] : any } ) {
260
- const resourceDiffLogicalIds = diff . resources . logicalIds ;
261
- for ( const logicalId of Object . keys ( _replacements ) ) {
262
- if ( ! ( resourceDiffLogicalIds . includes ( logicalId ) ) ) {
263
- const noChangeResourceDiff = impl . diffResource ( _newTemplateResources [ logicalId ] , _newTemplateResources [ logicalId ] ) ;
264
- diff . resources . add ( logicalId , noChangeResourceDiff ) ;
265
- }
266
-
267
- for ( const propertyName of Object . keys ( _replacements [ logicalId ] . propertiesReplaced ) ) {
268
- if ( propertyName in diff . resources . get ( logicalId ) . propertyUpdates ) {
269
- // If the property is already marked to be updated, then we don't need to do anything.
270
- continue ;
239
+ change . forEachDifference ( ( type : 'Property' | 'Other' , name : string , value : types . Difference < any > | types . PropertyDifference < any > ) => {
240
+ if ( type === 'Property' ) {
241
+ if ( ! replacements [ logicalId ] ) {
242
+ ( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . NO_CHANGE ;
243
+ ( value as types . PropertyDifference < any > ) . isDifferent = false ;
244
+ return ;
271
245
}
272
-
273
- const newProp = new types . PropertyDifference (
274
- // these fields will be decided below
275
- { } , { } , { changeImpact : undefined } ,
276
- ) ;
277
- newProp . isDifferent = true ;
278
- diff . resources . get ( logicalId ) . setPropertyChange ( propertyName , newProp ) ;
279
- }
280
- } ;
281
- }
282
-
283
- function _enhanceChangeImpacts ( _replacements : types . ResourceReplacements ) {
284
- diff . resources . forEachDifference ( ( logicalId : string , change : types . ResourceDifference ) => {
285
- if ( change . resourceType . includes ( 'AWS::Serverless' ) ) {
286
- // CFN applies the SAM transform before creating the changeset, so the changeset contains no information about SAM resources
287
- return ;
288
- }
289
- change . forEachDifference ( ( type : 'Property' | 'Other' , name : string , value : types . Difference < any > | types . PropertyDifference < any > ) => {
290
- if ( type === 'Property' ) {
291
- if ( ! _replacements [ logicalId ] ) {
246
+ switch ( replacements [ logicalId ] . propertiesReplaced [ name ] ) {
247
+ case 'Always' :
248
+ ( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . WILL_REPLACE ;
249
+ break ;
250
+ case 'Never' :
251
+ ( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . WILL_UPDATE ;
252
+ break ;
253
+ case 'Conditionally' :
254
+ ( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . MAY_REPLACE ;
255
+ break ;
256
+ case undefined :
292
257
( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . NO_CHANGE ;
293
258
( value as types . PropertyDifference < any > ) . isDifferent = false ;
294
- return ;
295
- }
296
- switch ( _replacements [ logicalId ] . propertiesReplaced [ name ] ) {
297
- case 'Always' :
298
- ( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . WILL_REPLACE ;
299
- break ;
300
- case 'Never' :
301
- ( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . WILL_UPDATE ;
302
- break ;
303
- case 'Conditionally' :
304
- ( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . MAY_REPLACE ;
305
- break ;
306
- case undefined :
307
- ( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . NO_CHANGE ;
308
- ( value as types . PropertyDifference < any > ) . isDifferent = false ;
309
- break ;
259
+ break ;
310
260
// otherwise, defer to the changeImpact from `diffTemplate`
311
- }
312
- } else if ( type === 'Other' ) {
313
- switch ( name ) {
314
- case 'Metadata' :
315
- change . setOtherChange ( 'Metadata' , new types . Difference < string > ( value . newValue , value . newValue ) ) ;
316
- break ;
317
- }
318
261
}
319
- } ) ;
262
+ } else if ( type === 'Other' ) {
263
+ switch ( name ) {
264
+ case 'Metadata' :
265
+ change . setOtherChange ( 'Metadata' , new types . Difference < string > ( value . newValue , value . newValue ) ) ;
266
+ break ;
267
+ }
268
+ }
320
269
} ) ;
321
- }
270
+ } ) ;
322
271
}
323
272
324
273
function findResourceImports ( changeSet : DescribeChangeSetOutput ) : string [ ] {
@@ -332,6 +281,33 @@ function findResourceImports(changeSet: DescribeChangeSetOutput): string[] {
332
281
return importedResourceLogicalIds ;
333
282
}
334
283
284
+ function findResourceReplacements ( changeSet : DescribeChangeSetOutput ) : types . ResourceReplacements {
285
+ const replacements : types . ResourceReplacements = { } ;
286
+ for ( const resourceChange of changeSet . Changes ?? [ ] ) {
287
+ const propertiesReplaced : { [ propName : string ] : types . ChangeSetReplacement } = { } ;
288
+ for ( const propertyChange of resourceChange . ResourceChange ?. Details ?? [ ] ) {
289
+ if ( propertyChange . Target ?. Attribute === 'Properties' ) {
290
+ const requiresReplacement = propertyChange . Target . RequiresRecreation === 'Always' ;
291
+ if ( requiresReplacement && propertyChange . Evaluation === 'Static' ) {
292
+ propertiesReplaced [ propertyChange . Target . Name ! ] = 'Always' ;
293
+ } else if ( requiresReplacement && propertyChange . Evaluation === 'Dynamic' ) {
294
+ // If Evaluation is 'Dynamic', then this may cause replacement, or it may not.
295
+ // see 'Replacement': https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_ResourceChange.html
296
+ propertiesReplaced [ propertyChange . Target . Name ! ] = 'Conditionally' ;
297
+ } else {
298
+ propertiesReplaced [ propertyChange . Target . Name ! ] = propertyChange . Target . RequiresRecreation as types . ChangeSetReplacement ;
299
+ }
300
+ }
301
+ }
302
+ replacements [ resourceChange . ResourceChange ?. LogicalResourceId ! ] = {
303
+ resourceReplaced : resourceChange . ResourceChange ?. Replacement === 'True' ,
304
+ propertiesReplaced,
305
+ } ;
306
+ }
307
+
308
+ return replacements ;
309
+ }
310
+
335
311
function normalize ( template : any ) {
336
312
if ( typeof template === 'object' ) {
337
313
for ( const key of ( Object . keys ( template ?? { } ) ) ) {
0 commit comments