From a1a83278b29389e784fd0d4815bab719d2477129 Mon Sep 17 00:00:00 2001 From: lemon0333 <147061193+lemon0333@users.noreply.github.com> Date: Sat, 18 Jul 2026 23:11:04 +0900 Subject: [PATCH] feat(cli): support notification-arns in cdk import An import is executed through the same CloudFormation change-set machinery as a deployment, and the deployment layer already supports notification ARNs. This exposes the --notification-arns option on the import command, merging and validating the ARNs with the same semantics as cdk deploy before forwarding them to the change set. Closes aws/aws-cdk#23548 --- .../lib/api/resource-import/importer.ts | 7 ++ .../test/api/resource-import/import.test.ts | 34 ++++++ packages/aws-cdk/README.md | 4 + packages/aws-cdk/lib/cli/cdk-toolkit.ts | 29 +++++ packages/aws-cdk/lib/cli/cli-config.ts | 1 + .../aws-cdk/lib/cli/cli-type-registry.json | 4 + packages/aws-cdk/lib/cli/cli.ts | 1 + .../aws-cdk/lib/cli/convert-to-user-input.ts | 2 + .../lib/cli/parse-command-line-arguments.ts | 6 + packages/aws-cdk/lib/cli/user-input.ts | 7 ++ packages/aws-cdk/test/cli/cdk-toolkit.test.ts | 108 +++++++++++++++++- .../aws-cdk/test/cli/import-options.test.ts | 31 +++++ 12 files changed, 233 insertions(+), 1 deletion(-) create mode 100644 packages/aws-cdk/test/cli/import-options.test.ts diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/resource-import/importer.ts b/packages/@aws-cdk/toolkit-lib/lib/api/resource-import/importer.ts index 0dadcf90b..f7dac743c 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/resource-import/importer.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/resource-import/importer.ts @@ -58,6 +58,13 @@ export interface ImportDeploymentOptions { * @default true */ readonly rollback?: boolean; + + /** + * ARNs of SNS topics that CloudFormation will notify with stack related events + * + * @default - No notifications + */ + readonly notificationArns?: string[]; } /** diff --git a/packages/@aws-cdk/toolkit-lib/test/api/resource-import/import.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/resource-import/import.test.ts index 65b47c56e..a562624d1 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/resource-import/import.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/resource-import/import.test.ts @@ -226,6 +226,40 @@ test('asks human to confirm automatic import if identifier is in template', asyn }); }); +test('notification arns are passed to the import deployment', async () => { + // GIVEN + givenCurrentStack(STACK_WITH_QUEUE.stackName, { Resources: {} }); + const importer = new ResourceImporter(STACK_WITH_QUEUE, props); + const { additions } = await importer.discoverImportableResources(); + const importMap: ImportMap = { + importResources: additions, + resourceMap: { + MyQueue: { QueueName: 'TheQueueName' }, + }, + }; + + // WHEN + await advanceTime(importer.importResourcesFromMap(importMap, { + notificationArns: ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic'], + })); + + // THEN + expect(mockCloudFormationClient).toHaveReceivedCommandWith(CreateChangeSetCommand, { + ChangeSetName: expect.any(String), + StackName: STACK_WITH_QUEUE.stackName, + TemplateBody: expect.any(String), + ChangeSetType: 'IMPORT', + NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic'], + ResourcesToImport: [ + { + LogicalResourceId: 'MyQueue', + ResourceIdentifier: { QueueName: 'TheQueueName' }, + ResourceType: 'AWS::SQS::Queue', + }, + ], + }); +}); + test('issue 1575: importing resources does not mutate the cached deployed template', async () => { // GIVEN a deployed stack with no resources, importing MyQueue givenCurrentStack(STACK_WITH_QUEUE.stackName, { Resources: {} }); diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index b065bfa58..356665bbf 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -818,6 +818,10 @@ To import an existing resource to a CDK stack, follow the following steps: 5. When `cdk import` reports success, the resource is managed by CDK. Any subsequent changes in the construct configuration will be reflected on the resource. +Use `--notification-arns` to specify ARNs of SNS topics that CloudFormation will +notify with stack related events during the import. These are added to ARNs +specified with the `notificationArns` stack property. + NOTE: You can also import existing resources by passing `--import-existing-resources` to `cdk deploy`. This parameter only works for resources that support custom physical names, such as S3 Buckets, DynamoDB Tables, etc... diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 68c415a9b..69cde76f9 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -860,6 +860,21 @@ export class CdkToolkit { return; } + // Following are the same semantics we apply with respect to Notification ARNs (dictated by the SDK) + // + // - undefined => cdk ignores it, as if it wasn't supported (allows external management). + // - []: => cdk manages it, and the user wants to wipe it out. + // - ['arn-1'] => cdk manages it, and the user wants to set it to ['arn-1']. + const notificationArns = (!!options.notificationArns || !!stack.notificationArns) + ? (options.notificationArns ?? []).concat(stack.notificationArns ?? []) + : undefined; + + for (const notificationArn of notificationArns ?? []) { + if (!validateSnsTopicArn(notificationArn)) { + throw new ToolkitError('InvalidSnsTopicArn', `Notification arn ${notificationArn} is not a valid arn for an SNS topic`); + } + } + // Import the resources according to the given mapping await this.ioHost.asIoHelper().defaults.info('%s: importing resources into stack...', chalk.bold(stack.displayName)); const tags = tagsForStack(stack); @@ -869,6 +884,7 @@ export class CdkToolkit { deploymentMethod: options.deploymentMethod, usePreviousParameters: true, rollback: options.rollback, + notificationArns, }); // Notify user of next steps @@ -1776,6 +1792,19 @@ export interface OrphanOptions { } export interface ImportOptions extends CfnDeployOptions { + /** + * ARNs of SNS topics that CloudFormation will notify with stack related events. + * + * The following semantics apply (dictated by the SDK): + * + * - undefined => cdk ignores it, as if it wasn't supported (allows external management). + * - []: => cdk manages it, and the user wants to wipe it out. + * - ['arn-1'] => cdk manages it, and the user wants to set it to ['arn-1']. + * + * @default - No notifications + */ + readonly notificationArns?: string[]; + /** * Build a physical resource mapping and write it to the given file, without performing the actual import operation * diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index 470587236..a1d93b954 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -295,6 +295,7 @@ export async function makeConfig(): Promise { options: { 'execute': { type: 'boolean', desc: 'Whether to execute the change set (--no-execute will NOT execute the change set)', default: true }, 'change-set-name': { type: 'string', desc: 'Name of the CloudFormation change set to create' }, + 'notification-arns': { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events. These will be added to ARNs specified with the \'notificationArns\' stack property.' }, 'toolkit-stack-name': { type: 'string', desc: 'The name of the CDK toolkit stack to create', requiresArg: true }, 'rollback': { type: 'boolean', diff --git a/packages/aws-cdk/lib/cli/cli-type-registry.json b/packages/aws-cdk/lib/cli/cli-type-registry.json index 02f71f7ff..c5ccf3e36 100644 --- a/packages/aws-cdk/lib/cli/cli-type-registry.json +++ b/packages/aws-cdk/lib/cli/cli-type-registry.json @@ -727,6 +727,10 @@ "type": "string", "desc": "Name of the CloudFormation change set to create" }, + "notification-arns": { + "type": "array", + "desc": "ARNs of SNS topics that CloudFormation will notify with stack related events. These will be added to ARNs specified with the 'notificationArns' stack property." + }, "toolkit-stack-name": { "type": "string", "desc": "The name of the CDK toolkit stack to create", diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 42da6f799..4fcfc7319 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -513,6 +513,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise): any { type: 'string', desc: 'Name of the CloudFormation change set to create', }) + .option('notification-arns', { + type: 'array', + desc: "ARNs of SNS topics that CloudFormation will notify with stack related events. These will be added to ARNs specified with the 'notificationArns' stack property.", + nargs: 1, + requiresArg: true, + }) .option('toolkit-stack-name', { default: undefined, type: 'string', diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index ee69df1fc..8f5ade2f3 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1177,6 +1177,13 @@ export interface ImportOptions { */ readonly changeSetName?: string; + /** + * ARNs of SNS topics that CloudFormation will notify with stack related events. These will be added to ARNs specified with the 'notificationArns' stack property. + * + * @default - undefined + */ + readonly notificationArns?: Array; + /** * The name of the CDK toolkit stack to create * diff --git a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts index 3952fc2e6..7fe0ca193 100644 --- a/packages/aws-cdk/test/cli/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cli/cdk-toolkit.test.ts @@ -73,7 +73,7 @@ import { CreateChangeSetCommand, DeleteChangeSetCommand, DescribeChangeSetComman import { GetParameterCommand } from '@aws-sdk/client-ssm'; import type { AwsStub } from 'aws-sdk-client-mock'; import * as fs from 'fs-extra'; -import { type Template, type SdkProvider, WorkGraphBuilder } from '../../lib/api'; +import { type Template, type SdkProvider, ResourceImporter, WorkGraphBuilder } from '../../lib/api'; import { Bootstrapper, type BootstrapSource } from '../../lib/api/bootstrap'; import type { DeployStackResult, @@ -1908,6 +1908,112 @@ describe('deploy', () => { }); }); +describe('import', () => { + const mapping = { MyQueue: { QueueName: 'TheQueueName' } }; + let discoverSpy: jest.SpyInstance; + let loadSpy: jest.SpyInstance; + let importFromMapSpy: jest.SpyInstance; + + beforeEach(() => { + const additions = [{ logicalId: 'MyQueue' }] as any; + discoverSpy = jest.spyOn(ResourceImporter.prototype, 'discoverImportableResources').mockResolvedValue({ + additions, + hasNonAdditions: false, + nonAdditionNames: [], + diffFormatter: {} as any, + }); + loadSpy = jest.spyOn(ResourceImporter.prototype, 'loadResourceIdentifiers').mockResolvedValue({ + importResources: additions, + resourceMap: mapping, + }); + importFromMapSpy = jest.spyOn(ResourceImporter.prototype, 'importResourcesFromMap').mockResolvedValue(undefined); + }); + + afterEach(() => { + discoverSpy.mockRestore(); + loadSpy.mockRestore(); + importFromMapSpy.mockRestore(); + }); + + describe('sns notification arns', () => { + test('with sns notification arns as options', async () => { + // GIVEN + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + const toolkit = defaultToolkitSetup(); + + // WHEN + await toolkit.import({ + selector: { patterns: ['Test-Stack-A-Display-Name'] }, + deploymentMethod: { method: 'change-set' }, + resourceMappingInline: JSON.stringify(mapping), + notificationArns, + }); + + // THEN + expect(importFromMapSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ notificationArns }), + ); + }); + + test('fail with incorrect sns notification arns as options', async () => { + // GIVEN + const notificationArns = ['arn:::cfn-my-cool-topic']; + const toolkit = defaultToolkitSetup(); + + // WHEN + await expect(() => + toolkit.import({ + selector: { patterns: ['Test-Stack-A-Display-Name'] }, + deploymentMethod: { method: 'change-set' }, + resourceMappingInline: JSON.stringify(mapping), + notificationArns, + }), + ).rejects.toThrow('Notification arn arn:::cfn-my-cool-topic is not a valid arn for an SNS topic'); + + // THEN + expect(importFromMapSpy).not.toHaveBeenCalled(); + }); + + test('with sns notification arns in the executable and as options', async () => { + // GIVEN + const notificationArns = [ + 'arn:aws:sns:us-east-2:444455556666:MyTopic', + 'arn:aws:sns:eu-west-1:111155556666:my-great-topic', + ]; + cloudExecutable = await MockCloudExecutable.create({ + stacks: [MockStack.MOCK_STACK_WITH_NOTIFICATION_ARNS], + }); + const toolkit = new CdkToolkit({ + ioHost, + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments: new FakeCloudFormation(), + }); + + // WHEN + await toolkit.import({ + selector: { patterns: ['Test-Stack-Notification-Arns'] }, + deploymentMethod: { method: 'change-set' }, + resourceMappingInline: JSON.stringify(mapping), + notificationArns, + }); + + // THEN + expect(importFromMapSpy).toHaveBeenCalledWith( + expect.anything(), + expect.objectContaining({ + notificationArns: notificationArns.concat(['arn:aws:sns:bermuda-triangle-1337:123456789012:MyTopic']), + }), + ); + }); + }); +}); + describe('watch', () => { test("fails when no 'watch' settings are found", async () => { const toolkit = defaultToolkitSetup(); diff --git a/packages/aws-cdk/test/cli/import-options.test.ts b/packages/aws-cdk/test/cli/import-options.test.ts new file mode 100644 index 000000000..46de9fa23 --- /dev/null +++ b/packages/aws-cdk/test/cli/import-options.test.ts @@ -0,0 +1,31 @@ +import * as cdkToolkitModule from '../../lib/cli/cdk-toolkit'; +import { exec } from '../../lib/cli/cli'; + +// Prevent actual toolkit operations +let importSpy: jest.SpyInstance; + +beforeEach(() => { + importSpy = jest.spyOn(cdkToolkitModule.CdkToolkit.prototype, 'import').mockResolvedValue(); +}); + +afterEach(() => { + jest.restoreAllMocks(); +}); + +describe('import --notification-arns', () => { + test('passes notification arns to CdkToolkit.import', async () => { + await exec(['import', '--app', 'echo', '--notification-arns', 'arn:aws:sns:us-east-2:444455556666:MyTopic', 'MyStack']); + + expect(importSpy).toHaveBeenCalledWith(expect.objectContaining({ + notificationArns: ['arn:aws:sns:us-east-2:444455556666:MyTopic'], + })); + }); + + test('notification arns are not set by default', async () => { + await exec(['import', '--app', 'echo', 'MyStack']); + + expect(importSpy).toHaveBeenCalledWith(expect.objectContaining({ + notificationArns: undefined, + })); + }); +});