From 647d6860b5205649dcf75e53e13867099db7c7d9 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Thu, 15 May 2025 12:23:15 +0200 Subject: [PATCH 1/4] chore(cdk-assets): remove stability warning (#498) v3 has now been released officially --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- packages/cdk-assets/README.md | 14 -------------- 1 file changed, 14 deletions(-) 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 From 319f1ad0fe8cc7a4420291d3b831e8e5b91449e8 Mon Sep 17 00:00:00 2001 From: GZ Date: Thu, 15 May 2025 19:42:16 +0800 Subject: [PATCH 2/4] feat(cli): allow change set imports resources that already exist (#447) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Allow `cdk diff` command to create a change set that imports existing resources. The current `cdk diff` command implicitly calls CloudFormation change set creation, providing high-level details such as "add", "delete", "modify", "import", and etc. like the following: ```s $ cdk diff [-] AWS::DynamoDB::Table MyTable orphan [+] AWS::DynamoDB::GlobalTable MyGlobalTable add ``` However, when the resource is meant to be imported, the `cdk diff` command still shows this as add. Adding `cdk diff --import-existing-resources` flag to show the new resource being imported instead of `add`. ```s $ cdk diff --import-existing-resources [-] AWS::DynamoDB::Table MyTable orphan [←] AWS::DynamoDB::GlobalTable MyGlobalTable import ``` Here is the underlying CFN change set JSON output ```json [ { "type": "Resource", "resourceChange": { "action": "Import", # NOTE THAT THIS SHOWS "Import" "logicalResourceId": "MyTable794EDED1", "physicalResourceId": "DemoStack-MyTable794EDED1-11W4MR8VZ0UPE", "resourceType": "AWS::DynamoDB::GlobalTable", "replacement": "True", "scope": [], "details": [], "afterContext": "..." } }, { "type": "Resource", "resourceChange": { "policyAction": "Retain", # Note that this is "Retain" "action": "Remove", "logicalResourceId": "MyTable794EDED1", "physicalResourceId": "DemoStack-MyTable794EDED1-11W4MR8VZ0UPE", "resourceType": "AWS::DynamoDB::Table", "scope": [], "details": [], "beforeContext": "..." } } ] ``` --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --------- Signed-off-by: github-actions Co-authored-by: Momo Kornher Co-authored-by: Rico Huijbers --- .../resources/cdk-apps/import-app/app.js | 38 ++++ .../resources/cdk-apps/import-app/cdk.json | 7 + ...isting-resources-shows-import.integtest.ts | 45 ++++ .../toolkit-lib/lib/actions/diff/index.ts | 7 + .../lib/actions/diff/private/helpers.ts | 3 + .../lib/api/deployments/cfn-api.ts | 4 + .../toolkit-lib/test/actions/diff.test.ts | 30 +++ packages/aws-cdk/README.md | 17 ++ packages/aws-cdk/lib/cli/cdk-toolkit.ts | 14 ++ packages/aws-cdk/lib/cli/cli-config.ts | 1 + 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 | 5 + packages/aws-cdk/lib/cli/user-input.ts | 7 + packages/aws-cdk/test/commands/diff.test.ts | 198 +++++++++++++++++- 15 files changed, 377 insertions(+), 2 deletions(-) create mode 100755 packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/import-app/app.js create mode 100644 packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/import-app/cdk.json create mode 100644 packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff--import-existing-resources-shows-import.integtest.ts diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/import-app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/import-app/app.js new file mode 100755 index 000000000..947696d6d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/import-app/app.js @@ -0,0 +1,38 @@ +const cdk = require('aws-cdk-lib/core'); +const dynamodb = require('aws-cdk-lib/aws-dynamodb'); + +const stackPrefix = process.env.STACK_NAME_PREFIX; +if (!stackPrefix) { + throw new Error(`the STACK_NAME_PREFIX environment variable is required`); +} + +class BaseStack extends cdk.Stack { + constructor(scope, id, props) { + super(scope, id, props); + + // Create a random table name with prefix + if (process.env.VERSION == 'v2') { + new dynamodb.TableV2(this, 'MyGlobalTable', { + partitionKey: { + name: 'PK', + type: dynamodb.AttributeType.STRING, + }, + tableName: 'integ-test-import-app-base-table-1', + }); + } else { + new dynamodb.Table(this, 'MyTable', { + partitionKey: { + name: 'PK', + type: dynamodb.AttributeType.STRING, + }, + tableName: 'integ-test-import-app-base-table-1', + removalPolicy: process.env.VERSION == 'v1' ? cdk.RemovalPolicy.RETAIN : cdk.RemovalPolicy.DESTROY, + }); + } + } +} + +const app = new cdk.App(); +new BaseStack(app, `${stackPrefix}-base-1`); + +app.synth(); diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/import-app/cdk.json b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/import-app/cdk.json new file mode 100644 index 000000000..44809158d --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/import-app/cdk.json @@ -0,0 +1,7 @@ +{ + "app": "node app.js", + "versionReporting": false, + "context": { + "aws-cdk:enableDiffNoFail": "true" + } +} diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff--import-existing-resources-shows-import.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff--import-existing-resources-shows-import.integtest.ts new file mode 100644 index 000000000..1832b71bb --- /dev/null +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-cdk-diff--import-existing-resources-shows-import.integtest.ts @@ -0,0 +1,45 @@ +import { integTest, withSpecificFixture } from '../../lib'; + +jest.setTimeout(2 * 60 * 60_000); // Includes the time to acquire locks, worst-case single-threaded runtime + +integTest( + 'cdk diff --import-existing-resources show resource being imported', + withSpecificFixture('import-app', async (fixture) => { + // 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/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/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/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, From 7e23e64aa5df87a7988e21096bee3a61cc054f89 Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Thu, 15 May 2025 16:12:05 +0200 Subject: [PATCH 3/4] fix(cli-lib-alpha): deprecate package (#500) Deprecated in favor of @aws-cdk/toolkit-lib, a newer approach providing similar functionality to this package. Please migrate. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- .projenrc.ts | 30 +++++++------- README.md | 2 +- .../lib/package-sources/cli-npm-source.ts | 1 + .../library-globalinstall-source.ts | 1 + packages/@aws-cdk/cli-lib-alpha/README.md | 39 +++++++++++-------- packages/@aws-cdk/cli-lib-alpha/package.json | 6 ++- 6 files changed, 47 insertions(+), 32 deletions(-) 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 --- -![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": { From 66e0b2d2c838ff1d3d72e171805622243fc27f73 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Fri, 16 May 2025 03:25:33 -0400 Subject: [PATCH 4/4] fix(cli): gc does not delete isolated assets when rollback-buffer-days is set (#502) The `Date` class allows the following for values: `new Date(value: number | string | Date)`, but sometimes does not work as expected. That's essentially what we are doing because we are storing the string value of the date in the tags and the conversion back to a date doesn't work correctly. A deep dive into the issue with Dates: ```ts console.log(Date.now()) // 1747346265800 console.log(Date(1747346265800)) // Thu May 15 2025 18:06:11 GMT-0400 (Eastern Daylight Time) console.log(Date('1747346265800')) // Thu May 15 2025 18:05:57 GMT-0400 (Eastern Daylight Time) console.log(new Date(1747346265800)) // 2025-05-15T21:57:45.800Z console.log(new Date('1747346265800')) // Invalid Date ``` I seem to have been unlucky with how I tried to create the date from a string. Also, the resulting dates with a number of milliseconds versus a string of milliseconds is _slightly_ different, 18:06:11 versus 18:05:57... This _was_ attempted to be unit tested but in the unit test I mistakenly mocked the tag to be an ISO String which _is_ a string so it had no problem getting converted into a Date. This test has been updated. Because we pessimistically handle errors; we just treated this as a nondeletable asset. I have added an integ test that tests this scenario and confirmed on my own local set up that this fixes the issue. --- By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license --- ...es-unused-s3-objects-rollback.integtest.ts | 69 +++++++++++++++++++ .../garbage-collection/garbage-collector.ts | 6 +- .../garbage-collection.test.ts | 2 +- 3 files changed, 74 insertions(+), 3 deletions(-) create mode 100644 packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cdk-gc-deletes-unused-s3-objects-rollback.integtest.ts 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/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/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() }] : [], })); }