From 496bb1cb5a625c6b7d04f5b59181b7425302f86a Mon Sep 17 00:00:00 2001 From: paulhcsun <47882901+paulhcsun@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:11:50 -0800 Subject: [PATCH 1/9] chore(cdk): remove weekly metrics workflow (#32015) ### Description of changes This generated weekly report is no longer useful so removing the workflow that creates it. ### Description of how you validated changes ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .github/workflows/repo-metrics-weekly.yml | 58 ----------------------- 1 file changed, 58 deletions(-) delete mode 100644 .github/workflows/repo-metrics-weekly.yml diff --git a/.github/workflows/repo-metrics-weekly.yml b/.github/workflows/repo-metrics-weekly.yml deleted file mode 100644 index 2663dcc423095..0000000000000 --- a/.github/workflows/repo-metrics-weekly.yml +++ /dev/null @@ -1,58 +0,0 @@ -name: Weekly repo metrics -on: - workflow_dispatch: - schedule: - - cron: '0 9 * * MON' - -permissions: - issues: write - pull-requests: read - -jobs: - build: - # this workflow will always fail in forks; bail if this isn't running in the upstream - if: github.repository == 'aws/aws-cdk' - name: metrics - runs-on: ubuntu-latest - - steps: - - name: Get dates for last week - shell: bash - run: | - # Calculate the date of the previous Monday - PREVIOUS_MONDAY=$(date -d "7 days ago" "+%Y-%m-%d") - - # Calculate the date of the current Sunday - CURRENT_SUNDAY=$(date -d "1 day ago" "+%Y-%m-%d") - - # Set an environment variable with the date range - echo "$PREVIOUS_MONDAY..$CURRENT_SUNDAY" - echo "last_week=$PREVIOUS_MONDAY..$CURRENT_SUNDAY" >> "$GITHUB_ENV" - - - name: Report on issues - uses: github/issue-metrics@v3 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SEARCH_QUERY: 'repo:aws/aws-cdk is:issue created:${{ env.last_week }} -reason:"not planned"' - - - name: Create report for issues - uses: peter-evans/create-issue-from-file@v5 - with: - title: Weekly issue metrics report - token: ${{ secrets.GITHUB_TOKEN }} - content-filepath: ./issue_metrics.md - assignees: paulhcsun - - - name: Report on PRs - uses: github/issue-metrics@v3 - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - SEARCH_QUERY: 'repo:aws/aws-cdk is:pr created:${{ env.last_week }} -is:draft' - - - name: Create report for PRs - uses: peter-evans/create-issue-from-file@v5 - with: - title: Weekly PR metrics report - token: ${{ secrets.GITHUB_TOKEN }} - content-filepath: ./issue_metrics.md - assignees: paulhcsun From 1466f93af1b08e190353d177644c7832b03423b1 Mon Sep 17 00:00:00 2001 From: paulhcsun <47882901+paulhcsun@users.noreply.github.com> Date: Mon, 4 Nov 2024 16:49:37 -0800 Subject: [PATCH 2/9] chore(kinesisfirehose-alpha): update README to clarify default destination encryption setting (#32016) ### Description of changes Clarify in the README that the default encryption for the destination is disabled but that it uses the default encryption setting of the destination S3 bucket. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/@aws-cdk/aws-kinesisfirehose-alpha/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/@aws-cdk/aws-kinesisfirehose-alpha/README.md b/packages/@aws-cdk/aws-kinesisfirehose-alpha/README.md index c151a72eb9101..ae52fb3c43aab 100644 --- a/packages/@aws-cdk/aws-kinesisfirehose-alpha/README.md +++ b/packages/@aws-cdk/aws-kinesisfirehose-alpha/README.md @@ -326,7 +326,7 @@ encryption with AWS Key Management Service (AWS KMS) for encrypting delivered da Amazon S3. You can choose to not encrypt the data or to encrypt with a key from the list of AWS KMS keys that you own. For more information, see [Protecting Data Using Server-Side Encryption with AWS KMS–Managed Keys (SSE-KMS)](https://docs.aws.amazon.com/AmazonS3/latest/dev/UsingKMSEncryption.html). -Data is not encrypted by default. +By default, encryption isn’t directly enabled on the delivery stream; instead, it uses the default encryption settings of the destination S3 bucket. ```ts declare const bucket: s3.Bucket; From 090c7be6d5751f2723600ad8ae0d14df83b8f104 Mon Sep 17 00:00:00 2001 From: Matsuda Date: Tue, 5 Nov 2024 10:30:16 +0900 Subject: [PATCH 3/9] chore(bedrock): support Claude 3.5 Haiku model (#32014) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add a new foundation model. Ref: [Anthropic’s Claude 3.5 Haiku model now available in Amazon Bedrock](https://aws.amazon.com/about-aws/whats-new/2024/11/anthropics-claude-3-5-haiku-model-amazon-bedrock/) ```sh % aws bedrock get-foundation-model --model-identifier anthropic.claude-3-5-haiku-20241022-v1:0 --region us-west-2 { "modelDetails": { "modelArn": "arn:aws:bedrock:us-west-2::foundation-model/anthropic.claude-3-5-haiku-20241022-v1:0", "modelId": "anthropic.claude-3-5-haiku-20241022-v1:0", "modelName": "Claude 3.5 Haiku", "providerName": "Anthropic", // omit ``` ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk-lib/aws-bedrock/lib/foundation-model.ts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/packages/aws-cdk-lib/aws-bedrock/lib/foundation-model.ts b/packages/aws-cdk-lib/aws-bedrock/lib/foundation-model.ts index 18de3869296b5..3d6f71a3f9553 100644 --- a/packages/aws-cdk-lib/aws-bedrock/lib/foundation-model.ts +++ b/packages/aws-cdk-lib/aws-bedrock/lib/foundation-model.ts @@ -134,6 +134,9 @@ export class FoundationModelIdentifier { /** Base model "anthropic.claude-3-haiku-20240307-v1:0:48k" */ public static readonly ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0_48K = new FoundationModelIdentifier('anthropic.claude-3-haiku-20240307-v1:0:48k'); + /** Base model "anthropic.claude-3-5-haiku-20241022-v1:0" */ + public static readonly ANTHROPIC_CLAUDE_3_5_HAIKU_20241022_V1_0 = new FoundationModelIdentifier('anthropic.claude-3-5-haiku-20241022-v1:0'); + /** Base model "anthropic.claude-3-haiku-20240307-v1:0:200k" */ public static readonly ANTHROPIC_CLAUDE_3_HAIKU_20240307_V1_0_200K = new FoundationModelIdentifier('anthropic.claude-3-haiku-20240307-v1:0:200k'); From ae29bb5bceaf3cb65c2adbaeaac13bbac32d186d Mon Sep 17 00:00:00 2001 From: Xia Zhao <78883180+xazhao@users.noreply.github.com> Date: Tue, 5 Nov 2024 00:05:18 -0800 Subject: [PATCH 4/9] chore(s3): add more details to prune and destinationKeyPrefix property (#32011) ### Reason for this change `prune` and `destinationKeyPrefix` properties documentation is not super clear for users. Adding more details to behaviors when setting/unsetting the property. ### Description of changes ### Description of how you validated changes ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-s3-deployment/lib/bucket-deployment.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts b/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts index d10bfb249fed0..cd32d6017be51 100644 --- a/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts +++ b/packages/aws-cdk-lib/aws-s3-deployment/lib/bucket-deployment.ts @@ -32,9 +32,12 @@ export interface BucketDeploymentProps { readonly destinationBucket: s3.IBucket; /** - * Key prefix in the destination bucket. + * Key prefix in the destination bucket. Must be <=104 characters * - * Must be <=104 characters + * If it's set with prune: true, it will only prune files with the prefix. + * + * We recommend to always configure the `destinationKeyPrefix` property. This will prevent the deployment + * from accidentally deleting data that wasn't uploaded by it. * * @default "/" (unzip to root of the destination bucket) */ @@ -71,6 +74,9 @@ export interface BucketDeploymentProps { readonly include?: string[]; /** + * By default, files in the destination bucket that don't exist in the source will be deleted + * when the BucketDeployment resource is created or updated. + * * If this is set to false, files in the destination bucket that * do not exist in the asset, will NOT be deleted during deployment (create/update). * From 5a3a32f2b52767c618d36ea3a7d81590986304a1 Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Tue, 5 Nov 2024 12:16:42 +0100 Subject: [PATCH 5/9] refactor: make node-bundle tests executable using ts-jest (#32022) Make `node-bundle` easier to test (in-process instead of using a subcommand that requires `.js` to have been compiled), and fix a bug in the tests that used `--license` instead of `--allowed-license` (configure `yargs` to be `strict`). ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- tools/@aws-cdk/node-bundle/package.json | 1 + tools/@aws-cdk/node-bundle/src/api/bundle.ts | 2 +- tools/@aws-cdk/node-bundle/src/cli-main.ts | 115 +++++++++++++++++++ tools/@aws-cdk/node-bundle/src/cli.ts | 105 +---------------- tools/@aws-cdk/node-bundle/test/cli.test.ts | 79 +++++++------ tools/@aws-cdk/node-bundle/tsconfig.json | 1 + yarn.lock | 2 +- 7 files changed, 167 insertions(+), 138 deletions(-) create mode 100644 tools/@aws-cdk/node-bundle/src/cli-main.ts diff --git a/tools/@aws-cdk/node-bundle/package.json b/tools/@aws-cdk/node-bundle/package.json index 3c3f188956568..b157c277819fa 100644 --- a/tools/@aws-cdk/node-bundle/package.json +++ b/tools/@aws-cdk/node-bundle/package.json @@ -17,6 +17,7 @@ "@types/license-checker": "^25.0.6", "@types/madge": "^5.0.3", "@types/node": "^16", + "@types/yargs": "^17", "@typescript-eslint/eslint-plugin": "^6.21.0", "@typescript-eslint/parser": "^6.21.0", "eslint": "^8", diff --git a/tools/@aws-cdk/node-bundle/src/api/bundle.ts b/tools/@aws-cdk/node-bundle/src/api/bundle.ts index 7036bf37c728e..bf8af98023b4f 100644 --- a/tools/@aws-cdk/node-bundle/src/api/bundle.ts +++ b/tools/@aws-cdk/node-bundle/src/api/bundle.ts @@ -327,7 +327,7 @@ export class Bundle { if (this.test) { const command = `${path.join(bundleDir, this.test)}`; - console.log(`Running santiy test: ${command}`); + console.log(`Running sanity test: ${command}`); shell(command, { cwd: bundleDir }); } diff --git a/tools/@aws-cdk/node-bundle/src/cli-main.ts b/tools/@aws-cdk/node-bundle/src/cli-main.ts new file mode 100644 index 0000000000000..754e03a18f99c --- /dev/null +++ b/tools/@aws-cdk/node-bundle/src/cli-main.ts @@ -0,0 +1,115 @@ +import * as path from 'path'; +import * as fs from 'fs-extra'; +import * as yargs from 'yargs'; +import { Bundle, BundleProps, BundleValidateOptions } from './api'; + +function versionNumber(): string { + return fs.readJSONSync(path.join(__dirname, '..', 'package.json')).version; +} + +export async function cliMain(cliArgs: string[]) { + const argv = await yargs + .usage('Usage: node-bundle COMMAND') + .option('entrypoint', { type: 'array', nargs: 1, desc: 'List of entrypoints to bundle' }) + .option('external', { type: 'array', nargs: 1, default: [], desc: 'Packages in this list will be excluded from the bundle and added as dependencies (example: fsevents:optional)' }) + .option('allowed-license', { type: 'array', nargs: 1, default: [], desc: 'List of valid licenses' }) + .option('resource', { type: 'array', nargs: 1, default: [], desc: 'List of resources that need to be explicitly copied to the bundle (example: node_modules/proxy-agent/contextify.js:bin/contextify.js)' }) + .option('dont-attribute', { type: 'string', desc: 'Dependencies matching this regular expressions wont be added to the notice file' }) + .option('test', { type: 'string', desc: 'Validation command to sanity test the bundle after its created' }) + .command('validate', 'Validate the package is ready for bundling', args => args + .option('fix', { type: 'boolean', default: false, alias: 'f', desc: 'Fix any fixable violations' }), + ) + .command('write', 'Write the bundled version of the project to a temp directory') + .command('pack', 'Write the bundle and create the tarball') + .demandCommand() // require a subcommand + .strict() // require a VALID subcommand, and only supported options + .fail((msg, err) => { + // Throw an error in test mode, exit with an error code otherwise + if (err) { throw err; } + if (process.env.NODE_ENV === 'test') { + throw new Error(msg); + } + console.error(msg); + process.exit(1); // exit() not exitCode, we must not return. + }) + .help() + .version(versionNumber()) + .parse(cliArgs); + + const command = argv._[0]; + + function undefinedIfEmpty(arr?: any[]): string[] | undefined { + if (!arr || arr.length === 0) return undefined; + return arr as string[]; + } + + const resources: any = {}; + for (const resource of (argv.resource as string[])) { + const parts = resource.split(':'); + resources[parts[0]] = parts[1]; + } + + const optionalExternals = []; + const runtimeExternals = []; + + for (const external of (argv.external as string[])) { + const parts = external.split(':'); + const name = parts[0]; + const type = parts[1]; + switch (type) { + case 'optional': + optionalExternals.push(name); + break; + case 'runtime': + runtimeExternals.push(name); + break; + default: + throw new Error(`Unsupported dependency type '${type}' for external package '${name}'. Supported types are: ['optional', 'runtime']`); + } + } + + const props: BundleProps = { + packageDir: process.cwd(), + entryPoints: undefinedIfEmpty(argv.entrypoint), + externals: { dependencies: runtimeExternals, optionalDependencies: optionalExternals }, + allowedLicenses: undefinedIfEmpty(argv['allowed-license']), + resources: resources, + dontAttribute: argv['dont-attribute'], + test: argv.test, + }; + + const bundle = new Bundle(props); + + switch (command) { + case 'validate': + // When using `yargs.command(command, builder [, handler])` without the handler + // as we do here, there is no typing for command-specific options. So force a cast. + const fix = argv.fix as boolean | undefined; + validate(bundle, { fix }); + break; + case 'write': + write(bundle); + break; + case 'pack': + pack(bundle); + break; + default: + throw new Error(`Unknown command: ${command}`); + } +} + +function write(bundle: Bundle) { + const bundleDir = bundle.write(); + console.log(bundleDir); +} + +function validate(bundle: Bundle, options: BundleValidateOptions = {}) { + const report = bundle.validate(options); + if (!report.success) { + throw new Error(report.summary); + } +} + +function pack(bundle: Bundle) { + bundle.pack(); +} diff --git a/tools/@aws-cdk/node-bundle/src/cli.ts b/tools/@aws-cdk/node-bundle/src/cli.ts index d286e0a78e024..f6c7ec729405e 100644 --- a/tools/@aws-cdk/node-bundle/src/cli.ts +++ b/tools/@aws-cdk/node-bundle/src/cli.ts @@ -1,107 +1,6 @@ -import * as path from 'path'; -import * as fs from 'fs-extra'; -import * as yargs from 'yargs'; -import { Bundle, BundleProps, BundleValidateOptions } from './api'; +import { cliMain } from './cli-main'; -function versionNumber(): string { - return fs.readJSONSync(path.join(__dirname, '..', 'package.json')).version; -} - -async function buildCommands() { - - const argv = yargs - .usage('Usage: node-bundle COMMAND') - .option('entrypoint', { type: 'array', nargs: 1, desc: 'List of entrypoints to bundle' }) - .option('external', { type: 'array', nargs: 1, default: [], desc: 'Packages in this list will be excluded from the bundle and added as dependencies (example: fsevents:optional)' }) - .option('allowed-license', { type: 'array', nargs: 1, default: [], desc: 'List of valid licenses' }) - .option('resource', { type: 'array', nargs: 1, default: [], desc: 'List of resources that need to be explicitly copied to the bundle (example: node_modules/proxy-agent/contextify.js:bin/contextify.js)' }) - .option('dont-attribute', { type: 'string', desc: 'Dependencies matching this regular expressions wont be added to the notice file' }) - .option('test', { type: 'string', desc: 'Validation command to sanity test the bundle after its created' }) - .command('validate', 'Validate the package is ready for bundling', args => args - .option('fix', { type: 'boolean', default: false, alias: 'f', desc: 'Fix any fixable violations' }), - ) - .command('write', 'Write the bundled version of the project to a temp directory') - .command('pack', 'Write the bundle and create the tarball') - .help() - .version(versionNumber()) - .argv; - - const command = argv._[0]; - - function undefinedIfEmpty(arr?: any[]): string[] | undefined { - if (!arr || arr.length === 0) return undefined; - return arr as string[]; - } - - const resources: any = {}; - for (const resource of (argv.resource as string[])) { - const parts = resource.split(':'); - resources[parts[0]] = parts[1]; - } - - const optionalExternals = []; - const runtimeExternals = []; - - for (const external of (argv.external as string[])) { - const parts = external.split(':'); - const name = parts[0]; - const type = parts[1]; - switch (type) { - case 'optional': - optionalExternals.push(name); - break; - case 'runtime': - runtimeExternals.push(name); - break; - default: - throw new Error(`Unsupported dependency type '${type}' for external package '${name}'. Supported types are: ['optional', 'runtime']`); - } - } - - const props: BundleProps = { - packageDir: process.cwd(), - entryPoints: undefinedIfEmpty(argv.entrypoint), - externals: { dependencies: runtimeExternals, optionalDependencies: optionalExternals }, - allowedLicenses: undefinedIfEmpty(argv['allowed-license']), - resources: resources, - dontAttribute: argv['dont-attribute'], - test: argv.test, - }; - - const bundle = new Bundle(props); - - switch (command) { - case 'validate': - validate(bundle, { fix: argv.fix }); - break; - case 'write': - write(bundle); - break; - case 'pack': - pack(bundle); - break; - default: - throw new Error(`Unknown command: ${command}`); - } -} - -function write(bundle: Bundle) { - const bundleDir = bundle.write(); - console.log(bundleDir); -} - -function validate(bundle: Bundle, options: BundleValidateOptions = {}) { - const report = bundle.validate(options); - if (!report.success) { - throw new Error(report.summary); - } -} - -function pack(bundle: Bundle) { - bundle.pack(); -} - -buildCommands() +cliMain(process.argv.slice(2)) .catch((err: Error) => { console.error(`Error: ${err.message}`); process.exitCode = 1; diff --git a/tools/@aws-cdk/node-bundle/test/cli.test.ts b/tools/@aws-cdk/node-bundle/test/cli.test.ts index 57cdc76a25df4..0e18d2936696e 100644 --- a/tools/@aws-cdk/node-bundle/test/cli.test.ts +++ b/tools/@aws-cdk/node-bundle/test/cli.test.ts @@ -1,9 +1,10 @@ import * as path from 'path'; import * as fs from 'fs-extra'; +import { cliMain } from '../src/cli-main'; import { Package } from './_package'; -import { shell } from '../src/api/_shell'; +import * as util from 'util'; -test('validate', () => { +test('validate', async () => { const pkg = Package.create({ name: 'consumer', licenses: ['Apache-2.0'], circular: true }); const dep1 = pkg.addDependency({ name: 'dep1', licenses: ['INVALID'] }); @@ -14,15 +15,14 @@ test('validate', () => { try { const command = [ - whereami(), '--entrypoint', pkg.entrypoint, '--resource', 'missing:bin/missing', - '--license', 'Apache-2.0', + '--allowed-license', 'Apache-2.0', 'validate', - ].join(' '); - shell(command, { cwd: pkg.dir, quiet: true }); + ]; + await runCliMain(pkg.dir, command); } catch (e: any) { - const violations = new Set(e.stderr.toString().trim().split('\n').filter((l: string) => l.startsWith('-'))); + const violations = new Set(e.message.trim().split('\n').filter((l: string) => l.startsWith('-'))); const expected = new Set([ `- invalid-license: Dependency ${dep1.name}@${dep1.version} has an invalid license: UNKNOWN`, `- multiple-license: Dependency ${dep2.name}@${dep2.version} has multiple licenses: Apache-2.0,MIT`, @@ -35,7 +35,7 @@ test('validate', () => { }); -test('write', () => { +test('write', async () => { const pkg = Package.create({ name: 'consumer', licenses: ['Apache-2.0'] }); pkg.addDependency({ name: 'dep1', licenses: ['MIT'] }); @@ -45,13 +45,12 @@ test('write', () => { pkg.install(); const command = [ - whereami(), '--entrypoint', pkg.entrypoint, - '--license', 'Apache-2.0', - '--license', 'MIT', + '--allowed-license', 'Apache-2.0', + '--allowed-license', 'MIT', 'write', - ].join(' '); - const bundleDir = shell(command, { cwd: pkg.dir, quiet: true }); + ]; + const bundleDir = await runCliMain(pkg.dir, command); expect(fs.existsSync(path.join(bundleDir, pkg.entrypoint))).toBeTruthy(); expect(fs.existsSync(path.join(bundleDir, 'package.json'))).toBeTruthy(); @@ -67,7 +66,7 @@ test('write', () => { }); -test('validate and fix', () => { +test('validate and fix', async () => { const pkg = Package.create({ name: 'consumer', licenses: ['Apache-2.0'] }); pkg.addDependency({ name: 'dep1', licenses: ['MIT'] }); @@ -76,33 +75,32 @@ test('validate and fix', () => { pkg.write(); pkg.install(); - const run = (sub: string) => { + const run = (sub: string[]) => { const command = [ - whereami(), '--entrypoint', pkg.entrypoint, - '--license', 'Apache-2.0', - '--license', 'MIT', - sub, - ].join(' '); - shell(command, { cwd: pkg.dir, quiet: true }); + '--allowed-license', 'Apache-2.0', + '--allowed-license', 'MIT', + ...sub, + ]; + return runCliMain(pkg.dir, command); }; try { - run('pack'); + await run(['pack']); throw new Error('Expected packing to fail before fixing'); } catch { // this should fix the fact we don't generate // the project with the correct attributions - run('validate --fix'); + await run(['validate', '--fix']); } - run('pack'); + await run(['pack']); const tarball = path.join(pkg.dir, `${pkg.name}-${pkg.version}.tgz`); expect(fs.existsSync(tarball)).toBeTruthy(); }); -test('pack', () => { +test('pack', async () => { const pkg = Package.create({ name: 'consumer', licenses: ['Apache-2.0'] }); const dep1 = pkg.addDependency({ name: 'dep1', licenses: ['MIT'] }); @@ -127,19 +125,34 @@ test('pack', () => { pkg.install(); const command = [ - whereami(), '--entrypoint', pkg.entrypoint, - '--license', 'Apache-2.0', - '--license', 'MIT', + '--allowed-license', 'Apache-2.0', + '--allowed-license', 'MIT', 'pack', - ].join(' '); - shell(command, { cwd: pkg.dir, quiet: true }); + ]; + await runCliMain(pkg.dir, command); const tarball = path.join(pkg.dir, `${pkg.name}-${pkg.version}.tgz`); expect(fs.existsSync(tarball)).toBeTruthy(); }); -function whereami() { - return path.join(path.join(__dirname, '..', 'bin', 'node-bundle')); -} +async function runCliMain(cwd: string, command: string[]): Promise { + const log: string[] = [] + const spy = jest + .spyOn(console, 'log') + .mockImplementation((...args) => { + log.push(util.format(...args)); + }); + + const curdir = process.cwd(); + process.chdir(cwd); + try { + await cliMain(command); + + return log.join('\n'); + } finally { + process.chdir(curdir); + spy.mockRestore(); + } +} \ No newline at end of file diff --git a/tools/@aws-cdk/node-bundle/tsconfig.json b/tools/@aws-cdk/node-bundle/tsconfig.json index 96cb12aa0b31d..4d10a59f0d283 100644 --- a/tools/@aws-cdk/node-bundle/tsconfig.json +++ b/tools/@aws-cdk/node-bundle/tsconfig.json @@ -25,6 +25,7 @@ "strictNullChecks": true, "strictPropertyInitialization": true, "stripInternal": true, + "noErrorTruncation": true, "target": "ES2019" }, "include": [ diff --git a/yarn.lock b/yarn.lock index adbadf5371a4b..4f083618498a2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5507,7 +5507,7 @@ dependencies: "@types/yargs-parser" "*" -"@types/yargs@^17.0.8": +"@types/yargs@^17", "@types/yargs@^17.0.8": version "17.0.33" resolved "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz#8c32303da83eec050a84b3c7ae7b9f922d13e32d" integrity sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA== From 4f8ecaeddc8d78ec221a281cab5de5cc2ef0cb4a Mon Sep 17 00:00:00 2001 From: Calvin Combs <66279577+comcalvi@users.noreply.github.com> Date: Tue, 5 Nov 2024 04:20:21 -0800 Subject: [PATCH 6/9] refactor(cli): generate yargs configuration from TS (#31850) ### Issue # (if applicable) N/A ### Reason for this change We'd like to create a programmatic interface to the CDK Toolkit. A bonus of this overhaul is moving to a single source of truth for both the programmatic interface to the CDK Toolkit and the command line interface to the CDK Toolkit. This PR generates the existing `yargs` configuration from a TS configuration. In the long term, we'd generate the `yargs` configuration purely from the programmatic interface to the Toolkit, but this is an improvement with less effort. ### Description of changes Creates a new package, `@aws-cdk/yargs-gen`, which generates our `yargs` configuration from a `CliConfig` defined in `aws-cdk/config.ts` using `@cdklabs/typewriter`. ### Description of how you validated changes N/A yet. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- lerna.json | 1 + package.json | 1 + .../cli-lib-alpha/THIRD_PARTY_LICENSES | 4 +- packages/aws-cdk/CONTRIBUTING.md | 23 + packages/aws-cdk/lib/cli.ts | 342 +------- packages/aws-cdk/lib/config.ts | 419 ++++++++++ packages/aws-cdk/lib/notices.ts | 4 +- .../lib/parse-command-line-arguments.ts | 777 ++++++++++++++++++ packages/aws-cdk/package.json | 5 + packages/aws-cdk/scripts/yargs-gen | 2 + packages/aws-cdk/scripts/yargs-gen.ts | 13 + packages/aws-cdk/test/notices.test.ts | 4 +- tools/@aws-cdk/yargs-gen/.eslintrc.js | 3 + tools/@aws-cdk/yargs-gen/.gitignore | 18 + tools/@aws-cdk/yargs-gen/.npmignore | 7 + tools/@aws-cdk/yargs-gen/LICENSE | 201 +++++ tools/@aws-cdk/yargs-gen/NOTICE | 2 + tools/@aws-cdk/yargs-gen/README.md | 21 + tools/@aws-cdk/yargs-gen/jest.config.js | 10 + tools/@aws-cdk/yargs-gen/lib/index.ts | 2 + tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts | 155 ++++ tools/@aws-cdk/yargs-gen/lib/yargs-types.ts | 80 ++ tools/@aws-cdk/yargs-gen/package.json | 57 ++ tools/@aws-cdk/yargs-gen/test/cli.test.ts | 75 ++ tools/@aws-cdk/yargs-gen/tsconfig.json | 21 + yarn.lock | 12 +- 26 files changed, 1924 insertions(+), 335 deletions(-) create mode 100644 packages/aws-cdk/lib/config.ts create mode 100644 packages/aws-cdk/lib/parse-command-line-arguments.ts create mode 100755 packages/aws-cdk/scripts/yargs-gen create mode 100644 packages/aws-cdk/scripts/yargs-gen.ts create mode 100644 tools/@aws-cdk/yargs-gen/.eslintrc.js create mode 100644 tools/@aws-cdk/yargs-gen/.gitignore create mode 100644 tools/@aws-cdk/yargs-gen/.npmignore create mode 100644 tools/@aws-cdk/yargs-gen/LICENSE create mode 100644 tools/@aws-cdk/yargs-gen/NOTICE create mode 100644 tools/@aws-cdk/yargs-gen/README.md create mode 100644 tools/@aws-cdk/yargs-gen/jest.config.js create mode 100644 tools/@aws-cdk/yargs-gen/lib/index.ts create mode 100644 tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts create mode 100644 tools/@aws-cdk/yargs-gen/lib/yargs-types.ts create mode 100644 tools/@aws-cdk/yargs-gen/package.json create mode 100644 tools/@aws-cdk/yargs-gen/test/cli.test.ts create mode 100644 tools/@aws-cdk/yargs-gen/tsconfig.json diff --git a/lerna.json b/lerna.json index 11ba780008542..bcc9be9ea0112 100644 --- a/lerna.json +++ b/lerna.json @@ -10,6 +10,7 @@ "packages/@aws-cdk-testing/*", "packages/@aws-cdk/*/lambda-packages/*", "tools/@aws-cdk/cdk-build-tools", + "tools/@aws-cdk/yargs-gen", "tools/@aws-cdk/cdk-release", "tools/@aws-cdk/node-bundle", "tools/@aws-cdk/pkglint", diff --git a/package.json b/package.json index e46aca75c6383..116fbfebd1304 100644 --- a/package.json +++ b/package.json @@ -78,6 +78,7 @@ "packages/@aws-cdk-testing/*", "packages/@aws-cdk/*/lambda-packages/*", "tools/@aws-cdk/cdk-build-tools", + "tools/@aws-cdk/yargs-gen", "tools/@aws-cdk/cdk-release", "tools/@aws-cdk/node-bundle", "tools/@aws-cdk/pkglint", diff --git a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES index df020abc2aac7..fbf6e87a533bd 100644 --- a/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES +++ b/packages/@aws-cdk/cli-lib-alpha/THIRD_PARTY_LICENSES @@ -207,7 +207,7 @@ The @aws-cdk/cli-lib-alpha package includes the following third-party software/l ---------------- -** @jsii/check-node@1.103.1 - https://www.npmjs.com/package/@jsii/check-node/v/1.103.1 | Apache-2.0 +** @jsii/check-node@1.104.0 - https://www.npmjs.com/package/@jsii/check-node/v/1.104.0 | Apache-2.0 jsii Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. @@ -3562,7 +3562,7 @@ THE SOFTWARE. ---------------- -** tslib@2.7.0 - https://www.npmjs.com/package/tslib/v/2.7.0 | 0BSD +** tslib@2.8.0 - https://www.npmjs.com/package/tslib/v/2.8.0 | 0BSD Copyright (c) Microsoft Corporation. Permission to use, copy, modify, and/or distribute this software for any diff --git a/packages/aws-cdk/CONTRIBUTING.md b/packages/aws-cdk/CONTRIBUTING.md index 59792a73c40b1..fb915d28cb39c 100644 --- a/packages/aws-cdk/CONTRIBUTING.md +++ b/packages/aws-cdk/CONTRIBUTING.md @@ -1,3 +1,26 @@ +## CLI Commands + +All CDK CLI Commands are defined in `lib/config.ts`. This file is translated +into a valid `yargs` configuration by `bin/yargs-gen`, which is generated by `@aws-cdk/yargs-gen`. +The `yargs` configuration is generated into the function `parseCommandLineArguments()`, +in `lib/parse-command-line-arguments.ts`, and is checked into git for readability and +inspectability; do not edit this file by hand, as every subsequent `yarn build` will +overwrite any manual edits. If you need to leverage a `yargs` feature not used by +the CLI, you must add support for it to `@aws-cdk/yargs-gen`. + +Note that `bin/yargs-gen` is executed by `ts-node`, which allows `config.ts` to +reference functions and other identifiers defined in the CLI before the CLI is +built. + +### Dynamic Values + +Some values, such as the user's platform, cannot be computed at build time. +Some commands depend on these values, and thus `yargs-gen` must generate the +code to compute these values at build time. + +The only way to do this today is to reference a parameter with `DynamicValue.fromParameter`. +The caller of `parseCommandLineArguments()` must pass the parameter. + ## Integration Tests Unit tests are automatically run as part of the regular build. Integration tests diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index e90062fb4048d..e779defa9ca35 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -3,10 +3,10 @@ import '@jsii/check-node/run'; import * as chalk from 'chalk'; import { install as enableSourceMapSupport } from 'source-map-support'; -import type { Argv } from 'yargs'; import { DeploymentMethod } from './api'; import { HotswapMode } from './api/hotswap/common'; import { ILock } from './api/util/rwlock'; +import { parseCommandLineArguments } from './parse-command-line-arguments'; import { checkForPlatformWarnings } from './platform-warnings'; import { enableTracing } from './util/tracing'; import { SdkProvider } from '../lib/api/aws-auth'; @@ -17,351 +17,37 @@ import { execProgram } from '../lib/api/cxapp/exec'; import { Deployments } from '../lib/api/deployments'; import { PluginHost } from '../lib/api/plugin'; import { ToolkitInfo } from '../lib/api/toolkit-info'; -import { StackActivityProgress } from '../lib/api/util/cloudformation/stack-activity-monitor'; import { CdkToolkit, AssetBuildTime } from '../lib/cdk-toolkit'; import { realHandler as context } from '../lib/commands/context'; import { realHandler as docs } from '../lib/commands/docs'; import { realHandler as doctor } from '../lib/commands/doctor'; import { MIGRATE_SUPPORTED_LANGUAGES, getMigrateScanType } from '../lib/commands/migrate'; -import { RequireApproval } from '../lib/diff'; import { availableInitLanguages, cliInit, printAvailableTemplates } from '../lib/init'; import { data, debug, error, print, setLogLevel, setCI } from '../lib/logging'; import { Notices } from '../lib/notices'; import { Command, Configuration, Settings } from '../lib/settings'; import * as version from '../lib/version'; -// https://github.com/yargs/yargs/issues/1929 -// https://github.com/evanw/esbuild/issues/1492 -// eslint-disable-next-line @typescript-eslint/no-require-imports -const yargs = require('yargs'); - /* eslint-disable max-len */ /* eslint-disable @typescript-eslint/no-shadow */ // yargs -async function parseCommandLineArguments(args: string[]) { - // Use the following configuration for array arguments: - // - // { type: 'array', default: [], nargs: 1, requiresArg: true } - // - // The default behavior of yargs is to eat all strings following an array argument: - // - // ./prog --arg one two positional => will parse to { arg: ['one', 'two', 'positional'], _: [] } (so no positional arguments) - // ./prog --arg one two -- positional => does not help, for reasons that I can't understand. Still gets parsed incorrectly. - // - // By using the config above, every --arg will only consume one argument, so you can do the following: - // - // ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. - - const defaultBrowserCommand: { [key in NodeJS.Platform]?: string } = { - darwin: 'open %u', - win32: 'start %u', - }; - - const initTemplateLanguages = await availableInitLanguages(); - return yargs - .env('CDK') - .usage('Usage: cdk -a COMMAND') - .option('app', { type: 'string', alias: 'a', desc: 'REQUIRED WHEN RUNNING APP: command-line for executing your app or a cloud assembly directory (e.g. "node bin/my-app.js"). Can also be specified in cdk.json or ~/.cdk.json', requiresArg: true }) - .option('build', { type: 'string', desc: 'Command-line for a pre-synth build' }) - .option('context', { type: 'array', alias: 'c', desc: 'Add contextual string parameter (KEY=VALUE)', nargs: 1, requiresArg: true }) - .option('plugin', { type: 'array', alias: 'p', desc: 'Name or path of a node package that extend the CDK features. Can be specified multiple times', nargs: 1 }) - .option('trace', { type: 'boolean', desc: 'Print trace for stack warnings' }) - .option('strict', { type: 'boolean', desc: 'Do not construct stacks with warnings' }) - .option('lookups', { type: 'boolean', desc: 'Perform context lookups (synthesis fails if this is disabled and context lookups need to be performed)', default: true }) - .option('ignore-errors', { type: 'boolean', default: false, desc: 'Ignores synthesis errors, which will likely produce an invalid output' }) - .option('json', { type: 'boolean', alias: 'j', desc: 'Use JSON output instead of YAML when templates are printed to STDOUT', default: false }) - .option('verbose', { type: 'boolean', alias: 'v', desc: 'Show debug logs (specify multiple times to increase verbosity)', default: false }) - .count('verbose') - .option('debug', { type: 'boolean', desc: 'Enable emission of additional debugging information, such as creation stack traces of tokens', default: false }) - .option('profile', { type: 'string', desc: 'Use the indicated AWS profile as the default environment', requiresArg: true }) - .option('proxy', { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified', requiresArg: true }) - .option('ca-bundle-path', { type: 'string', desc: 'Path to CA certificate to use when validating HTTPS requests. Will read from AWS_CA_BUNDLE environment variable if not specified', requiresArg: true }) - .option('ec2creds', { type: 'boolean', alias: 'i', default: undefined, desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status' }) - .option('version-reporting', { type: 'boolean', desc: 'Include the "AWS::CDK::Metadata" resource in synthesized templates (enabled by default)', default: undefined }) - .option('path-metadata', { type: 'boolean', desc: 'Include "aws:cdk:path" CloudFormation metadata for each resource (enabled by default)', default: undefined }) - .option('asset-metadata', { type: 'boolean', desc: 'Include "aws:asset:*" CloudFormation metadata for resources that uses assets (enabled by default)', default: undefined }) - .option('role-arn', { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined, requiresArg: true }) - .option('staging', { type: 'boolean', desc: 'Copy assets to the output directory (use --no-staging to disable the copy of assets which allows local debugging via the SAM CLI to reference the original source files)', default: true }) - .option('output', { type: 'string', alias: 'o', desc: 'Emits the synthesized cloud assembly into a directory (default: cdk.out)', requiresArg: true }) - .option('notices', { type: 'boolean', desc: 'Show relevant notices' }) - .option('no-color', { type: 'boolean', desc: 'Removes colors and other style from console output', default: false }) - .option('ci', { type: 'boolean', desc: 'Force CI detection. If CI=true then logs will be sent to stdout instead of stderr', default: process.env.CI !== undefined }) - .option('unstable', { type: 'array', desc: 'Opt in to specific unstable features. Can be specified multiple times.', default: [] }) - .command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) => yargs - .option('long', { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }) - .option('show-dependencies', { type: 'boolean', default: false, alias: 'd', desc: 'Display stack dependency information for each stack' }), - ) - .command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', (yargs: Argv) => yargs - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only synthesize requested stacks, don\'t include dependencies' }) - .option('validation', { type: 'boolean', desc: 'After synthesis, validate stacks with the "validateOnSynth" attribute set (can also be controlled with CDK_VALIDATION)', default: true }) - .option('quiet', { type: 'boolean', alias: 'q', desc: 'Do not output CloudFormation Template to stdout', default: false })) - .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', (yargs: Argv) => yargs - .option('bootstrap-bucket-name', { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined }) - .option('bootstrap-kms-key-id', { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined, conflicts: 'bootstrap-customer-key' }) - .option('example-permissions-boundary', { type: 'boolean', alias: 'epb', desc: 'Use the example permissions boundary.', default: undefined, conflicts: 'custom-permissions-boundary' }) - .option('custom-permissions-boundary', { type: 'string', alias: 'cpb', desc: 'Use the permissions boundary specified by name.', default: undefined, conflicts: 'example-permissions-boundary' }) - .option('bootstrap-customer-key', { type: 'boolean', desc: 'Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)', default: undefined, conflicts: 'bootstrap-kms-key-id' }) - .option('qualifier', { type: 'string', desc: 'String which must be unique for each bootstrap stack. You must configure it on your CDK app if you change this from the default.', default: undefined }) - .option('public-access-block-configuration', { type: 'boolean', desc: 'Block public access configuration on CDK toolkit bucket (enabled by default) ', default: undefined }) - .option('tags', { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] }) - .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) - .option('trust', { type: 'array', desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }) - .option('trust-for-lookup', { type: 'array', desc: 'The AWS account IDs that should be trusted to look up values in this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }) - .option('cloudformation-execution-policies', { type: 'array', desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }) - .option('force', { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false }) - .option('termination-protection', { type: 'boolean', default: undefined, desc: 'Toggle CloudFormation termination protection on the bootstrap stacks' }) - .option('show-template', { type: 'boolean', desc: 'Instead of actual bootstrapping, print the current CLI\'s bootstrapping template to stdout for customization', default: false }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack to create', requiresArg: true }) - .option('template', { type: 'string', requiresArg: true, desc: 'Use the template from the given file instead of the built-in one (use --show-template to obtain an example)' }) - .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }), - ) - .command('gc [ENVIRONMENTS..]', 'Garbage collect assets. Options detailed here: https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/README.md#cdk-gc', (yargs: Argv) => yargs - .option('action', { type: 'string', desc: 'The action (or sub-action) you want to perform. Valid entires are "print", "tag", "delete-tagged", "full".', default: 'full' }) - .option('type', { type: 'string', desc: 'Specify either ecr, s3, or all', default: 'all' }) - .option('rollback-buffer-days', { type: 'number', desc: 'Delete assets that have been marked as isolated for this many days', default: 0 }) - .option('created-buffer-days', { type: 'number', desc: 'Never delete assets younger than this (in days)', default: 1 }) - .option('confirm', { type: 'boolean', desc: 'Confirm via manual prompt before deletion', default: true }) - .option('bootstrap-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack, if different from the default "CDKToolkit"', requiresArg: true }), - ) - .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', (yargs: Argv) => yargs - .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' }) - .option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] }) - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' }) - .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' }) - .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true }) - // @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment - .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true }) - .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet) (deprecated)', deprecated: true }) - .option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create (only if method is not direct)' }) - .options('method', { - alias: 'm', - type: 'string', - choices: ['direct', 'change-set', 'prepare-change-set'], - requiresArg: true, - desc: 'How to perform the deployment. Direct is a bit faster but lacks progress information', - }) - .option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }) - .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) - .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) - .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the existing CDK toolkit stack (only used for app using legacy synthesis)', requiresArg: true }) - .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }) - .option('rollback', { - type: 'boolean', - desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + - 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', - }) - // Hack to get '-R' as an alias for '--no-rollback', suggested by: https://github.com/yargs/yargs/issues/1729 - .option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true) - .option('hotswap', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'but does not fall back to a full deployment if that is not possible. ' + - 'Instead, changes to any non-hotswappable properties are ignored.' + - 'Do not use this in production environments', - }) - .option('hotswap-fallback', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'which skips CloudFormation and updates the resources directly, ' + - 'and falls back to a full deployment if that is not possible. ' + - 'Do not use this in production environments', - }) - .option('watch', { - type: 'boolean', - desc: 'Continuously observe the project files, ' + - 'and deploy the given stack(s) automatically when changes are detected. ' + - 'Implies --hotswap by default', - }) - .options('logs', { - type: 'boolean', - default: true, - desc: 'Show CloudWatch log events from all resources in the selected Stacks in the terminal. ' + - "'true' by default, use --no-logs to turn off. " + - "Only in effect if specified alongside the '--watch' option", - }) - .option('concurrency', { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true }) - .option('asset-parallelism', { type: 'boolean', desc: 'Whether to build/publish assets in parallel' }) - .option('asset-prebuild', { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true }) - .option('ignore-no-stacks', { type: 'boolean', desc: 'Whether to deploy if the app contains no stacks', default: false }), - ) - .command('rollback [STACKS..]', 'Rolls back the stack(s) named STACKS to their last stable state', (yargs: Argv) => yargs - .option('all', { type: 'boolean', default: false, desc: 'Roll back all available stacks' }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack the environment is bootstrapped with', requiresArg: true }) - .option('force', { - alias: 'f', - type: 'boolean', - desc: 'Orphan all resources for which the rollback operation fails.', - }) - .option('validate-bootstrap-version', { - type: 'boolean', - desc: 'Whether to validate the bootstrap stack version. Defaults to \'true\', disable with --no-validate-bootstrap-version.', - }) - .option('orphan', { - // alias: 'o' conflicts with --output - type: 'array', - nargs: 1, - requiresArg: true, - desc: 'Orphan the given resources, identified by their logical ID (can be specified multiple times)', - default: [], - }), - ) - .command('import [STACK]', 'Import existing resource(s) into the given STACK', (yargs: Argv) => yargs - .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) - .option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the CDK toolkit stack to create', requiresArg: true }) - .option('rollback', { - type: 'boolean', - desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + - 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', - }) - .option('force', { - alias: 'f', - type: 'boolean', - desc: 'Do not abort if the template diff includes updates or deletes. This is probably safe but we\'re not sure, let us know how it goes.', - }) - .option('record-resource-mapping', { - type: 'string', - alias: 'r', - requiresArg: true, - desc: 'If specified, CDK will generate a mapping of existing physical resources to CDK resources to be imported as. The mapping ' + - 'will be written in the given file path. No actual import operation will be performed', - }) - .option('resource-mapping', { - type: 'string', - alias: 'm', - requiresArg: true, - desc: 'If specified, CDK will use the given file to map physical resources to CDK resources for import, instead of interactively ' + - 'asking the user. Can be run from scripts', - }), - ) - .command('watch [STACKS..]', "Shortcut for 'deploy --watch'", (yargs: Argv) => yargs - // I'm fairly certain none of these options, present for 'deploy', make sense for 'watch': - // .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' }) - // .option('ci', { type: 'boolean', desc: 'Force CI detection', default: process.env.CI !== undefined }) - // @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment - // .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true }) - // .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) - // These options, however, are more subtle - I could be convinced some of these should also be available for 'watch': - // .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' }) - // .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) - // .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }) - // .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) - // .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true }) - .option('build-exclude', { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] }) - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' }) - .option('change-set-name', { type: 'string', desc: 'Name of the CloudFormation change set to create' }) - .option('force', { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }) - .option('toolkit-stack-name', { type: 'string', desc: 'The name of the existing CDK toolkit stack (only used for app using legacy synthesis)', requiresArg: true }) - .option('progress', { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }) - .option('rollback', { - type: 'boolean', - desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + - 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', - }) - // same hack for -R as above in 'deploy' - .option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true) - .option('hotswap', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'but does not fall back to a full deployment if that is not possible. ' + - 'Instead, changes to any non-hotswappable properties are ignored.' + - "'true' by default, use --no-hotswap to turn off", - }) - .option('hotswap-fallback', { - type: 'boolean', - desc: "Attempts to perform a 'hotswap' deployment, " + - 'which skips CloudFormation and updates the resources directly, ' + - 'and falls back to a full deployment if that is not possible.', - }) - .options('logs', { - type: 'boolean', - default: true, - desc: 'Show CloudWatch log events from all resources in the selected Stacks in the terminal. ' + - "'true' by default, use --no-logs to turn off", - }) - .option('concurrency', { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true }), - ) - .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', (yargs: Argv) => yargs - .option('all', { type: 'boolean', default: false, desc: 'Destroy all available stacks' }) - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only destroy requested stacks, don\'t include dependees' }) - .option('force', { type: 'boolean', alias: 'f', desc: 'Do not ask for confirmation before destroying the stacks' })) - .command('diff [STACKS..]', 'Compares the specified stack with the deployed stack or a local template file, and returns with status 1 if any difference is found', (yargs: Argv) => yargs - .option('exclusively', { type: 'boolean', alias: 'e', desc: 'Only diff requested stacks, don\'t include dependencies' }) - .option('context-lines', { type: 'number', desc: 'Number of context lines to include in arbitrary JSON diff rendering', default: 3, requiresArg: true }) - .option('template', { type: 'string', desc: 'The path to the CloudFormation template to compare with', requiresArg: true }) - .option('strict', { type: 'boolean', desc: 'Do not filter out AWS::CDK::Metadata resources, mangled non-ASCII characters, or the CheckBootstrapVersionRule', default: false }) - .option('security-only', { type: 'boolean', desc: 'Only diff for broadened security changes', default: false }) - .option('fail', { type: 'boolean', desc: 'Fail with exit code 1 in case of diff' }) - .option('processed', { type: 'boolean', desc: 'Whether to compare against the template with Transforms already processed', default: false }) - .option('quiet', { type: 'boolean', alias: 'q', desc: 'Do not print stack name and default message when there is no diff to stdout', default: false }) - .option('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 })) - .command('metadata [STACK]', 'Returns all metadata associated with this stack') - .command(['acknowledge [ID]', 'ack [ID]'], 'Acknowledge a notice so that it does not show up anymore') - .command('notices', 'Returns a list of relevant notices', (yargs: Argv) => yargs - .option('unacknowledged', { type: 'boolean', alias: 'u', default: false, desc: 'Returns a list of unacknowledged notices' }), - ) - .command('init [TEMPLATE]', 'Create a new, empty CDK project from a template.', (yargs: Argv) => yargs - .option('language', { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: initTemplateLanguages }) - .option('list', { type: 'boolean', desc: 'List the available templates' }) - .option('generate-only', { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }), - ) - .command('migrate', false /* hidden from "cdk --help" */, (yargs: Argv) => yargs - .option('stack-name', { type: 'string', alias: 'n', desc: 'The name assigned to the stack created in the new project. The name of the app will be based off this name as well.', requiresArg: true }) - .option('language', { type: 'string', default: 'typescript', alias: 'l', desc: 'The language to be used for the new project', choices: MIGRATE_SUPPORTED_LANGUAGES }) - .option('account', { type: 'string', desc: 'The account to retrieve the CloudFormation stack template from' }) - .option('region', { type: 'string', desc: 'The region to retrieve the CloudFormation stack template from' }) - .option('from-path', { type: 'string', desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates' }) - .option('from-stack', { type: 'boolean', desc: 'Use this flag to retrieve the template for an existing CloudFormation stack' }) - .option('output-path', { type: 'string', desc: 'The output path for the migrated CDK app' }) - .option('from-scan', { - type: 'string', - desc: 'Determines if a new scan should be created, or the last successful existing scan should be used ' + - '\n options are "new" or "most-recent"', - }) - .option('filter', { - type: 'array', - desc: 'Filters the resource scan based on the provided criteria in the following format: "key1=value1,key2=value2"' + - '\n This field can be passed multiple times for OR style filtering: ' + - '\n filtering options: ' + - '\n resource-identifier: A key-value pair that identifies the target resource. i.e. {"ClusterName", "myCluster"}' + - '\n resource-type-prefix: A string that represents a type-name prefix. i.e. "AWS::DynamoDB::"' + - '\n tag-key: a string that matches resources with at least one tag with the provided key. i.e. "myTagKey"' + - '\n tag-value: a string that matches resources with at least one tag with the provided value. i.e. "myTagValue"', - }) - .option('compress', { type: 'boolean', desc: 'Use this flag to zip the generated CDK app' }), - ) - .command('context', 'Manage cached context values', (yargs: Argv) => yargs - .option('reset', { alias: 'e', desc: 'The context key (or its index) to reset', type: 'string', requiresArg: true }) - .option('force', { alias: 'f', desc: 'Ignore missing key error', type: 'boolean', default: false }) - .option('clear', { desc: 'Clear all context', type: 'boolean' })) - .command(['docs', 'doc'], 'Opens the reference documentation in a browser', (yargs: Argv) => yargs - .option('browser', { - alias: 'b', - desc: 'the command to use to open the browser, using %u as a placeholder for the path of the file to open', - type: 'string', - default: process.platform in defaultBrowserCommand ? defaultBrowserCommand[process.platform] : 'xdg-open %u', - })) - .command('doctor', 'Check your set-up for potential problems') - .version(version.DISPLAY_VERSION) - .demandCommand(1, '') // just print help - .recommendCommands() - .help() - .alias('h', 'help') - .epilogue([ - 'If your app has a single stack, there is no need to specify the stack name', - 'If one of cdk.json or ~/.cdk.json exists, options specified there will be used as defaults. Settings in cdk.json take precedence.', - ].join('\n\n')) - .parse(args); -} - if (!process.stdout.isTTY) { // Disable chalk color highlighting process.env.FORCE_COLOR = '0'; } export async function exec(args: string[], synthesizer?: Synthesizer): Promise { - const argv = await parseCommandLineArguments(args); + function makeBrowserDefault(): string { + const defaultBrowserCommand: { [key in NodeJS.Platform]?: string } = { + darwin: 'open %u', + win32: 'start %u', + }; + + const cmd = defaultBrowserCommand[process.platform]; + return cmd ?? 'xdg-open %u'; + } + + const argv = await parseCommandLineArguments(args, makeBrowserDefault(), await availableInitLanguages(), MIGRATE_SUPPORTED_LANGUAGES as string[], version.DISPLAY_VERSION, yargsNegativeAlias); if (argv.verbose) { setLogLevel(argv.verbose); @@ -399,7 +85,7 @@ export async function exec(args: string[], synthesizer?: Synthesizer): Promise x !== ''); } -function yargsNegativeAlias(shortName: S, longName: L) { +function yargsNegativeAlias(shortName: S, longName: L): (argv: T) => T { return (argv: T) => { if (shortName in argv && argv[shortName]) { (argv as any)[longName] = false; diff --git a/packages/aws-cdk/lib/config.ts b/packages/aws-cdk/lib/config.ts new file mode 100644 index 0000000000000..5606e65fd22f3 --- /dev/null +++ b/packages/aws-cdk/lib/config.ts @@ -0,0 +1,419 @@ +import { CliConfig, DynamicValue } from '@aws-cdk/yargs-gen'; +import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor'; +import { RequireApproval } from './diff'; + +/* eslint-disable quote-props */ + +/** + * Source of truth for all CDK CLI commands. `yargs-gen` translates this into the `yargs` definition + * in `lib/parse-command-line-arguments.ts`. + */ +export function makeConfig(): CliConfig { + return { + globalOptions: { + 'app': { type: 'string', alias: 'a', desc: 'REQUIRED WHEN RUNNING APP: command-line for executing your app or a cloud assembly directory (e.g. "node bin/my-app.js"). Can also be specified in cdk.json or ~/.cdk.json', requiresArg: true }, + 'build': { type: 'string', desc: 'Command-line for a pre-synth build' }, + 'context': { type: 'array', alias: 'c', desc: 'Add contextual string parameter (KEY=VALUE)', nargs: 1, requiresArg: true }, + 'plugin': { type: 'array', alias: 'p', desc: 'Name or path of a node package that extend the CDK features. Can be specified multiple times', nargs: 1 }, + 'trace': { type: 'boolean', desc: 'Print trace for stack warnings' }, + 'strict': { type: 'boolean', desc: 'Do not construct stacks with warnings' }, + 'lookups': { type: 'boolean', desc: 'Perform context lookups (synthesis fails if this is disabled and context lookups need to be performed)', default: true }, + 'ignore-errors': { type: 'boolean', default: false, desc: 'Ignores synthesis errors, which will likely produce an invalid output' }, + 'json': { type: 'boolean', alias: 'j', desc: 'Use JSON output instead of YAML when templates are printed to STDOUT', default: false }, + 'verbose': { type: 'boolean', alias: 'v', desc: 'Show debug logs (specify multiple times to increase verbosity)', default: false, count: true }, + 'debug': { type: 'boolean', desc: 'Enable emission of additional debugging information, such as creation stack traces of tokens', default: false }, + 'profile': { type: 'string', desc: 'Use the indicated AWS profile as the default environment', requiresArg: true }, + 'proxy': { type: 'string', desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified', requiresArg: true }, + 'ca-bundle-path': { type: 'string', desc: 'Path to CA certificate to use when validating HTTPS requests. Will read from AWS_CA_BUNDLE environment variable if not specified', requiresArg: true }, + 'ec2creds': { type: 'boolean', alias: 'i', default: undefined, desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status' }, + 'version-reporting': { type: 'boolean', desc: 'Include the "AWS::CDK::Metadata" resource in synthesized templates (enabled by default)', default: undefined }, + 'path-metadata': { type: 'boolean', desc: 'Include "aws:cdk:path" CloudFormation metadata for each resource (enabled by default)', default: undefined }, + 'asset-metadata': { type: 'boolean', desc: 'Include "aws:asset:*" CloudFormation metadata for resources that uses assets (enabled by default)', default: undefined }, + 'role-arn': { type: 'string', alias: 'r', desc: 'ARN of Role to use when invoking CloudFormation', default: undefined, requiresArg: true }, + 'staging': { type: 'boolean', desc: 'Copy assets to the output directory (use --no-staging to disable the copy of assets which allows local debugging via the SAM CLI to reference the original source files)', default: true }, + 'output': { type: 'string', alias: 'o', desc: 'Emits the synthesized cloud assembly into a directory (default: cdk.out)', requiresArg: true }, + 'notices': { type: 'boolean', desc: 'Show relevant notices' }, + 'no-color': { type: 'boolean', desc: 'Removes colors and other style from console output', default: false }, + 'ci': { type: 'boolean', desc: 'Force CI detection. If CI=true then logs will be sent to stdout instead of stderr', default: DynamicValue.fromInline(() => process.env.CI !== undefined) }, + 'unstable': { type: 'array', desc: 'Opt in to unstable features. The flag indicates that the scope and API of a feature might still change. Otherwise the feature is generally production ready and fully supported. Can be specified multiple times.', default: [] }, + }, + commands: { + 'list': { + arg: { + name: 'STACKS', + variadic: true, + }, + aliases: ['ls'], + description: 'Lists all stacks in the app', + options: { + 'long': { type: 'boolean', default: false, alias: 'l', desc: 'Display environment information for each stack' }, + 'show-dependencies': { type: 'boolean', default: false, alias: 'd', desc: 'Display stack dependency information for each stack' }, + }, + }, + 'synthesize': { + arg: { + name: 'STACKS', + variadic: true, + }, + aliases: ['synth'], + description: 'Synthesizes and prints the CloudFormation template for this stack', + options: { + 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only synthesize requested stacks, don\'t include dependencies' }, + 'validation': { type: 'boolean', desc: 'After synthesis, validate stacks with the "validateOnSynth" attribute set (can also be controlled with CDK_VALIDATION)', default: true }, + 'quiet': { type: 'boolean', alias: 'q', desc: 'Do not output CloudFormation Template to stdout', default: false }, + }, + }, + bootstrap: { + arg: { + name: 'ENVIRONMENTS', + variadic: true, + }, + description: 'Deploys the CDK toolkit stack into an AWS environment', + options: { + 'bootstrap-bucket-name': { type: 'string', alias: ['b', 'toolkit-bucket-name'], desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', default: undefined }, + 'bootstrap-kms-key-id': { type: 'string', desc: 'AWS KMS master key ID used for the SSE-KMS encryption', default: undefined, conflicts: 'bootstrap-customer-key' }, + 'example-permissions-boundary': { type: 'boolean', alias: 'epb', desc: 'Use the example permissions boundary.', default: undefined, conflicts: 'custom-permissions-boundary' }, + 'custom-permissions-boundary': { type: 'string', alias: 'cpb', desc: 'Use the permissions boundary specified by name.', default: undefined, conflicts: 'example-permissions-boundary' }, + 'bootstrap-customer-key': { type: 'boolean', desc: 'Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)', default: undefined, conflicts: 'bootstrap-kms-key-id' }, + 'qualifier': { type: 'string', desc: 'String which must be unique for each bootstrap stack. You must configure it on your CDK app if you change this from the default.', default: undefined }, + 'public-access-block-configuration': { type: 'boolean', desc: 'Block public access configuration on CDK toolkit bucket (enabled by default) ', default: undefined }, + 'tags': { type: 'array', alias: 't', desc: 'Tags to add for the stack (KEY=VALUE)', nargs: 1, requiresArg: true, default: [] }, + 'execute': { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }, + 'trust': { type: 'array', desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }, + 'trust-for-lookup': { type: 'array', desc: 'The AWS account IDs that should be trusted to look up values in this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }, + 'cloudformation-execution-policies': { type: 'array', desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)', default: [], nargs: 1, requiresArg: true }, + 'force': { alias: 'f', type: 'boolean', desc: 'Always bootstrap even if it would downgrade template version', default: false }, + 'termination-protection': { type: 'boolean', default: undefined, desc: 'Toggle CloudFormation termination protection on the bootstrap stacks' }, + 'show-template': { type: 'boolean', desc: 'Instead of actual bootstrapping, print the current CLI\'s bootstrapping template to stdout for customization', default: false }, + 'toolkit-stack-name': { type: 'string', desc: 'The name of the CDK toolkit stack to create', requiresArg: true }, + 'template': { type: 'string', requiresArg: true, desc: 'Use the template from the given file instead of the built-in one (use --show-template to obtain an example)' }, + 'previous-parameters': { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }, + }, + }, + gc: { + description: 'Garbage collect assets. Options detailed here: https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/README.md#cdk-gc', + arg: { + name: 'ENVIRONMENTS', + variadic: true, + }, + options: { + 'action': { type: 'string', desc: 'The action (or sub-action) you want to perform. Valid entires are "print", "tag", "delete-tagged", "full".', default: 'full' }, + 'type': { type: 'string', desc: 'Specify either ecr, s3, or all', default: 'all' }, + 'rollback-buffer-days': { type: 'number', desc: 'Delete assets that have been marked as isolated for this many days', default: 0 }, + 'created-buffer-days': { type: 'number', desc: 'Never delete assets younger than this (in days)', default: 1 }, + 'confirm': { type: 'boolean', desc: 'Confirm via manual prompt before deletion', default: true }, + 'bootstrap-stack-name': { type: 'string', desc: 'The name of the CDK toolkit stack, if different from the default "CDKToolkit"', requiresArg: true }, + }, + }, + deploy: { + description: 'Deploys the stack(s) named STACKS into your AWS account', + options: { + 'all': { type: 'boolean', desc: 'Deploy all available stacks', default: false }, + 'build-exclude': { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] }, + 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' }, + 'require-approval': { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' }, + 'notification-arns': { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true }, + // @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment + 'tags': { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true }, + 'execute': { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet) (deprecated)', deprecated: true }, + 'change-set-name': { type: 'string', desc: 'Name of the CloudFormation change set to create (only if method is not direct)' }, + 'method': { + alias: 'm', + type: 'string', + choices: ['direct', 'change-set', 'prepare-change-set'], + requiresArg: true, + desc: 'How to perform the deployment. Direct is a bit faster but lacks progress information', + }, + 'force': { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }, + 'parameters': { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }, + 'outputs-file': { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }, + 'previous-parameters': { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }, + 'toolkit-stack-name': { type: 'string', desc: 'The name of the existing CDK toolkit stack (only used for app using legacy synthesis)', requiresArg: true }, + 'progress': { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }, + 'rollback': { + type: 'boolean', + desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + + 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', + negativeAlias: 'R', + }, + 'R': { + type: 'boolean', + hidden: true, + // Hack to get '-R' as an alias for '--no-rollback', suggested by: https://github.com/yargs/yargs/issues/1729 + }, + 'hotswap': { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, " + + 'but does not fall back to a full deployment if that is not possible. ' + + 'Instead, changes to any non-hotswappable properties are ignored.' + + 'Do not use this in production environments', + }, + 'hotswap-fallback': { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, " + + 'which skips CloudFormation and updates the resources directly, ' + + 'and falls back to a full deployment if that is not possible. ' + + 'Do not use this in production environments', + }, + 'watch': { + type: 'boolean', + desc: 'Continuously observe the project files, ' + + 'and deploy the given stack(s) automatically when changes are detected. ' + + 'Implies --hotswap by default', + }, + 'logs': { + type: 'boolean', + default: true, + desc: 'Show CloudWatch log events from all resources in the selected Stacks in the terminal. ' + + "'true' by default, use --no-logs to turn off. " + + "Only in effect if specified alongside the '--watch' option", + }, + 'concurrency': { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true }, + 'asset-parallelism': { type: 'boolean', desc: 'Whether to build/publish assets in parallel' }, + 'asset-prebuild': { type: 'boolean', desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', default: true }, + 'ignore-no-stacks': { type: 'boolean', desc: 'Whether to deploy if the app contains no stacks', default: false }, + }, + arg: { + name: 'STACKS', + variadic: true, + }, + }, + rollback: { + description: 'Rolls back the stack(s) named STACKS to their last stable state', + arg: { + name: 'STACKS', + variadic: true, + }, + options: { + 'all': { type: 'boolean', default: false, desc: 'Roll back all available stacks' }, + 'toolkit-stack-name': { type: 'string', desc: 'The name of the CDK toolkit stack the environment is bootstrapped with', requiresArg: true }, + 'force': { + alias: 'f', + type: 'boolean', + desc: 'Orphan all resources for which the rollback operation fails.', + }, + 'validate-bootstrap-version': { + type: 'boolean', + desc: 'Whether to validate the bootstrap stack version. Defaults to \'true\', disable with --no-validate-bootstrap-version.', + }, + 'orphan': { + // alias: 'o' conflicts with --output + type: 'array', + nargs: 1, + requiresArg: true, + desc: 'Orphan the given resources, identified by their logical ID (can be specified multiple times)', + default: [], + }, + }, + }, + import: { + description: 'Import existing resource(s) into the given STACK', + arg: { + name: 'STACK', + variadic: false, + }, + options: { + 'execute': { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }, + 'change-set-name': { type: 'string', desc: 'Name of the CloudFormation change set to create' }, + 'toolkit-stack-name': { type: 'string', desc: 'The name of the CDK toolkit stack to create', requiresArg: true }, + 'rollback': { + type: 'boolean', + desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + + 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', + }, + 'force': { + alias: 'f', + type: 'boolean', + desc: 'Do not abort if the template diff includes updates or deletes. This is probably safe but we\'re not sure, let us know how it goes.', + }, + 'record-resource-mapping': { + type: 'string', + alias: 'r', + requiresArg: true, + desc: 'If specified, CDK will generate a mapping of existing physical resources to CDK resources to be imported as. The mapping ' + + 'will be written in the given file path. No actual import operation will be performed', + }, + 'resource-mapping': { + type: 'string', + alias: 'm', + requiresArg: true, + desc: 'If specified, CDK will use the given file to map physical resources to CDK resources for import, instead of interactively ' + + 'asking the user. Can be run from scripts', + }, + }, + }, + watch: { + description: "Shortcut for 'deploy --watch'", + arg: { + name: 'STACKS', + variadic: true, + }, + options: { + // I'm fairly certain none of these options, present for 'deploy', make sense for 'watch': + // .option('all', { type: 'boolean', default: false, desc: 'Deploy all available stacks' }) + // .option('ci', { type: 'boolean', desc: 'Force CI detection', default: process.env.CI !== undefined }) + // @deprecated(v2) -- tags are part of the Cloud Assembly and tags specified here will be overwritten on the next deployment + // .option('tags', { type: 'array', alias: 't', desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', nargs: 1, requiresArg: true }) + // .option('execute', { type: 'boolean', desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', default: true }) + // These options, however, are more subtle - I could be convinced some of these should also be available for 'watch': + // .option('require-approval', { type: 'string', choices: [RequireApproval.Never, RequireApproval.AnyChange, RequireApproval.Broadening], desc: 'What security-sensitive changes need manual approval' }) + // .option('parameters', { type: 'array', desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', nargs: 1, requiresArg: true, default: {} }) + // .option('previous-parameters', { type: 'boolean', default: true, desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)' }) + // .option('outputs-file', { type: 'string', alias: 'O', desc: 'Path to file where stack outputs will be written as JSON', requiresArg: true }) + // .option('notification-arns', { type: 'array', desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', nargs: 1, requiresArg: true }) + 'build-exclude': { type: 'array', alias: 'E', nargs: 1, desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', default: [] }, + 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only deploy requested stacks, don\'t include dependencies' }, + 'change-set-name': { type: 'string', desc: 'Name of the CloudFormation change set to create' }, + 'force': { alias: 'f', type: 'boolean', desc: 'Always deploy stack even if templates are identical', default: false }, + 'toolkit-stack-name': { type: 'string', desc: 'The name of the existing CDK toolkit stack (only used for app using legacy synthesis)', requiresArg: true }, + 'progress': { type: 'string', choices: [StackActivityProgress.BAR, StackActivityProgress.EVENTS], desc: 'Display mode for stack activity events' }, + 'rollback': { + type: 'boolean', + desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. " + + 'Note: do **not** disable this flag for deployments with resource replacements, as that will always fail', + negativeAlias: '-R', + }, + // same hack for -R as above in 'deploy' + 'R': { + type: 'boolean', + hidden: true, + }, + 'hotswap': { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, " + + 'but does not fall back to a full deployment if that is not possible. ' + + 'Instead, changes to any non-hotswappable properties are ignored.' + + "'true' by default, use --no-hotswap to turn off", + }, + 'hotswap-fallback': { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, " + + 'which skips CloudFormation and updates the resources directly, ' + + 'and falls back to a full deployment if that is not possible.', + }, + 'logs': { + type: 'boolean', + default: true, + desc: 'Show CloudWatch log events from all resources in the selected Stacks in the terminal. ' + + "'true' by default, use --no-logs to turn off", + }, + 'concurrency': { type: 'number', desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', default: 1, requiresArg: true }, + }, + }, + destroy: { + description: 'Destroy the stack(s) named STACKS', + arg: { + name: 'STACKS', + variadic: true, + }, + options: { + 'all': { type: 'boolean', default: false, desc: 'Destroy all available stacks' }, + 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only destroy requested stacks, don\'t include dependees' }, + 'force': { type: 'boolean', alias: 'f', desc: 'Do not ask for confirmation before destroying the stacks' }, + }, + }, + diff: { + description: 'Compares the specified stack with the deployed stack or a local template file, and returns with status 1 if any difference is found', + arg: { + name: 'STACKS', + variadic: true, + }, + options: { + 'exclusively': { type: 'boolean', alias: 'e', desc: 'Only diff requested stacks, don\'t include dependencies' }, + 'context-lines': { type: 'number', desc: 'Number of context lines to include in arbitrary JSON diff rendering', default: 3, requiresArg: true }, + 'template': { type: 'string', desc: 'The path to the CloudFormation template to compare with', requiresArg: true }, + 'strict': { type: 'boolean', desc: 'Do not filter out AWS::CDK::Metadata resources, mangled non-ASCII characters, or the CheckBootstrapVersionRule', default: false }, + 'security-only': { type: 'boolean', desc: 'Only diff for broadened security changes', default: false }, + 'fail': { type: 'boolean', desc: 'Fail with exit code 1 in case of diff' }, + '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 }, + }, + }, + metadata: { + description: 'Returns all metadata associated with this stack', + arg: { + name: 'STACK', + variadic: false, + }, + }, + acknowledge: { + aliases: ['ack'], + description: 'Acknowledge a notice so that it does not show up anymore', + arg: { + name: 'ID', + variadic: false, + }, + }, + notices: { + description: 'Returns a list of relevant notices', + options: { + 'unacknowledged': { type: 'boolean', alias: 'u', default: false, desc: 'Returns a list of unacknowledged notices' }, + }, + }, + init: { + description: 'Create a new, empty CDK project from a template.', + arg: { + name: 'TEMPLATE', + variadic: false, + }, + options: { + 'language': { type: 'string', alias: 'l', desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', choices: DynamicValue.fromParameter('availableInitLanguages') } as any, // TODO: preamble, this initTemplateLanguages variable needs to go as a statement there. + 'list': { type: 'boolean', desc: 'List the available templates' }, + 'generate-only': { type: 'boolean', default: false, desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project' }, + }, + }, + 'migrate': { + description: false as any, + options: { + 'stack-name': { type: 'string', alias: 'n', desc: 'The name assigned to the stack created in the new project. The name of the app will be based off this name as well.', requiresArg: true }, + 'language': { type: 'string', default: 'typescript', alias: 'l', desc: 'The language to be used for the new project', choices: DynamicValue.fromParameter('migrateSupportedLanguages') as any }, + 'account': { type: 'string', desc: 'The account to retrieve the CloudFormation stack template from' }, + 'region': { type: 'string', desc: 'The region to retrieve the CloudFormation stack template from' }, + 'from-path': { type: 'string', desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates' }, + 'from-stack': { type: 'boolean', desc: 'Use this flag to retrieve the template for an existing CloudFormation stack' }, + 'output-path': { type: 'string', desc: 'The output path for the migrated CDK app' }, + 'from-scan': { + type: 'string', + desc: 'Determines if a new scan should be created, or the last successful existing scan should be used ' + + '\n options are "new" or "most-recent"', + }, + 'filter': { + type: 'array', + desc: 'Filters the resource scan based on the provided criteria in the following format: "key1=value1,key2=value2"' + + '\n This field can be passed multiple times for OR style filtering: ' + + '\n filtering options: ' + + '\n resource-identifier: A key-value pair that identifies the target resource. i.e. {"ClusterName", "myCluster"}' + + '\n resource-type-prefix: A string that represents a type-name prefix. i.e. "AWS::DynamoDB::"' + + '\n tag-key: a string that matches resources with at least one tag with the provided key. i.e. "myTagKey"' + + '\n tag-value: a string that matches resources with at least one tag with the provided value. i.e. "myTagValue"', + }, + 'compress': { type: 'boolean', desc: 'Use this flag to zip the generated CDK app' }, + }, + }, + 'context': { + description: 'Manage cached context values', + options: { + 'reset': { alias: 'e', desc: 'The context key (or its index) to reset', type: 'string', requiresArg: true }, + 'force': { alias: 'f', desc: 'Ignore missing key error', type: 'boolean', default: false }, + 'clear': { desc: 'Clear all context', type: 'boolean' }, + }, + }, + 'docs': { + aliases: ['doc'], + description: 'Opens the reference documentation in a browser', + options: { + 'browser': { + alias: 'b', + desc: 'the command to use to open the browser, using %u as a placeholder for the path of the file to open', + type: 'string', + default: DynamicValue.fromParameter('browserDefault'), + }, + }, + }, + 'doctor': { + description: 'Check your set-up for potential problems', + }, + }, + }; +} diff --git a/packages/aws-cdk/lib/notices.ts b/packages/aws-cdk/lib/notices.ts index 7976a9c4ed448..39b793d6c8572 100644 --- a/packages/aws-cdk/lib/notices.ts +++ b/packages/aws-cdk/lib/notices.ts @@ -24,7 +24,7 @@ export interface NoticesProps { * * @default false */ - readonly includeAcknowlegded?: boolean; + readonly includeAcknowledged?: boolean; } @@ -223,7 +223,7 @@ export class Notices { private constructor(props: NoticesProps) { this.configuration = props.configuration; this.acknowledgedIssueNumbers = new Set(this.configuration.context.get('acknowledged-issue-numbers') ?? []); - this.includeAcknowlegded = props.includeAcknowlegded ?? false; + this.includeAcknowlegded = props.includeAcknowledged ?? false; } /** diff --git a/packages/aws-cdk/lib/parse-command-line-arguments.ts b/packages/aws-cdk/lib/parse-command-line-arguments.ts new file mode 100644 index 0000000000000..30379f899b104 --- /dev/null +++ b/packages/aws-cdk/lib/parse-command-line-arguments.ts @@ -0,0 +1,777 @@ +// ------------------------------------------------------------------------------------------- +// GENERATED FROM packages/aws-cdk/lib/config.ts. +// Do not edit by hand; all changes will be overwritten at build time from the config file. +// ------------------------------------------------------------------------------------------- +/* eslint-disable @typescript-eslint/comma-dangle, comma-spacing, max-len, quotes, quote-props */ +import { Argv } from 'yargs'; + +// @ts-ignore TS6133 +export function parseCommandLineArguments( + args: Array, + browserDefault: string, + availableInitLanguages: Array, + migrateSupportedLanguages: Array, + version: string, + yargsNegativeAlias: any +): any { + return yargs + .usage('Usage: cdk -a COMMAND') + .option('app', { + type: 'string', + alias: 'a', + desc: 'REQUIRED WHEN RUNNING APP: command-line for executing your app or a cloud assembly directory (e.g. "node bin/my-app.js"). Can also be specified in cdk.json or ~/.cdk.json', + requiresArg: true, + }) + .option('build', { + type: 'string', + desc: 'Command-line for a pre-synth build', + }) + .option('context', { + type: 'array', + alias: 'c', + desc: 'Add contextual string parameter (KEY=VALUE)', + nargs: 1, + requiresArg: true, + }) + .option('plugin', { + type: 'array', + alias: 'p', + desc: 'Name or path of a node package that extend the CDK features. Can be specified multiple times', + nargs: 1, + }) + .option('trace', { + type: 'boolean', + desc: 'Print trace for stack warnings', + }) + .option('strict', { + type: 'boolean', + desc: 'Do not construct stacks with warnings', + }) + .option('lookups', { + type: 'boolean', + desc: 'Perform context lookups (synthesis fails if this is disabled and context lookups need to be performed)', + default: true, + }) + .option('ignore-errors', { + type: 'boolean', + default: false, + desc: 'Ignores synthesis errors, which will likely produce an invalid output', + }) + .option('json', { + type: 'boolean', + alias: 'j', + desc: 'Use JSON output instead of YAML when templates are printed to STDOUT', + default: false, + }) + .option('verbose', { + type: 'boolean', + alias: 'v', + desc: 'Show debug logs (specify multiple times to increase verbosity)', + default: false, + count: true, + }) + .option('debug', { + type: 'boolean', + desc: 'Enable emission of additional debugging information, such as creation stack traces of tokens', + default: false, + }) + .option('profile', { + type: 'string', + desc: 'Use the indicated AWS profile as the default environment', + requiresArg: true, + }) + .option('proxy', { + type: 'string', + desc: 'Use the indicated proxy. Will read from HTTPS_PROXY environment variable if not specified', + requiresArg: true, + }) + .option('ca-bundle-path', { + type: 'string', + desc: 'Path to CA certificate to use when validating HTTPS requests. Will read from AWS_CA_BUNDLE environment variable if not specified', + requiresArg: true, + }) + .option('ec2creds', { + type: 'boolean', + alias: 'i', + default: undefined, + desc: 'Force trying to fetch EC2 instance credentials. Default: guess EC2 instance status', + }) + .option('version-reporting', { + type: 'boolean', + desc: 'Include the "AWS::CDK::Metadata" resource in synthesized templates (enabled by default)', + default: undefined, + }) + .option('path-metadata', { + type: 'boolean', + desc: 'Include "aws:cdk:path" CloudFormation metadata for each resource (enabled by default)', + default: undefined, + }) + .option('asset-metadata', { + type: 'boolean', + desc: 'Include "aws:asset:*" CloudFormation metadata for resources that uses assets (enabled by default)', + default: undefined, + }) + .option('role-arn', { + type: 'string', + alias: 'r', + desc: 'ARN of Role to use when invoking CloudFormation', + default: undefined, + requiresArg: true, + }) + .option('staging', { + type: 'boolean', + desc: 'Copy assets to the output directory (use --no-staging to disable the copy of assets which allows local debugging via the SAM CLI to reference the original source files)', + default: true, + }) + .option('output', { + type: 'string', + alias: 'o', + desc: 'Emits the synthesized cloud assembly into a directory (default: cdk.out)', + requiresArg: true, + }) + .option('notices', { + type: 'boolean', + desc: 'Show relevant notices', + }) + .option('no-color', { + type: 'boolean', + desc: 'Removes colors and other style from console output', + default: false, + }) + .option('ci', { + type: 'boolean', + desc: 'Force CI detection. If CI=true then logs will be sent to stdout instead of stderr', + default: process.env.CI !== undefined, + }) + .option('unstable', { + type: 'array', + desc: 'Opt in to unstable features. The flag indicates that the scope and API of a feature might still change. Otherwise the feature is generally production ready and fully supported. Can be specified multiple times.', + default: [], + }) + .command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) => + yargs + .option('long', { + type: 'boolean', + default: false, + alias: 'l', + desc: 'Display environment information for each stack', + }) + .option('show-dependencies', { + type: 'boolean', + default: false, + alias: 'd', + desc: 'Display stack dependency information for each stack', + }) + ) + .command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', (yargs: Argv) => + yargs + .option('exclusively', { + type: 'boolean', + alias: 'e', + desc: "Only synthesize requested stacks, don't include dependencies", + }) + .option('validation', { + type: 'boolean', + desc: 'After synthesis, validate stacks with the "validateOnSynth" attribute set (can also be controlled with CDK_VALIDATION)', + default: true, + }) + .option('quiet', { + type: 'boolean', + alias: 'q', + desc: 'Do not output CloudFormation Template to stdout', + default: false, + }) + ) + .command(['bootstrap [ENVIRONMENTS..]'], 'Deploys the CDK toolkit stack into an AWS environment', (yargs: Argv) => + yargs + .option('bootstrap-bucket-name', { + type: 'string', + alias: ['b', 'toolkit-bucket-name'], + desc: 'The name of the CDK toolkit bucket; bucket will be created and must not exist', + default: undefined, + }) + .option('bootstrap-kms-key-id', { + type: 'string', + desc: 'AWS KMS master key ID used for the SSE-KMS encryption', + default: undefined, + conflicts: 'bootstrap-customer-key', + }) + .option('example-permissions-boundary', { + type: 'boolean', + alias: 'epb', + desc: 'Use the example permissions boundary.', + default: undefined, + conflicts: 'custom-permissions-boundary', + }) + .option('custom-permissions-boundary', { + type: 'string', + alias: 'cpb', + desc: 'Use the permissions boundary specified by name.', + default: undefined, + conflicts: 'example-permissions-boundary', + }) + .option('bootstrap-customer-key', { + type: 'boolean', + desc: 'Create a Customer Master Key (CMK) for the bootstrap bucket (you will be charged but can customize permissions, modern bootstrapping only)', + default: undefined, + conflicts: 'bootstrap-kms-key-id', + }) + .option('qualifier', { + type: 'string', + desc: 'String which must be unique for each bootstrap stack. You must configure it on your CDK app if you change this from the default.', + default: undefined, + }) + .option('public-access-block-configuration', { + type: 'boolean', + desc: 'Block public access configuration on CDK toolkit bucket (enabled by default) ', + default: undefined, + }) + .option('tags', { + type: 'array', + alias: 't', + desc: 'Tags to add for the stack (KEY=VALUE)', + nargs: 1, + requiresArg: true, + default: [], + }) + .option('execute', { + type: 'boolean', + desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', + default: true, + }) + .option('trust', { + type: 'array', + desc: 'The AWS account IDs that should be trusted to perform deployments into this environment (may be repeated, modern bootstrapping only)', + default: [], + nargs: 1, + requiresArg: true, + }) + .option('trust-for-lookup', { + type: 'array', + desc: 'The AWS account IDs that should be trusted to look up values in this environment (may be repeated, modern bootstrapping only)', + default: [], + nargs: 1, + requiresArg: true, + }) + .option('cloudformation-execution-policies', { + type: 'array', + desc: 'The Managed Policy ARNs that should be attached to the role performing deployments into this environment (may be repeated, modern bootstrapping only)', + default: [], + nargs: 1, + requiresArg: true, + }) + .option('force', { + alias: 'f', + type: 'boolean', + desc: 'Always bootstrap even if it would downgrade template version', + default: false, + }) + .option('termination-protection', { + type: 'boolean', + default: undefined, + desc: 'Toggle CloudFormation termination protection on the bootstrap stacks', + }) + .option('show-template', { + type: 'boolean', + desc: "Instead of actual bootstrapping, print the current CLI's bootstrapping template to stdout for customization", + default: false, + }) + .option('toolkit-stack-name', { + type: 'string', + desc: 'The name of the CDK toolkit stack to create', + requiresArg: true, + }) + .option('template', { + type: 'string', + requiresArg: true, + desc: 'Use the template from the given file instead of the built-in one (use --show-template to obtain an example)', + }) + .option('previous-parameters', { + type: 'boolean', + default: true, + desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)', + }) + ) + .command(['gc [ENVIRONMENTS..]'], 'Garbage collect assets', (yargs: Argv) => + yargs + .option('action', { + type: 'string', + desc: 'The action (or sub-action) you want to perform. Valid entires are "print", "tag", "delete-tagged", "full".', + default: 'full', + }) + .option('type', { + type: 'string', + desc: 'Specify either ecr, s3, or all', + default: 'all', + }) + .option('rollback-buffer-days', { + type: 'number', + desc: 'Delete assets that have been marked as isolated for this many days', + default: 0, + }) + .option('created-buffer-days', { + type: 'number', + desc: 'Never delete assets younger than this (in days)', + default: 1, + }) + .option('confirm', { + type: 'boolean', + desc: 'Confirm via manual prompt before deletion', + default: true, + }) + .option('bootstrap-stack-name', { + type: 'string', + desc: 'The name of the CDK toolkit stack, if different from the default "CDKToolkit"', + requiresArg: true, + }) + ) + .command(['deploy [STACKS..]'], 'Deploys the stack(s) named STACKS into your AWS account', (yargs: Argv) => + yargs + .option('all', { + type: 'boolean', + desc: 'Deploy all available stacks', + default: false, + }) + .option('build-exclude', { + type: 'array', + alias: 'E', + nargs: 1, + desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', + default: [], + }) + .option('exclusively', { + type: 'boolean', + alias: 'e', + desc: "Only deploy requested stacks, don't include dependencies", + }) + .option('require-approval', { + type: 'string', + choices: ['never', 'any-change', 'broadening'], + desc: 'What security-sensitive changes need manual approval', + }) + .option('notification-arns', { + type: 'array', + desc: 'ARNs of SNS topics that CloudFormation will notify with stack related events', + nargs: 1, + requiresArg: true, + }) + .option('tags', { + type: 'array', + alias: 't', + desc: 'Tags to add to the stack (KEY=VALUE), overrides tags from Cloud Assembly (deprecated)', + nargs: 1, + requiresArg: true, + }) + .option('execute', { + type: 'boolean', + desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet) (deprecated)', + deprecated: true, + }) + .option('change-set-name', { + type: 'string', + desc: 'Name of the CloudFormation change set to create (only if method is not direct)', + }) + .option('method', { + alias: 'm', + type: 'string', + choices: ['direct', 'change-set', 'prepare-change-set'], + requiresArg: true, + desc: 'How to perform the deployment. Direct is a bit faster but lacks progress information', + }) + .option('force', { + alias: 'f', + type: 'boolean', + desc: 'Always deploy stack even if templates are identical', + default: false, + }) + .option('parameters', { + type: 'array', + desc: 'Additional parameters passed to CloudFormation at deploy time (STACK:KEY=VALUE)', + nargs: 1, + requiresArg: true, + default: {}, + }) + .option('outputs-file', { + type: 'string', + alias: 'O', + desc: 'Path to file where stack outputs will be written as JSON', + requiresArg: true, + }) + .option('previous-parameters', { + type: 'boolean', + default: true, + desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)', + }) + .option('toolkit-stack-name', { + type: 'string', + desc: 'The name of the existing CDK toolkit stack (only used for app using legacy synthesis)', + requiresArg: true, + }) + .option('progress', { + type: 'string', + choices: ['bar', 'events'], + desc: 'Display mode for stack activity events', + }) + .option('rollback', { + type: 'boolean', + desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. Note: do **not** disable this flag for deployments with resource replacements, as that will always fail", + }) + .middleware(yargsNegativeAlias('rollback', 'R'), true) + .option('R', { + type: 'boolean', + hidden: true, + }) + .option('hotswap', { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, but does not fall back to a full deployment if that is not possible. Instead, changes to any non-hotswappable properties are ignored.Do not use this in production environments", + }) + .option('hotswap-fallback', { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, which skips CloudFormation and updates the resources directly, and falls back to a full deployment if that is not possible. Do not use this in production environments", + }) + .option('watch', { + type: 'boolean', + desc: 'Continuously observe the project files, and deploy the given stack(s) automatically when changes are detected. Implies --hotswap by default', + }) + .option('logs', { + type: 'boolean', + default: true, + desc: "Show CloudWatch log events from all resources in the selected Stacks in the terminal. 'true' by default, use --no-logs to turn off. Only in effect if specified alongside the '--watch' option", + }) + .option('concurrency', { + type: 'number', + desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', + default: 1, + requiresArg: true, + }) + .option('asset-parallelism', { + type: 'boolean', + desc: 'Whether to build/publish assets in parallel', + }) + .option('asset-prebuild', { + type: 'boolean', + desc: 'Whether to build all assets before deploying the first stack (useful for failing Docker builds)', + default: true, + }) + .option('ignore-no-stacks', { + type: 'boolean', + desc: 'Whether to deploy if the app contains no stacks', + default: false, + }) + ) + .command(['rollback [STACKS..]'], 'Rolls back the stack(s) named STACKS to their last stable state', (yargs: Argv) => + yargs + .option('all', { + type: 'boolean', + default: false, + desc: 'Roll back all available stacks', + }) + .option('toolkit-stack-name', { + type: 'string', + desc: 'The name of the CDK toolkit stack the environment is bootstrapped with', + requiresArg: true, + }) + .option('force', { + alias: 'f', + type: 'boolean', + desc: 'Orphan all resources for which the rollback operation fails.', + }) + .option('validate-bootstrap-version', { + type: 'boolean', + desc: "Whether to validate the bootstrap stack version. Defaults to 'true', disable with --no-validate-bootstrap-version.", + }) + .option('orphan', { + type: 'array', + nargs: 1, + requiresArg: true, + desc: 'Orphan the given resources, identified by their logical ID (can be specified multiple times)', + default: [], + }) + ) + .command(['import [STACK]'], 'Import existing resource(s) into the given STACK', (yargs: Argv) => + yargs + .option('execute', { + type: 'boolean', + desc: 'Whether to execute ChangeSet (--no-execute will NOT execute the ChangeSet)', + default: true, + }) + .option('change-set-name', { + type: 'string', + desc: 'Name of the CloudFormation change set to create', + }) + .option('toolkit-stack-name', { + type: 'string', + desc: 'The name of the CDK toolkit stack to create', + requiresArg: true, + }) + .option('rollback', { + type: 'boolean', + desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. Note: do **not** disable this flag for deployments with resource replacements, as that will always fail", + }) + .option('force', { + alias: 'f', + type: 'boolean', + desc: "Do not abort if the template diff includes updates or deletes. This is probably safe but we're not sure, let us know how it goes.", + }) + .option('record-resource-mapping', { + type: 'string', + alias: 'r', + requiresArg: true, + desc: 'If specified, CDK will generate a mapping of existing physical resources to CDK resources to be imported as. The mapping will be written in the given file path. No actual import operation will be performed', + }) + .option('resource-mapping', { + type: 'string', + alias: 'm', + requiresArg: true, + desc: 'If specified, CDK will use the given file to map physical resources to CDK resources for import, instead of interactively asking the user. Can be run from scripts', + }) + ) + .command(['watch [STACKS..]'], "Shortcut for 'deploy --watch'", (yargs: Argv) => + yargs + .option('build-exclude', { + type: 'array', + alias: 'E', + nargs: 1, + desc: 'Do not rebuild asset with the given ID. Can be specified multiple times', + default: [], + }) + .option('exclusively', { + type: 'boolean', + alias: 'e', + desc: "Only deploy requested stacks, don't include dependencies", + }) + .option('change-set-name', { + type: 'string', + desc: 'Name of the CloudFormation change set to create', + }) + .option('force', { + alias: 'f', + type: 'boolean', + desc: 'Always deploy stack even if templates are identical', + default: false, + }) + .option('toolkit-stack-name', { + type: 'string', + desc: 'The name of the existing CDK toolkit stack (only used for app using legacy synthesis)', + requiresArg: true, + }) + .option('progress', { + type: 'string', + choices: ['bar', 'events'], + desc: 'Display mode for stack activity events', + }) + .option('rollback', { + type: 'boolean', + desc: "Rollback stack to stable state on failure. Defaults to 'true', iterate more rapidly with --no-rollback or -R. Note: do **not** disable this flag for deployments with resource replacements, as that will always fail", + }) + .middleware(yargsNegativeAlias('rollback', '-R'), true) + .option('R', { + type: 'boolean', + hidden: true, + }) + .option('hotswap', { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, but does not fall back to a full deployment if that is not possible. Instead, changes to any non-hotswappable properties are ignored.'true' by default, use --no-hotswap to turn off", + }) + .option('hotswap-fallback', { + type: 'boolean', + desc: "Attempts to perform a 'hotswap' deployment, which skips CloudFormation and updates the resources directly, and falls back to a full deployment if that is not possible.", + }) + .option('logs', { + type: 'boolean', + default: true, + desc: "Show CloudWatch log events from all resources in the selected Stacks in the terminal. 'true' by default, use --no-logs to turn off", + }) + .option('concurrency', { + type: 'number', + desc: 'Maximum number of simultaneous deployments (dependency permitting) to execute.', + default: 1, + requiresArg: true, + }) + ) + .command(['destroy [STACKS..]'], 'Destroy the stack(s) named STACKS', (yargs: Argv) => + yargs + .option('all', { + type: 'boolean', + default: false, + desc: 'Destroy all available stacks', + }) + .option('exclusively', { + type: 'boolean', + alias: 'e', + desc: "Only destroy requested stacks, don't include dependees", + }) + .option('force', { + type: 'boolean', + alias: 'f', + desc: 'Do not ask for confirmation before destroying the stacks', + }) + ) + .command( + ['diff [STACKS..]'], + 'Compares the specified stack with the deployed stack or a local template file, and returns with status 1 if any difference is found', + (yargs: Argv) => + yargs + .option('exclusively', { + type: 'boolean', + alias: 'e', + desc: "Only diff requested stacks, don't include dependencies", + }) + .option('context-lines', { + type: 'number', + desc: 'Number of context lines to include in arbitrary JSON diff rendering', + default: 3, + requiresArg: true, + }) + .option('template', { + type: 'string', + desc: 'The path to the CloudFormation template to compare with', + requiresArg: true, + }) + .option('strict', { + type: 'boolean', + desc: 'Do not filter out AWS::CDK::Metadata resources, mangled non-ASCII characters, or the CheckBootstrapVersionRule', + default: false, + }) + .option('security-only', { + type: 'boolean', + desc: 'Only diff for broadened security changes', + default: false, + }) + .option('fail', { + type: 'boolean', + desc: 'Fail with exit code 1 in case of diff', + }) + .option('processed', { + type: 'boolean', + desc: 'Whether to compare against the template with Transforms already processed', + default: false, + }) + .option('quiet', { + type: 'boolean', + alias: 'q', + desc: 'Do not print stack name and default message when there is no diff to stdout', + default: false, + }) + .option('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, + }) + ) + .command(['metadata [STACK]'], 'Returns all metadata associated with this stack') + .command(['acknowledge [ID]', 'ack [ID]'], 'Acknowledge a notice so that it does not show up anymore') + .command(['notices'], 'Returns a list of relevant notices', (yargs: Argv) => + yargs.option('unacknowledged', { + type: 'boolean', + alias: 'u', + default: false, + desc: 'Returns a list of unacknowledged notices', + }) + ) + .command(['init [TEMPLATE]'], 'Create a new, empty CDK project from a template.', (yargs: Argv) => + yargs + .option('language', { + type: 'string', + alias: 'l', + desc: 'The language to be used for the new project (default can be configured in ~/.cdk.json)', + choices: availableInitLanguages, + }) + .option('list', { + type: 'boolean', + desc: 'List the available templates', + }) + .option('generate-only', { + type: 'boolean', + default: false, + desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project', + }) + ) + .command(['migrate'], false, (yargs: Argv) => + yargs + .option('stack-name', { + type: 'string', + alias: 'n', + desc: 'The name assigned to the stack created in the new project. The name of the app will be based off this name as well.', + requiresArg: true, + }) + .option('language', { + type: 'string', + default: 'typescript', + alias: 'l', + desc: 'The language to be used for the new project', + choices: migrateSupportedLanguages, + }) + .option('account', { + type: 'string', + desc: 'The account to retrieve the CloudFormation stack template from', + }) + .option('region', { + type: 'string', + desc: 'The region to retrieve the CloudFormation stack template from', + }) + .option('from-path', { + type: 'string', + desc: 'The path to the CloudFormation template to migrate. Use this for locally stored templates', + }) + .option('from-stack', { + type: 'boolean', + desc: 'Use this flag to retrieve the template for an existing CloudFormation stack', + }) + .option('output-path', { + type: 'string', + desc: 'The output path for the migrated CDK app', + }) + .option('from-scan', { + type: 'string', + desc: 'Determines if a new scan should be created, or the last successful existing scan should be used \n options are "new" or "most-recent"', + }) + .option('filter', { + type: 'array', + desc: 'Filters the resource scan based on the provided criteria in the following format: "key1=value1,key2=value2"\n This field can be passed multiple times for OR style filtering: \n filtering options: \n resource-identifier: A key-value pair that identifies the target resource. i.e. {"ClusterName", "myCluster"}\n resource-type-prefix: A string that represents a type-name prefix. i.e. "AWS::DynamoDB::"\n tag-key: a string that matches resources with at least one tag with the provided key. i.e. "myTagKey"\n tag-value: a string that matches resources with at least one tag with the provided value. i.e. "myTagValue"', + }) + .option('compress', { + type: 'boolean', + desc: 'Use this flag to zip the generated CDK app', + }) + ) + .command(['context'], 'Manage cached context values', (yargs: Argv) => + yargs + .option('reset', { + alias: 'e', + desc: 'The context key (or its index) to reset', + type: 'string', + requiresArg: true, + }) + .option('force', { + alias: 'f', + desc: 'Ignore missing key error', + type: 'boolean', + default: false, + }) + .option('clear', { + desc: 'Clear all context', + type: 'boolean', + }) + ) + .command(['docs', 'doc '], 'Opens the reference documentation in a browser', (yargs: Argv) => + yargs.option('browser', { + alias: 'b', + desc: 'the command to use to open the browser, using %u as a placeholder for the path of the file to open', + type: 'string', + default: browserDefault, + }) + ) + .command(['doctor'], 'Check your set-up for potential problems') + .version(version) + .demandCommand(1, "''") + .recommendCommands() + .help() + .alias('h', 'help') + .epilogue( + 'If your app has a single stack, there is no need to specify the stack name\n\nIf one of cdk.json or ~/.cdk.json exists, options specified there will be used as defaults. Settings in cdk.json take precedence.' + ) + .parse(args); +} // eslint-disable-next-line @typescript-eslint/no-require-imports +const yargs = require('yargs'); diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index 2b062eb8164e4..85db0da1aa673 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -7,6 +7,7 @@ }, "scripts": { "build": "cdk-build", + "yargs-gen": "yarn ts-node --preferTsExts scripts/yargs-gen.ts", "watch": "cdk-watch", "lint": "cdk-lint", "pkglint": "pkglint -f", @@ -27,6 +28,9 @@ "attributions:update": "yarn node-bundle validate --entrypoint lib/index.ts --dont-attribute \"^@aws-cdk/|^cdk-assets|^cdk-cli-wrapper$\" --fix" }, "cdk-build": { + "pre": [ + "yarn yargs-gen" + ], "post": [ "cp ../../node_modules/cdk-from-cfn/index_bg.wasm ./lib/", "cp ../../node_modules/@aws-cdk/aws-service-spec/db.json.gz ./" @@ -100,6 +104,7 @@ "@aws-cdk/cloudformation-diff": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", + "@aws-cdk/yargs-gen": "0.0.0", "@jsii/check-node": "1.104.0", "archiver": "^5.3.2", "aws-sdk": "^2.1691.0", diff --git a/packages/aws-cdk/scripts/yargs-gen b/packages/aws-cdk/scripts/yargs-gen new file mode 100755 index 0000000000000..45571b6423707 --- /dev/null +++ b/packages/aws-cdk/scripts/yargs-gen @@ -0,0 +1,2 @@ +#!/usr/bin/env node +require('./yargs-gen.js'); diff --git a/packages/aws-cdk/scripts/yargs-gen.ts b/packages/aws-cdk/scripts/yargs-gen.ts new file mode 100644 index 0000000000000..f7b03c705a18e --- /dev/null +++ b/packages/aws-cdk/scripts/yargs-gen.ts @@ -0,0 +1,13 @@ +import * as fs from 'fs'; +// eslint-disable-next-line import/no-extraneous-dependencies +import { renderYargs } from '@aws-cdk/yargs-gen'; +import { makeConfig } from '../lib/config'; + +async function main() { + fs.writeFileSync('./lib/parse-command-line-arguments.ts', await renderYargs(makeConfig())); +} + +main().then(() => { +}).catch((e) => { + throw e; +}); \ No newline at end of file diff --git a/packages/aws-cdk/test/notices.test.ts b/packages/aws-cdk/test/notices.test.ts index b1a9cdf8bf31b..acc1fae841173 100644 --- a/packages/aws-cdk/test/notices.test.ts +++ b/packages/aws-cdk/test/notices.test.ts @@ -679,7 +679,7 @@ describe(Notices, () => { const configuration = new Configuration(); (configuration.context as any) = { get: (key: string) => context[key] }; - const notices = Notices.create({ configuration, includeAcknowlegded: true }); + const notices = Notices.create({ configuration, includeAcknowledged: true }); await notices.refresh({ dataSource: { fetch: async () => [BASIC_NOTICE, MULTIPLE_AFFECTED_VERSIONS_NOTICE] }, }); @@ -849,7 +849,7 @@ describe(Notices, () => { const configuration = new Configuration(); (configuration.context as any) = { get: (key: string) => context[key] }; - const notices = Notices.create({ configuration, includeAcknowlegded: true }); + const notices = Notices.create({ configuration, includeAcknowledged: true }); await notices.refresh({ dataSource: { fetch: async () => [BASIC_NOTICE, MULTIPLE_AFFECTED_VERSIONS_NOTICE] }, }); diff --git a/tools/@aws-cdk/yargs-gen/.eslintrc.js b/tools/@aws-cdk/yargs-gen/.eslintrc.js new file mode 100644 index 0000000000000..2658ee8727166 --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/tools/@aws-cdk/yargs-gen/.gitignore b/tools/@aws-cdk/yargs-gen/.gitignore new file mode 100644 index 0000000000000..39180f6eb0bb4 --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/.gitignore @@ -0,0 +1,18 @@ +# Build files +*.js +node_modules +*.js.map +*.d.ts +lib/services + +# Test artifacts +.LAST_BUILD +.nyc_output +coverage +nyc.config.js +*.snk +junit.xml + +# Keep configs +!.eslintrc.js +!jest.config.js diff --git a/tools/@aws-cdk/yargs-gen/.npmignore b/tools/@aws-cdk/yargs-gen/.npmignore new file mode 100644 index 0000000000000..79f3b5a763216 --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/.npmignore @@ -0,0 +1,7 @@ + +.LAST_BUILD +*.snk +junit.xml +.eslintrc.js +# exclude cdk artifacts +**/cdk.out \ No newline at end of file diff --git a/tools/@aws-cdk/yargs-gen/LICENSE b/tools/@aws-cdk/yargs-gen/LICENSE new file mode 100644 index 0000000000000..dcf28b52a83af --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/tools/@aws-cdk/yargs-gen/NOTICE b/tools/@aws-cdk/yargs-gen/NOTICE new file mode 100644 index 0000000000000..c0b1f046c881a --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2024 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/tools/@aws-cdk/yargs-gen/README.md b/tools/@aws-cdk/yargs-gen/README.md new file mode 100644 index 0000000000000..224b207afb367 --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/README.md @@ -0,0 +1,21 @@ +# yargs-gen + +Generates CDK CLI `yargs` configuration from the source of truth in `packages/aws-cdk/lib/config.ts` + +## Usage + +```ts +import { renderYargs } from '@aws-cdk/yargs-gen'; + +declare const config: CliConfig; + +fs.writeFileSync('./lib/parse-command-line-arguments.ts', await renderYargs(config)); +``` + +This package exports `renderYargs()`, which accepts the CLI command config as input and returns the yargs definition for it as a string. + +### Dynamic Values + +Some values must be computed at runtime, when a command is run. This is achieved with dynamic values; +if the framework sees a CLI option with a `dynamicValue`, then the framework will reference the corresponding parameter. +We should automatically generate the parameter definitions, instead of manually adding them, in the future. diff --git a/tools/@aws-cdk/yargs-gen/jest.config.js b/tools/@aws-cdk/yargs-gen/jest.config.js new file mode 100644 index 0000000000000..696b3d5b6e281 --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/jest.config.js @@ -0,0 +1,10 @@ +const baseConfig = require('@aws-cdk/cdk-build-tools/config/jest.config'); + +module.exports = { + ...baseConfig, + coverageThreshold: { + global: { + branches: 30, + }, + }, +}; diff --git a/tools/@aws-cdk/yargs-gen/lib/index.ts b/tools/@aws-cdk/yargs-gen/lib/index.ts new file mode 100644 index 0000000000000..9e1623713b0a3 --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/lib/index.ts @@ -0,0 +1,2 @@ +export * from './yargs-gen'; +export * from './yargs-types'; diff --git a/tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts b/tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts new file mode 100644 index 0000000000000..333d5da25d217 --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts @@ -0,0 +1,155 @@ +import { Expression, FreeFunction, Module, SelectiveModuleImport, Statement, Type, TypeScriptRenderer, code } from '@cdklabs/typewriter'; +import { EsLintRules } from '@cdklabs/typewriter/lib/eslint-rules'; +import * as prettier from 'prettier'; +import { CliConfig, YargsOption } from './yargs-types'; + +export async function renderYargs(config: CliConfig): Promise { + const scope = new Module('aws-cdk'); + + scope.documentation.push( '-------------------------------------------------------------------------------------------'); + scope.documentation.push('GENERATED FROM packages/aws-cdk/lib/config.ts.'); + scope.documentation.push('Do not edit by hand; all changes will be overwritten at build time from the config file.'); + scope.documentation.push('-------------------------------------------------------------------------------------------'); + + scope.addImport(new SelectiveModuleImport(scope, 'yargs', ['Argv'])); + + // 'https://github.com/yargs/yargs/issues/1929', + // 'https://github.com/evanw/esbuild/issues/1492', + scope.addInitialization(code.comment('eslint-disable-next-line @typescript-eslint/no-require-imports')); + scope.addInitialization(code.stmt.constVar(code.expr.ident('yargs'), code.expr.directCode("require('yargs')"))); + + const parseCommandLineArguments = new FreeFunction(scope, { + name: 'parseCommandLineArguments', + export: true, + returnType: Type.ANY, + parameters: [ + { name: 'args', type: Type.arrayOf(Type.STRING) }, + { name: 'browserDefault', type: Type.STRING }, + { name: 'availableInitLanguages', type: Type.arrayOf(Type.STRING) }, + { name: 'migrateSupportedLanguages', type: Type.arrayOf(Type.STRING) }, + { name: 'version', type: Type.STRING }, + { name: 'yargsNegativeAlias', type: Type.ANY }, + ], + }); + parseCommandLineArguments.addBody(makeYargs(config)); + + const ts = new TypeScriptRenderer({ + disabledEsLintRules: [ + EsLintRules.COMMA_DANGLE, + EsLintRules.COMMA_SPACING, + EsLintRules.MAX_LEN, + EsLintRules.QUOTES, + EsLintRules.QUOTE_PROPS, + ], + }).render(scope); + + return prettier.format(ts, { + parser: 'typescript', + printWidth: 150, + singleQuote: true, + }); +} + +// Use the following configuration for array arguments: +// +// { type: 'array', default: [], nargs: 1, requiresArg: true } +// +// The default behavior of yargs is to eat all strings following an array argument: +// +// ./prog --arg one two positional => will parse to { arg: ['one', 'two', 'positional'], _: [] } (so no positional arguments) +// ./prog --arg one two -- positional => does not help, for reasons that I can't understand. Still gets parsed incorrectly. +// +// By using the config above, every --arg will only consume one argument, so you can do the following: +// +// ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. +function makeYargs(config: CliConfig): Statement { + let yargsExpr: Expression = code.expr.ident('yargs'); + yargsExpr = yargsExpr.callMethod('usage', lit('Usage: cdk -a COMMAND')); + + // we must compute global options first, as they are not part of an argument to a command call + yargsExpr = makeOptions(yargsExpr, config.globalOptions); + + for (const command of Object.keys(config.commands)) { + const commandFacts = config.commands[command]; + const commandArg = commandFacts.arg + ? ` [${commandFacts.arg?.name}${commandFacts.arg?.variadic ? '..' : ''}]` + : ''; + const aliases = commandFacts.aliases + ? commandFacts.aliases.map((alias) => `, '${alias} ${commandArg}'`) + : ''; + + // must compute options before we compute the full command, because in yargs, the options are an argument to the command call. + let optionsExpr: Expression = code.expr.directCode('(yargs: Argv) => yargs'); + optionsExpr = makeOptions(optionsExpr, commandFacts.options ?? {}); + + yargsExpr = commandFacts.options + ? yargsExpr.callMethod('command', code.expr.directCode(`['${command}${commandArg}'${aliases}]`), lit(commandFacts.description), optionsExpr) + : yargsExpr.callMethod('command', code.expr.directCode(`['${command}${commandArg}'${aliases}]`), lit(commandFacts.description)); + } + + return code.stmt.ret(makeEpilogue(yargsExpr)); +} + +function makeOptions(prefix: Expression, options: { [optionName: string]: YargsOption }) { + let optionsExpr = prefix; + for (const option of Object.keys(options)) { + // each option can define at most one middleware call; if we need more, handle a list of these instead + let middlewareCallback: Expression | undefined = undefined; + const optionProps = options[option]; + const optionArgs: { [key: string]: Expression } = {}; + for (const optionProp of Object.keys(optionProps)) { + if (optionProp === 'negativeAlias') { + // middleware is a separate function call, so we can't store it with the regular option arguments, as those will all be treated as parameters: + // .option('R', { type: 'boolean', hidden: true }).middleware(yargsNegativeAlias('R', 'rollback'), true) + middlewareCallback = code.expr.builtInFn('yargsNegativeAlias', lit(option), lit(optionProps.negativeAlias)); + } else { + const optionValue = (optionProps as any)[optionProp]; + if (optionValue && optionValue.dynamicType === 'parameter') { + optionArgs[optionProp] = code.expr.ident(optionValue.dynamicValue); + } else if (optionValue && optionValue.dynamicType === 'function') { + const inlineFunction: string = optionValue.dynamicValue.toString(); + const NUMBER_OF_SPACES_BETWEEN_ARROW_AND_CODE = 3; + // this only works with arrow functions, like () => + optionArgs[optionProp] = code.expr.directCode(inlineFunction.substring(inlineFunction.indexOf('=>') + NUMBER_OF_SPACES_BETWEEN_ARROW_AND_CODE)); + } else { + optionArgs[optionProp] = lit(optionValue); + } + } + } + + optionsExpr = optionsExpr.callMethod('option', lit(option), code.expr.object(optionArgs)); + if (middlewareCallback) { + optionsExpr = optionsExpr.callMethod('middleware', middlewareCallback, lit(true)); + middlewareCallback = undefined; + } + } + + return optionsExpr; +} + +function makeEpilogue(prefix: Expression) { + let completeDefinition = prefix.callMethod('version', code.expr.ident('version')); + completeDefinition = completeDefinition.callMethod('demandCommand', lit(1), lit("''")); // just print help + completeDefinition = completeDefinition.callMethod('recommendCommands'); + completeDefinition = completeDefinition.callMethod('help'); + completeDefinition = completeDefinition.callMethod('alias', lit('h'), lit('help')); + completeDefinition = completeDefinition.callMethod('epilogue', lit([ + 'If your app has a single stack, there is no need to specify the stack name', + 'If one of cdk.json or ~/.cdk.json exists, options specified there will be used as defaults. Settings in cdk.json take precedence.', + ].join('\n\n'))); + + completeDefinition = completeDefinition.callMethod('parse', code.expr.ident('args')); + + return completeDefinition; +} + +function lit(value: any): Expression { + switch (value) { + case undefined: + return code.expr.UNDEFINED; + case null: + return code.expr.NULL; + default: + return code.expr.lit(value); + } +} diff --git a/tools/@aws-cdk/yargs-gen/lib/yargs-types.ts b/tools/@aws-cdk/yargs-gen/lib/yargs-types.ts new file mode 100644 index 0000000000000..3ab73594bd557 --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/lib/yargs-types.ts @@ -0,0 +1,80 @@ +interface YargsCommand { + description: string; + options?: { [optionName: string]: YargsOption }; + aliases?: string[]; + arg?: YargsArg; +} + +interface YargsArg { + name: string; + variadic: boolean; +} + +interface YargsCommand { + description: string; + options?: { [optionName: string]: YargsOption }; + aliases?: string[]; + arg?: YargsArg; +} + +interface YargsArg { + name: string; + variadic: boolean; +} + +export interface YargsOption { + type: 'string' | 'array' | 'number' | 'boolean' | 'count'; + desc?: string; + default?: any; + deprecated?: boolean | string; + choices?: ReadonlyArray; + alias?: string | string[]; + conflicts?: string | readonly string[] | { [key: string]: string | readonly string[] }; + nargs?: number; + requiresArg?: boolean; + hidden?: boolean; + count?: boolean; + negativeAlias?: string; +} + +export interface Middleware { + callback: string; + args: string[]; + applyBeforeValidation?: boolean; +} + +export interface CliConfig { + globalOptions: { [optionName: string]: YargsOption }; + commands: { [commandName: string]: YargsCommand }; +} + +/** + * The result of a DynamicValue call + */ +export interface DynamicResult { + dynamicType: 'parameter' | 'function'; + dynamicValue: string | (() => any); +} + +/** + * Informs the code library, `@aws-cdk/yargs-gen`, that + * this value references an entity not defined in this configuration file. + */ +export class DynamicValue { + /** + * Instructs `yargs-gen` to retrieve this value from the parameter with passed name. + */ + public static fromParameter(parameterName: string): DynamicResult { + return { + dynamicType: 'parameter', + dynamicValue: parameterName, + }; + } + + public static fromInline(f: () => any): DynamicResult { + return { + dynamicType: 'function', + dynamicValue: f, + }; + } +} diff --git a/tools/@aws-cdk/yargs-gen/package.json b/tools/@aws-cdk/yargs-gen/package.json new file mode 100644 index 0000000000000..7a794db607fa3 --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/package.json @@ -0,0 +1,57 @@ +{ + "name": "@aws-cdk/yargs-gen", + "private": true, + "version": "0.0.0", + "description": "Generate yargs", + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "tools/@aws-cdk/yargs-gen" + }, + "main": "./lib/index.js", + "types": "./lib/index.d.ts", + "scripts": { + "build": "cdk-build", + "watch": "cdk-watch", + "lint": "cdk-lint", + "test": "cdk-test", + "pkglint": "pkglint -f", + "build+test": "yarn build && yarn test", + "build+extract": "yarn build", + "build+test+extract": "yarn build+test", + "build+test+package": "yarn build+test" + }, + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "dependencies": { + "@cdklabs/typewriter": "^0.0.4", + "prettier": "^2.8.8" + }, + "devDependencies": { + "@aws-cdk/cdk-build-tools": "0.0.0", + "@aws-cdk/pkglint": "0.0.0", + "@types/jest": "^29.5.12", + "@types/node": "^18", + "jest": "^29.7.0" + }, + "keywords": [ + "aws", + "cdk" + ], + "homepage": "https://github.com/aws/aws-cdk", + "engines": { + "node": ">= 14.15.0" + }, + "ubergen": { + "exclude": true + }, + "pkglint": { + "exclude": [ + "dependencies/cdk-point-dependencies" + ] + } +} diff --git a/tools/@aws-cdk/yargs-gen/test/cli.test.ts b/tools/@aws-cdk/yargs-gen/test/cli.test.ts new file mode 100644 index 0000000000000..bdcea845d222d --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/test/cli.test.ts @@ -0,0 +1,75 @@ +import { CliConfig, renderYargs } from '../lib'; + +describe('render', () => { + test('can generate global options', async () => { + const config: CliConfig = { + globalOptions: { + one: { + type: 'string', + alias: 'o', + desc: 'text for one', + requiresArg: true, + }, + two: { type: 'number', desc: 'text for two' }, + three: { + type: 'array', + alias: 't', + desc: 'text for three', + nargs: 1, + requiresArg: true, + }, + }, + commands: {}, + }; + + expect(await renderYargs(config)).toMatchInlineSnapshot(` + "// ------------------------------------------------------------------------------------------- + // GENERATED FROM packages/aws-cdk/lib/config.ts. + // Do not edit by hand; all changes will be overwritten at build time from the config file. + // ------------------------------------------------------------------------------------------- + /* eslint-disable @typescript-eslint/comma-dangle, comma-spacing, max-len, quotes, quote-props */ + import { Argv } from 'yargs'; + + // @ts-ignore TS6133 + export function parseCommandLineArguments( + args: Array, + browserDefault: string, + availableInitLanguages: Array, + migrateSupportedLanguages: Array, + version: string, + yargsNegativeAlias: any + ): any { + return yargs + .usage('Usage: cdk -a COMMAND') + .option('one', { + type: 'string', + alias: 'o', + desc: 'text for one', + requiresArg: true, + }) + .option('two', { + type: 'number', + desc: 'text for two', + }) + .option('three', { + type: 'array', + alias: 't', + desc: 'text for three', + nargs: 1, + requiresArg: true, + }) + .version(version) + .demandCommand(1, "''") + .recommendCommands() + .help() + .alias('h', 'help') + .epilogue( + 'If your app has a single stack, there is no need to specify the stack name\\n\\nIf one of cdk.json or ~/.cdk.json exists, options specified there will be used as defaults. Settings in cdk.json take precedence.' + ) + .parse(args); + } // eslint-disable-next-line @typescript-eslint/no-require-imports + const yargs = require('yargs'); + " + `); + }); +}); diff --git a/tools/@aws-cdk/yargs-gen/tsconfig.json b/tools/@aws-cdk/yargs-gen/tsconfig.json new file mode 100644 index 0000000000000..8ac2abcd4fc9d --- /dev/null +++ b/tools/@aws-cdk/yargs-gen/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2020", + "module": "commonjs", + "lib": ["es2020", "dom"], + "strict": true, + "alwaysStrict": true, + "declaration": true, + "inlineSourceMap": true, + "inlineSources": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "resolveJsonModule": true, + "composite": true, + "incremental": true + }, + "include": ["**/*.ts"], + "exclude": ["**/*.d.ts"] +} diff --git a/yarn.lock b/yarn.lock index 4f083618498a2..058d904e961ea 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2996,6 +2996,11 @@ resolved "https://registry.npmjs.org/@cdklabs/typewriter/-/typewriter-0.0.3.tgz#37143d4cf004085bce7d1bbc9139bbf4bf4403a8" integrity sha512-dymXkqVKZLLQJGxZGvmCn9ZIDCiPM5hC1P7dABob8C0m5P0bf91W7HsPUu3yHomdFxoHAWFaXAZ9i3Q+uVeJ5g== +"@cdklabs/typewriter@^0.0.4": + version "0.0.4" + resolved "https://registry.npmjs.org/@cdklabs/typewriter/-/typewriter-0.0.4.tgz#4c2ae97c05eec921131549de08e37e5ecda80e43" + integrity sha512-FAcF8k0nNo3VmlGP3UHi4h2K5sohY/7Gcv4p7epMGwT4U3PbAsc3xWL42IAD1a/1g/rvrtIaRHbuGUp1O1VNvw== + "@colors/colors@1.5.0": version "1.5.0" resolved "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz#bb504579c1cae923e6576a4f5da43d25f97bdbd9" @@ -5278,7 +5283,7 @@ dependencies: "@types/istanbul-lib-report" "*" -"@types/jest@^29.5.14": +"@types/jest@^29.5.12", "@types/jest@^29.5.14": version "29.5.14" resolved "https://registry.npmjs.org/@types/jest/-/jest-29.5.14.tgz#2b910912fa1d6856cadcd0c1f95af7df1d6049e5" integrity sha512-ZN+4sdnLUbo8EVvVc2ao0GFW6oVrQRPn4K2lglySj7APvSrgzxHiNNK99us4WDMi57xxA2yggblIAMNhXOotLQ== @@ -13375,6 +13380,11 @@ prelude-ls@^1.2.1: resolved "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz#debc6489d7a6e6b0e7611888cec880337d316396" integrity sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g== +prettier@^2.8.8: + version "2.8.8" + resolved "https://registry.npmjs.org/prettier/-/prettier-2.8.8.tgz#e8c5d7e98a4305ffe3de2e1fc4aca1a71c28b1da" + integrity sha512-tdN8qQGvNjw4CHbY+XXk0JgCXn9QiF21a55rBe5LJAU+kDyC4WQn4+awm2Xfk2lQMk5fKup9XgzTZtGkjBdP9Q== + pretty-format@^29.0.0, pretty-format@^29.7.0: version "29.7.0" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz#ca42c758310f365bfa71a0bda0a807160b776812" From b3de7e6e8fe38003a8ac8f140f9ebf984c02d3ac Mon Sep 17 00:00:00 2001 From: Momo Kornher Date: Tue, 5 Nov 2024 13:46:30 +0000 Subject: [PATCH 7/9] refactor(cli): fix various minor codegen issues (#32024) ### Reason for this change Fixes some small issues that got missed before https://github.com/aws/aws-cdk/pull/31850 was unintentionally merged too early. ### Description of changes * re-add support for `CDK_` env variables * remove square bracktes when commands don't have an alias * remove extra space in command args * fixed `demandCommand` call ### Description of how you validated changes Run via tests, compared manually with cli options before we started to generate them. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../lib/parse-command-line-arguments.ts | 104 +++++++++--------- tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts | 24 +++- tools/@aws-cdk/yargs-gen/test/cli.test.ts | 3 +- 3 files changed, 74 insertions(+), 57 deletions(-) diff --git a/packages/aws-cdk/lib/parse-command-line-arguments.ts b/packages/aws-cdk/lib/parse-command-line-arguments.ts index 30379f899b104..3f7ebd16aa4d8 100644 --- a/packages/aws-cdk/lib/parse-command-line-arguments.ts +++ b/packages/aws-cdk/lib/parse-command-line-arguments.ts @@ -15,6 +15,7 @@ export function parseCommandLineArguments( yargsNegativeAlias: any ): any { return yargs + .env('CDK') .usage('Usage: cdk -a COMMAND') .option('app', { type: 'string', @@ -148,7 +149,7 @@ export function parseCommandLineArguments( desc: 'Opt in to unstable features. The flag indicates that the scope and API of a feature might still change. Otherwise the feature is generally production ready and fully supported. Can be specified multiple times.', default: [], }) - .command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) => + .command(['list [STACKS..]', 'ls [STACKS..]'], 'Lists all stacks in the app', (yargs: Argv) => yargs .option('long', { type: 'boolean', @@ -163,7 +164,7 @@ export function parseCommandLineArguments( desc: 'Display stack dependency information for each stack', }) ) - .command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', (yargs: Argv) => + .command(['synthesize [STACKS..]', 'synth [STACKS..]'], 'Synthesizes and prints the CloudFormation template for this stack', (yargs: Argv) => yargs .option('exclusively', { type: 'boolean', @@ -182,7 +183,7 @@ export function parseCommandLineArguments( default: false, }) ) - .command(['bootstrap [ENVIRONMENTS..]'], 'Deploys the CDK toolkit stack into an AWS environment', (yargs: Argv) => + .command('bootstrap [ENVIRONMENTS..]', 'Deploys the CDK toolkit stack into an AWS environment', (yargs: Argv) => yargs .option('bootstrap-bucket-name', { type: 'string', @@ -292,40 +293,43 @@ export function parseCommandLineArguments( desc: 'Use previous values for existing parameters (you must specify all parameters on every deployment if this is disabled)', }) ) - .command(['gc [ENVIRONMENTS..]'], 'Garbage collect assets', (yargs: Argv) => - yargs - .option('action', { - type: 'string', - desc: 'The action (or sub-action) you want to perform. Valid entires are "print", "tag", "delete-tagged", "full".', - default: 'full', - }) - .option('type', { - type: 'string', - desc: 'Specify either ecr, s3, or all', - default: 'all', - }) - .option('rollback-buffer-days', { - type: 'number', - desc: 'Delete assets that have been marked as isolated for this many days', - default: 0, - }) - .option('created-buffer-days', { - type: 'number', - desc: 'Never delete assets younger than this (in days)', - default: 1, - }) - .option('confirm', { - type: 'boolean', - desc: 'Confirm via manual prompt before deletion', - default: true, - }) - .option('bootstrap-stack-name', { - type: 'string', - desc: 'The name of the CDK toolkit stack, if different from the default "CDKToolkit"', - requiresArg: true, - }) + .command( + 'gc [ENVIRONMENTS..]', + 'Garbage collect assets. Options detailed here: https://github.com/aws/aws-cdk/blob/main/packages/aws-cdk/README.md#cdk-gc', + (yargs: Argv) => + yargs + .option('action', { + type: 'string', + desc: 'The action (or sub-action) you want to perform. Valid entires are "print", "tag", "delete-tagged", "full".', + default: 'full', + }) + .option('type', { + type: 'string', + desc: 'Specify either ecr, s3, or all', + default: 'all', + }) + .option('rollback-buffer-days', { + type: 'number', + desc: 'Delete assets that have been marked as isolated for this many days', + default: 0, + }) + .option('created-buffer-days', { + type: 'number', + desc: 'Never delete assets younger than this (in days)', + default: 1, + }) + .option('confirm', { + type: 'boolean', + desc: 'Confirm via manual prompt before deletion', + default: true, + }) + .option('bootstrap-stack-name', { + type: 'string', + desc: 'The name of the CDK toolkit stack, if different from the default "CDKToolkit"', + requiresArg: true, + }) ) - .command(['deploy [STACKS..]'], 'Deploys the stack(s) named STACKS into your AWS account', (yargs: Argv) => + .command('deploy [STACKS..]', 'Deploys the stack(s) named STACKS into your AWS account', (yargs: Argv) => yargs .option('all', { type: 'boolean', @@ -459,7 +463,7 @@ export function parseCommandLineArguments( default: false, }) ) - .command(['rollback [STACKS..]'], 'Rolls back the stack(s) named STACKS to their last stable state', (yargs: Argv) => + .command('rollback [STACKS..]', 'Rolls back the stack(s) named STACKS to their last stable state', (yargs: Argv) => yargs .option('all', { type: 'boolean', @@ -488,7 +492,7 @@ export function parseCommandLineArguments( default: [], }) ) - .command(['import [STACK]'], 'Import existing resource(s) into the given STACK', (yargs: Argv) => + .command('import [STACK]', 'Import existing resource(s) into the given STACK', (yargs: Argv) => yargs .option('execute', { type: 'boolean', @@ -526,7 +530,7 @@ export function parseCommandLineArguments( desc: 'If specified, CDK will use the given file to map physical resources to CDK resources for import, instead of interactively asking the user. Can be run from scripts', }) ) - .command(['watch [STACKS..]'], "Shortcut for 'deploy --watch'", (yargs: Argv) => + .command('watch [STACKS..]', "Shortcut for 'deploy --watch'", (yargs: Argv) => yargs .option('build-exclude', { type: 'array', @@ -589,7 +593,7 @@ export function parseCommandLineArguments( requiresArg: true, }) ) - .command(['destroy [STACKS..]'], 'Destroy the stack(s) named STACKS', (yargs: Argv) => + .command('destroy [STACKS..]', 'Destroy the stack(s) named STACKS', (yargs: Argv) => yargs .option('all', { type: 'boolean', @@ -608,7 +612,7 @@ export function parseCommandLineArguments( }) ) .command( - ['diff [STACKS..]'], + 'diff [STACKS..]', 'Compares the specified stack with the deployed stack or a local template file, and returns with status 1 if any difference is found', (yargs: Argv) => yargs @@ -660,9 +664,9 @@ export function parseCommandLineArguments( default: true, }) ) - .command(['metadata [STACK]'], 'Returns all metadata associated with this stack') - .command(['acknowledge [ID]', 'ack [ID]'], 'Acknowledge a notice so that it does not show up anymore') - .command(['notices'], 'Returns a list of relevant notices', (yargs: Argv) => + .command('metadata [STACK]', 'Returns all metadata associated with this stack') + .command(['acknowledge [ID]', 'ack [ID]'], 'Acknowledge a notice so that it does not show up anymore') + .command('notices', 'Returns a list of relevant notices', (yargs: Argv) => yargs.option('unacknowledged', { type: 'boolean', alias: 'u', @@ -670,7 +674,7 @@ export function parseCommandLineArguments( desc: 'Returns a list of unacknowledged notices', }) ) - .command(['init [TEMPLATE]'], 'Create a new, empty CDK project from a template.', (yargs: Argv) => + .command('init [TEMPLATE]', 'Create a new, empty CDK project from a template.', (yargs: Argv) => yargs .option('language', { type: 'string', @@ -688,7 +692,7 @@ export function parseCommandLineArguments( desc: 'If true, only generates project files, without executing additional operations such as setting up a git repo, installing dependencies or compiling the project', }) ) - .command(['migrate'], false, (yargs: Argv) => + .command('migrate', false, (yargs: Argv) => yargs .option('stack-name', { type: 'string', @@ -736,7 +740,7 @@ export function parseCommandLineArguments( desc: 'Use this flag to zip the generated CDK app', }) ) - .command(['context'], 'Manage cached context values', (yargs: Argv) => + .command('context', 'Manage cached context values', (yargs: Argv) => yargs .option('reset', { alias: 'e', @@ -755,7 +759,7 @@ export function parseCommandLineArguments( type: 'boolean', }) ) - .command(['docs', 'doc '], 'Opens the reference documentation in a browser', (yargs: Argv) => + .command(['docs', 'doc'], 'Opens the reference documentation in a browser', (yargs: Argv) => yargs.option('browser', { alias: 'b', desc: 'the command to use to open the browser, using %u as a placeholder for the path of the file to open', @@ -763,9 +767,9 @@ export function parseCommandLineArguments( default: browserDefault, }) ) - .command(['doctor'], 'Check your set-up for potential problems') + .command('doctor', 'Check your set-up for potential problems') .version(version) - .demandCommand(1, "''") + .demandCommand(1, '') .recommendCommands() .help() .alias('h', 'help') diff --git a/tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts b/tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts index 333d5da25d217..d19ff83041367 100644 --- a/tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts +++ b/tools/@aws-cdk/yargs-gen/lib/yargs-gen.ts @@ -64,7 +64,9 @@ export async function renderYargs(config: CliConfig): Promise { // ./prog --arg one --arg two position => will parse to { arg: ['one', 'two'], _: ['positional'] }. function makeYargs(config: CliConfig): Statement { let yargsExpr: Expression = code.expr.ident('yargs'); - yargsExpr = yargsExpr.callMethod('usage', lit('Usage: cdk -a COMMAND')); + yargsExpr = yargsExpr + .callMethod('env', lit('CDK')) + .callMethod('usage', lit('Usage: cdk -a COMMAND')); // we must compute global options first, as they are not part of an argument to a command call yargsExpr = makeOptions(yargsExpr, config.globalOptions); @@ -75,16 +77,26 @@ function makeYargs(config: CliConfig): Statement { ? ` [${commandFacts.arg?.name}${commandFacts.arg?.variadic ? '..' : ''}]` : ''; const aliases = commandFacts.aliases - ? commandFacts.aliases.map((alias) => `, '${alias} ${commandArg}'`) + ? commandFacts.aliases.map((alias) => `, '${alias}${commandArg}'`) : ''; // must compute options before we compute the full command, because in yargs, the options are an argument to the command call. let optionsExpr: Expression = code.expr.directCode('(yargs: Argv) => yargs'); optionsExpr = makeOptions(optionsExpr, commandFacts.options ?? {}); - yargsExpr = commandFacts.options - ? yargsExpr.callMethod('command', code.expr.directCode(`['${command}${commandArg}'${aliases}]`), lit(commandFacts.description), optionsExpr) - : yargsExpr.callMethod('command', code.expr.directCode(`['${command}${commandArg}'${aliases}]`), lit(commandFacts.description)); + const commandCallArgs: Array = []; + if (aliases) { + commandCallArgs.push(code.expr.directCode(`['${command}${commandArg}'${aliases}]`)); + } else { + commandCallArgs.push(code.expr.directCode(`'${command}${commandArg}'`)); + } + commandCallArgs.push(lit(commandFacts.description)); + + if (commandFacts.options) { + commandCallArgs.push(optionsExpr); + } + + yargsExpr = yargsExpr.callMethod('command', ...commandCallArgs); } return code.stmt.ret(makeEpilogue(yargsExpr)); @@ -129,7 +141,7 @@ function makeOptions(prefix: Expression, options: { [optionName: string]: YargsO function makeEpilogue(prefix: Expression) { let completeDefinition = prefix.callMethod('version', code.expr.ident('version')); - completeDefinition = completeDefinition.callMethod('demandCommand', lit(1), lit("''")); // just print help + completeDefinition = completeDefinition.callMethod('demandCommand', lit(1), lit('')); // just print help completeDefinition = completeDefinition.callMethod('recommendCommands'); completeDefinition = completeDefinition.callMethod('help'); completeDefinition = completeDefinition.callMethod('alias', lit('h'), lit('help')); diff --git a/tools/@aws-cdk/yargs-gen/test/cli.test.ts b/tools/@aws-cdk/yargs-gen/test/cli.test.ts index bdcea845d222d..d6c99e849664b 100644 --- a/tools/@aws-cdk/yargs-gen/test/cli.test.ts +++ b/tools/@aws-cdk/yargs-gen/test/cli.test.ts @@ -40,6 +40,7 @@ describe('render', () => { yargsNegativeAlias: any ): any { return yargs + .env('CDK') .usage('Usage: cdk -a COMMAND') .option('one', { type: 'string', @@ -59,7 +60,7 @@ describe('render', () => { requiresArg: true, }) .version(version) - .demandCommand(1, "''") + .demandCommand(1, '') .recommendCommands() .help() .alias('h', 'help') From 2f9fb1e050331efbbe84bb0d5943ff7798cbf3fe Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Tue, 5 Nov 2024 16:25:35 +0100 Subject: [PATCH 8/9] feat(cli): automatically roll back stacks if necessary (#31920) If a user is deploying with `--no-rollback`, and the stack contains replacements (or the `--no-rollback` flag is dropped), then a rollback needs to be performed before a regular deployment can happen again. In this PR, we add a prompt where we ask the user to confirm that they are okay with performing a rollback and then a normal deployment. The way this works is that `deployStack` detects a disallowed combination (replacement and no-rollback, or being in a stuck state and not being called with no-rollback), and returns a special status code. The driver of the calls, `CdkToolkit`, will see those special return codes, prompt the user, and retry. Also get rid of a stray `Stack undefined` that gets printed to the console. Closes #30546, Closes https://github.com/aws/aws-cdk/issues/31685 ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../cdk-apps/rollback-test-app/app.js | 14 +- .../tests/cli-integ-tests/cli.integtest.ts | 97 +++++++++ .../cloudformation-diff/lib/format.ts | 2 +- packages/aws-cdk/README.md | 19 +- .../api/bootstrap/bootstrap-environment.ts | 10 +- .../lib/api/bootstrap/deploy-bootstrap.ts | 13 +- packages/aws-cdk/lib/api/deploy-stack.ts | 65 ++++++- .../aws-cdk/lib/api/hotswap-deployments.ts | 6 +- .../api/util/cloudformation/stack-status.ts | 4 + packages/aws-cdk/lib/cdk-toolkit.ts | 184 +++++++++++++----- packages/aws-cdk/lib/diff.ts | 9 +- packages/aws-cdk/lib/import.ts | 3 + packages/aws-cdk/test/api/bootstrap2.test.ts | 19 +- .../aws-cdk/test/api/deploy-stack.test.ts | 65 ++++++- .../test/api/hotswap/hotswap-test-setup.ts | 4 +- packages/aws-cdk/test/cdk-toolkit.test.ts | 73 ++++++- packages/aws-cdk/test/diff.test.ts | 19 +- 17 files changed, 500 insertions(+), 106 deletions(-) diff --git a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/app.js b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/app.js index 419e30898c9bf..dd117b62a9dd9 100644 --- a/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/app.js +++ b/packages/@aws-cdk-testing/cli-integ/resources/cdk-apps/rollback-test-app/app.js @@ -1,15 +1,17 @@ const cdk = require('aws-cdk-lib'); const lambda = require('aws-cdk-lib/aws-lambda'); +const sqs = require('aws-cdk-lib/aws-sqs'); const cr = require('aws-cdk-lib/custom-resources'); /** * This stack will be deployed in multiple phases, to achieve a very specific effect * - * It contains resources r1 and r2, where r1 gets deployed first. + * It contains resources r1 and r2, and a queue q, where r1 gets deployed first. * * - PHASE = 1: both resources deploy regularly. * - PHASE = 2a: r1 gets updated, r2 will fail to update * - PHASE = 2b: r1 gets updated, r2 will fail to update, and r1 will fail its rollback. + * - PHASE = 3: q gets replaced w.r.t. phases 1 and 2 * * To exercise this app: * @@ -22,7 +24,7 @@ const cr = require('aws-cdk-lib/custom-resources'); * # This will start a rollback that will fail because r1 fails its rollabck * * env PHASE=2b npx cdk rollback --force - * # This will retry the rollabck and skip r1 + * # This will retry the rollback and skip r1 * ``` */ class RollbacktestStack extends cdk.Stack { @@ -31,6 +33,7 @@ class RollbacktestStack extends cdk.Stack { let r1props = {}; let r2props = {}; + let fifo = false; const phase = process.env.PHASE; switch (phase) { @@ -46,6 +49,9 @@ class RollbacktestStack extends cdk.Stack { r1props.FailRollback = true; r2props.FailUpdate = true; break; + case '3': + fifo = true; + break; } const fn = new lambda.Function(this, 'Fun', { @@ -76,6 +82,10 @@ class RollbacktestStack extends cdk.Stack { properties: r2props, }); r2.node.addDependency(r1); + + new sqs.Queue(this, 'Queue', { + fifo, + }); } } diff --git a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts index 25d669f8bedb6..ec6e0307777d4 100644 --- a/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts +++ b/packages/@aws-cdk-testing/cli-integ/tests/cli-integ-tests/cli.integtest.ts @@ -2450,6 +2450,103 @@ integTest( }), ); +integTest( + 'automatic rollback if paused and change contains a replacement', + withSpecificFixture('rollback-test-app', async (fixture) => { + let phase = '1'; + + // Should succeed + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + }); + try { + phase = '2a'; + + // Should fail + const deployOutput = await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + allowErrExit: true, + }); + expect(deployOutput).toContain('UPDATE_FAILED'); + + // Do a deployment with a replacement and --force: this will roll back first and then deploy normally + phase = '3'; + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback', '--force'], + modEnv: { PHASE: phase }, + verbose: false, + }); + } finally { + await fixture.cdkDestroy('test-rollback'); + } + }), +); + +integTest( + 'automatic rollback if paused and --no-rollback is removed from flags', + withSpecificFixture('rollback-test-app', async (fixture) => { + let phase = '1'; + + // Should succeed + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + }); + try { + phase = '2a'; + + // Should fail + const deployOutput = await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + allowErrExit: true, + }); + expect(deployOutput).toContain('UPDATE_FAILED'); + + // Do a deployment removing --no-rollback: this will roll back first and then deploy normally + phase = '1'; + await fixture.cdkDeploy('test-rollback', { + options: ['--force'], + modEnv: { PHASE: phase }, + verbose: false, + }); + } finally { + await fixture.cdkDestroy('test-rollback'); + } + }), +); + +integTest( + 'automatic rollback if replacement and --no-rollback is removed from flags', + withSpecificFixture('rollback-test-app', async (fixture) => { + let phase = '1'; + + // Should succeed + await fixture.cdkDeploy('test-rollback', { + options: ['--no-rollback'], + modEnv: { PHASE: phase }, + verbose: false, + }); + try { + // Do a deployment with a replacement and removing --no-rollback: this will do a regular rollback deploy + phase = '3'; + await fixture.cdkDeploy('test-rollback', { + options: ['--force'], + modEnv: { PHASE: phase }, + verbose: false, + }); + } finally { + await fixture.cdkDestroy('test-rollback'); + } + }), +); + integTest( 'test cdk rollback --force', withSpecificFixture('rollback-test-app', async (fixture) => { diff --git a/packages/@aws-cdk/cloudformation-diff/lib/format.ts b/packages/@aws-cdk/cloudformation-diff/lib/format.ts index 61f44fc15e5fa..a29155f87399a 100644 --- a/packages/@aws-cdk/cloudformation-diff/lib/format.ts +++ b/packages/@aws-cdk/cloudformation-diff/lib/format.ts @@ -160,7 +160,7 @@ export class Formatter { const resourceType = diff.isRemoval ? diff.oldResourceType : diff.newResourceType; // eslint-disable-next-line max-len - this.print(`${this.formatResourcePrefix(diff)} ${this.formatValue(resourceType, chalk.cyan)} ${this.formatLogicalId(logicalId)} ${this.formatImpact(diff.changeImpact)}`); + this.print(`${this.formatResourcePrefix(diff)} ${this.formatValue(resourceType, chalk.cyan)} ${this.formatLogicalId(logicalId)} ${this.formatImpact(diff.changeImpact)}`.trimEnd()); if (diff.isUpdate) { const differenceCount = diff.differenceCount; diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 05948cd497295..cc012275c6274 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -205,11 +205,14 @@ $ cdk deploy -R ``` If a deployment fails you can update your code and immediately retry the -deployment from the point of failure. If you would like to explicitly roll back a failed, paused deployment, -use `cdk rollback`. +deployment from the point of failure. If you would like to explicitly roll back +a failed, paused deployment, use `cdk rollback`. -NOTE: you cannot use `--no-rollback` for any updates that would cause a resource replacement, only for updates -and creations of new resources. +`--no-rollback` deployments cannot contain resource replacements. If the CLI +detects that a resource is being replaced, it will prompt you to perform +a regular replacement instead. If the stack rollback is currently paused +and you are trying to perform an deployment that contains a replacement, you +will be prompted to roll back first. #### Deploying multiple stacks @@ -801,7 +804,7 @@ In practice this means for any resource in the provided template, for example, } ``` -There must not exist a resource of that type with the same identifier in the desired region. In this example that identfier +There must not exist a resource of that type with the same identifier in the desired region. In this example that identfier would be "amzn-s3-demo-bucket" ##### **The provided template is not deployed to CloudFormation in the account/region, and there *is* overlap with existing resources in the account/region** @@ -900,7 +903,7 @@ CDK Garbage Collection. > API of feature might still change. Otherwise the feature is generally production > ready and fully supported. -`cdk gc` garbage collects unused assets from your bootstrap bucket via the following mechanism: +`cdk gc` garbage collects unused assets from your bootstrap bucket via the following mechanism: - for each object in the bootstrap S3 Bucket, check to see if it is referenced in any existing CloudFormation templates - if not, it is treated as unused and gc will either tag it or delete it, depending on your configuration. @@ -938,7 +941,7 @@ Found X objects to delete based off of the following criteria: Delete this batch (yes/no/delete-all)? ``` -Since it's quite possible that the bootstrap bucket has many objects, we work in batches of 1000 objects or 100 images. +Since it's quite possible that the bootstrap bucket has many objects, we work in batches of 1000 objects or 100 images. To skip the prompt either reply with `delete-all`, or use the `--confirm=false` option. ```console @@ -948,7 +951,7 @@ cdk gc --unstable=gc --confirm=false If you are concerned about deleting assets too aggressively, there are multiple levers you can configure: - rollback-buffer-days: this is the amount of days an asset has to be marked as isolated before it is elligible for deletion. -- created-buffer-days: this is the amount of days an asset must live before it is elligible for deletion. +- created-buffer-days: this is the amount of days an asset must live before it is elligible for deletion. When using `rollback-buffer-days`, instead of deleting unused objects, `cdk gc` will tag them with today's date instead. It will also check if any objects have been tagged by previous runs of `cdk gc` diff --git a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts index f3041fd3864ec..46b5fd5bd909a 100644 --- a/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts +++ b/packages/aws-cdk/lib/api/bootstrap/bootstrap-environment.ts @@ -8,7 +8,7 @@ import { warning } from '../../logging'; import { loadStructuredFile, serializeStructure } from '../../serialize'; import { rootDir } from '../../util/directories'; import { ISDK, Mode, SdkProvider } from '../aws-auth'; -import { DeployStackResult } from '../deploy-stack'; +import { SuccessfulDeployStackResult } from '../deploy-stack'; /* eslint-disable max-len */ @@ -21,7 +21,7 @@ export class Bootstrapper { constructor(private readonly source: BootstrapSource) { } - public bootstrapEnvironment(environment: cxapi.Environment, sdkProvider: SdkProvider, options: BootstrapEnvironmentOptions = {}): Promise { + public bootstrapEnvironment(environment: cxapi.Environment, sdkProvider: SdkProvider, options: BootstrapEnvironmentOptions = {}): Promise { switch (this.source.source) { case 'legacy': return this.legacyBootstrap(environment, sdkProvider, options); @@ -41,7 +41,7 @@ export class Bootstrapper { * Deploy legacy bootstrap stack * */ - private async legacyBootstrap(environment: cxapi.Environment, sdkProvider: SdkProvider, options: BootstrapEnvironmentOptions = {}): Promise { + private async legacyBootstrap(environment: cxapi.Environment, sdkProvider: SdkProvider, options: BootstrapEnvironmentOptions = {}): Promise { const params = options.parameters ?? {}; if (params.trustedAccounts?.length) { @@ -71,7 +71,7 @@ export class Bootstrapper { private async modernBootstrap( environment: cxapi.Environment, sdkProvider: SdkProvider, - options: BootstrapEnvironmentOptions = {}): Promise { + options: BootstrapEnvironmentOptions = {}): Promise { const params = options.parameters ?? {}; @@ -291,7 +291,7 @@ export class Bootstrapper { private async customBootstrap( environment: cxapi.Environment, sdkProvider: SdkProvider, - options: BootstrapEnvironmentOptions = {}): Promise { + options: BootstrapEnvironmentOptions = {}): Promise { // Look at the template, decide whether it's most likely a legacy or modern bootstrap // template, and use the right bootstrapper for that. diff --git a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts index 501122697eab5..b1f0cb506837f 100644 --- a/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts +++ b/packages/aws-cdk/lib/api/bootstrap/deploy-bootstrap.ts @@ -6,7 +6,7 @@ import * as fs from 'fs-extra'; import { BOOTSTRAP_VERSION_OUTPUT, BootstrapEnvironmentOptions, BOOTSTRAP_VERSION_RESOURCE, BOOTSTRAP_VARIANT_PARAMETER, DEFAULT_BOOTSTRAP_VARIANT } from './bootstrap-props'; import * as logging from '../../logging'; import { Mode, SdkProvider, ISDK } from '../aws-auth'; -import { deployStack, DeployStackResult } from '../deploy-stack'; +import { assertIsSuccessfulDeployStackResult, deployStack, SuccessfulDeployStackResult } from '../deploy-stack'; import { NoBootstrapStackEnvironmentResources } from '../environment-resources'; import { DEFAULT_TOOLKIT_STACK_NAME, ToolkitInfo } from '../toolkit-info'; @@ -63,14 +63,15 @@ export class BootstrapStack { template: any, parameters: Record, options: Omit, - ): Promise { + ): Promise { if (this.currentToolkitInfo.found && !options.force) { // Safety checks const abortResponse = { + type: 'did-deploy-stack', noOp: true, outputs: {}, stackArn: this.currentToolkitInfo.bootstrapStack.stackId, - }; + } satisfies SuccessfulDeployStackResult; // Validate that the bootstrap stack we're trying to replace is from the same variant as the one we're trying to deploy const currentVariant = this.currentToolkitInfo.variant; @@ -110,7 +111,7 @@ export class BootstrapStack { const assembly = builder.buildAssembly(); - return deployStack({ + const ret = await deployStack({ stack: assembly.getStackByName(this.toolkitStackName), resolvedEnvironment: this.resolvedEnvironment, sdk: this.sdk, @@ -124,6 +125,10 @@ export class BootstrapStack { // Obviously we can't need a bootstrap stack to deploy a bootstrap stack envResources: new NoBootstrapStackEnvironmentResources(this.resolvedEnvironment, this.sdk), }); + + assertIsSuccessfulDeployStackResult(ret); + + return ret; } } diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index fff70866b617c..5fe7b39bd7c89 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -21,12 +21,37 @@ import { determineAllowCrossAccountAssetPublishing } from './util/checks'; import { publishAssets } from '../util/asset-publishing'; import { StringWithoutPlaceholders } from './util/placeholders'; -export interface DeployStackResult { +export type DeployStackResult = + | SuccessfulDeployStackResult + | NeedRollbackFirstDeployStackResult + | ReplacementRequiresNoRollbackStackResult + ; + +/** Successfully deployed a stack */ +export interface SuccessfulDeployStackResult { + readonly type: 'did-deploy-stack'; readonly noOp: boolean; readonly outputs: { [name: string]: string }; readonly stackArn: string; } +/** The stack is currently in a failpaused state, and needs to be rolled back before the deployment */ +export interface NeedRollbackFirstDeployStackResult { + readonly type: 'failpaused-need-rollback-first'; + readonly reason: 'not-norollback' | 'replacement'; +} + +/** The upcoming change has a replacement, which requires deploying without --no-rollback */ +export interface ReplacementRequiresNoRollbackStackResult { + readonly type: 'replacement-requires-norollback'; +} + +export function assertIsSuccessfulDeployStackResult(x: DeployStackResult): asserts x is SuccessfulDeployStackResult { + if (x.type !== 'did-deploy-stack') { + throw new Error(`Unexpected deployStack result. This should not happen: ${JSON.stringify(x)}. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose.`); + } +} + export interface DeployStackOptions { /** * The stack to be deployed @@ -283,6 +308,7 @@ export async function deployStack(options: DeployStackOptions): Promise { + private async executeChangeSet(changeSet: CloudFormation.DescribeChangeSetOutput): Promise { debug('Initiating execution of changeset %s on stack %s', changeSet.ChangeSetId, this.stackName); await this.cfn.executeChangeSet({ @@ -482,7 +522,7 @@ class FullCloudFormationDeployment { } } - private async directDeployment(): Promise { + private async directDeployment(): Promise { print('%s: %s stack...', chalk.bold(this.stackName), this.update ? 'updating' : 'creating'); const startTime = new Date(); @@ -500,7 +540,7 @@ class FullCloudFormationDeployment { } catch (err: any) { if (err.message === 'No updates are to be performed.') { debug('No updates are to be performed for stack %s', this.stackName); - return { noOp: true, outputs: this.cloudFormationStack.outputs, stackArn: this.cloudFormationStack.stackId }; + return { type: 'did-deploy-stack', noOp: true, outputs: this.cloudFormationStack.outputs, stackArn: this.cloudFormationStack.stackId }; } throw err; } @@ -522,7 +562,7 @@ class FullCloudFormationDeployment { } } - private async monitorDeployment(startTime: Date, expectedChanges: number | undefined): Promise { + private async monitorDeployment(startTime: Date, expectedChanges: number | undefined): Promise { const monitor = this.options.quiet ? undefined : StackActivityMonitor.withDefaultPrinter(this.cfn, this.stackName, this.stackArtifact, { resourcesTotal: expectedChanges, progress: this.options.progress, @@ -543,7 +583,7 @@ class FullCloudFormationDeployment { await monitor?.stop(); } debug('Stack %s has completed updating', this.stackName); - return { noOp: false, outputs: finalState.outputs, stackArn: finalState.stackId }; + return { type: 'did-deploy-stack', noOp: false, outputs: finalState.outputs, stackArn: finalState.stackId }; } /** @@ -722,3 +762,10 @@ function suffixWithErrors(msg: string, errors?: string[]) { function arrayEquals(a: any[], b: any[]): boolean { return a.every(item => b.includes(item)) && b.every(item => a.includes(item)); } + +function hasReplacement(cs: AWS.CloudFormation.DescribeChangeSetOutput) { + return (cs.Changes ?? []).some(c => { + const a = c.ResourceChange?.PolicyAction; + return a === 'ReplaceAndDelete' || a === 'ReplaceAndRetain' || a === 'ReplaceAndSnapshot'; + }); +} diff --git a/packages/aws-cdk/lib/api/hotswap-deployments.ts b/packages/aws-cdk/lib/api/hotswap-deployments.ts index 427561fce67a6..7944d499af889 100644 --- a/packages/aws-cdk/lib/api/hotswap-deployments.ts +++ b/packages/aws-cdk/lib/api/hotswap-deployments.ts @@ -2,7 +2,7 @@ import * as cfn_diff from '@aws-cdk/cloudformation-diff'; import * as cxapi from '@aws-cdk/cx-api'; import * as chalk from 'chalk'; import { ISDK, Mode, SdkProvider } from './aws-auth'; -import { DeployStackResult } from './deploy-stack'; +import { SuccessfulDeployStackResult } from './deploy-stack'; import { EvaluateCloudFormationTemplate } from './evaluate-cloudformation-template'; import { print } from '../logging'; import { isHotswappableAppSyncChange } from './hotswap/appsync-mapping-templates'; @@ -66,7 +66,7 @@ export async function tryHotswapDeployment( sdkProvider: SdkProvider, assetParams: { [key: string]: string }, cloudFormationStack: CloudFormationStack, stackArtifact: cxapi.CloudFormationStackArtifact, hotswapMode: HotswapMode, hotswapPropertyOverrides: HotswapPropertyOverrides, -): Promise { +): Promise { // resolve the environment, so we can substitute things like AWS::Region in CFN expressions const resolvedEnv = await sdkProvider.resolveEnvironment(stackArtifact.environment); // create a new SDK using the CLI credentials, because the default one will not work for new-style synthesis - @@ -104,7 +104,7 @@ export async function tryHotswapDeployment( // apply the short-circuitable changes await applyAllHotswappableChanges(sdk, hotswappableChanges); - return { noOp: hotswappableChanges.length === 0, stackArn: cloudFormationStack.stackId, outputs: cloudFormationStack.outputs }; + return { type: 'did-deploy-stack', noOp: hotswappableChanges.length === 0, stackArn: cloudFormationStack.stackId, outputs: cloudFormationStack.outputs }; } /** diff --git a/packages/aws-cdk/lib/api/util/cloudformation/stack-status.ts b/packages/aws-cdk/lib/api/util/cloudformation/stack-status.ts index 4dd113aaa30db..e4555aef93dcb 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation/stack-status.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation/stack-status.ts @@ -67,6 +67,10 @@ export class StackStatus { } } + get isRollbackable(): boolean { + return [RollbackChoice.START_ROLLBACK, RollbackChoice.CONTINUE_UPDATE_ROLLBACK].includes(this.rollbackChoice); + } + public toString(): string { return this.name + (this.reason ? ` (${this.reason})` : ''); } diff --git a/packages/aws-cdk/lib/cdk-toolkit.ts b/packages/aws-cdk/lib/cdk-toolkit.ts index 57dabce74a5dd..f6955b6e14449 100644 --- a/packages/aws-cdk/lib/cdk-toolkit.ts +++ b/packages/aws-cdk/lib/cdk-toolkit.ts @@ -6,7 +6,7 @@ import * as chokidar from 'chokidar'; import * as fs from 'fs-extra'; import * as promptly from 'promptly'; import * as uuid from 'uuid'; -import { DeploymentMethod } from './api'; +import { DeploymentMethod, SuccessfulDeployStackResult } from './api'; import { SdkProvider } from './api/aws-auth'; import { Bootstrapper, BootstrapEnvironmentOptions } from './api/bootstrap'; import { CloudAssembly, DefaultSelection, ExtendedStackSelection, StackCollection, StackSelector } from './api/cxapp/cloud-assembly'; @@ -36,6 +36,12 @@ import { environmentsFromDescriptors, globEnvironmentsFromStacks, looksLikeGlob // eslint-disable-next-line @typescript-eslint/no-require-imports const pLimit: typeof import('p-limit') = require('p-limit'); +let TESTING = false; + +export function markTesting() { + TESTING = true; +} + export interface CdkToolkitProps { /** @@ -266,8 +272,8 @@ export class CdkToolkit { }); }; - const deployStack = async (assetNode: StackNode) => { - const stack = assetNode.stack; + const deployStack = async (stackNode: StackNode) => { + const stack = stackNode.stack; if (stackCollection.stackCount !== 1) { highlight(stack.displayName); } if (!stack.environment) { @@ -295,24 +301,11 @@ export class CdkToolkit { if (requireApproval !== RequireApproval.Never) { const currentTemplate = await this.props.deployments.readCurrentTemplate(stack); if (printSecurityDiff(currentTemplate, stack, requireApproval)) { - await withCorkedLogging(async () => { - // only talk to user if STDIN is a terminal (otherwise, fail) - if (!process.stdin.isTTY) { - throw new Error( - '"--require-approval" is enabled and stack includes security-sensitive updates, ' + - 'but terminal (TTY) is not attached so we are unable to get a confirmation from the user'); - } - - // only talk to user if concurrency is 1 (otherwise, fail) - if (concurrency > 1) { - throw new Error( - '"--require-approval" is enabled and stack includes security-sensitive updates, ' + - 'but concurrency is greater than 1 so we are unable to get a confirmation from the user'); - } - - const confirmed = await promptly.confirm('Do you wish to deploy these changes (y/n)?'); - if (!confirmed) { throw new Error('Aborted by user'); } - }); + await askUserConfirmation( + concurrency, + '"--require-approval" is enabled and stack includes security-sensitive updates', + 'Do you wish to deploy these changes', + ); } } @@ -337,31 +330,95 @@ export class CdkToolkit { let elapsedDeployTime = 0; try { - const result = await this.props.deployments.deployStack({ - stack, - deployName: stack.stackName, - roleArn: options.roleArn, - toolkitStackName: options.toolkitStackName, - reuseAssets: options.reuseAssets, - notificationArns, - tags, - execute: options.execute, - changeSetName: options.changeSetName, - deploymentMethod: options.deploymentMethod, - force: options.force, - parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]), - usePreviousParameters: options.usePreviousParameters, - progress, - ci: options.ci, - rollback: options.rollback, - hotswap: options.hotswap, - hotswapPropertyOverrides: hotswapPropertyOverrides, - extraUserAgent: options.extraUserAgent, - assetParallelism: options.assetParallelism, - ignoreNoStacks: options.ignoreNoStacks, - }); + let deployResult: SuccessfulDeployStackResult | undefined; - const message = result.noOp + let rollback = options.rollback; + let iteration = 0; + while (!deployResult) { + if (++iteration > 2) { + throw new Error('This loop should have stabilized in 2 iterations, but didn\'t. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose'); + } + + const r = await this.props.deployments.deployStack({ + stack, + deployName: stack.stackName, + roleArn: options.roleArn, + toolkitStackName: options.toolkitStackName, + reuseAssets: options.reuseAssets, + notificationArns, + tags, + execute: options.execute, + changeSetName: options.changeSetName, + deploymentMethod: options.deploymentMethod, + force: options.force, + parameters: Object.assign({}, parameterMap['*'], parameterMap[stack.stackName]), + usePreviousParameters: options.usePreviousParameters, + progress, + ci: options.ci, + rollback, + hotswap: options.hotswap, + hotswapPropertyOverrides: hotswapPropertyOverrides, + extraUserAgent: options.extraUserAgent, + assetParallelism: options.assetParallelism, + ignoreNoStacks: options.ignoreNoStacks, + }); + + switch (r.type) { + case 'did-deploy-stack': + deployResult = r; + break; + + case 'failpaused-need-rollback-first': { + const motivation = r.reason === 'replacement' + ? 'Stack is in a paused fail state and change includes a replacement which cannot be deployed with "--no-rollback"' + : 'Stack is in a paused fail state and command line arguments do not include "--no-rollback"'; + + if (options.force) { + warning(`${motivation}. Rolling back first (--force).`); + } else { + await askUserConfirmation( + concurrency, + motivation, + `${motivation}. Roll back first and then proceed with deployment`, + ); + } + + // Perform a rollback + await this.rollback({ + selector: { patterns: [stack.hierarchicalId] }, + toolkitStackName: options.toolkitStackName, + force: options.force, + }); + + // Go around through the 'while' loop again but switch rollback to true. + rollback = true; + break; + } + + case 'replacement-requires-norollback': { + const motivation = 'Change includes a replacement which cannot be deployed with "--no-rollback"'; + + if (options.force) { + warning(`${motivation}. Proceeding with regular deployment (--force).`); + } else { + await askUserConfirmation( + concurrency, + motivation, + `${motivation}. Perform a regular deployment`, + ); + } + + // Go around through the 'while' loop again but switch rollback to false. + rollback = true; + break; + } + + default: + throw new Error(`Unexpected result type from deployStack: ${JSON.stringify(r)}. If you are seeing this error, please report it at https://github.com/aws/aws-cdk/issues/new/choose`); + } + } + + const message = deployResult.noOp ? ' ✅ %s (no changes)' : ' ✅ %s'; @@ -369,20 +426,20 @@ export class CdkToolkit { elapsedDeployTime = new Date().getTime() - startDeployTime; print('\n✨ Deployment time: %ss\n', formatTime(elapsedDeployTime)); - if (Object.keys(result.outputs).length > 0) { + if (Object.keys(deployResult.outputs).length > 0) { print('Outputs:'); - stackOutputs[stack.stackName] = result.outputs; + stackOutputs[stack.stackName] = deployResult.outputs; } - for (const name of Object.keys(result.outputs).sort()) { - const value = result.outputs[name]; + for (const name of Object.keys(deployResult.outputs).sort()) { + const value = deployResult.outputs[name]; print('%s.%s = %s', chalk.cyan(stack.id), chalk.cyan(name), chalk.underline(chalk.cyan(value))); } print('Stack ARN:'); - data(result.stackArn); + data(deployResult.stackArn); } catch (e: any) { // It has to be exactly this string because an integration test tests for // "bold(stackname) failed: ResourceNotReady: " @@ -1729,3 +1786,30 @@ function obscureTemplate(template: any = {}) { return template; } + +/** + * Ask the user for a yes/no confirmation + * + * Automatically fail the confirmation in case we're in a situation where the confirmation + * cannot be interactively obtained from a human at the keyboard. + */ +async function askUserConfirmation( + concurrency: number, + motivation: string, + question: string, +) { + await withCorkedLogging(async () => { + // only talk to user if STDIN is a terminal (otherwise, fail) + if (!TESTING && !process.stdin.isTTY) { + throw new Error(`${motivation}, but terminal (TTY) is not attached so we are unable to get a confirmation from the user`); + } + + // only talk to user if concurrency is 1 (otherwise, fail) + if (concurrency > 1) { + throw new Error(`${motivation}, but concurrency is greater than 1 so we are unable to get a confirmation from the user`); + } + + const confirmed = await promptly.confirm(`${chalk.cyan(question)} (y/n)?`); + if (!confirmed) { throw new Error('Aborted by user'); } + }); +} diff --git a/packages/aws-cdk/lib/diff.ts b/packages/aws-cdk/lib/diff.ts index c940efb46fca7..487201108d293 100644 --- a/packages/aws-cdk/lib/diff.ts +++ b/packages/aws-cdk/lib/diff.ts @@ -119,19 +119,16 @@ export function printSecurityDiff( oldTemplate: any, newTemplate: cxapi.CloudFormationStackArtifact, requireApproval: RequireApproval, - quiet?: boolean, + _quiet?: boolean, stackName?: string, changeSet?: DescribeChangeSetOutput, stream: FormatStream = process.stderr, ): boolean { const diff = fullDiff(oldTemplate, newTemplate.template, changeSet); - // must output the stack name if there are differences, even if quiet - if (!quiet || !diff.isEmpty) { + if (diffRequiresApproval(diff, requireApproval)) { stream.write(format('Stack %s\n', chalk.bold(stackName))); - } - if (difRequiresApproval(diff, requireApproval)) { // eslint-disable-next-line max-len warning(`This deployment will make potentially sensitive changes according to your current security approval level (--require-approval ${requireApproval}).`); warning('Please confirm you intend to make the following modifications:\n'); @@ -148,7 +145,7 @@ export function printSecurityDiff( * TODO: Filter the security impact determination based off of an enum that allows * us to pick minimum "severities" to alert on. */ -function difRequiresApproval(diff: TemplateDiff, requireApproval: RequireApproval) { +function diffRequiresApproval(diff: TemplateDiff, requireApproval: RequireApproval) { switch (requireApproval) { case RequireApproval.Never: return false; case RequireApproval.AnyChange: return diff.permissionsAnyChanges; diff --git a/packages/aws-cdk/lib/import.ts b/packages/aws-cdk/lib/import.ts index cd6f70cebb03f..b0a94b54ded92 100644 --- a/packages/aws-cdk/lib/import.ts +++ b/packages/aws-cdk/lib/import.ts @@ -6,6 +6,7 @@ import * as chalk from 'chalk'; import * as fs from 'fs-extra'; import * as promptly from 'promptly'; import { DeploymentMethod } from './api'; +import { assertIsSuccessfulDeployStackResult } from './api/deploy-stack'; import { Deployments } from './api/deployments'; import { ResourceIdentifierProperties, ResourcesToImport } from './api/util/cloudformation'; import { StackActivityProgress } from './api/util/cloudformation/stack-activity-monitor'; @@ -153,6 +154,8 @@ export class ResourceImporter { resourcesToImport, }); + assertIsSuccessfulDeployStackResult(result); + const message = result.noOp ? ' ✅ %s (no changes)' : ' ✅ %s'; diff --git a/packages/aws-cdk/test/api/bootstrap2.test.ts b/packages/aws-cdk/test/api/bootstrap2.test.ts index d9ec9d563768a..4be0ec22ea2f6 100644 --- a/packages/aws-cdk/test/api/bootstrap2.test.ts +++ b/packages/aws-cdk/test/api/bootstrap2.test.ts @@ -1,10 +1,7 @@ /* eslint-disable import/order */ -const mockDeployStack = jest.fn(); - -jest.mock('../../lib/api/deploy-stack', () => ({ - deployStack: mockDeployStack, -})); +import * as deployStack from '../../lib/api/deploy-stack'; +const mockDeployStack = jest.spyOn(deployStack, 'deployStack'); import { IAM } from 'aws-sdk'; import { Bootstrapper, DeployStackOptions, ToolkitInfo } from '../../lib/api'; @@ -53,6 +50,12 @@ describe('Bootstrapping v2', () => { createPolicy: mockCreatePolicyIamCode, getPolicy: mockGetPolicyIamCode, }); + mockDeployStack.mockResolvedValue({ + type: 'did-deploy-stack', + noOp: false, + outputs: {}, + stackArn: 'arn:stack', + }); }); afterEach(() => { @@ -341,6 +344,12 @@ describe('Bootstrapping v2', () => { let template: any; mockDeployStack.mockImplementation((args: DeployStackOptions) => { template = args.stack.template; + return Promise.resolve({ + type: 'did-deploy-stack', + noOp: false, + outputs: {}, + stackArn: 'arn:stack', + }); }); await bootstrapper.bootstrapEnvironment(env, sdk, { diff --git a/packages/aws-cdk/test/api/deploy-stack.test.ts b/packages/aws-cdk/test/api/deploy-stack.test.ts index 7afcea68f4c60..cfc6f92a709df 100644 --- a/packages/aws-cdk/test/api/deploy-stack.test.ts +++ b/packages/aws-cdk/test/api/deploy-stack.test.ts @@ -1,5 +1,5 @@ /* eslint-disable import/order */ -import { deployStack, DeployStackOptions } from '../../lib/api'; +import { assertIsSuccessfulDeployStackResult, deployStack, DeployStackOptions } from '../../lib/api'; import { HotswapMode } from '../../lib/api/hotswap/common'; import { tryHotswapDeployment } from '../../lib/api/hotswap-deployments'; import { setCI } from '../../lib/logging'; @@ -129,7 +129,7 @@ test("calls tryHotswapDeployment() if 'hotswap' is `HotswapMode.HOTSWAP_ONLY`", }); // THEN - expect(deployStackResult.noOp).toEqual(true); + expect(deployStackResult.type === 'did-deploy-stack' && deployStackResult.noOp).toEqual(true); expect(tryHotswapDeployment).toHaveBeenCalled(); // check that the extra User-Agent is honored expect(sdk.appendCustomUserAgent).toHaveBeenCalledWith('extra-user-agent'); @@ -275,7 +275,7 @@ test('do deploy executable change set with 0 changes', async () => { }); // THEN - expect(ret.noOp).toBeFalsy(); + expect(ret.type === 'did-deploy-stack' && ret.noOp).toBeFalsy(); expect(cfnMocks.executeChangeSet).toHaveBeenCalled(); }); @@ -625,7 +625,7 @@ test('deployStack reports no change if describeChangeSet returns specific error' }); // THEN - expect(deployResult.noOp).toEqual(true); + expect(deployResult.type === 'did-deploy-stack' && deployResult.noOp).toEqual(true); }); test('deploy not skipped if template did not change but one tag removed', async () => { @@ -916,7 +916,33 @@ describe('disable rollback', () => { DisableRollback: true, })); }); +}); + +test.each([ + ['UPDATE_FAILED', 'failpaused-need-rollback-first'], + ['CREATE_COMPLETE', 'replacement-requires-norollback'], +])('no-rollback and replacement is disadvised: %p -> %p', async (stackStatus, expectedType) => { + // GIVEN + givenTemplateIs(FAKE_STACK.template); + givenStackExists({ + NotificationARNs: ['arn:aws:sns:bermuda-triangle-1337:123456789012:TestTopic'], + StackStatus: stackStatus, + }); + givenChangeSetContainsReplacement(); + + // WHEN + const result = await deployStack({ + ...standardDeployStackArguments(), + stack: FAKE_STACK, + rollback: false, + }); + + // THEN + expect(result.type).toEqual(expectedType); +}); +test('assertIsSuccessfulDeployStackResult does what it says', () => { + expect(() => assertIsSuccessfulDeployStackResult({ type: 'replacement-requires-norollback' })).toThrow(); }); /** @@ -955,3 +981,34 @@ function givenTemplateIs(template: any) { TemplateBody: JSON.stringify(template), }); } + +function givenChangeSetContainsReplacement() { + cfnMocks.describeChangeSet?.mockReturnValue({ + Status: 'CREATE_COMPLETE', + Changes: [ + { + Type: 'Resource', + ResourceChange: { + PolicyAction: 'ReplaceAndDelete', + Action: 'Modify', + LogicalResourceId: 'Queue4A7E3555', + PhysicalResourceId: 'https://sqs.eu-west-1.amazonaws.com/111111111111/Queue4A7E3555-P9C8nK3uv8v6.fifo', + ResourceType: 'AWS::SQS::Queue', + Replacement: 'True', + Scope: ['Properties'], + Details: [ + { + Target: { + Attribute: 'Properties', + Name: 'FifoQueue', + RequiresRecreation: 'Always', + }, + Evaluation: 'Static', + ChangeSource: 'DirectModification', + }, + ], + }, + }, + ], + }); +} diff --git a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts index 1288c827f2300..3150d20b5e806 100644 --- a/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts +++ b/packages/aws-cdk/test/api/hotswap/hotswap-test-setup.ts @@ -3,7 +3,7 @@ import * as AWS from 'aws-sdk'; import * as codebuild from 'aws-sdk/clients/codebuild'; import * as lambda from 'aws-sdk/clients/lambda'; import * as stepfunctions from 'aws-sdk/clients/stepfunctions'; -import { DeployStackResult } from '../../../lib/api'; +import { SuccessfulDeployStackResult } from '../../../lib/api'; import { HotswapMode, HotswapPropertyOverrides } from '../../../lib/api/hotswap/common'; import * as deployments from '../../../lib/api/hotswap-deployments'; import { CloudFormationStack, Template } from '../../../lib/api/util/cloudformation'; @@ -180,7 +180,7 @@ export class HotswapMockSdkProvider { stackArtifact: cxapi.CloudFormationStackArtifact, assetParams: { [key: string]: string } = {}, hotswapPropertyOverrides?: HotswapPropertyOverrides, - ): Promise { + ): Promise { let hotswapProps = hotswapPropertyOverrides || new HotswapPropertyOverrides(); return deployments.tryHotswapDeployment(this.mockSdkProvider, assetParams, currentCfnStack, stackArtifact, hotswapMode, hotswapProps); } diff --git a/packages/aws-cdk/test/cdk-toolkit.test.ts b/packages/aws-cdk/test/cdk-toolkit.test.ts index 4c108ba418474..a36bf5efd224c 100644 --- a/packages/aws-cdk/test/cdk-toolkit.test.ts +++ b/packages/aws-cdk/test/cdk-toolkit.test.ts @@ -60,18 +60,21 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Manifest } from '@aws-cdk/cloud-assembly-schema'; import * as cxapi from '@aws-cdk/cx-api'; import * as fs from 'fs-extra'; +import * as promptly from 'promptly'; import { instanceMockFrom, MockCloudExecutable, TestStackArtifact } from './util'; import { MockSdkProvider } from './util/mock-sdk'; import { Bootstrapper } from '../lib/api/bootstrap'; -import { DeployStackResult } from '../lib/api/deploy-stack'; +import { DeployStackResult, SuccessfulDeployStackResult } from '../lib/api/deploy-stack'; import { Deployments, DeployStackOptions, DestroyStackOptions, RollbackStackOptions, RollbackStackResult } from '../lib/api/deployments'; import { HotswapMode } from '../lib/api/hotswap/common'; import { Template } from '../lib/api/util/cloudformation'; -import { CdkToolkit, Tag } from '../lib/cdk-toolkit'; +import { CdkToolkit, markTesting, Tag } from '../lib/cdk-toolkit'; import { RequireApproval } from '../lib/diff'; import { Configuration } from '../lib/settings'; import { flatten } from '../lib/util'; +markTesting(); + process.env.CXAPI_DISABLE_SELECT_BY_ID = '1'; let cloudExecutable: MockCloudExecutable; @@ -436,6 +439,7 @@ describe('deploy', () => { // GIVEN const mockCfnDeployments = instanceMockFrom(Deployments); mockCfnDeployments.deployStack.mockReturnValue(Promise.resolve({ + type: 'did-deploy-stack', noOp: false, outputs: {}, stackArn: 'stackArn', @@ -1251,6 +1255,68 @@ describe('synth', () => { expect(mockedRollback).toHaveBeenCalled(); }); + + test.each([ + [{ type: 'failpaused-need-rollback-first', reason: 'replacement' }, false], + [{ type: 'failpaused-need-rollback-first', reason: 'replacement' }, true], + [{ type: 'failpaused-need-rollback-first', reason: 'not-norollback' }, false], + [{ type: 'replacement-requires-norollback' }, false], + [{ type: 'replacement-requires-norollback' }, true], + ] satisfies Array<[DeployStackResult, boolean]>)('no-rollback deployment that cant proceed will be called with rollback on retry: %p (using force: %p)', async (firstResult, useForce) => { + cloudExecutable = new MockCloudExecutable({ + stacks: [ + MockStack.MOCK_STACK_C, + ], + }); + + const deployments = new Deployments({ sdkProvider: new MockSdkProvider() }); + + // Rollback might be called -- just don't do nothing. + const mockRollbackStack = jest.spyOn(deployments, 'rollbackStack').mockResolvedValue({}); + + const mockedDeployStack = jest + .spyOn(deployments, 'deployStack') + .mockResolvedValueOnce(firstResult) + .mockResolvedValueOnce({ + type: 'did-deploy-stack', + noOp: false, + outputs: {}, + stackArn: 'stack:arn', + }); + + const mockedConfirm = jest.spyOn(promptly, 'confirm').mockResolvedValue(true); + + const toolkit = new CdkToolkit({ + cloudExecutable, + configuration: cloudExecutable.configuration, + sdkProvider: cloudExecutable.sdkProvider, + deployments, + }); + + await toolkit.deploy({ + selector: { patterns: [] }, + hotswap: HotswapMode.FULL_DEPLOYMENT, + rollback: false, + requireApproval: RequireApproval.Never, + force: useForce, + }); + + if (firstResult.type === 'failpaused-need-rollback-first') { + expect(mockRollbackStack).toHaveBeenCalled(); + } + + if (!useForce) { + // Questions will have been asked only if --force is not specified + if (firstResult.type === 'failpaused-need-rollback-first') { + expect(mockedConfirm).toHaveBeenCalledWith(expect.stringContaining('Roll back first and then proceed with deployment')); + } else { + expect(mockedConfirm).toHaveBeenCalledWith(expect.stringContaining('Perform a regular deployment')); + } + } + + expect(mockedDeployStack).toHaveBeenNthCalledWith(1, expect.objectContaining({ rollback: false })); + expect(mockedDeployStack).toHaveBeenNthCalledWith(2, expect.objectContaining({ rollback: true })); + }); }); class MockStack { @@ -1402,7 +1468,7 @@ class FakeCloudFormation extends Deployments { this.expectedNotificationArns = expectedNotificationArns ?? []; } - public deployStack(options: DeployStackOptions): Promise { + public deployStack(options: DeployStackOptions): Promise { expect([ MockStack.MOCK_STACK_A.stackName, MockStack.MOCK_STACK_B.stackName, @@ -1420,6 +1486,7 @@ class FakeCloudFormation extends Deployments { expect(options.notificationArns).toEqual(this.expectedNotificationArns); return Promise.resolve({ + type: 'did-deploy-stack', stackArn: `arn:aws:cloudformation:::stack/${options.stack.stackName}/MockedOut`, noOp: false, outputs: { StackName: options.stack.stackName }, diff --git a/packages/aws-cdk/test/diff.test.ts b/packages/aws-cdk/test/diff.test.ts index 7267f746b1af9..635e0157fbd88 100644 --- a/packages/aws-cdk/test/diff.test.ts +++ b/packages/aws-cdk/test/diff.test.ts @@ -72,7 +72,7 @@ describe('fixed template', () => { const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); expect(exitCode).toBe(0); expect(plainTextOutput.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, '')).toContain(`Resources -[~] AWS::SomeService::SomeResource SomeResource +[~] AWS::SomeService::SomeResource SomeResource └─ [~] Something ├─ [-] old-value └─ [+] new-value @@ -152,6 +152,7 @@ describe('imports', () => { }); }); cloudFormation.deployStack.mockImplementation((options) => Promise.resolve({ + type: 'did-deploy-stack', noOp: true, outputs: {}, stackArn: '', @@ -272,6 +273,7 @@ describe('non-nested stacks', () => { }); }); cloudFormation.deployStack.mockImplementation((options) => Promise.resolve({ + type: 'did-deploy-stack', noOp: true, outputs: {}, stackArn: '', @@ -485,6 +487,7 @@ describe('stack exists checks', () => { }); }); cloudFormation.deployStack.mockImplementation((options) => Promise.resolve({ + type: 'did-deploy-stack', noOp: true, outputs: {}, stackArn: '', @@ -1044,6 +1047,14 @@ describe('--strict', () => { beforeEach(() => { const oldTemplate = {}; + cloudFormation = instanceMockFrom(Deployments); + cloudFormation.readCurrentTemplateWithNestedStacks.mockImplementation((_stackArtifact: CloudFormationStackArtifact) => { + return Promise.resolve({ + deployedRootTemplate: {}, + nestedStacks: {}, + }); + }); + cloudExecutable = new MockCloudExecutable({ stacks: [{ stackName: 'A', @@ -1095,8 +1106,8 @@ describe('--strict', () => { const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); expect(plainTextOutput.trim()).toEqual(`Stack A Resources -[+] AWS::CDK::Metadata MetadataResource -[+] AWS::Something::Amazing SomeOtherResource +[+] AWS::CDK::Metadata MetadataResource +[+] AWS::Something::Amazing SomeOtherResource Other Changes [+] Unknown Rules: {\"CheckBootstrapVersion\":{\"newCheck\":\"newBootstrapVersion\"}} @@ -1120,7 +1131,7 @@ Other Changes const plainTextOutput = buffer.data.replace(/\x1B\[[0-?]*[ -/]*[@-~]/g, ''); expect(plainTextOutput.trim()).toEqual(`Stack A Resources -[+] AWS::Something::Amazing SomeOtherResource +[+] AWS::Something::Amazing SomeOtherResource ✨ Number of stacks with differences: 1`); From 9027cd2822303e10eb4a5131da73503e367b1458 Mon Sep 17 00:00:00 2001 From: "Kenta Goto (k.goto)" <24818752+go-to-k@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:43:46 +0900 Subject: [PATCH 9/9] chore(assertions): remove unnecessary condition for if statement in `Template` (#32028) ### Reason for this change The following is the code in the `Template` constructor in the assertions module: ```ts if (!templateParsingOptions?.skipCyclicalDependenciesCheck ?? true) { checkTemplateForCyclicDependencies(this.template); } ``` However, since the left operand (`!templateParsingOptions?.skipCyclicalDependenciesCheck`) is never undefined (null), the right operand (`?? true`) should not be needed. And the `templateParsingOptions` is not optional arg. ### Description of changes ```diff - if (!templateParsingOptions?.skipCyclicalDependenciesCheck ?? true) { + if (!templateParsingOptions.skipCyclicalDependenciesCheck) { checkTemplateForCyclicDependencies(this.template); } ``` ### Description of how you validated changes A unit test. ### Checklist - [x] My code adheres to the [CONTRIBUTING GUIDE](https://github.com/aws/aws-cdk/blob/main/CONTRIBUTING.md) and [DESIGN GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md) ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../aws-cdk-lib/assertions/lib/template.ts | 2 +- .../assertions/test/template.test.ts | 23 ++++++++++++++++++- 2 files changed, 23 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk-lib/assertions/lib/template.ts b/packages/aws-cdk-lib/assertions/lib/template.ts index fc219f9756633..c0603708b5620 100644 --- a/packages/aws-cdk-lib/assertions/lib/template.ts +++ b/packages/aws-cdk-lib/assertions/lib/template.ts @@ -54,7 +54,7 @@ export class Template { private constructor(template: { [key: string]: any }, templateParsingOptions: TemplateParsingOptions = {}) { this.template = template as TemplateType; - if (!templateParsingOptions?.skipCyclicalDependenciesCheck ?? true) { + if (!templateParsingOptions.skipCyclicalDependenciesCheck) { checkTemplateForCyclicDependencies(this.template); } } diff --git a/packages/aws-cdk-lib/assertions/test/template.test.ts b/packages/aws-cdk-lib/assertions/test/template.test.ts index 46f4df1b4356e..261ee3168d073 100644 --- a/packages/aws-cdk-lib/assertions/test/template.test.ts +++ b/packages/aws-cdk-lib/assertions/test/template.test.ts @@ -1368,7 +1368,28 @@ describe('Template', () => { }).toThrow(/dependency cycle/); }); - test('does not throw when given a template with cyclic dependencies if check is skipped', () => { + test('throws when given a template with cyclic dependencies if skipCyclicalDependenciesCheck is false', () => { + expect(() => { + Template.fromJSON({ + Resources: { + Res1: { + Type: 'Foo', + Properties: { + Thing: { Ref: 'Res2' }, + }, + }, + Res2: { + Type: 'Foo', + DependsOn: ['Res1'], + }, + }, + }, { + skipCyclicalDependenciesCheck: false, + }); + }).toThrow(/dependency cycle/); + }); + + test('does not throw when given a template with cyclic dependencies if skipCyclicalDependenciesCheck is true', () => { expect(() => { Template.fromJSON({ Resources: {