Skip to content
Open
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
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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: {} });
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -849,6 +849,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...
Expand Down
29 changes: 29 additions & 0 deletions packages/aws-cdk/lib/cli/cdk-toolkit.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -869,6 +884,7 @@ export class CdkToolkit {
deploymentMethod: options.deploymentMethod,
usePreviousParameters: true,
rollback: options.rollback,
notificationArns,
});

// Notify user of next steps
Expand Down Expand Up @@ -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
*
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli-config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,7 @@ export async function makeConfig(): Promise<CliConfig> {
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',
Expand Down
4 changes: 4 additions & 0 deletions packages/aws-cdk/lib/cli/cli-type-registry.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
1 change: 1 addition & 0 deletions packages/aws-cdk/lib/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -513,6 +513,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise<n
selector,
toolkitStackName,
roleArn: args.roleArn,
notificationArns: args.notificationArns,
deploymentMethod: {
method: 'change-set',
execute: args.execute,
Expand Down
2 changes: 2 additions & 0 deletions packages/aws-cdk/lib/cli/convert-to-user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ export function convertYargsToUserInput(args: any): UserInput {
commandOptions = {
execute: args.execute,
changeSetName: args.changeSetName,
notificationArns: args.notificationArns,
toolkitStackName: args.toolkitStackName,
rollback: args.rollback,
force: args.force,
Expand Down Expand Up @@ -512,6 +513,7 @@ export function convertConfigToUserInput(config: any): UserInput {
const importOptions = {
execute: config.import?.execute,
changeSetName: config.import?.changeSetName,
notificationArns: config.import?.notificationArns,
toolkitStackName: config.import?.toolkitStackName,
rollback: config.import?.rollback,
force: config.import?.force,
Expand Down
6 changes: 6 additions & 0 deletions packages/aws-cdk/lib/cli/parse-command-line-arguments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -735,6 +735,12 @@ export function parseCommandLineArguments(args: Array<string>): 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',
Expand Down
7 changes: 7 additions & 0 deletions packages/aws-cdk/lib/cli/user-input.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string>;

/**
* The name of the CDK toolkit stack to create
*
Expand Down
108 changes: 107 additions & 1 deletion packages/aws-cdk/test/cli/cdk-toolkit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down
31 changes: 31 additions & 0 deletions packages/aws-cdk/test/cli/import-options.test.ts
Original file line number Diff line number Diff line change
@@ -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,
}));
});
});
Loading