@@ -55,7 +55,7 @@ export function fullDiff(
5555 normalize ( newTemplate ) ;
5656 const theDiff = diffTemplate ( currentTemplate , newTemplate ) ;
5757 if ( changeSet ) {
58- refineDiffWithChangeSet ( theDiff , changeSet , newTemplate . Resources ) ;
58+ filterFalsePositives ( theDiff , changeSet ) ;
5959 addImportInformation ( theDiff , changeSet ) ;
6060 } else if ( isImport ) {
6161 makeAllResourceChangesImports ( theDiff ) ;
@@ -143,6 +143,13 @@ function calculateTemplateDiff(currentTemplate: { [key: string]: any }, newTempl
143143 return new types . TemplateDiff ( differences ) ;
144144}
145145
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+
146153/**
147154 * Replace all references to the given logicalID on the given template, in-place
148155 *
@@ -222,103 +229,45 @@ function makeAllResourceChangesImports(diff: types.TemplateDiff) {
222229 } ) ;
223230}
224231
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 ;
254238 }
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 ;
271245 }
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 :
292257 ( value as types . PropertyDifference < any > ) . changeImpact = types . ResourceImpact . NO_CHANGE ;
293258 ( 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 ;
310260 // 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- }
318261 }
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+ }
320269 } ) ;
321- }
270+ } ) ;
322271}
323272
324273function findResourceImports ( changeSet : DescribeChangeSetOutput ) : string [ ] {
@@ -332,6 +281,33 @@ function findResourceImports(changeSet: DescribeChangeSetOutput): string[] {
332281 return importedResourceLogicalIds ;
333282}
334283
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+
335311function normalize ( template : any ) {
336312 if ( typeof template === 'object' ) {
337313 for ( const key of ( Object . keys ( template ?? { } ) ) ) {
0 commit comments