diff --git a/.projenrc.ts b/.projenrc.ts index 8387f5535..74b009a5d 100644 --- a/.projenrc.ts +++ b/.projenrc.ts @@ -1293,7 +1293,7 @@ const CLI_LIB_EXCLUDE_PATTERNS = [ 'lib/init-templates/*/typescript/*/*.template.ts', ]; -const cliLib = configureProject( +const cliLibAlpha = configureProject( new yarn.TypeScriptWorkspace({ ...genericCdkProps(), parent: repo, @@ -1329,12 +1329,12 @@ const cliLib = configureProject( ); // Do include all .ts files inside init-templates -cliLib.npmignore?.addPatterns( +cliLibAlpha.npmignore?.addPatterns( '!lib/init-templates/**/*.ts', '!lib/api/bootstrap/bootstrap-template.yaml', ); -cliLib.gitignore.addPatterns( +cliLibAlpha.gitignore.addPatterns( ...ADDITIONAL_CLI_IGNORE_PATTERNS, 'lib/**/*.yaml', 'lib/**/*.yml', @@ -1342,7 +1342,7 @@ cliLib.gitignore.addPatterns( 'cdk.out', ); -new JsiiBuild(cliLib, { +new JsiiBuild(cliLibAlpha, { jsiiVersion: TYPESCRIPT_VERSION, publishToNuget: { dotNetNamespace: 'Amazon.CDK.Cli.Lib.Alpha', @@ -1362,29 +1362,33 @@ new JsiiBuild(cliLib, { pypiClassifiers: [ 'Framework :: AWS CDK', 'Framework :: AWS CDK :: 2', + 'Development Status :: 7 - Inactive', ], publishToGo: { moduleName: 'github.com/aws/aws-cdk-go', packageName: 'awscdkclilibalpha', }, rosettaStrict: true, - stability: Stability.EXPERIMENTAL, + stability: Stability.DEPRECATED, composite: true, excludeTypescript: CLI_LIB_EXCLUDE_PATTERNS, }); +// the package is deprecated +cliLibAlpha.package.addField('deprecated', 'Deprecated in favor of @aws-cdk/toolkit-lib, a newer approach providing similar functionality to this package. Please migrate.'); + // clilib needs to bundle some resources, same as the CLI -cliLib.postCompileTask.exec('node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute "^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$"'); -cliLib.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/'); +cliLibAlpha.postCompileTask.exec('node-bundle validate --external=fsevents:optional --entrypoint=lib/index.js --fix --dont-attribute "^@aws-cdk/|^cdk-assets$|^cdk-cli-wrapper$|^aws-cdk$"'); +cliLibAlpha.postCompileTask.exec('mkdir -p ./lib/api/bootstrap/ && cp ../../aws-cdk/lib/api/bootstrap/bootstrap-template.yaml ./lib/api/bootstrap/'); for (const resourceCommand of includeCliResourcesCommands) { - cliLib.postCompileTask.exec(resourceCommand); + cliLibAlpha.postCompileTask.exec(resourceCommand); } -cliLib.postCompileTask.exec('cp $(node -p \'require.resolve("aws-cdk/build-info.json")\') .'); -cliLib.postCompileTask.exec('esbuild --bundle lib/index.ts --target=node18 --platform=node --external:fsevents --minify-whitespace --outfile=lib/main.js'); -cliLib.postCompileTask.exec('node ./lib/main.js >/dev/null /dev/null { await shell(['node', require.resolve('npm'), 'install', `aws-cdk@${this.range}`], { cwd: tempDir, + show: 'error', }); const installedVersion = await npmQueryInstalledVersion('aws-cdk', tempDir); diff --git a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/library-globalinstall-source.ts b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/library-globalinstall-source.ts index ac3996572..44169c04e 100644 --- a/packages/@aws-cdk-testing/cli-integ/lib/package-sources/library-globalinstall-source.ts +++ b/packages/@aws-cdk-testing/cli-integ/lib/package-sources/library-globalinstall-source.ts @@ -23,6 +23,7 @@ export class RunnerLibraryGlobalInstallSource implements IRunnerSource { + // GIVEN + await fixture.cdkDeploy('base-1', { + modEnv: { + VERSION: 'v1', + }, + }); + + // THEN + let diff = await fixture.cdk(['diff', '--import-existing-resources', fixture.fullStackName('base-1')], { + modEnv: { + VERSION: 'v2', + }, + }); + + // Assert there are no changes and diff shows import + expect(diff).not.toContain('There were no differences'); + expect(diff).toContain('[←]'); + expect(diff).toContain('import'); + + // THEN + diff = await fixture.cdk(['diff', fixture.fullStackName('base-1')], { + modEnv: { + VERSION: 'v2', + }, + }); + + // Assert there are no changes and diff shows add + expect(diff).not.toContain('There were no differences'); + expect(diff).toContain('[+]'); + + // Deploy the stack with v3 to set table removal policy as destroy + await fixture.cdkDeploy('base-1', { + modEnv: { + VERSION: 'v3', + }, + }); + }), +); diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-deletes-unused-s3-objects-rollback.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-deletes-unused-s3-objects-rollback.integtest.ts new file mode 100644 index 000000000..6fea1e082 --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-deletes-unused-s3-objects-rollback.integtest.ts @@ -0,0 +1,69 @@ +import { ListObjectsV2Command, PutObjectTaggingCommand } from '@aws-sdk/client-s3'; +import { integTest, withoutBootstrap, randomString } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +const DAY = 24 * 60 * 60 * 1000; +const S3_ISOLATED_TAG = 'aws-cdk:isolated'; + +integTest( + 'Garbage Collection deletes unused s3 objects with rollback-buffer-days', + withoutBootstrap(async (fixture) => { + const toolkitStackName = fixture.bootstrapStackName; + const bootstrapBucketName = `aws-cdk-garbage-collect-integ-test-bckt-${randomString()}`; + fixture.rememberToDeleteBucket(bootstrapBucketName); // just in case + + await fixture.cdkBootstrapModern({ + toolkitStackName, + bootstrapBucketName, + }); + + await fixture.cdkDeploy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + fixture.log('Setup complete!'); + + await fixture.cdkDestroy('lambda', { + options: [ + '--context', `bootstrapBucket=${bootstrapBucketName}`, + '--context', `@aws-cdk/core:bootstrapQualifier=${fixture.qualifier}`, + '--toolkit-stack-name', toolkitStackName, + '--force', + ], + }); + + // Pretend the assets were tagged with an old date > 1 day ago so that garbage collection + // should pick up and delete asset even with rollbackBufferDays=1 + const res = await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })); + for (const contents of res.Contents ?? []) { + await fixture.aws.s3.send(new PutObjectTaggingCommand({ + Bucket: bootstrapBucketName, + Key: contents.Key, + Tagging: { + TagSet: [{ + Key: S3_ISOLATED_TAG, + Value: String(Date.now() - (30 * DAY)), + }], + }, + })); + } + + await fixture.cdkGarbageCollect({ + rollbackBufferDays: 1, + type: 's3', + bootstrapStackName: toolkitStackName, + }); + fixture.log('Garbage collection complete!'); + + // assert that the bootstrap bucket is empty + await fixture.aws.s3.send(new ListObjectsV2Command({ Bucket: bootstrapBucketName })) + .then((result) => { + expect(result.Contents).toBeUndefined(); + }); + }), +); diff --git a/packages/@aws-cdk/cli-lib-alpha/README.md b/packages/@aws-cdk/cli-lib-alpha/README.md index d58bc2aee..a7f2e99e3 100644 --- a/packages/@aws-cdk/cli-lib-alpha/README.md +++ b/packages/@aws-cdk/cli-lib-alpha/README.md @@ -1,31 +1,27 @@ -# AWS CDK CLI Library +# AWS CDK CLI Library (deprecated) + --- -![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) +![@aws-cdk/cli-lib-lpha: Deprecated](https://img.shields.io/badge/@aws--cdk/cli--lib--alpha-deprectated-red.svg?style=for-the-badge) -> The APIs of higher level constructs in this module are experimental and under active development. -> They are subject to non-backward compatible changes or removal in any future version. These are -> not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be -> announced in the release notes. This means that while you may use them, you may need to update -> your source code when upgrading to a newer version of this package. +> This package has been deprecated in favor of [@aws-cdk/toolkit-lib](https://github.com/aws/aws-cdk-cli/issues/155), +> a newer approach providing similar functionality to what this package offered. +> Please migrate as soon as possible. +> For any migration problems, please open [an issue](https://github.com/aws/aws-cdk-cli/issues/new/choose). +> We are committed to supporting the same feature set that this package offered. --- -## ⚠️ Experimental module +## ⚠️ Deprecated module -This package is highly experimental. Expect frequent API changes and incomplete features. -Known issues include: +This package is has been deprecated. +Already published versions can be used, but no support is provided whatsoever and we will soon stop publishing new versions. -- **JavaScript/TypeScript only**\ - The jsii packages are currently not in a working state. -- **No useful return values**\ - All output is currently printed to stdout/stderr -- **Missing or Broken options**\ - Some CLI options might not be available in this package or broken +Instead, please use [@aws-cdk/toolkit-lib](https://github.com/aws/aws-cdk-cli/issues/155). ## Overview @@ -38,6 +34,17 @@ Currently the package includes implementations for: - `cdk destroy` - `cdk list` +## Known issues + +- **JavaScript/TypeScript only**\ + The jsii packages are currently not in a working state. +- **No useful return values**\ + All output is currently printed to stdout/stderr +- **Missing or Broken options**\ + Some CLI options might not be available in this package or broken + +Due to the deprecation of the package, this issues will not be resolved. + ## Setup ### AWS CDK app directory diff --git a/packages/@aws-cdk/cli-lib-alpha/package.json b/packages/@aws-cdk/cli-lib-alpha/package.json index 32cf3f575..7fc009c73 100644 --- a/packages/@aws-cdk/cli-lib-alpha/package.json +++ b/packages/@aws-cdk/cli-lib-alpha/package.json @@ -83,7 +83,8 @@ }, "version": "0.0.0", "types": "lib/index.d.ts", - "stability": "experimental", + "stability": "deprecated", + "deprecated": "Deprecated in favor of @aws-cdk/toolkit-lib, a newer approach providing similar functionality to this package. Please migrate.", "jsii": { "outdir": "dist", "targets": { @@ -99,7 +100,8 @@ "module": "aws_cdk.cli_lib_alpha", "classifiers": [ "Framework :: AWS CDK", - "Framework :: AWS CDK :: 2" + "Framework :: AWS CDK :: 2", + "Development Status :: 7 - Inactive" ] }, "dotnet": { 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 7c8a20b72..e66b1ee3e 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/actions/diff/index.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/actions/diff/index.ts @@ -26,6 +26,13 @@ export interface ChangeSetDiffOptions extends CloudFormationDiffOptions { * @default - no parameters */ readonly parameters?: { [name: string]: string | undefined }; + + /** + * Whether or not the change set imports resources that already exist + * + * @default false + */ + readonly importExistingResources?: boolean; } export interface LocalFileDiffOptions { 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 114c294f4..42acf69e9 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 @@ -89,6 +89,7 @@ async function cfnDiff( resourcesToImport, methodOptions.parameters, methodOptions.fallbackToTemplate, + methodOptions.importExistingResources, ) : undefined; templateInfos.push({ @@ -111,6 +112,7 @@ async function changeSetDiff( resourcesToImport?: ResourcesToImport, parameters: { [name: string]: string | undefined } = {}, fallBackToTemplate: boolean = true, + importExistingResources: boolean = false, ): Promise { let stackExists = false; try { @@ -139,6 +141,7 @@ async function changeSetDiff( parameters: parameters, resourcesToImport, failOnError: !fallBackToTemplate, + importExistingResources, }); } else { if (!fallBackToTemplate) { diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/deployments/cfn-api.ts b/packages/@aws-cdk/toolkit-lib/lib/api/deployments/cfn-api.ts index 82c9be090..2bb5f6325 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/deployments/cfn-api.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/deployments/cfn-api.ts @@ -142,6 +142,7 @@ export type PrepareChangeSetOptions = { sdkProvider: SdkProvider; parameters: { [name: string]: string | undefined }; resourcesToImport?: ResourcesToImport; + importExistingResources?: boolean; /** * Default behavior is to log AWS CloudFormation errors and move on. Set this property to true to instead * fail on errors received by AWS CloudFormation. @@ -161,6 +162,7 @@ export type CreateChangeSetOptions = { bodyParameter: TemplateBodyParameter; parameters: { [name: string]: string | undefined }; resourcesToImport?: ResourceToImport[]; + importExistingResources?: boolean; role?: string; }; @@ -244,6 +246,7 @@ async function uploadBodyParameterAndCreateChangeSet( bodyParameter, parameters: options.parameters, resourcesToImport: options.resourcesToImport, + importExistingResources: options.importExistingResources, role: executionRoleArn, }); } catch (e: any) { @@ -309,6 +312,7 @@ export async function createChangeSet( TemplateBody: options.bodyParameter.TemplateBody, Parameters: stackParams.apiParameters, ResourcesToImport: options.resourcesToImport, + ImportExistingResources: options.importExistingResources, RoleARN: options.role, Tags: toCfnTags(options.stack.tags), Capabilities: ['CAPABILITY_IAM', 'CAPABILITY_NAMED_IAM', 'CAPABILITY_AUTO_EXPAND'], diff --git a/packages/@aws-cdk/toolkit-lib/lib/api/garbage-collection/garbage-collector.ts b/packages/@aws-cdk/toolkit-lib/lib/api/garbage-collection/garbage-collector.ts index c7da36ce3..338d94ecb 100644 --- a/packages/@aws-cdk/toolkit-lib/lib/api/garbage-collection/garbage-collector.ts +++ b/packages/@aws-cdk/toolkit-lib/lib/api/garbage-collection/garbage-collector.ts @@ -55,7 +55,8 @@ export class ImageAsset { if (!dateIsolated || dateIsolated == '') { return false; } - return new Date(dateIsolated) < date; + + return new Date(Number(dateIsolated)) < date; } public buildImageTag(inc: number) { @@ -115,7 +116,8 @@ export class ObjectAsset { if (!tagValue || tagValue == '') { return false; } - return new Date(tagValue) < date; + + return new Date(Number(tagValue)) < date; } } 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 1f110525d..d784f49b5 100644 --- a/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/actions/diff.test.ts @@ -5,6 +5,7 @@ import * as awsauth from '../../lib/api/aws-auth/private'; import { StackSelectionStrategy } from '../../lib/api/cloud-assembly'; import * as deployments from '../../lib/api/deployments'; import { RequireApproval } from '../../lib/api/require-approval'; +import { cfnApi } from '../../lib/api/shared-private'; import { Toolkit } from '../../lib/toolkit'; import { builderFixture, disposableCloudAssemblySource, TestIoHost } from '../_helpers'; import { MockSdk, restoreSdkMocksToDefault, setDefaultSTSMocks } from '../_helpers/mock-sdk'; @@ -242,6 +243,35 @@ describe('diff', () => { })); }); + test('ChangeSet diff method with import existing resources option enabled', async () => { + // Setup mock BEFORE calling the function + const createDiffChangeSetMock = jest.spyOn(cfnApi, 'createDiffChangeSet').mockImplementationOnce(async () => { + return { + $metadata: {}, + Changes: [ + { + ResourceChange: { + Action: 'Import', + LogicalResourceId: 'MyBucketF68F3FF0', + }, + }, + ], + }; + }); + + // WHEN + ioHost.level = 'debug'; + const cx = await builderFixture(toolkit, 'stack-with-bucket'); + const result = await toolkit.diff(cx, { + stacks: { strategy: StackSelectionStrategy.ALL_STACKS }, + method: DiffMethod.ChangeSet({ importExistingResources: true }), + }); + + // THEN + expect(createDiffChangeSetMock).toHaveBeenCalled(); + expect(result.Stack1.resources.get('MyBucketF68F3FF0').isImport).toBe(true); + }); + test('ChangeSet diff method throws if changeSet fails and fallBackToTemplate = false', async () => { // WHEN const cx = await builderFixture(toolkit, 'stack-with-bucket'); diff --git a/packages/@aws-cdk/toolkit-lib/test/api/garbage-collection/garbage-collection.test.ts b/packages/@aws-cdk/toolkit-lib/test/api/garbage-collection/garbage-collection.test.ts index 739f3cc21..242d66571 100644 --- a/packages/@aws-cdk/toolkit-lib/test/api/garbage-collection/garbage-collection.test.ts +++ b/packages/@aws-cdk/toolkit-lib/test/api/garbage-collection/garbage-collection.test.ts @@ -916,7 +916,7 @@ describe('Garbage Collection with large # of objects', () => { // of the 2000 in use assets, 1000 are tagged. s3Client.on(GetObjectTaggingCommand).callsFake((params) => ({ TagSet: Number(params.Key[params.Key.length - 5]) % 2 === 0 - ? [{ Key: S3_ISOLATED_TAG, Value: new Date(2000, 1, 1).toISOString() }] + ? [{ Key: S3_ISOLATED_TAG, Value: new Date(2000, 1, 1).getTime() }] : [], })); } diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 881eda183..6ee919717 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -189,6 +189,23 @@ The `change-set` flag will make `diff` create a change set and extract resource The `--no-change-set` mode will consider any change to a property that requires replacement to be a resource replacement, even if the change is purely cosmetic (like replacing a resource reference with a hardcoded arn). +The `--import-existing-resources` option will make `diff` create a change set and compare it using +the CloudFormation resource import mechanism. This allows CDK to detect changes and show report of resources that +will be imported rather added. Use this flag when preparing to import existing resources into a CDK stack to +ensure and validate the changes are correctly reflected by showing 'import'. + +```console +$ cdk diff +[+] AWS::DynamoDB::GlobalTable MyGlobalTable MyGlobalTable5DC12DB4 + +$ cdk diff --import-existing-resources +[←] AWS::DynamoDB::GlobalTable MyGlobalTable MyGlobalTable5DC12DB4 import +``` + +In the output above: +[+] indicates a new resource that would be created. +[←] indicates a resource that would be imported into the stack instead. + ### `cdk deploy` Deploys a stack of your CDK app to its environment. During the deployment, the toolkit will output progress diff --git a/packages/aws-cdk/lib/cli/cdk-toolkit.ts b/packages/aws-cdk/lib/cli/cdk-toolkit.ts index 12e6b0fda..eb44098ab 100644 --- a/packages/aws-cdk/lib/cli/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cli/cdk-toolkit.ts @@ -213,6 +213,12 @@ export class CdkToolkit { throw new ToolkitError(`There is no file at ${options.templatePath}`); } + if (options.importExistingResources) { + throw new ToolkitError( + 'Can only use --import-existing-resources flag when comparing against deployed stacks.', + ); + } + const template = deserializeStructure(await fs.readFile(options.templatePath, { encoding: 'UTF-8' })); const formatter = new DiffFormatter({ ioHelper: asIoHelper(this.ioHost, 'diff'), @@ -287,6 +293,7 @@ export class CdkToolkit { sdkProvider: this.props.sdkProvider, parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]), resourcesToImport, + importExistingResources: options.importExistingResources, }); } else { debug( @@ -1514,6 +1521,13 @@ export interface DiffOptions { * @default true */ readonly changeSet?: boolean; + + /** + * Whether or not the change set imports resources that already exist. + * + * @default false + */ + readonly importExistingResources?: boolean; } interface CfnDeployOptions { diff --git a/packages/aws-cdk/lib/cli/cli-config.ts b/packages/aws-cdk/lib/cli/cli-config.ts index d39df8d1b..62ffa1bc8 100644 --- a/packages/aws-cdk/lib/cli/cli-config.ts +++ b/packages/aws-cdk/lib/cli/cli-config.ts @@ -336,6 +336,7 @@ export async function makeConfig(): Promise { 'processed': { type: 'boolean', desc: 'Whether to compare against the template with Transforms already processed', default: false }, '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 }, }, }, metadata: { diff --git a/packages/aws-cdk/lib/cli/cli.ts b/packages/aws-cdk/lib/cli/cli.ts index 80607a557..1b0da8fb7 100644 --- a/packages/aws-cdk/lib/cli/cli.ts +++ b/packages/aws-cdk/lib/cli/cli.ts @@ -258,6 +258,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise): any { 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.', + }) + .option('import-existing-resources', { + default: false, + type: 'boolean', + desc: 'Whether or not the change set imports resources that already exist', }), ) .command('metadata [STACK]', 'Returns all metadata associated with this stack') diff --git a/packages/aws-cdk/lib/cli/user-input.ts b/packages/aws-cdk/lib/cli/user-input.ts index b2a10eca1..d116b0adc 100644 --- a/packages/aws-cdk/lib/cli/user-input.ts +++ b/packages/aws-cdk/lib/cli/user-input.ts @@ -1152,6 +1152,13 @@ export interface DiffOptions { */ readonly changeSet?: boolean; + /** + * Whether or not the change set imports resources that already exist + * + * @default - false + */ + readonly importExistingResources?: boolean; + /** * Positional argument for diff */ diff --git a/packages/aws-cdk/test/commands/diff.test.ts b/packages/aws-cdk/test/commands/diff.test.ts index d852206b1..92d4fa2a1 100644 --- a/packages/aws-cdk/test/commands/diff.test.ts +++ b/packages/aws-cdk/test/commands/diff.test.ts @@ -96,8 +96,8 @@ describe('fixed template', () => { }); // THEN - const plainTextOutput = notifySpy.mock.calls[0][0].message.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); - expect(plainTextOutput.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')).toContain(`Resources + const plainTextOutput = notifySpy.mock.calls.map(x => x[0].message).join('\n').replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); + expect(plainTextOutput).toContain(`Resources [~] AWS::SomeService::SomeResource SomeResource └─ [~] Something ├─ [-] old-value @@ -111,6 +111,200 @@ describe('fixed template', () => { }); }); +describe('import existing resources', () => { + let createDiffChangeSet: jest.SpyInstance< + Promise, + [ioHelper: IoHelper, options: cfnApi.PrepareChangeSetOptions], + any + >; + + beforeEach(() => { + // Default implementations + cloudFormation = instanceMockFrom(Deployments); + cloudFormation.readCurrentTemplateWithNestedStacks.mockImplementation( + (_stackArtifact: CloudFormationStackArtifact) => { + return Promise.resolve({ + deployedRootTemplate: { + Resources: { + MyTable: { + Type: 'AWS::DynamoDB::Table', + Properties: { + TableName: 'MyTableName-12345ABC', + }, + DeletionPolicy: 'Retain', + }, + }, + }, + nestedStacks: {}, + }); + }, + ); + cloudFormation.stackExists = jest.fn().mockReturnValue(Promise.resolve(true)); + cloudExecutable = new MockCloudExecutable({ + stacks: [ + { + stackName: 'A', + template: { + Resources: { + MyGlobalTable: { + Type: 'AWS::DynamoDB::GlobalTable', + Properties: { + TableName: 'MyTableName-12345ABC', + }, + }, + }, + }, + }, + ], + }, undefined, ioHost); + + toolkit = new CdkToolkit({ + cloudExecutable, + deployments: cloudFormation, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + }); + }); + + test('import action in change set output', async () => { + createDiffChangeSet = jest.spyOn(cfnApi, 'createDiffChangeSet').mockImplementationOnce(async () => { + return { + $metadata: {}, + Changes: [ + { + ResourceChange: { + Action: 'Import', + LogicalResourceId: 'MyGlobalTable', + }, + }, + ], + }; + }); + + // WHEN + const exitCode = await toolkit.diff({ + stackNames: ['A'], + changeSet: true, + importExistingResources: true, + }); + + expect(createDiffChangeSet).toHaveBeenCalled(); + + // THEN + const plainTextOutput = notifySpy.mock.calls[0][0].message.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); + expect(plainTextOutput.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')).toContain(` +Resources +[-] AWS::DynamoDB::Table MyTable orphan +[←] AWS::DynamoDB::GlobalTable MyGlobalTable import +`); + + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('✨ Number of stacks with differences: 1'), + })); + expect(exitCode).toBe(0); + }); + + test('import action in change set output when not using --import-exsting-resources', async () => { + createDiffChangeSet = jest.spyOn(cfnApi, 'createDiffChangeSet').mockImplementationOnce(async () => { + return { + $metadata: {}, + Changes: [ + { + ResourceChange: { + Action: 'Add', + LogicalResourceId: 'MyGlobalTable', + }, + }, + ], + }; + }); + + // WHEN + const exitCode = await toolkit.diff({ + stackNames: ['A'], + changeSet: true, + importExistingResources: false, + }); + + expect(createDiffChangeSet).toHaveBeenCalled(); + + // THEN + const plainTextOutput = notifySpy.mock.calls[0][0].message.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); + expect(plainTextOutput.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')).toContain(` +Resources +[-] AWS::DynamoDB::Table MyTable orphan +[+] AWS::DynamoDB::GlobalTable MyGlobalTable +`); + + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('✨ Number of stacks with differences: 1'), + })); + expect(exitCode).toBe(0); + }); + + test('when invoked with no changeSet flag', async () => { + // WHEN + createDiffChangeSet = jest.spyOn(cfnApi, 'createDiffChangeSet').mockImplementationOnce(async () => { + return { + $metadata: {}, + Changes: [ + { + ResourceChange: { + Action: 'Add', + LogicalResourceId: 'MyGlobalTable', + }, + }, + ], + }; + }); + + const exitCode = await toolkit.diff({ + stackNames: ['A'], + changeSet: undefined, + importExistingResources: true, + }); + + expect(createDiffChangeSet).not.toHaveBeenCalled(); + + // THEN + const plainTextOutput = notifySpy.mock.calls[0][0].message.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); + expect(plainTextOutput.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')).toContain(` +Resources +[-] AWS::DynamoDB::Table MyTable orphan +[+] AWS::DynamoDB::GlobalTable MyGlobalTable +`); + + expect(notifySpy).toHaveBeenCalledWith(expect.objectContaining({ + message: expect.stringContaining('✨ Number of stacks with differences: 1'), + })); + expect(exitCode).toBe(0); + }); + + test('when invoked with local template path', async () => { + const templatePath = 'oldTemplate.json'; + const oldTemplate = { + Resources: { + SomeResource: { + Type: 'AWS::SomeService::SomeResource', + Properties: { + Something: 'old-value', + }, + }, + }, + }; + fs.writeFileSync(templatePath, JSON.stringify(oldTemplate)); + // WHEN + await expect(async () => { + await toolkit.diff({ + stackNames: ['A'], + changeSet: undefined, + templatePath: templatePath, + importExistingResources: true, + }); + }).rejects.toThrow(/Can only use --import-existing-resources flag when comparing against deployed stacks/); + }); +}); + describe('imports', () => { let createDiffChangeSet: jest.SpyInstance< Promise, diff --git a/packages/cdk-assets/README.md b/packages/cdk-assets/README.md index e291d2f4f..5517f0c3e 100644 --- a/packages/cdk-assets/README.md +++ b/packages/cdk-assets/README.md @@ -1,19 +1,5 @@ # cdk-assets - - ---- - -> V3 of cdk-assets is still under active development and is subject to non-backward compatible changes while -> being released with the `rc` suffix. -> -> These changes are not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes -> will be announced in the release notes. - ---- - - - A tool for publishing CDK assets to AWS environments. ## Overview