|
| 1 | +// The SDK is only used to reference `DescribeChangeSetOutput`, so the SDK is added as a devDependency. |
| 2 | +// The SDK should not make network calls here |
| 3 | +// eslint-disable-next-line import/no-extraneous-dependencies |
| 4 | +import { CloudFormation } from 'aws-sdk'; |
1 | 5 | import * as impl from './diff';
|
2 | 6 | import * as types from './diff/types';
|
3 | 7 | import { deepEqual, diffKeyedEntities, unionOf } from './diff/util';
|
@@ -33,12 +37,33 @@ const DIFF_HANDLERS: HandlerRegistry = {
|
33 | 37 | *
|
34 | 38 | * @param currentTemplate the current state of the stack.
|
35 | 39 | * @param newTemplate the target state of the stack.
|
| 40 | + * @param changeSet the change set for this stack. |
36 | 41 | *
|
37 | 42 | * @returns a +types.TemplateDiff+ object that represents the changes that will happen if
|
38 | 43 | * a stack which current state is described by +currentTemplate+ is updated with
|
39 | 44 | * the template +newTemplate+.
|
40 | 45 | */
|
41 |
| -export function diffTemplate(currentTemplate: { [key: string]: any }, newTemplate: { [key: string]: any }): types.TemplateDiff { |
| 46 | +export function fullDiff( |
| 47 | + currentTemplate: { [key: string]: any }, |
| 48 | + newTemplate: { [key: string]: any }, |
| 49 | + changeSet?: CloudFormation.DescribeChangeSetOutput, |
| 50 | +): types.TemplateDiff { |
| 51 | + |
| 52 | + normalize(currentTemplate); |
| 53 | + normalize(newTemplate); |
| 54 | + const theDiff = diffTemplate(currentTemplate, newTemplate); |
| 55 | + if (changeSet) { |
| 56 | + filterFalsePositivies(theDiff, changeSet); |
| 57 | + } |
| 58 | + |
| 59 | + return theDiff; |
| 60 | +} |
| 61 | + |
| 62 | +function diffTemplate( |
| 63 | + currentTemplate: { [key: string]: any }, |
| 64 | + newTemplate: { [key: string]: any }, |
| 65 | +): types.TemplateDiff { |
| 66 | + |
42 | 67 | // Base diff
|
43 | 68 | const theDiff = calculateTemplateDiff(currentTemplate, newTemplate);
|
44 | 69 |
|
@@ -105,7 +130,6 @@ function calculateTemplateDiff(currentTemplate: { [key: string]: any }, newTempl
|
105 | 130 | const handler: DiffHandler = DIFF_HANDLERS[key]
|
106 | 131 | || ((_diff, oldV, newV) => unknown[key] = impl.diffUnknown(oldV, newV));
|
107 | 132 | handler(differences, oldValue, newValue);
|
108 |
| - |
109 | 133 | }
|
110 | 134 | if (Object.keys(unknown).length > 0) {
|
111 | 135 | differences.unknown = new types.DifferenceCollection(unknown);
|
@@ -184,3 +208,93 @@ function deepCopy(x: any): any {
|
184 | 208 |
|
185 | 209 | return x;
|
186 | 210 | }
|
| 211 | + |
| 212 | +function filterFalsePositivies(diff: types.TemplateDiff, changeSet: CloudFormation.DescribeChangeSetOutput) { |
| 213 | + const replacements = findResourceReplacements(changeSet); |
| 214 | + diff.resources.forEachDifference((logicalId: string, change: types.ResourceDifference) => { |
| 215 | + change.forEachDifference((type: 'Property' | 'Other', name: string, value: types.Difference<any> | types.PropertyDifference<any>) => { |
| 216 | + if (type === 'Property') { |
| 217 | + if (!replacements[logicalId]) { |
| 218 | + (value as types.PropertyDifference<any>).changeImpact = types.ResourceImpact.NO_CHANGE; |
| 219 | + (value as types.PropertyDifference<any>).isDifferent = false; |
| 220 | + return; |
| 221 | + } |
| 222 | + switch (replacements[logicalId].propertiesReplaced[name]) { |
| 223 | + case 'Always': |
| 224 | + (value as types.PropertyDifference<any>).changeImpact = types.ResourceImpact.WILL_REPLACE; |
| 225 | + break; |
| 226 | + case 'Never': |
| 227 | + (value as types.PropertyDifference<any>).changeImpact = types.ResourceImpact.WILL_UPDATE; |
| 228 | + break; |
| 229 | + case 'Conditionally': |
| 230 | + (value as types.PropertyDifference<any>).changeImpact = types.ResourceImpact.MAY_REPLACE; |
| 231 | + break; |
| 232 | + case undefined: |
| 233 | + (value as types.PropertyDifference<any>).changeImpact = types.ResourceImpact.NO_CHANGE; |
| 234 | + (value as types.PropertyDifference<any>).isDifferent = false; |
| 235 | + break; |
| 236 | + // otherwise, defer to the changeImpact from `diffTemplate` |
| 237 | + } |
| 238 | + } else if (type === 'Other') { |
| 239 | + switch (name) { |
| 240 | + case 'Metadata': |
| 241 | + change.setOtherChange('Metadata', new types.Difference<string>(value.newValue, value.newValue)); |
| 242 | + break; |
| 243 | + } |
| 244 | + } |
| 245 | + }); |
| 246 | + }); |
| 247 | +} |
| 248 | + |
| 249 | +function findResourceReplacements(changeSet: CloudFormation.DescribeChangeSetOutput): types.ResourceReplacements { |
| 250 | + const replacements: types.ResourceReplacements = {}; |
| 251 | + for (const resourceChange of changeSet.Changes ?? []) { |
| 252 | + const propertiesReplaced: { [propName: string]: types.ChangeSetReplacement } = {}; |
| 253 | + for (const propertyChange of resourceChange.ResourceChange?.Details ?? []) { |
| 254 | + if (propertyChange.Target?.Attribute === 'Properties') { |
| 255 | + const requiresReplacement = propertyChange.Target.RequiresRecreation === 'Always'; |
| 256 | + if (requiresReplacement && propertyChange.Evaluation === 'Static') { |
| 257 | + propertiesReplaced[propertyChange.Target.Name!] = 'Always'; |
| 258 | + } else if (requiresReplacement && propertyChange.Evaluation === 'Dynamic') { |
| 259 | + // If Evaluation is 'Dynamic', then this may cause replacement, or it may not. |
| 260 | + // see 'Replacement': https://docs.aws.amazon.com/AWSCloudFormation/latest/APIReference/API_ResourceChange.html |
| 261 | + propertiesReplaced[propertyChange.Target.Name!] = 'Conditionally'; |
| 262 | + } else { |
| 263 | + propertiesReplaced[propertyChange.Target.Name!] = propertyChange.Target.RequiresRecreation as types.ChangeSetReplacement; |
| 264 | + } |
| 265 | + } |
| 266 | + } |
| 267 | + replacements[resourceChange.ResourceChange?.LogicalResourceId!] = { |
| 268 | + resourceReplaced: resourceChange.ResourceChange?.Replacement === 'True', |
| 269 | + propertiesReplaced, |
| 270 | + }; |
| 271 | + } |
| 272 | + |
| 273 | + return replacements; |
| 274 | +} |
| 275 | + |
| 276 | +function normalize(template: any) { |
| 277 | + if (typeof template === 'object') { |
| 278 | + for (const key of (Object.keys(template ?? {}))) { |
| 279 | + if (key === 'Fn::GetAtt' && typeof template[key] === 'string') { |
| 280 | + template[key] = template[key].split('.'); |
| 281 | + continue; |
| 282 | + } else if (key === 'DependsOn') { |
| 283 | + if (typeof template[key] === 'string') { |
| 284 | + template[key] = [template[key]]; |
| 285 | + } else if (Array.isArray(template[key])) { |
| 286 | + template[key] = template[key].sort(); |
| 287 | + } |
| 288 | + continue; |
| 289 | + } |
| 290 | + |
| 291 | + if (Array.isArray(template[key])) { |
| 292 | + for (const element of (template[key])) { |
| 293 | + normalize(element); |
| 294 | + } |
| 295 | + } else { |
| 296 | + normalize(template[key]); |
| 297 | + } |
| 298 | + } |
| 299 | + } |
| 300 | +} |
0 commit comments