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 693690c0e..3b61d7310 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/context.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/context.ts @@ -28,6 +28,7 @@ export interface RefactoringContextOptions { overrides?: ResourceMapping[]; assumeRoleArn?: string; ignoreModifications?: boolean; + toolkitStackName?: string; } /** @@ -38,6 +39,7 @@ export class RefactoringContext { private readonly ambiguousMoves: ResourceMove[] = []; private readonly localStacks: CloudFormationStack[]; private readonly assumeRoleArn?: string; + private readonly toolkitStackName?: string; public readonly environment: Environment; constructor(props: RefactoringContextOptions) { @@ -49,6 +51,7 @@ export class RefactoringContext { this.ambiguousMoves = ambiguousMoves; this.localStacks = props.localStacks; this.assumeRoleArn = props.assumeRoleArn; + this.toolkitStackName = props.toolkitStackName; this._mappings = resourceMappings(nonAmbiguousMoves); } @@ -102,7 +105,7 @@ export class RefactoringContext { } private async checkBootstrapVersion(sdk: SDK, ioHelper: IoHelper) { - const environmentResourcesRegistry = new EnvironmentResourcesRegistry(); + const environmentResourcesRegistry = new EnvironmentResourcesRegistry(this.toolkitStackName); const envResources = environmentResourcesRegistry.for(this.environment, sdk, ioHelper); let bootstrapVersion: number | undefined = undefined; try { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/stack-definitions.ts b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/stack-definitions.ts index 7d837e2ae..b353f038f 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/stack-definitions.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/refactoring/stack-definitions.ts @@ -1,5 +1,5 @@ import * as util from 'node:util'; -import type { Environment } from '@aws-cdk/cx-api'; +import type { Environment } from '@aws-cdk/cloud-assembly-api'; import type { StackDefinition } from '@aws-sdk/client-cloudformation'; import chalk from 'chalk'; import type { CloudFormationStack, ResourceMapping } from './cloudformation'; @@ -23,6 +23,7 @@ export async function generateStackDefinitions( environment: Environment, sdkProvider: SdkProvider, ioHelper: IoHelper, + toolkitStackName?: string, ): Promise { const deployedStackMap: Map = new Map(deployedStacks.map((s) => [s.stackName, s])); @@ -96,7 +97,7 @@ export async function generateStackDefinitions( } const sdk = (await sdkProvider.forEnvironment(environment, Mode.ForWriting)).sdk; - const environmentResourcesRegistry = new EnvironmentResourcesRegistry(); + const environmentResourcesRegistry = new EnvironmentResourcesRegistry(toolkitStackName); const envResources = environmentResourcesRegistry.for(environment, sdk, ioHelper); const toolkitInfo = await envResources.lookupToolkit(); diff --git a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts index 912613040..67c1bfaaf 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/toolkit/toolkit.ts @@ -1621,6 +1621,7 @@ export class Toolkit extends CloudAssemblySourceBuilder { localStacks, assumeRoleArn: options.roleArn, overrides: getOverrides(environment, deployedStacks, localStacks), + toolkitStackName: this.toolkitStackName, }); const mappings = context.mappings; @@ -1637,7 +1638,15 @@ export class Toolkit extends CloudAssemblySourceBuilder { let refactorMessage = formatTypedMappings(typedMappings); const refactorResult: RefactorResult = { typedMappings }; - const stackDefinitions = await generateStackDefinitions(mappings, deployedStacks, localStacks, environment, sdkProvider, ioHelper); + const stackDefinitions = await generateStackDefinitions( + mappings, + deployedStacks, + localStacks, + environment, + sdkProvider, + ioHelper, + this.toolkitStackName, + ); if (context.ambiguousPaths.length > 0) { const paths = context.ambiguousPaths; diff --git a/packages/@aws-cdk/toolkit-lib/test/api/refactoring/refactoring.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/refactoring/refactoring.test.ts index eb3d82b94..49bd097ce 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/refactoring/refactoring.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/refactoring/refactoring.test.ts @@ -1659,4 +1659,103 @@ describe(generateStackDefinitions, () => { const templateSize = JSON.stringify(smallResources).length; expect(templateSize).toBeLessThan(50 * 1024); }); + + test('looks up the toolkit stack under the given custom name', async () => { + const largeResources: any = {}; + for (let i = 0; i < 500; i++) { + largeResources[`Bucket${i}`] = { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: `my-bucket-${i}`, + Tags: [ + { Key: 'Environment', Value: 'Production' }, + { Key: 'Application', Value: 'MyApp' }, + { Key: 'Owner', Value: 'TeamA' }, + { Key: 'CostCenter', Value: '12345' }, + ], + }, + }; + } + + const deployedStack: CloudFormationStack = { + environment, + stackName: 'Stack1', + template: { + Resources: { + Bucket1: { + Type: 'AWS::S3::Bucket', + }, + }, + }, + }; + + const localStack: CloudFormationStack = { + environment, + stackName: 'Stack1', + template: { + Resources: largeResources, + }, + }; + + const result = await generateStackDefinitions( + [], [deployedStack], [localStack], environment, mockSdkProvider, mockIoHelper, 'MyCustomToolkit', + ); + + expect(result[0].TemplateURL).toBeDefined(); + expect(ToolkitInfo.lookup).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + 'MyCustomToolkit', + ); + }); + + test('falls back to the default toolkit stack name when none is given', async () => { + const largeResources: any = {}; + for (let i = 0; i < 500; i++) { + largeResources[`Bucket${i}`] = { + Type: 'AWS::S3::Bucket', + Properties: { + BucketName: `my-bucket-${i}`, + Tags: [ + { Key: 'Environment', Value: 'Production' }, + { Key: 'Application', Value: 'MyApp' }, + { Key: 'Owner', Value: 'TeamA' }, + { Key: 'CostCenter', Value: '12345' }, + ], + }, + }; + } + + const deployedStack: CloudFormationStack = { + environment, + stackName: 'Stack1', + template: { + Resources: { + Bucket1: { + Type: 'AWS::S3::Bucket', + }, + }, + }, + }; + + const localStack: CloudFormationStack = { + environment, + stackName: 'Stack1', + template: { + Resources: largeResources, + }, + }; + + await generateStackDefinitions( + [], [deployedStack], [localStack], environment, mockSdkProvider, mockIoHelper, + ); + + expect(ToolkitInfo.lookup).toHaveBeenCalledWith( + expect.anything(), + expect.anything(), + expect.anything(), + undefined, + ); + }); }); diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index b8884ce70..1d5e93c63 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -580,6 +580,11 @@ export async function makeConfig(): Promise { default: false, desc: 'Whether to do the refactor without asking for confirmation', }, + 'toolkit-stack-name': { + type: 'string', + requiresArg: true, + desc: 'The name of the existing CDK toolkit stack (used to find the staging bucket for templates that are too large to be sent inline)', + }, }, arg: { name: 'STACKS', diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index b7811ac1b..ae414c0bf 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -1213,6 +1213,11 @@ "type": "boolean", "default": false, "desc": "Whether to do the refactor without asking for confirmation" + }, + "toolkit-stack-name": { + "type": "string", + "requiresArg": true, + "desc": "The name of the existing CDK toolkit stack (used to find the staging bucket for templates that are too large to be sent inline)" } }, "arg": { diff --git a/packages/aws-cdk/lib/cli/convert-to-user-input.ts b/packages/aws-cdk/lib/cli/convert-to-user-input.ts index e624c782e..1f41ad7a0 100644 --- a/packages/aws-cdk/lib/cli/convert-to-user-input.ts +++ b/packages/aws-cdk/lib/cli/convert-to-user-input.ts @@ -348,6 +348,7 @@ export function convertYargsToUserInput(args: any): UserInput { overrideFile: args.overrideFile, revert: args.revert, force: args.force, + toolkitStackName: args.toolkitStackName, STACKS: args.STACKS, }; break; @@ -607,6 +608,7 @@ export function convertConfigToUserInput(config: any): UserInput { overrideFile: config.refactor?.overrideFile, revert: config.refactor?.revert, force: config.refactor?.force, + toolkitStackName: config.refactor?.toolkitStackName, }; const cliTelemetryOptions = { enable: config.cliTelemetry?.enable, diff --git a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts index 1e9ef75e2..cc6635263 100644 --- a/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/cli/parse-command-line-arguments.ts @@ -1160,6 +1160,12 @@ export function parseCommandLineArguments(args: Array): any { default: false, type: 'boolean', desc: 'Whether to do the refactor without asking for confirmation', + }) + .option('toolkit-stack-name', { + default: undefined, + type: 'string', + requiresArg: true, + desc: 'The name of the existing CDK toolkit stack (used to find the staging bucket for templates that are too large to be sent inline)', }), ) .command('cli-telemetry', 'Enable or disable anonymous telemetry', (yargs: Argv) => diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index ef6df7b98..c24469456 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1848,6 +1848,13 @@ export interface RefactorOptions { */ readonly force?: boolean; + /** + * The name of the existing CDK toolkit stack (used to find the staging bucket for templates that are too large to be sent inline) + * + * @default - undefined + */ + readonly toolkitStackName?: string; + /** * Positional argument for refactor */ diff --git a/packages/aws-cdk/test/cli/cli-arguments.test.ts b/packages/aws-cdk/test/cli/cli-arguments.test.ts index defb0d3a5..51a56da05 100644 --- a/packages/aws-cdk/test/cli/cli-arguments.test.ts +++ b/packages/aws-cdk/test/cli/cli-arguments.test.ts @@ -144,6 +144,20 @@ describe('yargs', () => { globalOptions: expect.anything(), }); }); + + test('refactor --toolkit-stack-name is correctly passed through', async () => { + const input = await parseCommandLineArguments(['refactor', '--toolkit-stack-name', 'MyCustomToolkit']); + + const result = convertYargsToUserInput(input); + + expect(result).toEqual({ + command: 'refactor', + refactor: expect.objectContaining({ + toolkitStackName: 'MyCustomToolkit', + }), + globalOptions: expect.anything(), + }); + }); }); describe('config', () => { diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/--dry-run_detects_a_renamed_resource_without_creating_a_refactor.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/--dry-run_detects_a_renamed_resource_without_creating_a_refactor.ndjson new file mode 100644 index 000000000..7f4cac1a8 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/--dry-run_detects_a_renamed_resource_without_creating_a_refactor.ndjson @@ -0,0 +1,6 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"result","code":"CDK_TOOLKIT_I8900","message":"The following resources were moved or renamed:\n┌─────────────────┬───────────────────────────┬───────────────────────────┐\n│ Resource Type │ Old Construct Path │ New Construct Path │\n├─────────────────┼───────────────────────────┼───────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/OldBucket/Resource │ Stack1/NewBucket/Resource │\n└─────────────────┴───────────────────────────┴───────────────────────────┘\n "} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/--dry-run_detects_a_resource_moved_to_another_stack.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/--dry-run_detects_a_resource_moved_to_another_stack.ndjson new file mode 100644 index 000000000..c56ff1a9e --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/--dry-run_detects_a_resource_moved_to_another_stack.ndjson @@ -0,0 +1,6 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"result","code":"CDK_TOOLKIT_I8900","message":"The following resources were moved or renamed:\n┌─────────────────┬──────────────────────────┬──────────────────────────┐\n│ Resource Type │ Old Construct Path │ New Construct Path │\n├─────────────────┼──────────────────────────┼──────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/MyBucket/Resource │ Stack2/MyBucket/Resource │\n└─────────────────┴──────────────────────────┴──────────────────────────┘\n "} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/ambiguity_--revert_swaps_the_direction_of_the_override_file.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/ambiguity_--revert_swaps_the_direction_of_the_override_file.ndjson new file mode 100644 index 000000000..97db21d96 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/ambiguity_--revert_swaps_the_direction_of_the_override_file.ndjson @@ -0,0 +1,6 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"result","code":"CDK_TOOLKIT_I8900","message":"The following resources were moved or renamed:\n┌─────────────────┬─────────────────────────┬─────────────────────────┐\n│ Resource Type │ Old Construct Path │ New Construct Path │\n├─────────────────┼─────────────────────────┼─────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/Bucket3/Resource │ Stack1/Bucket1/Resource │\n├─────────────────┼─────────────────────────┼─────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/Bucket4/Resource │ Stack1/Bucket2/Resource │\n└─────────────────┴─────────────────────────┴─────────────────────────┘\n "} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/ambiguity_an_override_file_resolves_the_ambiguity.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/ambiguity_an_override_file_resolves_the_ambiguity.ndjson new file mode 100644 index 000000000..419d98dfc --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/ambiguity_an_override_file_resolves_the_ambiguity.ndjson @@ -0,0 +1,6 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"result","code":"CDK_TOOLKIT_I8900","message":"The following resources were moved or renamed:\n┌─────────────────┬─────────────────────────┬─────────────────────────┐\n│ Resource Type │ Old Construct Path │ New Construct Path │\n├─────────────────┼─────────────────────────┼─────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/Bucket1/Resource │ Stack1/Bucket3/Resource │\n├─────────────────┼─────────────────────────┼─────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/Bucket2/Resource │ Stack1/Bucket4/Resource │\n└─────────────────┴─────────────────────────┴─────────────────────────┘\n "} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/ambiguity_reports_ambiguous_mappings_and_refuses_to_refactor.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/ambiguity_reports_ambiguous_mappings_and_refuses_to_refactor.ndjson new file mode 100644 index 000000000..5710af2f1 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/ambiguity_reports_ambiguous_mappings_and_refuses_to_refactor.ndjson @@ -0,0 +1,6 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"result","code":"CDK_TOOLKIT_I8900","message":"Detected ambiguities:\n┌───┬─────────────────────────┐\n│ │ Resource │\n├───┼─────────────────────────┤\n│ - │ Stack1/Bucket1/Resource │\n│ │ Stack1/Bucket2/Resource │\n├───┼─────────────────────────┤\n│ + │ Stack1/Bucket3/Resource │\n│ │ Stack1/Bucket4/Resource │\n└───┴─────────────────────────┘\n \nPlease provide an override file to resolve these ambiguous mappings.\n "} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/execution_a_bootstrap_stack_that_is_too_old_to_refactor_is_rejected.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/execution_a_bootstrap_stack_that_is_too_old_to_refactor_is_rejected.ndjson new file mode 100644 index 000000000..500ea4531 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/execution_a_bootstrap_stack_that_is_too_old_to_refactor_is_rejected.ndjson @@ -0,0 +1,10 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"result","code":"CDK_TOOLKIT_I8900","message":"The following resources were moved or renamed:\n┌─────────────────┬───────────────────────────┬───────────────────────────┐\n│ Resource Type │ Old Construct Path │ New Construct Path │\n├─────────────────┼───────────────────────────┼───────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/OldBucket/Resource │ Stack1/NewBucket/Resource │\n└─────────────────┴───────────────────────────┴───────────────────────────┘\n "} +{"seq":6,"type":"notify","action":"refactor","level":"info","code":null,"message":"Refactoring..."} +{"seq":7,"type":"notify","action":"refactor","level":"debug","code":null,"message":"Waiting for stack CDKToolkit to finish creating or updating..."} +{"seq":8,"type":"notify","action":"refactor","level":"error","code":"CDK_TOOLKIT_E8900","message":"❌ Refactor failed: The CDK toolkit stack in environment aws://123456789012/bermuda-triangle-1 doesn't support refactoring. Please run 'cdk bootstrap aws://123456789012/bermuda-triangle-1' to update it."} +{"seq":9,"type":"notify","action":"refactor","level":"debug","code":null,"message":"The CDK toolkit stack in environment aws://123456789012/bermuda-triangle-1 doesn't support refactoring. Please run 'cdk bootstrap aws://123456789012/bermuda-triangle-1' to update it."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/execution_creates_and_executes_the_refactor_then_deploys_the_stacks.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/execution_creates_and_executes_the_refactor_then_deploys_the_stacks.ndjson new file mode 100644 index 000000000..84bf7af8a --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/execution_creates_and_executes_the_refactor_then_deploys_the_stacks.ndjson @@ -0,0 +1,20 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"result","code":"CDK_TOOLKIT_I8900","message":"The following resources were moved or renamed:\n┌─────────────────┬───────────────────────────┬───────────────────────────┐\n│ Resource Type │ Old Construct Path │ New Construct Path │\n├─────────────────┼───────────────────────────┼───────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/OldBucket/Resource │ Stack1/NewBucket/Resource │\n└─────────────────┴───────────────────────────┴───────────────────────────┘\n "} +{"seq":6,"type":"notify","action":"refactor","level":"info","code":null,"message":"Refactoring..."} +{"seq":7,"type":"notify","action":"refactor","level":"debug","code":null,"message":"Waiting for stack CDKToolkit to finish creating or updating..."} +{"seq":8,"type":"notify","action":"refactor","level":"info","code":null,"message":"✅ Stack refactor complete"} +{"seq":9,"type":"notify","action":"refactor","level":"info","code":null,"message":"Deploying updated stacks to finalize refactor..."} +{"seq":10,"type":"notify","action":"deploy","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":11,"type":"notify","action":"deploy","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":12,"type":"notify","action":"deploy","level":"debug","code":null,"message":"Checking for previously published assets"} +{"seq":13,"type":"notify","action":"deploy","level":"debug","code":null,"message":"0 total assets, 0 still need to be published"} +{"seq":14,"type":"request","action":"deploy","level":"info","code":"CDK_TOOLKIT_I5060","message":"Stack Stack1\nResources\n[+] AWS::S3::Bucket NewBucket NewBucket\n\n\n\nStack includes updates. Do you wish to deploy these changes?","response":true} +{"seq":15,"type":"notify","action":"deploy","level":"info","code":"CDK_TOOLKIT_I5100","message":"Stack1: deploying... [1/1]"} +{"seq":16,"type":"notify","action":"deploy","level":"result","code":"CDK_TOOLKIT_I5900","message":"\n✅ Stack1"} +{"seq":17,"type":"notify","action":"deploy","level":"info","code":"CDK_TOOLKIT_I5000","message":"✨ Deployment time: "} +{"seq":18,"type":"notify","action":"deploy","level":"info","code":"CDK_TOOLKIT_I5901","message":"Stack ARN:\narn:aws:cloudformation:bermuda-triangle-1:123456789012:stack/Stack1/abcd"} +{"seq":19,"type":"notify","action":"deploy","level":"info","code":"CDK_TOOLKIT_I5001","message":"✨ Total time: "} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/large_templates_are_uploaded_to_the_bucket_of_a_custom_toolkit_stack.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/large_templates_are_uploaded_to_the_bucket_of_a_custom_toolkit_stack.ndjson new file mode 100644 index 000000000..e8d58d826 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/large_templates_are_uploaded_to_the_bucket_of_a_custom_toolkit_stack.ndjson @@ -0,0 +1,8 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"debug","code":null,"message":"Waiting for stack MyCustomToolkit to finish creating or updating..."} +{"seq":6,"type":"notify","action":"refactor","level":"debug","code":null,"message":"Storing template for stack Stack1 in S3 at: https://MyCustomToolkit-bucket.s3.amazonaws.com/cdk-refactor/Stack1/414006b004b5209a7dbe586333d988417e9e7d4ceb6bdf880521bb776a619425.json"} +{"seq":7,"type":"notify","action":"refactor","level":"result","code":"CDK_TOOLKIT_I8900","message":"The following resources were moved or renamed:\n┌─────────────────┬─────────────────────────┬───────────────────────────────┐\n│ Resource Type │ Old Construct Path │ New Construct Path │\n├─────────────────┼─────────────────────────┼───────────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/Bucket0/Resource │ Stack1/RenamedBucket/Resource │\n└─────────────────┴─────────────────────────┴───────────────────────────────┘\n "} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/large_templates_are_uploaded_to_the_bucket_of_the_default_toolkit_stack.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/large_templates_are_uploaded_to_the_bucket_of_the_default_toolkit_stack.ndjson new file mode 100644 index 000000000..c9b3d5b88 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/large_templates_are_uploaded_to_the_bucket_of_the_default_toolkit_stack.ndjson @@ -0,0 +1,8 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"debug","code":null,"message":"Waiting for stack CDKToolkit to finish creating or updating..."} +{"seq":6,"type":"notify","action":"refactor","level":"debug","code":null,"message":"Storing template for stack Stack1 in S3 at: https://CDKToolkit-bucket.s3.amazonaws.com/cdk-refactor/Stack1/414006b004b5209a7dbe586333d988417e9e7d4ceb6bdf880521bb776a619425.json"} +{"seq":7,"type":"notify","action":"refactor","level":"result","code":"CDK_TOOLKIT_I8900","message":"The following resources were moved or renamed:\n┌─────────────────┬─────────────────────────┬───────────────────────────────┐\n│ Resource Type │ Old Construct Path │ New Construct Path │\n├─────────────────┼─────────────────────────┼───────────────────────────────┤\n│ AWS::S3::Bucket │ Stack1/Bucket0/Resource │ Stack1/RenamedBucket/Resource │\n└─────────────────┴─────────────────────────┴───────────────────────────────┘\n "} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/large_templates_fail_when_the_environment_is_not_bootstrapped.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/large_templates_fail_when_the_environment_is_not_bootstrapped.ndjson new file mode 100644 index 000000000..96ad4faef --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/large_templates_fail_when_the_environment_is_not_bootstrapped.ndjson @@ -0,0 +1,11 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"debug","code":null,"message":"Waiting for stack CDKToolkit to finish creating or updating..."} +{"seq":6,"type":"notify","action":"refactor","level":"debug","code":null,"message":"Stack CDKToolkit does not exist"} +{"seq":7,"type":"notify","action":"refactor","level":"debug","code":null,"message":"The environment aws://123456789012/bermuda-triangle-1 doesn't have the CDK toolkit stack (CDKToolkit) installed. Use cdk bootstrap \"aws://123456789012/bermuda-triangle-1\" to setup your environment for use with the toolkit."} +{"seq":8,"type":"notify","action":"refactor","level":"error","code":null,"message":"The template for stack \"Stack1\" is 173KiB. Templates larger than 50KiB must be uploaded to S3.\nRun the following command in order to setup an S3 bucket in this environment, and then re-refactor:\n\n \t$ cdk bootstrap aws://123456789012/bermuda-triangle-1\n"} +{"seq":9,"type":"notify","action":"refactor","level":"error","code":"CDK_TOOLKIT_E8900","message":"❌ Refactor failed: Template too large to refactor (\"cdk bootstrap\" is required)"} +{"seq":10,"type":"notify","action":"refactor","level":"debug","code":null,"message":"Template too large to refactor (\"cdk bootstrap\" is required)"} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/nothing_to_do_reports_that_there_is_nothing_to_refactor_when_local_and_deployed_templates_match.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/nothing_to_do_reports_that_there_is_nothing_to_refactor_when_local_and_deployed_templates_match.ndjson new file mode 100644 index 000000000..e6863cbba --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/nothing_to_do_reports_that_there_is_nothing_to_refactor_when_local_and_deployed_templates_match.ndjson @@ -0,0 +1,6 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"info","code":null,"message":"Nothing to refactor."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/refusals_--revert_without_--override-file_fails_before_doing_anything.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/refusals_--revert_without_--override-file_fails_before_doing_anything.ndjson new file mode 100644 index 000000000..e69de29bb diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/refusals_a_refactor_that_adds_or_removes_resources_is_rejected.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/refusals_a_refactor_that_adds_or_removes_resources_is_rejected.ndjson new file mode 100644 index 000000000..efdfb47a6 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/refusals_a_refactor_that_adds_or_removes_resources_is_rejected.ndjson @@ -0,0 +1,7 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"error","code":"CDK_TOOLKIT_E8900","message":"❌ Refactor failed: A refactor operation cannot add, remove or update resources. Only resource moves and renames are allowed.\n\nThe following resources are present only in your CDK application:\n [+] Stack1/MyQueue/Resource\n\nThe following stacks were used in the comparison:\n - Deployed: Stack1\n - Local: Stack1\n\nHint: by default, only deployed stacks that have the same name as a local stack are included.\nIf you want to include additional deployed stacks for comparison, re-run the command with the option '--additional-stack-name=' for each stack."} +{"seq":6,"type":"notify","action":"refactor","level":"debug","code":null,"message":"A refactor operation cannot add, remove or update resources. Only resource moves and renames are allowed.\n\nThe following resources are present only in your CDK application:\n [+] Stack1/MyQueue/Resource\n\nThe following stacks were used in the comparison:\n - Deployed: Stack1\n - Local: Stack1\n\nHint: by default, only deployed stacks that have the same name as a local stack are included.\nIf you want to include additional deployed stacks for comparison, re-run the command with the option '--additional-stack-name=' for each stack."} diff --git a/packages/aws-cdk/test/commands/__io_snapshots__/refactor/refusals_stacks_that_are_not_deployed_yet_are_rejected.ndjson b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/refusals_stacks_that_are_not_deployed_yet_are_rejected.ndjson new file mode 100644 index 000000000..abf699287 --- /dev/null +++ b/packages/aws-cdk/test/commands/__io_snapshots__/refactor/refusals_stacks_that_are_not_deployed_yet_are_rejected.ndjson @@ -0,0 +1,6 @@ +{"seq":0,"type":"notify","action":"refactor","level":"trace","code":"CDK_TOOLKIT_I1001","message":"Starting Synthesis ..."} +{"seq":1,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1000","message":"Starting Synthesis ..."} +{"seq":2,"type":"notify","action":"refactor","level":"trace","code":"CDK_CLI_I1001","message":"\n✨ Synthesis time: \n"} +{"seq":3,"type":"notify","action":"refactor","level":"info","code":"CDK_TOOLKIT_I1000","message":"✨ Synthesis time: "} +{"seq":4,"type":"notify","action":"refactor","level":"info","code":null,"message":"aws://123456789012/bermuda-triangle-1\n"} +{"seq":5,"type":"notify","action":"refactor","level":"error","code":null,"message":"The following stack is new: Stack1\nCreation of new stacks is not yet supported by the refactor command. Please deploy any new stacks separately before refactoring your stacks."} diff --git a/packages/aws-cdk/test/commands/refactor.test.ts b/packages/aws-cdk/test/commands/refactor.test.ts new file mode 100644 index 000000000..1298db0d1 --- /dev/null +++ b/packages/aws-cdk/test/commands/refactor.test.ts @@ -0,0 +1,506 @@ +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { + CreateStackRefactorCommand, + DescribeStackRefactorCommand, + DescribeStacksCommand, + ExecuteStackRefactorCommand, + GetTemplateCommand, + ListStacksCommand, +} from '@aws-sdk/client-cloudformation'; +import { PutObjectCommand } from '@aws-sdk/client-s3'; +import { Deployments } from '../../lib/api'; +import { CdkToolkit } from '../../lib/cli/cdk-toolkit'; +import { CliIoHost } from '../../lib/cli/io-host'; +import type { TestStackArtifact } from '../_helpers'; +import { MockCloudExecutable } from '../_helpers'; +import { IoHostRecorder } from '../_helpers/io-recorder'; +import { + mockCloudFormationClient, + mockS3Client, + restoreSdkMocksToDefault, + setDefaultSTSMocks, +} from '../_helpers/mock-sdk'; + +// `cdk refactor` emits its entire plan (the mapping table, ambiguities, +// confirmation and progress) to the user. These tests capture the *whole* +// ordered stream of messages the command sends to the CliIoHost as an NDJSON +// snapshot, so a change to the user-facing output of any refactor variant shows +// up as a diff in review (see test/_helpers/io-recorder.ts). + +const ACCOUNT = '123456789012'; +const REGION = 'bermuda-triangle-1'; +const ENV = `aws://${ACCOUNT}/${REGION}`; + +/** + * A bucket as the CDK framework would synthesize it, at the given construct path. + */ +function bucket(cdkPath: string) { + return { + Type: 'AWS::S3::Bucket', + UpdateReplacePolicy: 'Retain', + DeletionPolicy: 'Retain', + Metadata: { 'aws:cdk:path': cdkPath }, + }; +} + +function queue(cdkPath: string) { + return { + Type: 'AWS::SQS::Queue', + UpdateReplacePolicy: 'Delete', + DeletionPolicy: 'Delete', + Metadata: { 'aws:cdk:path': cdkPath }, + }; +} + +/** + * A template big enough (> 50KiB) that the refactor has to upload it to S3 + * instead of sending it inline. + */ +function largeTemplate() { + const resources: Record = {}; + for (let i = 0; i < 500; i++) { + resources[`Bucket${i}`] = { + ...bucket(`Stack1/Bucket${i}/Resource`), + Properties: { + BucketName: `my-bucket-${i}`, + Tags: [ + { Key: 'Environment', Value: 'Production' }, + { Key: 'Application', Value: 'MyApp' }, + { Key: 'Owner', Value: 'TeamA' }, + { Key: 'CostCenter', Value: '12345' }, + ], + }, + }; + } + return { Resources: resources }; +} + +/** + * A large local template in which `Bucket0` has been renamed, so there is + * exactly one mapping to compute. + */ +function largeTemplateWithRename() { + const template = largeTemplate(); + template.Resources.RenamedBucket = { + ...template.Resources.Bucket0, + Metadata: { 'aws:cdk:path': 'Stack1/RenamedBucket/Resource' }, + }; + delete template.Resources.Bucket0; + return template; +} + +/** + * Pretend the given stacks are deployed in the target environment, with the + * given templates. + */ +function givenDeployedStacks(templates: Record) { + mockCloudFormationClient.on(ListStacksCommand).resolves({ + StackSummaries: Object.keys(templates).map((stackName) => ({ + StackName: stackName, + StackId: `arn:aws:cloudformation:${REGION}:${ACCOUNT}:stack/${stackName}`, + StackStatus: 'CREATE_COMPLETE', + CreationTime: new Date(), + })), + }); + + for (const [stackName, template] of Object.entries(templates)) { + mockCloudFormationClient + .on(GetTemplateCommand, { StackName: stackName }) + .resolves({ TemplateBody: JSON.stringify(template) }); + } +} + +/** + * Pretend the environment is bootstrapped under the given toolkit stack name. + * + * Only a lookup for exactly that name resolves; any other name (e.g. the + * default `CDKToolkit`) falls through to the "no stacks" default and is + * reported as "not bootstrapped". That is what makes the custom-toolkit-stack + * tests below meaningful. + */ +function givenBootstrapStack(toolkitStackName: string, version = '28') { + mockCloudFormationClient.on(DescribeStacksCommand, { StackName: toolkitStackName }).resolves({ + Stacks: [ + { + StackName: toolkitStackName, + CreationTime: new Date(), + StackStatus: 'CREATE_COMPLETE', + Outputs: [ + { OutputKey: 'BucketName', OutputValue: `${toolkitStackName}-bucket` }, + { OutputKey: 'BucketDomainName', OutputValue: `${toolkitStackName}-bucket.s3.amazonaws.com` }, + { OutputKey: 'BootstrapVersion', OutputValue: version }, + ], + }, + ], + }); +} + +let ioHost = CliIoHost.instance(); +let recorder: IoHostRecorder; +let originalIsTTY: boolean; + +async function makeToolkit(stacks: TestStackArtifact[], toolkitStackName?: string) { + const cloudExecutable = await MockCloudExecutable.create({ stacks }, undefined, ioHost, 'refactor'); + return new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new Deployments({ + sdkProvider: cloudExecutable.sdkProvider, + ioHelper: ioHost.asIoHelper(), + toolkitStackName, + }), + toolkitStackName, + }); +} + +beforeAll(() => { + originalIsTTY = process.stdout.isTTY; +}); + +afterAll(() => { + process.stdout.isTTY = originalIsTTY; +}); + +beforeEach(() => { + jest.restoreAllMocks(); + restoreSdkMocksToDefault(); + setDefaultSTSMocks(); + + ioHost = CliIoHost.instance(); + ioHost.isCI = false; + ioHost.currentAction = 'refactor'; + + // Non-interactive: the refactor only asks for confirmation on a TTY. + process.stdout.isTTY = false; + + // The refactor finishes by deploying the updated stacks. Keep that off the + // wire; the deploy stream itself is covered by the deploy tests. + jest.spyOn(Deployments.prototype, 'readCurrentTemplate').mockResolvedValue({}); + jest.spyOn(Deployments.prototype, 'isSingleAssetPublished').mockResolvedValue(false); + jest.spyOn(Deployments.prototype, 'deployStack').mockImplementation(async (options: any) => ({ + type: 'did-deploy-stack', + stackArn: `arn:aws:cloudformation:${REGION}:${ACCOUNT}:stack/${options.stack.stackName}/abcd`, + noOp: false, + outputs: {}, + deleteFailures: [], + stabilizingResources: [], + }) as any); + + recorder = IoHostRecorder.create(ioHost); +}); + +afterEach(() => { + // Snapshot every message this refactor variant sent to the CliIoHost. + recorder.matchSnapshot(); +}); + +describe('nothing to do', () => { + test('reports that there is nothing to refactor when local and deployed templates match', async () => { + givenDeployedStacks({ Stack1: { Resources: { MyBucket: bucket('Stack1/MyBucket/Resource') } } }); + + const toolkit = await makeToolkit([{ + stackName: 'Stack1', + env: ENV, + template: { Resources: { MyBucket: bucket('Stack1/MyBucket/Resource') } }, + }]); + + await toolkit.refactor({ dryRun: true }); + + expect(mockCloudFormationClient).not.toHaveReceivedCommand(CreateStackRefactorCommand); + }); +}); + +describe('--dry-run', () => { + test('detects a renamed resource without creating a refactor', async () => { + givenDeployedStacks({ Stack1: { Resources: { OldBucket: bucket('Stack1/OldBucket/Resource') } } }); + + const toolkit = await makeToolkit([{ + stackName: 'Stack1', + env: ENV, + template: { Resources: { NewBucket: bucket('Stack1/NewBucket/Resource') } }, + }]); + + await toolkit.refactor({ dryRun: true }); + + expect(mockCloudFormationClient).not.toHaveReceivedCommand(CreateStackRefactorCommand); + }); + + test('detects a resource moved to another stack', async () => { + givenDeployedStacks({ + Stack1: { Resources: { MyBucket: bucket('Stack1/MyBucket/Resource') } }, + Stack2: { Resources: { MyQueue: queue('Stack2/MyQueue/Resource') } }, + }); + + const toolkit = await makeToolkit([ + { + stackName: 'Stack1', + env: ENV, + template: { Resources: {} }, + }, + { + stackName: 'Stack2', + env: ENV, + template: { + Resources: { + MyQueue: queue('Stack2/MyQueue/Resource'), + MyBucket: bucket('Stack2/MyBucket/Resource'), + }, + }, + }, + ]); + + await toolkit.refactor({ dryRun: true }); + + expect(mockCloudFormationClient).not.toHaveReceivedCommand(CreateStackRefactorCommand); + }); +}); + +describe('ambiguity', () => { + test('reports ambiguous mappings and refuses to refactor', async () => { + givenDeployedStacks({ + Stack1: { + Resources: { + Bucket1: bucket('Stack1/Bucket1/Resource'), + Bucket2: bucket('Stack1/Bucket2/Resource'), + }, + }, + }); + + const toolkit = await makeToolkit([{ + stackName: 'Stack1', + env: ENV, + template: { + Resources: { + Bucket3: bucket('Stack1/Bucket3/Resource'), + Bucket4: bucket('Stack1/Bucket4/Resource'), + }, + }, + }]); + + await toolkit.refactor({ dryRun: false }); + + expect(mockCloudFormationClient).not.toHaveReceivedCommand(CreateStackRefactorCommand); + }); + + test('an override file resolves the ambiguity', async () => { + givenDeployedStacks({ + Stack1: { + Resources: { + Bucket1: bucket('Stack1/Bucket1/Resource'), + Bucket2: bucket('Stack1/Bucket2/Resource'), + }, + }, + }); + + const toolkit = await makeToolkit([{ + stackName: 'Stack1', + env: ENV, + template: { + Resources: { + Bucket3: bucket('Stack1/Bucket3/Resource'), + Bucket4: bucket('Stack1/Bucket4/Resource'), + }, + }, + }]); + + const overrideFile = writeOverrideFile({ + environments: [{ + account: ACCOUNT, + region: REGION, + resources: { + 'Stack1.Bucket1': 'Stack1.Bucket3', + 'Stack1.Bucket2': 'Stack1.Bucket4', + }, + }], + }); + + await toolkit.refactor({ dryRun: true, overrideFile }); + }); + + test('--revert swaps the direction of the override file', async () => { + givenDeployedStacks({ + Stack1: { + Resources: { + Bucket3: bucket('Stack1/Bucket3/Resource'), + Bucket4: bucket('Stack1/Bucket4/Resource'), + }, + }, + }); + + const toolkit = await makeToolkit([{ + stackName: 'Stack1', + env: ENV, + template: { + Resources: { + Bucket1: bucket('Stack1/Bucket1/Resource'), + Bucket2: bucket('Stack1/Bucket2/Resource'), + }, + }, + }]); + + const overrideFile = writeOverrideFile({ + environments: [{ + account: ACCOUNT, + region: REGION, + resources: { + 'Stack1.Bucket1': 'Stack1.Bucket3', + 'Stack1.Bucket2': 'Stack1.Bucket4', + }, + }], + }); + + await toolkit.refactor({ dryRun: true, overrideFile, revert: true }); + }); +}); + +describe('refusals', () => { + test('--revert without --override-file fails before doing anything', async () => { + const toolkit = await makeToolkit([{ stackName: 'Stack1', env: ENV, template: { Resources: {} } }]); + + await expect(toolkit.refactor({ dryRun: true, revert: true })).rejects.toThrow( + 'The --revert option can only be used with the --override-file option.', + ); + }); + + test('a refactor that adds or removes resources is rejected', async () => { + givenDeployedStacks({ Stack1: { Resources: { MyBucket: bucket('Stack1/MyBucket/Resource') } } }); + + const toolkit = await makeToolkit([{ + stackName: 'Stack1', + env: ENV, + template: { + Resources: { + MyBucket: bucket('Stack1/MyBucket/Resource'), + MyQueue: queue('Stack1/MyQueue/Resource'), + }, + }, + }]); + + await toolkit.refactor({ dryRun: true }); + + expect(mockCloudFormationClient).not.toHaveReceivedCommand(CreateStackRefactorCommand); + }); + + test('stacks that are not deployed yet are rejected', async () => { + givenDeployedStacks({}); + + const toolkit = await makeToolkit([{ + stackName: 'Stack1', + env: ENV, + template: { Resources: { MyBucket: bucket('Stack1/MyBucket/Resource') } }, + }]); + + await toolkit.refactor({ dryRun: true }); + + expect(mockCloudFormationClient).not.toHaveReceivedCommand(CreateStackRefactorCommand); + }); +}); + +describe('execution', () => { + test('creates and executes the refactor, then deploys the stacks', async () => { + givenDeployedStacks({ Stack1: { Resources: { OldBucket: bucket('Stack1/OldBucket/Resource') } } }); + givenBootstrapStack('CDKToolkit'); + + mockCloudFormationClient.on(CreateStackRefactorCommand).resolves({ StackRefactorId: 'refactor-id' }); + mockCloudFormationClient.on(DescribeStackRefactorCommand).resolves({ + Status: 'CREATE_COMPLETE', + ExecutionStatus: 'EXECUTE_COMPLETE', + }); + mockCloudFormationClient.on(ExecuteStackRefactorCommand).resolves({}); + + const toolkit = await makeToolkit([{ + stackName: 'Stack1', + env: ENV, + template: { Resources: { NewBucket: bucket('Stack1/NewBucket/Resource') } }, + }]); + + await toolkit.refactor({ dryRun: false }); + + expect(mockCloudFormationClient).toHaveReceivedCommandWith(CreateStackRefactorCommand, { + ResourceMappings: [ + { + Source: { StackName: 'Stack1', LogicalResourceId: 'OldBucket' }, + Destination: { StackName: 'Stack1', LogicalResourceId: 'NewBucket' }, + }, + ], + StackDefinitions: [ + { + StackName: 'Stack1', + TemplateBody: JSON.stringify({ Resources: { NewBucket: bucket('Stack1/NewBucket/Resource') } }), + }, + ], + }); + expect(mockCloudFormationClient).toHaveReceivedCommandWith(ExecuteStackRefactorCommand, { + StackRefactorId: 'refactor-id', + }); + }); + + test('a bootstrap stack that is too old to refactor is rejected', async () => { + givenDeployedStacks({ Stack1: { Resources: { OldBucket: bucket('Stack1/OldBucket/Resource') } } }); + givenBootstrapStack('CDKToolkit', '27'); + + const toolkit = await makeToolkit([{ + stackName: 'Stack1', + env: ENV, + template: { Resources: { NewBucket: bucket('Stack1/NewBucket/Resource') } }, + }]); + + await toolkit.refactor({ dryRun: false }); + + expect(mockCloudFormationClient).not.toHaveReceivedCommand(CreateStackRefactorCommand); + }); +}); + +describe('large templates', () => { + test('are uploaded to the bucket of the default toolkit stack', async () => { + givenDeployedStacks({ Stack1: largeTemplate() }); + givenBootstrapStack('CDKToolkit'); + + const toolkit = await makeToolkit([{ stackName: 'Stack1', env: ENV, template: largeTemplateWithRename() }]); + + await toolkit.refactor({ dryRun: true }); + + expect(mockS3Client).toHaveReceivedCommandWith(PutObjectCommand, { + Bucket: 'CDKToolkit-bucket', + }); + }); + + test('are uploaded to the bucket of a custom toolkit stack', async () => { + // Only `MyCustomToolkit` is bootstrapped; a lookup of the default + // `CDKToolkit` name would report "not bootstrapped" and fail the refactor. + givenDeployedStacks({ Stack1: largeTemplate() }); + givenBootstrapStack('MyCustomToolkit'); + + const toolkit = await makeToolkit([{ stackName: 'Stack1', env: ENV, template: largeTemplateWithRename() }], 'MyCustomToolkit'); + + await toolkit.refactor({ dryRun: true }); + + expect(mockCloudFormationClient).toHaveReceivedCommandWith(DescribeStacksCommand, { + StackName: 'MyCustomToolkit', + }); + expect(mockS3Client).toHaveReceivedCommandWith(PutObjectCommand, { + Bucket: 'MyCustomToolkit-bucket', + }); + }); + + test('fail when the environment is not bootstrapped', async () => { + givenDeployedStacks({ Stack1: largeTemplate() }); + + const toolkit = await makeToolkit([{ stackName: 'Stack1', env: ENV, template: largeTemplateWithRename() }]); + + await toolkit.refactor({ dryRun: true }); + + expect(mockCloudFormationClient).not.toHaveReceivedCommand(CreateStackRefactorCommand); + }); +}); + +function writeOverrideFile(content: any): string { + const dir = fs.mkdtempSync(path.join(fs.realpathSync(os.tmpdir()), 'cdk-refactor-test-')); + const file = path.join(dir, 'overrides.json'); + fs.writeFileSync(file, JSON.stringify(content)); + return file; +}