diff --git a/packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts b/packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts index 52df47ff9..baa9dc062 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/diff/types.ts @@ -547,6 +547,12 @@ export interface Resource { [key: string]: any; } +export interface Move { + readonly direction: 'from' | 'to'; + readonly stackName: string; + readonly resourceLogicalId: string; +} + /** * Change to a single resource between two CloudFormation templates * @@ -568,6 +574,8 @@ export class ResourceDifference implements IDifference { */ public isImport?: boolean; + public move?: Move; + /** Property-level changes on the resource */ private readonly propertyDiffs: { [key: string]: PropertyDifference }; diff --git a/packages/@aws-cdk/cloudformation-diff/lib/format.ts b/packages/@aws-cdk/cloudformation-diff/lib/format.ts index dd76f92ca..4686debee 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/format.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/format.ts @@ -1,6 +1,6 @@ import { format } from 'util'; import * as chalk from 'chalk'; -import type { DifferenceCollection, TemplateDiff } from './diff/types'; +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'; @@ -166,8 +166,15 @@ export class Formatter { const resourceType = diff.isRemoval ? diff.oldResourceType : diff.newResourceType; - // eslint-disable-next-line @stylistic/max-len - this.print(`${this.formatResourcePrefix(diff)} ${this.formatValue(resourceType, chalk.cyan)} ${this.formatLogicalId(logicalId)} ${this.formatImpact(diff.changeImpact)}`.trimEnd()); + const message = [ + this.formatResourcePrefix(diff), + this.formatValue(resourceType, chalk.cyan), + this.formatLogicalId(logicalId), + this.formatImpact(diff.changeImpact), + this.formatMove(diff.move), + ].filter(Boolean).join(' '); + + this.print(message); if (diff.isUpdate) { const differenceCount = diff.differenceCount; @@ -239,6 +246,10 @@ export class Formatter { } } + private formatMove(move?: Move): string { + return !move ? '' : chalk.yellow('(OR', chalk.italic(chalk.bold('move')), `${move.direction} ${move.stackName}.${move.resourceLogicalId} via refactoring)`); + } + /** * Renders a tree of differences under a particular name. * @param name - the name of the root of the tree. diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/diff/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/diff/index.ts index 439c5ab9e..9e200d645 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/diff/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/diff/index.ts @@ -129,4 +129,12 @@ export interface DiffOptions { * @default 3 */ readonly contextLines?: number; + + /** + * Whether to include resource moves in the diff. These are the same moves that are detected + * by the `refactor` command. + * + * @default false + */ + readonly includeMoves?: boolean; } diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/diff/private/helpers.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/diff/private/helpers.ts index e790a61d5..af528c15c 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/diff/private/helpers.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/diff/private/helpers.ts @@ -13,6 +13,7 @@ import type { ResourcesToImport } from '../../../api/resource-import'; import { removeNonImportResources, ResourceMigrator } from '../../../api/resource-import'; import { ToolkitError } from '../../../toolkit/toolkit-error'; import { deserializeStructure, formatErrorMessage } from '../../../util'; +import { mappingsByEnvironment } from '../../refactor/index'; export function prepareDiff( ioHelper: IoHelper, @@ -67,6 +68,10 @@ async function cfnDiff( const templateInfos = []; const methodOptions = (options.method?.options ?? {}) as ChangeSetDiffOptions; + const allMappings = options.includeMoves + ? await mappingsByEnvironment(stacks.stackArtifacts, sdkProvider, true) + : []; + // Compare N stacks against deployed templates for (const stack of stacks.stackArtifacts) { const templateWithNestedStacks = await deployments.readCurrentTemplateWithNestedStacks( @@ -93,12 +98,17 @@ async function cfnDiff( methodOptions.importExistingResources, ) : undefined; + const mappings = allMappings.find(m => + m.environment.region === stack.environment.region && m.environment.account === stack.environment.account, + )?.mappings ?? {}; + templateInfos.push({ oldTemplate: currentTemplate, newTemplate: stack, isImport: !!resourcesToImport, nestedStacks, changeSet, + mappings, }); } diff --git a/packages/@aws-cdk/toolkit-lib/lib/actions/refactor/index.ts b/packages/@aws-cdk/toolkit-lib/lib/actions/refactor/index.ts index be8091380..64da26a38 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/refactor/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/refactor/index.ts @@ -1,6 +1,8 @@ +import type * as cxapi from '@aws-cdk/cx-api'; import type { StackSelector } from '../../api'; +import type { SdkProvider } from '../../api/aws-auth/sdk-provider'; import type { ExcludeList } from '../../api/refactoring'; -import { InMemoryExcludeList, NeverExclude } from '../../api/refactoring'; +import { groupStacks, InMemoryExcludeList, NeverExclude, RefactoringContext } from '../../api/refactoring'; import { ToolkitError } from '../../toolkit/toolkit-error'; type MappingType = 'auto' | 'explicit'; @@ -142,3 +144,28 @@ export function parseMappingGroups(s: string) { } } } + +export interface EnvironmentSpecificMappings { + readonly environment: cxapi.Environment; + readonly mappings: Record; +} + +export async function mappingsByEnvironment( + stackArtifacts: cxapi.CloudFormationStackArtifact[], + sdkProvider: SdkProvider, + ignoreModifications?: boolean, +): Promise { + const groups = await groupStacks(sdkProvider, stackArtifacts, []); + return groups.map((group) => { + const context = new RefactoringContext({ + ...group, + ignoreModifications, + }); + return { + environment: context.environment, + mappings: Object.fromEntries( + context.mappings.map((m) => [m.source.toLocationString(), m.destination.toLocationString()]), + ), + }; + }); +} diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts b/packages/@aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts index efc777ffa..6adc19273 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/diff/diff-formatter.ts @@ -5,6 +5,7 @@ import { formatSecurityChanges, fullDiff, mangleLikeCloudFormation, + type ResourceDifference, type TemplateDiff, } from '@aws-cdk/cloudformation-diff'; import type * as cxapi from '@aws-cdk/cx-api'; @@ -122,6 +123,14 @@ export interface TemplateInfo { readonly nestedStacks?: { [nestedStackLogicalId: string]: NestedStackTemplates; }; + + /** + * Mappings of old locations to new locations. If these are provided, + * for all resources that were moved, their corresponding addition + * and removal lines will be augmented with the location they were + * moved fom and to, respectively. + */ + readonly mappings?: Record; } /** @@ -134,6 +143,7 @@ export class DiffFormatter { private readonly changeSet?: any; private readonly nestedStacks: { [nestedStackLogicalId: string]: NestedStackTemplates } | undefined; private readonly isImport: boolean; + private readonly mappings: Record; /** * Stores the TemplateDiffs that get calculated in this DiffFormatter, @@ -148,6 +158,7 @@ export class DiffFormatter { this.changeSet = props.templateInfo.changeSet; this.nestedStacks = props.templateInfo.nestedStacks; this.isImport = props.templateInfo.isImport ?? false; + this.mappings = props.templateInfo.mappings ?? {}; } public get diffs() { @@ -159,16 +170,38 @@ export class DiffFormatter { * If it creates the diff, it stores the result in a map for * easier retrieval later. */ - private diff(stackName?: string, oldTemplate?: any) { + private diff(stackName?: string, oldTemplate?: any, mappings: Record = {}) { const realStackName = stackName ?? this.stackName; if (!this._diffs[realStackName]) { - this._diffs[realStackName] = fullDiff( + const templateDiff = fullDiff( oldTemplate ?? this.oldTemplate, this.newTemplate.template, this.changeSet, this.isImport, ); + + const setMove = (change: ResourceDifference, direction: 'from' | 'to', location?: string)=> { + if (location != null) { + const [sourceStackName, sourceLogicalId] = location.split('.'); + change.move = { + direction, + stackName: sourceStackName, + resourceLogicalId: sourceLogicalId, + }; + } + }; + + templateDiff.resources.forEachDifference((id, change) => { + const location = `${realStackName}.${id}`; + if (change.isAddition && Object.values(mappings).includes(location)) { + setMove(change, 'from', Object.keys(mappings).find(k => mappings[k] === location)); + } else if (change.isRemoval && Object.keys(mappings).includes(location)) { + setMove(change, 'to', mappings[location]); + } + }); + + this._diffs[realStackName] = templateDiff; } return this._diffs[realStackName]; } @@ -199,6 +232,7 @@ export class DiffFormatter { this.stackName, this.nestedStacks, options, + this.mappings, ); } @@ -207,8 +241,9 @@ export class DiffFormatter { stackName: string, nestedStackTemplates: { [nestedStackLogicalId: string]: NestedStackTemplates } | undefined, options: ReusableStackDiffOptions, + mappings: Record = {}, ) { - let diff = this.diff(stackName, oldTemplate); + let diff = this.diff(stackName, oldTemplate, mappings); // The stack diff is formatted via `Formatter`, which takes in a stream // and sends its output directly to that stream. To facilitate use of the @@ -279,6 +314,7 @@ export class DiffFormatter { nestedStack.physicalName ?? nestedStackLogicalId, nestedStack.nestedStackTemplates, options, + this.mappings, ); numStacksWithChanges += nextDiff.numStacksWithChanges; formattedDiff += nextDiff.formattedDiff; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/cloudformation.ts b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/cloudformation.ts index 5f2c51461..2c06e4e8d 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/cloudformation.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/cloudformation.ts @@ -31,8 +31,7 @@ export class ResourceLocation { } public toPath(): string { - const stack = this.stack; - const resource = stack.template.Resources?.[this.logicalResourceId]; + const resource = this.stack.template.Resources?.[this.logicalResourceId]; const result = resource?.Metadata?.['aws:cdk:path']; if (result != null) { @@ -40,7 +39,11 @@ export class ResourceLocation { } // If the path is not available, we can use stack name and logical ID - return `${stack.stackName}.${this.logicalResourceId}`; + return this.toLocationString(); + } + + public toLocationString() { + return `${this.stack.stackName}.${this.logicalResourceId}`; } public getType(): string { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/context.ts b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/context.ts index 9cdfeab26..5ad66625b 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/context.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/context.ts @@ -13,11 +13,12 @@ import { equalSets } from '../../util/sets'; */ type ResourceMove = [ResourceLocation[], ResourceLocation[]]; -export interface RefactorManagerOptions { +export interface RefactoringContextOptions { environment: Environment; localStacks: CloudFormationStack[]; deployedStacks: CloudFormationStack[]; overrides?: ResourceMapping[]; + ignoreModifications?: boolean; } /** @@ -28,9 +29,9 @@ export class RefactoringContext { private readonly ambiguousMoves: ResourceMove[] = []; public readonly environment: Environment; - constructor(props: RefactorManagerOptions) { + constructor(props: RefactoringContextOptions) { this.environment = props.environment; - const moves = resourceMoves(props.deployedStacks, props.localStacks, 'direct'); + const moves = resourceMoves(props.deployedStacks, props.localStacks, 'direct', props.ignoreModifications); const additionalOverrides = structuralOverrides(props.deployedStacks, props.localStacks); const overrides = (props.overrides ?? []).concat(additionalOverrides); const [nonAmbiguousMoves, ambiguousMoves] = partitionByAmbiguity(overrides, moves); @@ -70,20 +71,28 @@ export class RefactoringContext { * */ function structuralOverrides(deployedStacks: CloudFormationStack[], localStacks: CloudFormationStack[]): ResourceMapping[] { - const moves = resourceMoves(deployedStacks, localStacks, 'opposite'); + const moves = resourceMoves(deployedStacks, localStacks, 'opposite', true); const [nonAmbiguousMoves] = partitionByAmbiguity([], moves); return resourceMappings(nonAmbiguousMoves); } -function resourceMoves(before: CloudFormationStack[], after: CloudFormationStack[], direction: GraphDirection): ResourceMove[] { +function resourceMoves( + before: CloudFormationStack[], + after: CloudFormationStack[], + direction: GraphDirection = 'direct', + ignoreModifications: boolean = false): ResourceMove[] { const digestsBefore = resourceDigests(before, direction); const digestsAfter = resourceDigests(after, direction); - const stackNames = (stacks: CloudFormationStack[]) => stacks.map((s) => s.stackName).sort().join(', '); - if (!isomorphic(digestsBefore, digestsAfter)) { + const stackNames = (stacks: CloudFormationStack[]) => + stacks + .map((s) => s.stackName) + .sort() + .join(', '); + if (!(ignoreModifications || isomorphic(digestsBefore, digestsAfter))) { const message = [ 'A refactor operation cannot add, remove or update resources. Only resource moves and renames are allowed. ', - 'Run \'cdk diff\' to compare the local templates to the deployed stacks.\n', + "Run 'cdk diff' to compare the local templates to the deployed stacks.\n", `Deployed stacks: ${stackNames(before)}`, `Local stacks: ${stackNames(after)}`, ]; diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/index.ts b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/index.ts index a9b2f3dd2..5ed26b49a 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/index.ts @@ -17,6 +17,7 @@ import type { MappingGroup } from '../../actions'; import { ToolkitError } from '../../toolkit/toolkit-error'; export * from './exclude'; +export * from './context'; interface StackGroup { environment: cxapi.Environment; diff --git a/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts b/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts index 4634fa2fc..1662b5812 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts @@ -1,4 +1,5 @@ import * as path from 'path'; +import { GetTemplateCommand, ListStacksCommand } from '@aws-sdk/client-cloudformation'; import * as chalk from 'chalk'; import { DiffMethod } from '../../lib/actions/diff'; import * as awsauth from '../../lib/api/aws-auth/private'; @@ -7,7 +8,7 @@ import * as deployments from '../../lib/api/deployments'; import * as cfnApi from '../../lib/api/deployments/cfn-api'; import { Toolkit } from '../../lib/toolkit'; import { builderFixture, disposableCloudAssemblySource, TestIoHost } from '../_helpers'; -import { MockSdk, restoreSdkMocksToDefault, setDefaultSTSMocks } from '../_helpers/mock-sdk'; +import { mockCloudFormationClient, MockSdk, restoreSdkMocksToDefault, setDefaultSTSMocks } from '../_helpers/mock-sdk'; let ioHost: TestIoHost; let toolkit: Toolkit; @@ -87,6 +88,78 @@ describe('diff', () => { })); }); + const resources = { + OldLogicalID: { + Type: 'AWS::S3::Bucket', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Metadata: { 'aws:cdk:path': 'Stack1/OldLogicalID/Resource' }, + }, + }; + + test('returns diff augmented with moves', async () => { + // GIVEN + mockCloudFormationClient.on(ListStacksCommand).resolves({ + StackSummaries: [ + { + StackName: 'Stack1', + StackId: 'arn:aws:cloudformation:us-east-1:999999999999:stack/Stack1', + StackStatus: 'CREATE_COMPLETE', + CreationTime: new Date(), + }, + ], + }); + + jest.spyOn(deployments.Deployments.prototype, 'readCurrentTemplateWithNestedStacks').mockResolvedValue({ + deployedRootTemplate: { + Parameters: {}, + resources, + }, + nestedStacks: [] as any, + }); + + mockCloudFormationClient + .on(GetTemplateCommand, { + StackName: 'Stack1', + }) + .resolves({ + TemplateBody: JSON.stringify({ + Resources: resources, + }), + }); + + // WHEN + const cx = await builderFixture(toolkit, 'stack-with-bucket'); + const result = await toolkit.diff(cx, { + stacks: { strategy: StackSelectionStrategy.ALL_STACKS }, + includeMoves: true, + }); + + // THEN + expect(result.Stack1).toMatchObject(expect.objectContaining({ + resources: { + diffs: expect.objectContaining({ + MyBucketF68F3FF0: expect.objectContaining({ + isAddition: true, + isRemoval: false, + oldValue: undefined, + move: { + direction: 'from', + resourceLogicalId: 'OldLogicalID', + stackName: 'Stack1', + }, + newValue: { + Type: 'AWS::S3::Bucket', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Metadata: { 'aws:cdk:path': 'Stack1/MyBucket/Resource' }, + }, + }), + }), + }, + })); + }); + test('returns multiple template diffs', async () => { // WHEN const cx = await builderFixture(toolkit, 'two-different-stacks'); diff --git a/packages/@aws-cdk/toolkit-lib/test/api/diff/diff.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/diff/diff.test.ts index 9580ca7c2..8214e7493 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/diff/diff.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/diff/diff.test.ts @@ -92,6 +92,29 @@ describe('formatStackDiff', () => { ); }); + test('formats differences showing resource moves', () => { + // WHEN + const formatter = new DiffFormatter({ + templateInfo: { + oldTemplate: {}, + newTemplate: mockNewTemplate, + mappings: { + 'test-stack.OldName': 'test-stack.Func', + }, + }, + }); + const result = formatter.formatStackDiff(); + + // THEN + expect(result.formattedDiff).toBeDefined(); + const sanitizedDiff = result.formattedDiff!.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '').trim(); + expect(sanitizedDiff).toBe( + 'Stack test-stack\n' + + 'Resources\n' + + '[+] AWS::Lambda::Function Func (OR move from test-stack.OldName via refactoring)', + ); + }); + test('handles nested stack templates', () => { // GIVEN const nestedStacks = { diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 60d1eefd2..e5a28801e 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -3,7 +3,7 @@ import { format } from 'util'; import { RequireApproval } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import type { DeploymentMethod, ToolkitAction, ToolkitOptions } from '@aws-cdk/toolkit-lib'; -import { parseMappingGroups, PermissionChangeType, Toolkit, ToolkitError } from '@aws-cdk/toolkit-lib'; +import { parseMappingGroups, PermissionChangeType, Toolkit, ToolkitError, mappingsByEnvironment } from '@aws-cdk/toolkit-lib'; import * as chalk from 'chalk'; import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; @@ -266,6 +266,10 @@ export class CdkToolkit { await this.ioHost.asIoHelper().defaults.info(diff.formattedDiff); } } else { + const allMappings = options.includeMoves + ? await mappingsByEnvironment(stacks.stackArtifacts, this.props.sdkProvider, true) + : []; + // Compare N stacks against deployed templates for (const stack of stacks.stackArtifacts) { const templateWithNestedStacks = await this.props.deployments.readCurrentTemplateWithNestedStacks( @@ -322,6 +326,10 @@ export class CdkToolkit { } } + const mappings = allMappings.find(m => + m.environment.region === stack.environment.region && m.environment.account === stack.environment.account, + )?.mappings ?? {}; + const formatter = new DiffFormatter({ templateInfo: { oldTemplate: currentTemplate, @@ -329,6 +337,7 @@ export class CdkToolkit { changeSet, isImport: !!resourcesToImport, nestedStacks, + mappings, }, }); @@ -1538,6 +1547,13 @@ export interface DiffOptions { * @default false */ readonly importExistingResources?: boolean; + + /** + * Whether to include resource moves in the diff + * + * @default false + */ + readonly includeMoves?: boolean; } interface CfnDeployOptions { diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 1278b36a3..ef49b1588 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -340,6 +340,7 @@ export async function makeConfig(): Promise { 'quiet': { type: 'boolean', alias: 'q', desc: 'Do not print stack name and default message when there is no diff to stdout', default: false }, 'change-set': { type: 'boolean', alias: 'changeset', desc: 'Whether to create a changeset to analyze resource replacements. In this mode, diff will use the deploy role instead of the lookup role.', default: true }, 'import-existing-resources': { type: 'boolean', desc: 'Whether or not the change set imports resources that already exist', default: false }, + 'include-moves': { type: 'boolean', desc: 'Whether to include moves in the diff', default: false }, }, }, 'drift': { diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 2a2e8ea2f..0065a9f41 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -722,6 +722,11 @@ "type": "boolean", "desc": "Whether or not the change set imports resources that already exist", "default": false + }, + "include-moves": { + "type": "boolean", + "desc": "Whether to include moves in the diff", + "default": false } } }, diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 7730069e7..9612fc59b 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -266,6 +266,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise): any { default: false, type: 'boolean', desc: 'Whether or not the change set imports resources that already exist', + }) + .option('include-moves', { + default: false, + type: 'boolean', + desc: 'Whether to include moves in the diff', }), ) .command('drift [STACKS..]', 'Detect drifts in the given CloudFormation stack(s)', (yargs: Argv) => diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index fe14892cf..646f7082f 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1174,6 +1174,13 @@ export interface DiffOptions { */ readonly importExistingResources?: boolean; + /** + * Whether to include moves in the diff + * + * @default - false + */ + readonly includeMoves?: boolean; + /** * Positional argument for diff */