Skip to content

Commit

Permalink
Change replacement diffing to not trigger IAM changes dialog
Browse files Browse the repository at this point in the history
We used to modify the new template in place, causing changes to
"logicalIDs" of replaced resources, which would trigger downstream
changes and potential downstream replacements, etc.

This would lead to the changed 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.

Fixes #1495.
  • Loading branch information
rix0rrr committed Jan 14, 2019
1 parent e4fec19 commit 51f8052
Show file tree
Hide file tree
Showing 8 changed files with 246 additions and 122 deletions.
2 changes: 1 addition & 1 deletion packages/@aws-cdk/assert/lib/assertions/match-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ class StackMatchesTemplateAssertion extends Assertion<StackInspector> {
private isDiffAcceptable(diff: cfnDiff.TemplateDiff): boolean {
switch (this.matchStyle) {
case MatchStyle.EXACT:
return diff.count === 0;
return diff.differenceCount === 0;
case MatchStyle.NO_REPLACES:
for (const key of Object.keys(diff.resources.changes)) {
const change = diff.resources.changes[key]!;
Expand Down
79 changes: 57 additions & 22 deletions packages/@aws-cdk/cloudformation-diff/lib/diff-template.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,42 +39,77 @@ const DIFF_HANDLERS: HandlerRegistry = {
* the template +newTemplate+.
*/
export function diffTemplate(currentTemplate: { [key: string]: any }, newTemplate: { [key: string]: any }): types.TemplateDiff {
// Base diff
const theDiff = calculateTemplateDiff(currentTemplate, newTemplate);

// We're going to modify this in-place
newTemplate = deepCopy(newTemplate);

while (true) {
const differences: types.ITemplateDiff = {};
const unknown: { [key: string]: types.Difference<any> } = {};
for (const key of unionOf(Object.keys(currentTemplate), Object.keys(newTemplate)).sort()) {
const oldValue = currentTemplate[key];
const newValue = newTemplate[key];
if (deepEqual(oldValue, newValue)) { continue; }
const handler: DiffHandler = DIFF_HANDLERS[key]
|| ((_diff, oldV, newV) => unknown[key] = impl.diffUnknown(oldV, newV));
handler(differences, oldValue, newValue);
const newTemplateCopy = deepCopy(newTemplate);

}
if (Object.keys(unknown).length > 0) { differences.unknown = new types.DifferenceCollection(unknown); }
let didPropagateReferenceChanges;
let diffWithReplacements;
do {
diffWithReplacements = calculateTemplateDiff(currentTemplate, newTemplateCopy);

// Propagate replacements for replaced resources
let didPropagateReferenceChanges = false;
if (differences.resources) {
differences.resources.forEach((logicalId, change) => {
didPropagateReferenceChanges = false;
if (diffWithReplacements.resources) {
diffWithReplacements.resources.forEachDifference((logicalId, change) => {
if (change.changeImpact === types.ResourceImpact.WILL_REPLACE) {
if (propagateReplacedReferences(newTemplate, logicalId)) {
if (propagateReplacedReferences(newTemplateCopy, logicalId)) {
didPropagateReferenceChanges = true;
}
}
});
}
} while (didPropagateReferenceChanges);

// Copy "replaced" states from `diffWithReplacements` to `theDiff`.
diffWithReplacements.resources
.filter(r => isReplacement(r!.changeImpact))
.forEachDifference((logicalId, downstreamReplacement) => {
const resource = theDiff.resources.get(logicalId);

if (resource.changeImpact !== downstreamReplacement.changeImpact) {
propagatePropertyReplacement(downstreamReplacement, resource);
}
});

return theDiff;
}

function isReplacement(impact: types.ResourceImpact) {
return impact === types.ResourceImpact.MAY_REPLACE || impact === types.ResourceImpact.WILL_REPLACE;
}

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

function calculateTemplateDiff(currentTemplate: { [key: string]: any }, newTemplate: { [key: string]: any }): types.TemplateDiff {
const differences: types.ITemplateDiff = {};
const unknown: { [key: string]: types.Difference<any> } = {};
for (const key of unionOf(Object.keys(currentTemplate), Object.keys(newTemplate)).sort()) {
const oldValue = currentTemplate[key];
const newValue = newTemplate[key];
if (deepEqual(oldValue, newValue)) { continue; }
const handler: DiffHandler = DIFF_HANDLERS[key]
|| ((_diff, oldV, newV) => unknown[key] = impl.diffUnknown(oldV, newV));
handler(differences, oldValue, newValue);

}
if (Object.keys(unknown).length > 0) { differences.unknown = new types.DifferenceCollection(unknown); }

return new types.TemplateDiff(differences);
}

/**
* Compare two CloudFormation resources and return semantic differences between them
*/
Expand Down Expand Up @@ -109,7 +144,7 @@ function propagateReplacedReferences(template: object, logicalId: string): boole

if (key === 'Ref') {
if (obj.Ref === logicalId) {
obj.Ref = logicalId + '(replaced)';
obj.Ref = logicalId + ' (replaced)';
ret = true;
}
return true;
Expand Down
20 changes: 9 additions & 11 deletions packages/@aws-cdk/cloudformation-diff/lib/diff/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import cfnspec = require('@aws-cdk/cfnspec');
import types = require('./types');
import { diffKeyedEntities } from './util';
import { deepEqual, diffKeyedEntities } from './util';

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

if (resourceType.oldType !== undefined && resourceType.oldType === resourceType.newType) {
// Only makes sense to inspect deeper if the types stayed the same
const typeSpec = cfnspec.filteredSpecification(resourceType.oldType);
const impl = typeSpec.ResourceTypes[resourceType.oldType];
propertyUpdates = diffKeyedEntities(oldValue!.Properties,
propertyDiffs = diffKeyedEntities(oldValue!.Properties,
newValue!.Properties,
(oldVal, newVal, key) => _diffProperty(oldVal, newVal, key, impl));
otherChanges = diffKeyedEntities(oldValue, newValue, _diffOther);
delete otherChanges.Properties;
otherDiffs = diffKeyedEntities(oldValue, newValue, _diffOther);
delete otherDiffs.Properties;
}

return new types.ResourceDifference(oldValue, newValue, {
resourceType, propertyUpdates, otherChanges,
oldProperties: oldValue && oldValue.Properties,
newProperties: newValue && newValue.Properties,
resourceType, propertyDiffs, otherDiffs,
});

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

const spec = resourceSpec && resourceSpec.Properties && resourceSpec.Properties[key];
if (spec) {
if (spec && !deepEqual(oldV, newV)) {
switch (spec.UpdateType) {
case 'Immutable':
changeImpact = types.ResourceImpact.WILL_REPLACE;
Expand Down
Loading

0 comments on commit 51f8052

Please sign in to comment.