Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion packages/@aws-cdk/cloudformation-diff/lib/diff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,18 @@ export function diffParameter(oldValue: types.Parameter, newValue: types.Paramet
return new types.ParameterDifference(oldValue, newValue);
}

export function diffResource(oldValue?: types.Resource, newValue?: types.Resource): types.ResourceDifference {
export function diffResource(oldValue?: types.Resource, newValue?: types.Resource, logicalId?: string): types.ResourceDifference {
// Fn::ForEach entries are arrays, not standard {Type, Properties} resource objects.
// Synthesize a virtual resource type so isDifferent works correctly.
if (logicalId && logicalId.startsWith('Fn::ForEach::')) {
const forEachType = 'Fn::ForEach';
return new types.ResourceDifference(oldValue, newValue, {
resourceType: { oldType: oldValue ? forEachType : undefined, newType: newValue ? forEachType : undefined },
propertyDiffs: {},
otherDiffs: !deepEqual(oldValue, newValue) ? { Value: new types.Difference(oldValue, newValue) } : {},
});
}

const resourceType = {
oldType: oldValue && oldValue.Type,
newType: newValue && newValue.Type,
Expand Down
122 changes: 122 additions & 0 deletions packages/@aws-cdk/cloudformation-diff/lib/format-foreach.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
import * as chalk from 'chalk';
import { deepEqual } from './diff/util';

const ADDITION = chalk.green('[+]');
const UPDATE = chalk.yellow('[~]');
const REMOVAL = chalk.red('[-]');

/**
* Formatter for Fn::ForEach diff output.
*
* Fn::ForEach is a CloudFormation intrinsic that expands a template fragment
* over a collection at deploy time. Its value is an array:
* [collection, { "LogicalId${Var}": { Type, Properties } }]
*/
export class ForEachDiffFormatter {
/**
* Format a ForEach resource difference
*/
public formatForEach(
key: string,
oldValue: any | undefined,
newValue: any | undefined,
): string[] {
const lines: string[] = [];
const changeType = this.getChangeType(oldValue, newValue);
const value = newValue ?? oldValue;

if (!Array.isArray(value) || value.length < 2) {
lines.push(`${this.changeSymbol(changeType)} ${chalk.cyan(key)} (unrecognized ForEach structure)`);
return lines;
}

const loopName = key.replace('Fn::ForEach::', '');
const [collection, templateObj] = value;
const entries = Object.entries(templateObj as Record<string, any>);

if (entries.length === 0) {
lines.push(`${this.changeSymbol(changeType)} ${chalk.cyan(key)} (empty ForEach template)`);
return lines;
}

const [[templateKey, templateValue]] = entries;

const count = Array.isArray(collection)
? `${collection.length} resources`
: 'dynamic count';

lines.push(
`${this.changeSymbol(changeType)} ${chalk.cyan(key)} (expands to ${count} at deploy time)`,
);
lines.push(` Loop variable: ${chalk.blue(loopName)}`);
lines.push(` Collection: ${this.formatCollection(collection)}`);
lines.push(` └── ${templateKey} ${chalk.cyan(templateValue?.Type ?? 'Unknown')}`);

if (changeType === 'update' && oldValue && newValue) {
const oldProps = oldValue[1]?.[Object.keys(oldValue[1])[0]]?.Properties ?? {};
const newProps = newValue[1]?.[Object.keys(newValue[1])[0]]?.Properties ?? {};
const propDiff = this.diffProperties(oldProps, newProps);
lines.push(...propDiff.map(l => ` ${l}`));
} else {
for (const [propKey, propValue] of Object.entries(templateValue?.Properties ?? {})) {
lines.push(` ${propKey}: ${this.formatValue(propValue)}`);
}
}

return lines;
}

private changeSymbol(type: 'add' | 'remove' | 'update'): string {
switch (type) {
case 'add': return ADDITION;
case 'remove': return REMOVAL;
case 'update': return UPDATE;
}
}

private formatCollection(collection: any): string {
if (Array.isArray(collection)) {
if (collection.length <= 5) return JSON.stringify(collection);
return `[${collection.slice(0, 3).join(', ')}, ... +${collection.length - 3} more]`;
}
return JSON.stringify(collection);
}

private getChangeType(oldVal: any, newVal: any): 'add' | 'remove' | 'update' {
if (!oldVal) return 'add';
if (!newVal) return 'remove';
return 'update';
}

private formatValue(value: any): string {
if (typeof value === 'string') return value;
return JSON.stringify(value);
}

private diffProperties(oldProps: Record<string, any>, newProps: Record<string, any>): string[] {
const lines: string[] = [];
const allKeys = new Set([...Object.keys(oldProps), ...Object.keys(newProps)]);

for (const key of allKeys) {
const oldVal = oldProps[key];
const newVal = newProps[key];

if (oldVal === undefined) {
lines.push(`${ADDITION} ${key}: ${this.formatValue(newVal)}`);
} else if (newVal === undefined) {
lines.push(`${REMOVAL} ${key}: ${this.formatValue(oldVal)}`);
} else if (!deepEqual(oldVal, newVal)) {
lines.push(`${UPDATE} ${key}: ${this.formatValue(oldVal)} → ${this.formatValue(newVal)}`);
}
}

return lines;
}
}

/**
* Check if a logical ID represents a ForEach construct
*/
export function isForEachKey(logicalId: string): boolean {
return logicalId.startsWith('Fn::ForEach::');
}
11 changes: 11 additions & 0 deletions packages/@aws-cdk/cloudformation-diff/lib/format.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import type { DifferenceCollection, Move, TemplateDiff } from './diff/types';
import { deepEqual } from './diff/util';
import type { Difference, ResourceDifference } from './diff-template';
import { isPropertyDifference, ResourceImpact } from './diff-template';
import { ForEachDiffFormatter, isForEachKey } from './format-foreach';
import { formatTable } from './format-table';
import type { IamChanges } from './iam/iam-changes';
import type { SecurityGroupChanges } from './network/security-group-changes';
Expand Down Expand Up @@ -164,6 +165,16 @@ export class Formatter {
return;
}

// Handle Fn::ForEach resources specially
if (isForEachKey(logicalId)) {
const forEachFormatter = new ForEachDiffFormatter();
const lines = forEachFormatter.formatForEach(logicalId, diff.oldValue, diff.newValue);
for (const line of lines) {
this.print(line);
}
return;
}

const resourceType = diff.isRemoval ? diff.oldResourceType : diff.newResourceType;

const message = [
Expand Down
1 change: 1 addition & 0 deletions packages/@aws-cdk/cloudformation-diff/lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './diff-template';
export * from './format';
export * from './format-foreach';
export * from './format-table';
export * from './mappings';
export { deepEqual, mangleLikeCloudFormation } from './diff/util';
Loading
Loading