Skip to content

Commit 3270b47

Browse files
authored
fix(aws-cdk): Improvements to IAM diff rendering (#1542)
Contains the following changes: - Print table to STDOUT instead of STDERR. This ensures the diff output and the "confirm (y/n)" prompt that follows it don't interleave on the terminal, degrading readability. - Control table width to not exceed the terminal width, so that it doesn't wrap and lead to a mess of lines. - Switch back to the new version of the `table` library (instead of `cli-table`) which now has in-cell newline support, to get rid of the rendering bugs in `cli-table` that would occasionally eat newlines. - Render resource `replace` impact as red instead of yellow, to indicate that data loss will be happening here (fixes #1458). - Change replacement diffing in order to not trigger IAM changes dialog (fixes #1495). - Print a message instead of an empty table if 'cdk context' has no information in it (fixes #1549). Explanation: We used to modify the new target template in place, causing changes to the logical ids of replaced resources, which would trigger downstream changes, which would further trigger potential downstream replacements, etc. This would properly calculate the set of impacted resources, but would also lead to the modified logical ID popping up in the IAM diff, which was not desirable. In this change, we do the same processing but we throw away the template after all changes have been propagated, and only copy the resultant property *statuses* onto the diff object that gets returned to the user. This leads to the same displayed result without the changes to the template actually propagating. In the course of this modification, the diff classes have been changed to also have objects in places of resources and properties that didn't actually change (so that they could be modified in-place), and diff objects have a boolean telling whether they are actual changes or not.
1 parent 6642ca2 commit 3270b47

File tree

15 files changed

+395
-189
lines changed

15 files changed

+395
-189
lines changed

packages/@aws-cdk/assert/lib/assertions/match-template.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ class StackMatchesTemplateAssertion extends Assertion<StackInspector> {
5555
private isDiffAcceptable(diff: cfnDiff.TemplateDiff): boolean {
5656
switch (this.matchStyle) {
5757
case MatchStyle.EXACT:
58-
return diff.count === 0;
58+
return diff.differenceCount === 0;
5959
case MatchStyle.NO_REPLACES:
6060
for (const key of Object.keys(diff.resources.changes)) {
6161
const change = diff.resources.changes[key]!;

packages/@aws-cdk/cloudformation-diff/lib/diff-template.ts

+57-22
Original file line numberDiff line numberDiff line change
@@ -39,42 +39,77 @@ const DIFF_HANDLERS: HandlerRegistry = {
3939
* the template +newTemplate+.
4040
*/
4141
export function diffTemplate(currentTemplate: { [key: string]: any }, newTemplate: { [key: string]: any }): types.TemplateDiff {
42+
// Base diff
43+
const theDiff = calculateTemplateDiff(currentTemplate, newTemplate);
44+
4245
// We're going to modify this in-place
43-
newTemplate = deepCopy(newTemplate);
44-
45-
while (true) {
46-
const differences: types.ITemplateDiff = {};
47-
const unknown: { [key: string]: types.Difference<any> } = {};
48-
for (const key of unionOf(Object.keys(currentTemplate), Object.keys(newTemplate)).sort()) {
49-
const oldValue = currentTemplate[key];
50-
const newValue = newTemplate[key];
51-
if (deepEqual(oldValue, newValue)) { continue; }
52-
const handler: DiffHandler = DIFF_HANDLERS[key]
53-
|| ((_diff, oldV, newV) => unknown[key] = impl.diffUnknown(oldV, newV));
54-
handler(differences, oldValue, newValue);
46+
const newTemplateCopy = deepCopy(newTemplate);
5547

56-
}
57-
if (Object.keys(unknown).length > 0) { differences.unknown = new types.DifferenceCollection(unknown); }
48+
let didPropagateReferenceChanges;
49+
let diffWithReplacements;
50+
do {
51+
diffWithReplacements = calculateTemplateDiff(currentTemplate, newTemplateCopy);
5852

5953
// Propagate replacements for replaced resources
60-
let didPropagateReferenceChanges = false;
61-
if (differences.resources) {
62-
differences.resources.forEach((logicalId, change) => {
54+
didPropagateReferenceChanges = false;
55+
if (diffWithReplacements.resources) {
56+
diffWithReplacements.resources.forEachDifference((logicalId, change) => {
6357
if (change.changeImpact === types.ResourceImpact.WILL_REPLACE) {
64-
if (propagateReplacedReferences(newTemplate, logicalId)) {
58+
if (propagateReplacedReferences(newTemplateCopy, logicalId)) {
6559
didPropagateReferenceChanges = true;
6660
}
6761
}
6862
});
6963
}
64+
} while (didPropagateReferenceChanges);
65+
66+
// Copy "replaced" states from `diffWithReplacements` to `theDiff`.
67+
diffWithReplacements.resources
68+
.filter(r => isReplacement(r!.changeImpact))
69+
.forEachDifference((logicalId, downstreamReplacement) => {
70+
const resource = theDiff.resources.get(logicalId);
71+
72+
if (resource.changeImpact !== downstreamReplacement.changeImpact) {
73+
propagatePropertyReplacement(downstreamReplacement, resource);
74+
}
75+
});
76+
77+
return theDiff;
78+
}
79+
80+
function isReplacement(impact: types.ResourceImpact) {
81+
return impact === types.ResourceImpact.MAY_REPLACE || impact === types.ResourceImpact.WILL_REPLACE;
82+
}
7083

71-
// We're done only if we didn't have to propagate any more replacements.
72-
if (!didPropagateReferenceChanges) {
73-
return new types.TemplateDiff(differences);
84+
/**
85+
* For all properties in 'source' that have a "replacement" impact, propagate that impact to "dest"
86+
*/
87+
function propagatePropertyReplacement(source: types.ResourceDifference, dest: types.ResourceDifference) {
88+
for (const [propertyName, diff] of Object.entries(source.propertyUpdates)) {
89+
if (diff.changeImpact && isReplacement(diff.changeImpact)) {
90+
// Use the propertydiff of source in target. The result of this happens to be clear enough.
91+
dest.setPropertyChange(propertyName, diff);
7492
}
7593
}
7694
}
7795

96+
function calculateTemplateDiff(currentTemplate: { [key: string]: any }, newTemplate: { [key: string]: any }): types.TemplateDiff {
97+
const differences: types.ITemplateDiff = {};
98+
const unknown: { [key: string]: types.Difference<any> } = {};
99+
for (const key of unionOf(Object.keys(currentTemplate), Object.keys(newTemplate)).sort()) {
100+
const oldValue = currentTemplate[key];
101+
const newValue = newTemplate[key];
102+
if (deepEqual(oldValue, newValue)) { continue; }
103+
const handler: DiffHandler = DIFF_HANDLERS[key]
104+
|| ((_diff, oldV, newV) => unknown[key] = impl.diffUnknown(oldV, newV));
105+
handler(differences, oldValue, newValue);
106+
107+
}
108+
if (Object.keys(unknown).length > 0) { differences.unknown = new types.DifferenceCollection(unknown); }
109+
110+
return new types.TemplateDiff(differences);
111+
}
112+
78113
/**
79114
* Compare two CloudFormation resources and return semantic differences between them
80115
*/
@@ -109,7 +144,7 @@ function propagateReplacedReferences(template: object, logicalId: string): boole
109144

110145
if (key === 'Ref') {
111146
if (obj.Ref === logicalId) {
112-
obj.Ref = logicalId + '(replaced)';
147+
obj.Ref = logicalId + ' (replaced)';
113148
ret = true;
114149
}
115150
return true;

packages/@aws-cdk/cloudformation-diff/lib/diff/index.ts

+9-11
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import cfnspec = require('@aws-cdk/cfnspec');
22
import types = require('./types');
3-
import { diffKeyedEntities } from './util';
3+
import { deepEqual, diffKeyedEntities } from './util';
44

55
export function diffAttribute(oldValue: any, newValue: any): types.Difference<string> {
66
return new types.Difference<string>(_asString(oldValue), _asString(newValue));
@@ -31,31 +31,29 @@ export function diffResource(oldValue?: types.Resource, newValue?: types.Resourc
3131
oldType: oldValue && oldValue.Type,
3232
newType: newValue && newValue.Type
3333
};
34-
let propertyUpdates: { [key: string]: types.PropertyDifference<any> } = {};
35-
let otherChanges: { [key: string]: types.Difference<any> } = {};
34+
let propertyDiffs: { [key: string]: types.PropertyDifference<any> } = {};
35+
let otherDiffs: { [key: string]: types.Difference<any> } = {};
3636

3737
if (resourceType.oldType !== undefined && resourceType.oldType === resourceType.newType) {
3838
// Only makes sense to inspect deeper if the types stayed the same
3939
const typeSpec = cfnspec.filteredSpecification(resourceType.oldType);
4040
const impl = typeSpec.ResourceTypes[resourceType.oldType];
41-
propertyUpdates = diffKeyedEntities(oldValue!.Properties,
41+
propertyDiffs = diffKeyedEntities(oldValue!.Properties,
4242
newValue!.Properties,
4343
(oldVal, newVal, key) => _diffProperty(oldVal, newVal, key, impl));
44-
otherChanges = diffKeyedEntities(oldValue, newValue, _diffOther);
45-
delete otherChanges.Properties;
44+
otherDiffs = diffKeyedEntities(oldValue, newValue, _diffOther);
45+
delete otherDiffs.Properties;
4646
}
4747

4848
return new types.ResourceDifference(oldValue, newValue, {
49-
resourceType, propertyUpdates, otherChanges,
50-
oldProperties: oldValue && oldValue.Properties,
51-
newProperties: newValue && newValue.Properties,
49+
resourceType, propertyDiffs, otherDiffs,
5250
});
5351

5452
function _diffProperty(oldV: any, newV: any, key: string, resourceSpec?: cfnspec.schema.ResourceType) {
55-
let changeImpact;
53+
let changeImpact = types.ResourceImpact.NO_CHANGE;
5654

5755
const spec = resourceSpec && resourceSpec.Properties && resourceSpec.Properties[key];
58-
if (spec) {
56+
if (spec && !deepEqual(oldV, newV)) {
5957
switch (spec.UpdateType) {
6058
case 'Immutable':
6159
changeImpact = types.ResourceImpact.WILL_REPLACE;

0 commit comments

Comments
 (0)