From 6190c14666855a42d883853a58e8f39bea6cc5fe Mon Sep 17 00:00:00 2001 From: Kazuho Cryer-Shinozuka Date: Tue, 5 Nov 2024 07:13:02 +0900 Subject: [PATCH 01/24] chore(glue): fix broken link (#31994) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ### Issue # (if applicable) None ### Reason for this change The generated document is broken due to an incorrect format. ```ts @see https://example.com Some redundant phrases ``` スクリーンショット 2024-11-02 20 00 25 ### Description of changes Remove redundant description. ### Description of how you validated changes None ### 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-glue-alpha/lib/job.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/@aws-cdk/aws-glue-alpha/lib/job.ts b/packages/@aws-cdk/aws-glue-alpha/lib/job.ts index 1af8dc8b5a4a7..6c6a1d5168f5d 100644 --- a/packages/@aws-cdk/aws-glue-alpha/lib/job.ts +++ b/packages/@aws-cdk/aws-glue-alpha/lib/job.ts @@ -74,7 +74,7 @@ export class WorkerType { /** * Job states emitted by Glue to CloudWatch Events. * - * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#glue-event-types for more information. + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/events/EventTypes.html#glue-event-types */ export enum JobState { /** @@ -583,7 +583,8 @@ export interface JobProps { /** * The default arguments for this job, specified as name-value pairs. * - * @see https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-etl-glue-arguments.html for a list of reserved parameters + * @see https://docs.aws.amazon.com/glue/latest/dg/aws-glue-programming-etl-glue-arguments.html + * * @default - no arguments */ readonly defaultArguments?: { [key: string]: string }; From c8cdec6d2dcd219ec6c3771b0da21fbda0667d55 Mon Sep 17 00:00:00 2001 From: Kaizen Conroy <36202692+kaizencc@users.noreply.github.com> Date: Mon, 4 Nov 2024 17:44:50 -0500 Subject: [PATCH 02/24] docs(cli): document what `--unstable` means for cdk gc (#32013) Minor updates to how `cdk gc` is presented in docs, specifically with regards to the `--unstable` flag. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- packages/aws-cdk/README.md | 5 ++++- packages/aws-cdk/lib/cli.ts | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/aws-cdk/README.md b/packages/aws-cdk/README.md index 99b02ec187665..05948cd497295 100644 --- a/packages/aws-cdk/README.md +++ b/packages/aws-cdk/README.md @@ -895,7 +895,10 @@ cdk bootstrap --no-previous-parameters CDK Garbage Collection. > [!CAUTION] -> CDK Garbage Collection is under development and therefore must be opted in via the `--unstable` flag: `cdk gc --unstable=gc`. +> CDK Garbage Collection is under development and therefore must be opted in via the +>`--unstable` flag: `cdk gc --unstable=gc`. `--unstable` indicates that the scope and +> 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: diff --git a/packages/aws-cdk/lib/cli.ts b/packages/aws-cdk/lib/cli.ts index 1cc58f1312435..e90062fb4048d 100644 --- a/packages/aws-cdk/lib/cli.ts +++ b/packages/aws-cdk/lib/cli.ts @@ -115,7 +115,7 @@ async function parseCommandLineArguments(args: string[]) { .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 + .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 }) 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 03/24] 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 04/24] 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 05/24] 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 06/24] 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 07/24] 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 08/24] 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 09/24] 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 10/24] 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 11/24] 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: { From 420be7a313391664e6d8744e8775c129c49f6842 Mon Sep 17 00:00:00 2001 From: seanCladonema <162403220+seanCladonema@users.noreply.github.com> Date: Wed, 6 Nov 2024 07:30:34 +0900 Subject: [PATCH 12/24] chore(ec2): add missing interface VPC endpoint (#31991) ### Issue # (if applicable) None ### Reason for this change Cost optimization hub interface VPC endpoint is not supported by AWS CDK. ### Description of changes Add cost optimization hub interface VPC endpoint. ### Description of how you validated changes ```sh aws ec2 describe-vpc-endpoint-services --filters Name=service-type,Values=Interface Name=owner,Values=amazon --region us-east-1 --query ServiceNames | grep cost-optimization-hub "com.amazonaws.us-east-1.cost-optimization-hub", ``` ### 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-ec2/lib/vpc-endpoint.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts b/packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts index 800c961cb4bae..0485690f1e740 100644 --- a/packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/aws-cdk-lib/aws-ec2/lib/vpc-endpoint.ts @@ -365,6 +365,7 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ public static readonly CONNECT_WISDOM = new InterfaceVpcEndpointAwsService('wisdom'); public static readonly CONTROL_CATALOG = new InterfaceVpcEndpointAwsService('controlcatalog'); public static readonly COST_EXPLORER = new InterfaceVpcEndpointAwsService('ce'); + public static readonly COST_OPTIMIZATION_HUB = new InterfaceVpcEndpointAwsService('cost-optimization-hub'); public static readonly DATA_EXCHANGE = new InterfaceVpcEndpointAwsService('dataexchange'); public static readonly DATA_EXPORTS = new InterfaceVpcEndpointAwsService('bcm-data-exports', 'aws.api'); public static readonly DATASYNC = new InterfaceVpcEndpointAwsService('datasync'); From a21a0b1392faecbb0a19a6ca90b64a1da8ad5d81 Mon Sep 17 00:00:00 2001 From: Leonardo Gama <51037424+Leo10Gama@users.noreply.github.com> Date: Tue, 5 Nov 2024 15:19:23 -0800 Subject: [PATCH 13/24] chore: add saiyush to core contributors (#32030) ### Reason for this change Include @saiyush to list of core contributors. ### Description of how you validated changes Copy-pasted the username verbatim. ### 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/github-merit-badger.yml | 2 +- .mergify.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-merit-badger.yml b/.github/workflows/github-merit-badger.yml index 1261b45103e44..56896b5b75f57 100644 --- a/.github/workflows/github-merit-badger.yml +++ b/.github/workflows/github-merit-badger.yml @@ -17,4 +17,4 @@ jobs: badges: '[beginning-contributor,repeat-contributor,valued-contributor,admired-contributor,star-contributor,distinguished-contributor]' thresholds: '[0,3,6,13,25,50]' badge-type: 'achievement' - ignore-usernames: '[rix0rrr,iliapolo,otaviomacedo,kaizencc,comcalvi,TheRealAmazonKendra,mrgrain,pahud,kellertk,ashishdhingra,HBobertz,sumupitchayan,colifran,khushail,moelasmar,paulhcsun,GavinZZ,aaythapa,xazhao,gracelu0,jfuss,shikha372,kirtishrinkhala,godwingrs22,bergjaak,IanKonlog,Leo10Gama,samson-keung,scorbiere,michelle-wangg,jiayiwang7,1kaileychen,aws-cdk-automation,dependabot[bot],mergify[bot]]' + ignore-usernames: '[rix0rrr,iliapolo,otaviomacedo,kaizencc,comcalvi,TheRealAmazonKendra,mrgrain,pahud,kellertk,ashishdhingra,HBobertz,sumupitchayan,colifran,khushail,moelasmar,paulhcsun,GavinZZ,aaythapa,xazhao,gracelu0,jfuss,shikha372,kirtishrinkhala,godwingrs22,bergjaak,IanKonlog,Leo10Gama,samson-keung,scorbiere,michelle-wangg,jiayiwang7,1kaileychen,saiyush,aws-cdk-automation,dependabot[bot],mergify[bot]]' diff --git a/.mergify.yml b/.mergify.yml index 55a700cee4e2f..c1f27c9a0866b 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -43,7 +43,7 @@ pull_request_rules: label: add: [ contribution/core ] conditions: - - author~=^(rix0rrr|iliapolo|otaviomacedo|kaizencc|comcalvi|TheRealAmazonKendra|mrgrain|pahud|ashishdhingra|kellertk|HBobertz|sumupitchayan|colifran|moelasmar|paulhcsun|GavinZZ|aaythapa|xazhao|gracelu0|jfuss|shikha372|kirtishrinkhala|godwingrs22|bergjaak|samson-keung|IanKonlog|Leo10Gama|scorbiere|michelle-wangg|jiayiwang7|1kaileychen)$ + - author~=^(rix0rrr|iliapolo|otaviomacedo|kaizencc|comcalvi|TheRealAmazonKendra|mrgrain|pahud|ashishdhingra|kellertk|HBobertz|sumupitchayan|colifran|moelasmar|paulhcsun|GavinZZ|aaythapa|xazhao|gracelu0|jfuss|shikha372|kirtishrinkhala|godwingrs22|bergjaak|samson-keung|IanKonlog|Leo10Gama|scorbiere|michelle-wangg|jiayiwang7|1kaileychen|saiyush)$ - -label~="contribution/core" - name: automatic merge actions: From 1c6f23f76069ada6d56bb132be67604bcf4eeef8 Mon Sep 17 00:00:00 2001 From: Di Wu Date: Tue, 5 Nov 2024 23:49:59 -0800 Subject: [PATCH 14/24] chore: add 5d to mergify and merit badger (#32031) ### Issue # (if applicable) Closes #. ### Reason for this change - add myself `5d` to contributor list ### Description of changes - add github user `5d` to contributor list ### 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/github-merit-badger.yml | 2 +- .mergify.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/github-merit-badger.yml b/.github/workflows/github-merit-badger.yml index 56896b5b75f57..449066bbe920f 100644 --- a/.github/workflows/github-merit-badger.yml +++ b/.github/workflows/github-merit-badger.yml @@ -17,4 +17,4 @@ jobs: badges: '[beginning-contributor,repeat-contributor,valued-contributor,admired-contributor,star-contributor,distinguished-contributor]' thresholds: '[0,3,6,13,25,50]' badge-type: 'achievement' - ignore-usernames: '[rix0rrr,iliapolo,otaviomacedo,kaizencc,comcalvi,TheRealAmazonKendra,mrgrain,pahud,kellertk,ashishdhingra,HBobertz,sumupitchayan,colifran,khushail,moelasmar,paulhcsun,GavinZZ,aaythapa,xazhao,gracelu0,jfuss,shikha372,kirtishrinkhala,godwingrs22,bergjaak,IanKonlog,Leo10Gama,samson-keung,scorbiere,michelle-wangg,jiayiwang7,1kaileychen,saiyush,aws-cdk-automation,dependabot[bot],mergify[bot]]' + ignore-usernames: '[rix0rrr,iliapolo,otaviomacedo,kaizencc,comcalvi,TheRealAmazonKendra,mrgrain,pahud,kellertk,ashishdhingra,HBobertz,sumupitchayan,colifran,khushail,moelasmar,paulhcsun,GavinZZ,aaythapa,xazhao,gracelu0,jfuss,shikha372,kirtishrinkhala,godwingrs22,bergjaak,IanKonlog,Leo10Gama,samson-keung,scorbiere,michelle-wangg,jiayiwang7,1kaileychen,saiyush,5d,aws-cdk-automation,dependabot[bot],mergify[bot]]' diff --git a/.mergify.yml b/.mergify.yml index c1f27c9a0866b..b9ffeae147913 100644 --- a/.mergify.yml +++ b/.mergify.yml @@ -43,7 +43,7 @@ pull_request_rules: label: add: [ contribution/core ] conditions: - - author~=^(rix0rrr|iliapolo|otaviomacedo|kaizencc|comcalvi|TheRealAmazonKendra|mrgrain|pahud|ashishdhingra|kellertk|HBobertz|sumupitchayan|colifran|moelasmar|paulhcsun|GavinZZ|aaythapa|xazhao|gracelu0|jfuss|shikha372|kirtishrinkhala|godwingrs22|bergjaak|samson-keung|IanKonlog|Leo10Gama|scorbiere|michelle-wangg|jiayiwang7|1kaileychen|saiyush)$ + - author~=^(rix0rrr|iliapolo|otaviomacedo|kaizencc|comcalvi|TheRealAmazonKendra|mrgrain|pahud|ashishdhingra|kellertk|HBobertz|sumupitchayan|colifran|moelasmar|paulhcsun|GavinZZ|aaythapa|xazhao|gracelu0|jfuss|shikha372|kirtishrinkhala|godwingrs22|bergjaak|samson-keung|IanKonlog|Leo10Gama|scorbiere|michelle-wangg|jiayiwang7|1kaileychen|saiyush|5d)$ - -label~="contribution/core" - name: automatic merge actions: From b1e59dda47a2ad2faeaab56e8babea77edfdd1bd Mon Sep 17 00:00:00 2001 From: Grace Luo <54298030+gracelu0@users.noreply.github.com> Date: Wed, 6 Nov 2024 00:41:16 -0800 Subject: [PATCH 15/24] chore(scheduler-alpha): clean up readme (#32034) small changes to README before developer preview, mostly formatting and some grammar fixes ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../@aws-cdk/aws-scheduler-alpha/README.md | 69 ++++++++++--------- 1 file changed, 38 insertions(+), 31 deletions(-) diff --git a/packages/@aws-cdk/aws-scheduler-alpha/README.md b/packages/@aws-cdk/aws-scheduler-alpha/README.md index 6dc21f3098c46..1babcd6e8671b 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/README.md +++ b/packages/@aws-cdk/aws-scheduler-alpha/README.md @@ -16,18 +16,18 @@ [Amazon EventBridge Scheduler](https://aws.amazon.com/blogs/compute/introducing-amazon-eventbridge-scheduler/) is a feature from Amazon EventBridge -that allows you to create, run, and manage scheduled tasks at scale. With EventBridge Scheduler, you can schedule one-time or recurrently tens +that allows you to create, run, and manage scheduled tasks at scale. With EventBridge Scheduler, you can schedule one-time or recurrently tens of millions of tasks across many AWS services without provisioning or managing underlying infrastructure. -1. **Schedule**: A schedule is the main resource you create, configure, and manage using Amazon EventBridge Scheduler. Every schedule has a schedule expression that determines when, and with what frequency, the schedule runs. EventBridge Scheduler supports three types of schedules: rate, cron, and one-time schedules. When you create a schedule, you configure a target for the schedule to invoke. -2. **Targets**: A target is an API operation that EventBridge Scheduler calls on your behalf every time your schedule runs. EventBridge Scheduler -supports two types of targets: templated targets and universal targets. Templated targets invoke common API operations across a core groups of -services. For example, EventBridge Scheduler supports templated targets for invoking AWS Lambda Function or starting execution of Step Function state +1. **Schedule**: A schedule is the main resource you create, configure, and manage using Amazon EventBridge Scheduler. Every schedule has a schedule expression that determines when, and with what frequency, the schedule runs. EventBridge Scheduler supports three types of schedules: rate, cron, and one-time schedules. When you create a schedule, you configure a target for the schedule to invoke. +2. **Target**: A target is an API operation that EventBridge Scheduler calls on your behalf every time your schedule runs. EventBridge Scheduler +supports two types of targets: templated targets and universal targets. Templated targets invoke common API operations across a core groups of +services. For example, EventBridge Scheduler supports templated targets for invoking AWS Lambda Function or starting execution of Step Functions state machine. For API operations that are not supported by templated targets you can use customizable universal targets. Universal targets support calling more than 6,000 API operations across over 270 AWS services. 3. **Schedule Group**: A schedule group is an Amazon EventBridge Scheduler resource that you use to organize your schedules. Your AWS account comes -with a default scheduler group. A new schedule will always be added to a scheduling group. If you do not provide a scheduling group to add to, it -will be added to the default scheduling group. You can create up to 500 schedule groups in your AWS account. Groups can be used to organize the +with a default scheduler group. A new schedule will always be added to a scheduling group. If you do not provide a scheduling group to add to, it +will be added to the default scheduling group. You can create up to 500 schedule groups in your AWS account. Groups can be used to organize the schedules logically, access the schedule metrics and manage permissions at group granularity (see details below). Scheduling groups support tagging: with EventBridge Scheduler, you apply tags to schedule groups, not to individual schedules to organize your resources. @@ -35,7 +35,7 @@ This module is part of the [AWS Cloud Development Kit](https://github.com/aws/aw > This module is in active development. Some features may not be implemented yet. -## Defining a schedule +## Defining a schedule ```ts declare const fn: lambda.Function; @@ -55,10 +55,12 @@ const schedule = new Schedule(this, 'Schedule', { ### Schedule Expressions -You can choose from three schedule types when configuring your schedule: rate-based, cron-based, and one-time schedules. +You can choose from three schedule types when configuring your schedule: rate-based, cron-based, and one-time schedules. -Both rate-based and cron-based schedules are recurring schedules. You can configure each recurring schedule type using a schedule expression. For -cron-based schedule you can specify a time zone in which EventBridge Scheduler evaluates the expression. +Both rate-based and cron-based schedules are recurring schedules. You can configure each recurring schedule type using a schedule expression. + +For +cron-based schedules you can specify a time zone in which EventBridge Scheduler evaluates the expression. ```ts declare const target: targets.LambdaInvoke; @@ -82,7 +84,7 @@ const cronBasedSchedule = new Schedule(this, 'Schedule', { }); ``` -A one-time schedule is a schedule that invokes a target only once. You configure a one-time schedule when by specifying the time of the day, date, +A one-time schedule is a schedule that invokes a target only once. You configure a one-time schedule by specifying the time of day, date, and time zone in which EventBridge Scheduler evaluates the schedule. ```ts @@ -100,13 +102,13 @@ const oneTimeSchedule = new Schedule(this, 'Schedule', { ### Grouping Schedules -Your AWS account comes with a default scheduler group. You can access default group in CDK with: +Your AWS account comes with a default scheduler group. You can access the default group in CDK with: ```ts const defaultGroup = Group.fromDefaultGroup(this, "DefaultGroup"); ``` -If not specified a schedule is added to the default group. However, you can also add the schedule to a custom scheduling group managed by you: +You can add a schedule to a custom scheduling group managed by you. If a custom group is not specified, the schedule is added to the default group. ```ts declare const target: targets.LambdaInvoke; @@ -154,13 +156,18 @@ new Schedule(this, 'Schedule', { ## Scheduler Targets The `@aws-cdk/aws-scheduler-targets-alpha` module includes classes that implement the `IScheduleTarget` interface for -various AWS services. EventBridge Scheduler supports two types of targets: templated targets invoke common API -operations across a core groups of services, and customizable universal targets that you can use to call more -than 6,000 operations across over 270 services. A list of supported targets can be found at `@aws-cdk/aws-scheduler-targets-alpha`. +various AWS services. EventBridge Scheduler supports two types of targets: + +1. **Templated targets** which invoke common API +operations across a core groups of services, and +2. **Universal targets** that you can customize to call more +than 6,000 operations across over 270 services. + +A list of supported targets can be found at `@aws-cdk/aws-scheduler-targets-alpha`. -### Input +### Input -Target can be invoked with a custom input. Class `ScheduleTargetInput` supports free form text input and JSON-formatted object input: +Targets can be invoked with a custom input. The `ScheduleTargetInput`class supports free-form text input and JSON-formatted object input: ```ts const input = ScheduleTargetInput.fromObject({ @@ -182,14 +189,14 @@ const text = `Attempt number: ${ContextAttribute.attemptNumber}`; const input = ScheduleTargetInput.fromText(text); ``` -### Specifying Execution Role +### Specifying an execution role An execution role is an IAM role that EventBridge Scheduler assumes in order to interact with other AWS services on your behalf. The classes for templated schedule targets automatically create an IAM role with all the minimum necessary permissions to interact with the templated target. If you wish you may specify your own IAM role, then the templated targets -will grant minimal required permissions. For example: for invoking Lambda function target `LambdaInvoke` will grant -execution IAM role permission to `lambda:InvokeFunction`. +will grant minimal required permissions. For example, the target `LambdaInvoke` will grant the +IAM execution role `lambda:InvokeFunction` permission to invoke the Lambda function. ```ts declare const fn: lambda.Function; @@ -206,14 +213,14 @@ const target = new targets.LambdaInvoke(fn, { }); ``` -### Specifying Encryption key +### Specifying an encryption key -EventBridge Scheduler integrates with AWS Key Management Service (AWS KMS) to encrypt and decrypt your data using an AWS KMS key. +EventBridge Scheduler integrates with AWS Key Management Service (AWS KMS) to encrypt and decrypt your data using an AWS KMS key. EventBridge Scheduler supports two types of KMS keys: AWS owned keys, and customer managed keys. -By default, all events in Scheduler are encrypted with a key that AWS owns and manages. -If you wish you can also provide a customer managed key to encrypt and decrypt the payload that your schedule delivers to its target using the `key` property. -Target classes will automatically add AWS KMS Decrypt permission to your schedule's execution role permissions policy. +By default, all events in Scheduler are encrypted with a key that AWS owns and manages. +If you wish you can also provide a customer managed key to encrypt and decrypt the payload that your schedule delivers to its target using the `key` property. +Target classes will automatically add AWS `kms:Decrypt` permission to your schedule's execution role permissions policy. ```ts declare const key: kms.Key; @@ -251,7 +258,7 @@ const schedule = new Schedule(this, 'Schedule', { > Visit [Configuring flexible time windows](https://docs.aws.amazon.com/scheduler/latest/UserGuide/managing-schedule-flexible-time-windows.html) for more details. -## Error-handling +## Error-handling You can configure how your schedule handles failures, when EventBridge Scheduler is unable to deliver an event successfully to a target, by using two primary mechanisms: a retry policy, and a dead-letter queue (DLQ). @@ -280,10 +287,10 @@ const target = new targets.LambdaInvoke(fn, { ## Monitoring -You can monitor Amazon EventBridge Scheduler using CloudWatch, which collects raw data -and processes it into readable, near real-time metrics. EventBridge Scheduler emits +You can monitor Amazon EventBridge Scheduler using CloudWatch, which collects raw data +and processes it into readable, near real-time metrics. EventBridge Scheduler emits a set of metrics for all schedules, and an additional set of metrics for schedules that -have an associated dead-letter queue (DLQ). If you configure a DLQ for your schedule, +have an associated dead-letter queue (DLQ). If you configure a DLQ for your schedule, EventBridge Scheduler publishes additional metrics when your schedule exhausts its retry policy. ### Metrics for all schedules From db52cc2a09b3c5b32fa310d3d4557df6ac5a1e10 Mon Sep 17 00:00:00 2001 From: Ayush Singh <116115048+saiyush@users.noreply.github.com> Date: Wed, 6 Nov 2024 01:12:14 -0800 Subject: [PATCH 16/24] chore(doc): update ROADMAP.md (#32029) ### Issue # (if applicable) Closes #. ### Reason for this change ### Description of changes ### Description of how you validated changes ### Checklist - [ ] 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* --- ROADMAP.md | 23 ++++++++++------------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index f7de45cc733c2..9197ba8f3917b 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1,6 +1,6 @@ # AWS CDK Roadmap -> Updated 9/3/2024 +> Updated 11/5/2024 The roadmap priorities for the AWS CDK (Cloud Development Kit) are informed by what we hear from customers and interactions on Github, CDK.dev Slack, Stack Overflow, and Twitter. This document outlines the high level direction we are working towards, and for each project there is a tracking issue where you can leave feedback. We update this doc on a quarterly basis to reflect any changing priorities. @@ -37,15 +37,11 @@ Over the course of the last few years, the CDK team has spent time speaking with ### L2 Abstractions The CDK team is committed to supporting our existing library of AWS L2 abstractions. We continue to solicit community feedback on where additional L2 coverage makes it simpler and more efficient to build with AWS. Current L2s that the team’s working on are listed below. Please be aware that this list will update throughout the year. If you have feedback on other L2s that should be prioritized by our team, feel free to submit a separate RFC. - -* 🚀 [VPC Construct with Full Control](https://github.com/aws/aws-cdk/tree/main/packages/%40aws-cdk/aws-ec2-alpha) -* 🚀 [AWS APIGateway_v2 graduation](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_apigatewayv2-readme.html) -* 🚀 [AWS Batch graduation](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_batch-readme.html) -* 🚀 [AWS Synthetics graduation](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_synthetics-readme.html) -* 🛠️ [CloudFront Origin Access Control L2](https://github.com/aws/aws-cdk-rfcs/issues/617) -* 🛠️ [AWS Cognito_Identity_pool graduation](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cognito-identitypool-alpha-readme.html) -* 🛠️ [AWS Kinesis_firehose graduation](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-kinesisfirehose-alpha-readme.html) -* 🚦️ [Rewrite EKS L2](https://github.com/aws/aws-cdk-rfcs/issues/605) + +* 🚀 [CloudFront Origin Access Control L2](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cdk-lib.aws_cloudfront_origins-readme.html) +* 🚀 [AWS Cognito_Identity_pool graduation to Developer Preview](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-cognito-identitypool-alpha-readme.html) +* 🚀 [AWS Kinesis_firehose graduation to Developer Preview](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-kinesisfirehose-alpha-readme.html) +* 🛠️ [Rewrite EKS L2](https://github.com/aws/aws-cdk-rfcs/issues/605) * 🔍 [aws-lambda-python graduation](https://docs.aws.amazon.com/cdk/api/v2/docs/aws-lambda-python-alpha-readme.html) * 🔍 [Auto-generation of L2 constructs](https://github.com/aws/aws-cdk-rfcs/issues/611) * 🔍 [Glue Alpha Construct Graduation](https://github.com/aws/aws-cdk/issues/7534) @@ -58,9 +54,10 @@ The CDK team is committed to supporting our existing library of AWS L2 abstracti ### User Experience -- 🛠️ [Garbage Collection for Assets](https://github.com/aws/aws-cdk-rfcs/issues/64) -- 🚦️ [Programmatic Access of the AWS CDK CLI](https://github.com/aws/aws-cdk-rfcs/issues/300) -- 👂🏽 [CDK Refactoring](https://github.com/aws/aws-cdk-rfcs/issues/162) +- 🚀 [Garbage Collection for Assets](https://github.com/aws/aws-cdk-rfcs/issues/64) +- 🛠️ [Programmatic Access of the AWS CDK CLI](https://github.com/aws/aws-cdk-rfcs/issues/300) +- 🚦 [CDK Refactoring](https://github.com/aws/aws-cdk-rfcs/issues/162) +- 🚦 [cli: Enable client-side telemetry and analytics](https://github.com/aws/aws-cdk/issues/32010) ### Community contribution call-outs From 80ddefbd92dba7439ca77e12dd0f9c2c81eb1f34 Mon Sep 17 00:00:00 2001 From: Michael Kaiser Date: Wed, 6 Nov 2024 03:45:14 -0600 Subject: [PATCH 17/24] chore(synthetics): runtime puppeteer 9_1 (#32026) ### Issue # (if applicable) No issue ### Reason for this change There is a new runtime that was published. [syn-nodejs-puppeteer-9.1](https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-9.1) ### Description of changes Just adding the new runtime. ### Description of how you validated changes Against documentation and updating that the template deploys with the new runtime version. ### 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-synthetics/lib/runtime.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/packages/aws-cdk-lib/aws-synthetics/lib/runtime.ts b/packages/aws-cdk-lib/aws-synthetics/lib/runtime.ts index e41cfae47fa11..0454c61084942 100644 --- a/packages/aws-cdk-lib/aws-synthetics/lib/runtime.ts +++ b/packages/aws-cdk-lib/aws-synthetics/lib/runtime.ts @@ -234,6 +234,19 @@ export class Runtime { */ public static readonly SYNTHETICS_NODEJS_PUPPETEER_9_0 = new Runtime('syn-nodejs-puppeteer-9.0', RuntimeFamily.NODEJS); + /** + * `syn-nodejs-puppeteer-9.1` includes the following: + * - Lambda runtime Node.js 20.x + * - Puppeteer-core version 22.12.1 + * - Chromium version 126.0.6478.126 + * + * New Features: + * - **Bug fixes** Bug fix related to date ranges and pending requests in HAR files. + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_Library_nodejs_puppeteer.html#CloudWatch_Synthetics_runtimeversion-nodejs-puppeteer-9.1 + */ + public static readonly SYNTHETICS_NODEJS_PUPPETEER_9_1 = new Runtime('syn-nodejs-puppeteer-9.1', RuntimeFamily.NODEJS); + /** * `syn-python-selenium-1.0` includes the following: * - Lambda runtime Python 3.8 From 4af1564cc3c67f1f58464f2db9c81c2e505d0c34 Mon Sep 17 00:00:00 2001 From: Ren Yamanashi <96362223+ren-yamanashi@users.noreply.github.com> Date: Wed, 6 Nov 2024 19:16:11 +0900 Subject: [PATCH 18/24] chore(docs): fix invalid anchor links in DESIGN_GUIDELINES (#31997) ### Issue # (if applicable) n/A ### Description of changes Fix invalid anchor links in [DESIGN_GUIDELINES](https://github.com/aws/aws-cdk/blob/main/docs/DESIGN_GUIDELINES.md). ### 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* --- docs/DESIGN_GUIDELINES.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/DESIGN_GUIDELINES.md b/docs/DESIGN_GUIDELINES.md index 03953981534be..d55a2fcab35f1 100644 --- a/docs/DESIGN_GUIDELINES.md +++ b/docs/DESIGN_GUIDELINES.md @@ -9,7 +9,7 @@ the AWS Construct Library in order to ensure a consistent and integrated experience across the entire AWS surface area. * [Preface](#preface) -* [What's Included](#what-s-included) +* [What's Included](#whats-included) * [API Design](#api-design) * [Modules](#modules) * [Construct Class](#construct-class) @@ -66,7 +66,7 @@ experience across the entire AWS surface area. * [Unit tests](#unit-tests) * [Integration tests](#integration-tests) * [Versioning](#versioning) -* [Naming & Style](#naming---style) +* [Naming & Style](#naming--style) * [Naming Conventions](#naming-conventions) * [Coding Style](#coding-style) From 1f39cb9e0770b5c02302b9b36ac874ee6bf53329 Mon Sep 17 00:00:00 2001 From: Matsuda Date: Thu, 7 Nov 2024 04:46:21 +0900 Subject: [PATCH 19/24] feat(synthetics): add `artifactS3Encryption` property to the Canary Construct. (#30197) ### Issue # (if applicable) Closes #30190. ### Reason for this change To select encryption options. ### Description of changes Add `artifactS3Encryption` property to the Canary Construct. ### Description of how you validated changes Add unit tests and integ tests. ### 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* --- ...efaultTestDeployAssert3AD5A094.assets.json | 19 + ...aultTestDeployAssert3AD5A094.template.json | 36 + .../index.js | 1 + .../index.js | 1 + .../canary-artifact-s3-encryption.assets.json | 45 + ...anary-artifact-s3-encryption.template.json | 1043 +++++++++++++++ .../cdk.out | 1 + .../integ.json | 12 + .../manifest.json | 233 ++++ .../tree.json | 1122 +++++++++++++++++ .../integ.canary-artifact-s3-encryption.ts | 68 + packages/aws-cdk-lib/aws-synthetics/README.md | 26 + .../aws-cdk-lib/aws-synthetics/lib/canary.ts | 72 ++ .../aws-synthetics/test/canary.test.ts | 156 +++ 14 files changed, 2835 insertions(+) create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/IntegCanaryTestDefaultTestDeployAssert3AD5A094.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/IntegCanaryTestDefaultTestDeployAssert3AD5A094.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/asset.d57a897c2081bda5ce88819548c8b944dff49e721e1f7f7e9e3dd8323e6ccb0d/index.js create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/canary-artifact-s3-encryption.assets.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/canary-artifact-s3-encryption.template.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/integ.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/tree.json create mode 100644 packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.ts diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/IntegCanaryTestDefaultTestDeployAssert3AD5A094.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/IntegCanaryTestDefaultTestDeployAssert3AD5A094.assets.json new file mode 100644 index 0000000000000..ceb305d3c2f6e --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/IntegCanaryTestDefaultTestDeployAssert3AD5A094.assets.json @@ -0,0 +1,19 @@ +{ + "version": "36.0.0", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "IntegCanaryTestDefaultTestDeployAssert3AD5A094.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/IntegCanaryTestDefaultTestDeployAssert3AD5A094.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/IntegCanaryTestDefaultTestDeployAssert3AD5A094.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/IntegCanaryTestDefaultTestDeployAssert3AD5A094.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js new file mode 100644 index 0000000000000..1002ba018e9fb --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js @@ -0,0 +1 @@ +"use strict";var f=Object.create;var i=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var w=Object.getPrototypeOf,P=Object.prototype.hasOwnProperty;var A=(t,e)=>{for(var o in e)i(t,o,{get:e[o],enumerable:!0})},d=(t,e,o,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of C(e))!P.call(t,s)&&s!==o&&i(t,s,{get:()=>e[s],enumerable:!(r=I(e,s))||r.enumerable});return t};var l=(t,e,o)=>(o=t!=null?f(w(t)):{},d(e||!t||!t.__esModule?i(o,"default",{value:t,enumerable:!0}):o,t)),B=t=>d(i({},"__esModule",{value:!0}),t);var q={};A(q,{autoDeleteHandler:()=>S,handler:()=>H});module.exports=B(q);var h=require("@aws-sdk/client-s3");var y=l(require("https")),m=l(require("url")),a={sendHttpRequest:D,log:T,includeStackTraces:!0,userHandlerIndex:"./index"},p="AWSCDK::CustomResourceProviderFramework::CREATE_FAILED",L="AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID";function R(t){return async(e,o)=>{let r={...e,ResponseURL:"..."};if(a.log(JSON.stringify(r,void 0,2)),e.RequestType==="Delete"&&e.PhysicalResourceId===p){a.log("ignoring DELETE event caused by a failed CREATE event"),await u("SUCCESS",e);return}try{let s=await t(r,o),n=k(e,s);await u("SUCCESS",n)}catch(s){let n={...e,Reason:a.includeStackTraces?s.stack:s.message};n.PhysicalResourceId||(e.RequestType==="Create"?(a.log("CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored"),n.PhysicalResourceId=p):a.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(e)}`)),await u("FAILED",n)}}}function k(t,e={}){let o=e.PhysicalResourceId??t.PhysicalResourceId??t.RequestId;if(t.RequestType==="Delete"&&o!==t.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from "${t.PhysicalResourceId}" to "${e.PhysicalResourceId}" during deletion`);return{...t,...e,PhysicalResourceId:o}}async function u(t,e){let o={Status:t,Reason:e.Reason??t,StackId:e.StackId,RequestId:e.RequestId,PhysicalResourceId:e.PhysicalResourceId||L,LogicalResourceId:e.LogicalResourceId,NoEcho:e.NoEcho,Data:e.Data},r=m.parse(e.ResponseURL),s=`${r.protocol}//${r.hostname}/${r.pathname}?***`;a.log("submit response to cloudformation",s,o);let n=JSON.stringify(o),E={hostname:r.hostname,path:r.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(n,"utf8")}};await O({attempts:5,sleep:1e3},a.sendHttpRequest)(E,n)}async function D(t,e){return new Promise((o,r)=>{try{let s=y.request(t,n=>{n.resume(),!n.statusCode||n.statusCode>=400?r(new Error(`Unsuccessful HTTP response: ${n.statusCode}`)):o()});s.on("error",r),s.write(e),s.end()}catch(s){r(s)}})}function T(t,...e){console.log(t,...e)}function O(t,e){return async(...o)=>{let r=t.attempts,s=t.sleep;for(;;)try{return await e(...o)}catch(n){if(r--<=0)throw n;await b(Math.floor(Math.random()*s)),s*=2}}}async function b(t){return new Promise(e=>setTimeout(e,t))}var g="aws-cdk:auto-delete-objects",x=JSON.stringify({Version:"2012-10-17",Statement:[]}),c=new h.S3({}),H=R(S);async function S(t){switch(t.RequestType){case"Create":return;case"Update":return{PhysicalResourceId:(await F(t)).PhysicalResourceId};case"Delete":return N(t.ResourceProperties?.BucketName)}}async function F(t){let e=t,o=e.OldResourceProperties?.BucketName;return{PhysicalResourceId:e.ResourceProperties?.BucketName??o}}async function _(t){try{let e=(await c.getBucketPolicy({Bucket:t}))?.Policy??x,o=JSON.parse(e);o.Statement.push({Principal:"*",Effect:"Deny",Action:["s3:PutObject"],Resource:[`arn:aws:s3:::${t}/*`]}),await c.putBucketPolicy({Bucket:t,Policy:JSON.stringify(o)})}catch(e){if(e.name==="NoSuchBucket")throw e;console.log(`Could not set new object deny policy on bucket '${t}' prior to deletion.`)}}async function U(t){let e;do{e=await c.listObjectVersions({Bucket:t});let o=[...e.Versions??[],...e.DeleteMarkers??[]];if(o.length===0)return;let r=o.map(s=>({Key:s.Key,VersionId:s.VersionId}));await c.deleteObjects({Bucket:t,Delete:{Objects:r}})}while(e?.IsTruncated)}async function N(t){if(!t)throw new Error("No BucketName was provided.");try{if(!await W(t)){console.log(`Bucket does not have '${g}' tag, skipping cleaning.`);return}await _(t),await U(t)}catch(e){if(e.name==="NoSuchBucket"){console.log(`Bucket '${t}' does not exist.`);return}throw e}}async function W(t){return(await c.getBucketTagging({Bucket:t})).TagSet?.some(o=>o.Key===g&&o.Value==="true")}0&&(module.exports={autoDeleteHandler,handler}); diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/asset.d57a897c2081bda5ce88819548c8b944dff49e721e1f7f7e9e3dd8323e6ccb0d/index.js b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/asset.d57a897c2081bda5ce88819548c8b944dff49e721e1f7f7e9e3dd8323e6ccb0d/index.js new file mode 100644 index 0000000000000..f9cc630970c96 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/asset.d57a897c2081bda5ce88819548c8b944dff49e721e1f7f7e9e3dd8323e6ccb0d/index.js @@ -0,0 +1 @@ +"use strict";var I=Object.create;var i=Object.defineProperty;var g=Object.getOwnPropertyDescriptor;var S=Object.getOwnPropertyNames;var w=Object.getPrototypeOf,A=Object.prototype.hasOwnProperty;var P=(o,e)=>{for(var t in e)i(o,t,{get:e[t],enumerable:!0})},l=(o,e,t,n)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of S(e))!A.call(o,s)&&s!==t&&i(o,s,{get:()=>e[s],enumerable:!(n=g(e,s))||n.enumerable});return o};var m=(o,e,t)=>(t=o!=null?I(w(o)):{},l(e||!o||!o.__esModule?i(t,"default",{value:o,enumerable:!0}):t,o)),L=o=>l(i({},"__esModule",{value:!0}),o);var W={};P(W,{autoDeleteHandler:()=>E,handler:()=>_});module.exports=L(W);var c=require("@aws-sdk/client-lambda"),u=require("@aws-sdk/client-synthetics");var y=m(require("https")),R=m(require("url")),a={sendHttpRequest:T,log:F,includeStackTraces:!0,userHandlerIndex:"./index"},p="AWSCDK::CustomResourceProviderFramework::CREATE_FAILED",D="AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID";function C(o){return async(e,t)=>{let n={...e,ResponseURL:"..."};if(a.log(JSON.stringify(n,void 0,2)),e.RequestType==="Delete"&&e.PhysicalResourceId===p){a.log("ignoring DELETE event caused by a failed CREATE event"),await d("SUCCESS",e);return}try{let s=await o(n,t),r=b(e,s);await d("SUCCESS",r)}catch(s){let r={...e,Reason:a.includeStackTraces?s.stack:s.message};r.PhysicalResourceId||(e.RequestType==="Create"?(a.log("CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored"),r.PhysicalResourceId=p):a.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(e)}`)),await d("FAILED",r)}}}function b(o,e={}){let t=e.PhysicalResourceId??o.PhysicalResourceId??o.RequestId;if(o.RequestType==="Delete"&&t!==o.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from "${o.PhysicalResourceId}" to "${e.PhysicalResourceId}" during deletion`);return{...o,...e,PhysicalResourceId:t}}async function d(o,e){let t={Status:o,Reason:e.Reason??o,StackId:e.StackId,RequestId:e.RequestId,PhysicalResourceId:e.PhysicalResourceId||D,LogicalResourceId:e.LogicalResourceId,NoEcho:e.NoEcho,Data:e.Data},n=R.parse(e.ResponseURL),s=`${n.protocol}//${n.hostname}/${n.pathname}?***`;a.log("submit response to cloudformation",s,t);let r=JSON.stringify(t),f={hostname:n.hostname,path:n.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(r,"utf8")}};await x({attempts:5,sleep:1e3},a.sendHttpRequest)(f,r)}async function T(o,e){return new Promise((t,n)=>{try{let s=y.request(o,r=>{r.resume(),!r.statusCode||r.statusCode>=400?n(new Error(`Unsuccessful HTTP response: ${r.statusCode}`)):t()});s.on("error",n),s.write(e),s.end()}catch(s){n(s)}})}function F(o,...e){console.log(o,...e)}function x(o,e){return async(...t)=>{let n=o.attempts,s=o.sleep;for(;;)try{return await e(...t)}catch(r){if(n--<=0)throw r;await N(Math.floor(Math.random()*s)),s*=2}}}async function N(o){return new Promise(e=>setTimeout(e,o))}var h="aws-cdk:auto-delete-underlying-resources",H=new c.LambdaClient({}),U=new u.SyntheticsClient({}),_=C(E);async function E(o){switch(o.RequestType){case"Create":return{PhyscialResourceId:o.ResourceProperties?.CanaryName};case"Update":return{PhysicalResourceId:(await k(o)).PhysicalResourceId};case"Delete":return q(o.ResourceProperties?.CanaryName)}}async function k(o){return{PhysicalResourceId:o.ResourceProperties?.CanaryName}}async function q(o){if(console.log(`Deleting lambda function associated with ${o}`),!o)throw new Error("No CanaryName was provided.");try{let e=await U.send(new u.GetCanaryCommand({Name:o}));if(e.Canary===void 0||e.Canary.Id===void 0)return;if(e.Canary.EngineArn===void 0)return;if(!O(e.Canary.Tags)){console.log(`Canary does not have '${h}' tag, skipping deletion.`);return}let t=e.Canary.EngineArn.split(":");t.at(-1)?.includes(e.Canary.Id)||t.pop();let n=t.join(":");console.log(`Deleting lambda ${n}`),await H.send(new c.DeleteFunctionCommand({FunctionName:n}))}catch(e){if(e.name!=="ResourceNotFoundException")throw e}}function O(o){return o?Object.keys(o).some(e=>e===h&&o[e]==="true"):!1}0&&(module.exports={autoDeleteHandler,handler}); diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/canary-artifact-s3-encryption.assets.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/canary-artifact-s3-encryption.assets.json new file mode 100644 index 0000000000000..0dbbb97843db7 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/canary-artifact-s3-encryption.assets.json @@ -0,0 +1,45 @@ +{ + "version": "36.0.0", + "files": { + "44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61": { + "source": { + "path": "asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "d57a897c2081bda5ce88819548c8b944dff49e721e1f7f7e9e3dd8323e6ccb0d": { + "source": { + "path": "asset.d57a897c2081bda5ce88819548c8b944dff49e721e1f7f7e9e3dd8323e6ccb0d", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "d57a897c2081bda5ce88819548c8b944dff49e721e1f7f7e9e3dd8323e6ccb0d.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "f6aad262db732f8a3d154c577c54fbd4f8d3a3ac9cf0ab7bcf5aa7bcc9f79f50": { + "source": { + "path": "canary-artifact-s3-encryption.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "f6aad262db732f8a3d154c577c54fbd4f8d3a3ac9cf0ab7bcf5aa7bcc9f79f50.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/canary-artifact-s3-encryption.template.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/canary-artifact-s3-encryption.template.json new file mode 100644 index 0000000000000..99f1dca59faf3 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/canary-artifact-s3-encryption.template.json @@ -0,0 +1,1043 @@ +{ + "Resources": { + "MyTestBucket81062429": { + "Type": "AWS::S3::Bucket", + "Properties": { + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "MyTestBucketPolicyE11AF29F": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "MyTestBucket81062429" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*", + "s3:PutBucketPolicy" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "MyTestBucketAutoDeleteObjectsCustomResource1E1AC890": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "MyTestBucket81062429" + } + }, + "DependsOn": [ + "MyTestBucketPolicyE11AF29F" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": { + "Fn::FindInMap": [ + "LatestNodeRuntimeMap", + { + "Ref": "AWS::Region" + }, + "value" + ] + }, + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "MyTestBucket81062429" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "CanarySseS3ServiceRoleC3DFF4A1": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + "/integ/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "canaryPolicy" + } + ] + } + }, + "CanarySseS377E9DBF2": { + "Type": "AWS::Synthetics::Canary", + "Properties": { + "ArtifactConfig": { + "S3Encryption": { + "EncryptionMode": "SSE_S3" + } + }, + "ArtifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "MyTestBucket81062429" + }, + "/integ" + ] + ] + }, + "Code": { + "Handler": "index.handler", + "Script": "\n exports.handler = async () => {\n console.log('hello world');\n };" + }, + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "CanarySseS3ServiceRoleC3DFF4A1", + "Arn" + ] + }, + "Name": "canaryartifactseee471", + "RuntimeVersion": "syn-nodejs-puppeteer-7.0", + "Schedule": { + "DurationInSeconds": "0", + "Expression": "rate(1 minute)" + }, + "StartCanaryAfterCreation": true, + "Tags": [ + { + "Key": "aws-cdk:auto-delete-underlying-resources", + "Value": "true" + } + ] + } + }, + "CanarySseS3AutoDeleteUnderlyingResourcesCustomResource683951EB": { + "Type": "Custom::SyntheticsAutoDeleteUnderlyingResources", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomSyntheticsAutoDeleteUnderlyingResourcesCustomResourceProviderHandler26776D4E", + "Arn" + ] + }, + "CanaryName": { + "Ref": "CanarySseS377E9DBF2" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomSyntheticsAutoDeleteUnderlyingResourcesCustomResourceProviderRole2D11A112": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ], + "Policies": [ + { + "PolicyName": "Inline", + "PolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Action": [ + "lambda:DeleteFunction" + ], + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":lambda:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":function:cwsyn-*" + ] + ] + } + }, + { + "Effect": "Allow", + "Action": [ + "synthetics:GetCanary" + ], + "Resource": "*" + } + ] + } + } + ] + } + }, + "CustomSyntheticsAutoDeleteUnderlyingResourcesCustomResourceProviderHandler26776D4E": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "d57a897c2081bda5ce88819548c8b944dff49e721e1f7f7e9e3dd8323e6ccb0d.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomSyntheticsAutoDeleteUnderlyingResourcesCustomResourceProviderRole2D11A112", + "Arn" + ] + }, + "Runtime": { + "Fn::FindInMap": [ + "LatestNodeRuntimeMap", + { + "Ref": "AWS::Region" + }, + "value" + ] + }, + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting underlying resources created by ", + { + "Ref": "CanarySseS377E9DBF2" + }, + "." + ] + ] + } + }, + "DependsOn": [ + "CustomSyntheticsAutoDeleteUnderlyingResourcesCustomResourceProviderRole2D11A112" + ] + }, + "CanarySseKmsWithoutKeySettingServiceRole50435BBC": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + "/integ/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "canaryPolicy" + } + ] + } + }, + "CanarySseKmsWithoutKeySettingServiceRoleDefaultPolicyAC49E578": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CanarySseKmsWithoutKeySettingKey11BDE817", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CanarySseKmsWithoutKeySettingServiceRoleDefaultPolicyAC49E578", + "Roles": [ + { + "Ref": "CanarySseKmsWithoutKeySettingServiceRole50435BBC" + } + ] + } + }, + "CanarySseKmsWithoutKeySettingKey11BDE817": { + "Type": "AWS::KMS::Key", + "Properties": { + "Description": "Created by canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting", + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Retain", + "DeletionPolicy": "Retain" + }, + "CanarySseKmsWithoutKeySettingD8C26A32": { + "Type": "AWS::Synthetics::Canary", + "Properties": { + "ArtifactConfig": { + "S3Encryption": { + "EncryptionMode": "SSE_KMS", + "KmsKeyArn": { + "Fn::GetAtt": [ + "CanarySseKmsWithoutKeySettingKey11BDE817", + "Arn" + ] + } + } + }, + "ArtifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "MyTestBucket81062429" + }, + "/integ" + ] + ] + }, + "Code": { + "Handler": "index.handler", + "Script": "\n exports.handler = async () => {\n console.log('hello world');\n };" + }, + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "CanarySseKmsWithoutKeySettingServiceRole50435BBC", + "Arn" + ] + }, + "Name": "canaryartifactsefa4b6", + "RuntimeVersion": "syn-nodejs-puppeteer-7.0", + "Schedule": { + "DurationInSeconds": "0", + "Expression": "rate(1 minute)" + }, + "StartCanaryAfterCreation": true, + "Tags": [ + { + "Key": "aws-cdk:auto-delete-underlying-resources", + "Value": "true" + } + ] + } + }, + "CanarySseKmsWithoutKeySettingAutoDeleteUnderlyingResourcesCustomResourceB288EFE0": { + "Type": "Custom::SyntheticsAutoDeleteUnderlyingResources", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomSyntheticsAutoDeleteUnderlyingResourcesCustomResourceProviderHandler26776D4E", + "Arn" + ] + }, + "CanaryName": { + "Ref": "CanarySseKmsWithoutKeySettingD8C26A32" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "Key961B73FD": { + "Type": "AWS::KMS::Key", + "Properties": { + "KeyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CanarySseKmsWithServiceRoleDE325788": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "Policies": [ + { + "PolicyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + "/integ/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" + ] + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "canaryPolicy" + } + ] + } + }, + "CanarySseKmsWithServiceRoleDefaultPolicyBD214DF4": { + "Type": "AWS::IAM::Policy", + "Properties": { + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "PolicyName": "CanarySseKmsWithServiceRoleDefaultPolicyBD214DF4", + "Roles": [ + { + "Ref": "CanarySseKmsWithServiceRoleDE325788" + } + ] + } + }, + "CanarySseKmsWith1F191227": { + "Type": "AWS::Synthetics::Canary", + "Properties": { + "ArtifactConfig": { + "S3Encryption": { + "EncryptionMode": "SSE_KMS", + "KmsKeyArn": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + } + } + }, + "ArtifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "MyTestBucket81062429" + }, + "/integ" + ] + ] + }, + "Code": { + "Handler": "index.handler", + "Script": "\n exports.handler = async () => {\n console.log('hello world');\n };" + }, + "ExecutionRoleArn": { + "Fn::GetAtt": [ + "CanarySseKmsWithServiceRoleDE325788", + "Arn" + ] + }, + "Name": "canaryartifacts4881f3", + "RuntimeVersion": "syn-nodejs-puppeteer-7.0", + "Schedule": { + "DurationInSeconds": "0", + "Expression": "rate(1 minute)" + }, + "StartCanaryAfterCreation": true, + "Tags": [ + { + "Key": "aws-cdk:auto-delete-underlying-resources", + "Value": "true" + } + ] + } + }, + "CanarySseKmsWithAutoDeleteUnderlyingResourcesCustomResource4F111E39": { + "Type": "Custom::SyntheticsAutoDeleteUnderlyingResources", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomSyntheticsAutoDeleteUnderlyingResourcesCustomResourceProviderHandler26776D4E", + "Arn" + ] + }, + "CanaryName": { + "Ref": "CanarySseKmsWith1F191227" + } + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + } + }, + "Mappings": { + "LatestNodeRuntimeMap": { + "af-south-1": { + "value": "nodejs20.x" + }, + "ap-east-1": { + "value": "nodejs20.x" + }, + "ap-northeast-1": { + "value": "nodejs20.x" + }, + "ap-northeast-2": { + "value": "nodejs20.x" + }, + "ap-northeast-3": { + "value": "nodejs20.x" + }, + "ap-south-1": { + "value": "nodejs20.x" + }, + "ap-south-2": { + "value": "nodejs20.x" + }, + "ap-southeast-1": { + "value": "nodejs20.x" + }, + "ap-southeast-2": { + "value": "nodejs20.x" + }, + "ap-southeast-3": { + "value": "nodejs20.x" + }, + "ap-southeast-4": { + "value": "nodejs20.x" + }, + "ap-southeast-5": { + "value": "nodejs20.x" + }, + "ap-southeast-7": { + "value": "nodejs20.x" + }, + "ca-central-1": { + "value": "nodejs20.x" + }, + "ca-west-1": { + "value": "nodejs20.x" + }, + "cn-north-1": { + "value": "nodejs18.x" + }, + "cn-northwest-1": { + "value": "nodejs18.x" + }, + "eu-central-1": { + "value": "nodejs20.x" + }, + "eu-central-2": { + "value": "nodejs20.x" + }, + "eu-isoe-west-1": { + "value": "nodejs18.x" + }, + "eu-north-1": { + "value": "nodejs20.x" + }, + "eu-south-1": { + "value": "nodejs20.x" + }, + "eu-south-2": { + "value": "nodejs20.x" + }, + "eu-west-1": { + "value": "nodejs20.x" + }, + "eu-west-2": { + "value": "nodejs20.x" + }, + "eu-west-3": { + "value": "nodejs20.x" + }, + "il-central-1": { + "value": "nodejs20.x" + }, + "me-central-1": { + "value": "nodejs20.x" + }, + "me-south-1": { + "value": "nodejs20.x" + }, + "mx-central-1": { + "value": "nodejs20.x" + }, + "sa-east-1": { + "value": "nodejs20.x" + }, + "us-east-1": { + "value": "nodejs20.x" + }, + "us-east-2": { + "value": "nodejs20.x" + }, + "us-gov-east-1": { + "value": "nodejs18.x" + }, + "us-gov-west-1": { + "value": "nodejs18.x" + }, + "us-iso-east-1": { + "value": "nodejs18.x" + }, + "us-iso-west-1": { + "value": "nodejs18.x" + }, + "us-isob-east-1": { + "value": "nodejs18.x" + }, + "us-west-1": { + "value": "nodejs20.x" + }, + "us-west-2": { + "value": "nodejs20.x" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/cdk.out b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/cdk.out new file mode 100644 index 0000000000000..1f0068d32659a --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"36.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/integ.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/integ.json new file mode 100644 index 0000000000000..0546bb1684300 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "36.0.0", + "testCases": { + "IntegCanaryTest/DefaultTest": { + "stacks": [ + "canary-artifact-s3-encryption" + ], + "assertionStack": "IntegCanaryTest/DefaultTest/DeployAssert", + "assertionStackName": "IntegCanaryTestDefaultTestDeployAssert3AD5A094" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/manifest.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/manifest.json new file mode 100644 index 0000000000000..355bcc7683236 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/manifest.json @@ -0,0 +1,233 @@ +{ + "version": "36.0.0", + "artifacts": { + "canary-artifact-s3-encryption.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "canary-artifact-s3-encryption.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "canary-artifact-s3-encryption": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "canary-artifact-s3-encryption.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/f6aad262db732f8a3d154c577c54fbd4f8d3a3ac9cf0ab7bcf5aa7bcc9f79f50.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "canary-artifact-s3-encryption.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "canary-artifact-s3-encryption.assets" + ], + "metadata": { + "/canary-artifact-s3-encryption/MyTestBucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyTestBucket81062429" + } + ], + "/canary-artifact-s3-encryption/MyTestBucket/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "MyTestBucketPolicyE11AF29F" + } + ], + "/canary-artifact-s3-encryption/MyTestBucket/AutoDeleteObjectsCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "MyTestBucketAutoDeleteObjectsCustomResource1E1AC890" + } + ], + "/canary-artifact-s3-encryption/LatestNodeRuntimeMap": [ + { + "type": "aws:cdk:logicalId", + "data": "LatestNodeRuntimeMap" + } + ], + "/canary-artifact-s3-encryption/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + } + ], + "/canary-artifact-s3-encryption/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F" + } + ], + "/canary-artifact-s3-encryption/CanarySseS3/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseS3ServiceRoleC3DFF4A1" + } + ], + "/canary-artifact-s3-encryption/CanarySseS3/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseS377E9DBF2" + } + ], + "/canary-artifact-s3-encryption/CanarySseS3/AutoDeleteUnderlyingResourcesCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseS3AutoDeleteUnderlyingResourcesCustomResource683951EB" + } + ], + "/canary-artifact-s3-encryption/Custom::SyntheticsAutoDeleteUnderlyingResourcesCustomResourceProvider/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomSyntheticsAutoDeleteUnderlyingResourcesCustomResourceProviderRole2D11A112" + } + ], + "/canary-artifact-s3-encryption/Custom::SyntheticsAutoDeleteUnderlyingResourcesCustomResourceProvider/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomSyntheticsAutoDeleteUnderlyingResourcesCustomResourceProviderHandler26776D4E" + } + ], + "/canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseKmsWithoutKeySettingServiceRole50435BBC" + } + ], + "/canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseKmsWithoutKeySettingServiceRoleDefaultPolicyAC49E578" + } + ], + "/canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/Key/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseKmsWithoutKeySettingKey11BDE817" + } + ], + "/canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseKmsWithoutKeySettingD8C26A32" + } + ], + "/canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/AutoDeleteUnderlyingResourcesCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseKmsWithoutKeySettingAutoDeleteUnderlyingResourcesCustomResourceB288EFE0" + } + ], + "/canary-artifact-s3-encryption/Key/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Key961B73FD" + } + ], + "/canary-artifact-s3-encryption/CanarySseKmsWith/ServiceRole/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseKmsWithServiceRoleDE325788" + } + ], + "/canary-artifact-s3-encryption/CanarySseKmsWith/ServiceRole/DefaultPolicy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseKmsWithServiceRoleDefaultPolicyBD214DF4" + } + ], + "/canary-artifact-s3-encryption/CanarySseKmsWith/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseKmsWith1F191227" + } + ], + "/canary-artifact-s3-encryption/CanarySseKmsWith/AutoDeleteUnderlyingResourcesCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "CanarySseKmsWithAutoDeleteUnderlyingResourcesCustomResource4F111E39" + } + ], + "/canary-artifact-s3-encryption/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/canary-artifact-s3-encryption/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "canary-artifact-s3-encryption" + }, + "IntegCanaryTestDefaultTestDeployAssert3AD5A094.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "IntegCanaryTestDefaultTestDeployAssert3AD5A094.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "IntegCanaryTestDefaultTestDeployAssert3AD5A094": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "IntegCanaryTestDefaultTestDeployAssert3AD5A094.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "IntegCanaryTestDefaultTestDeployAssert3AD5A094.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "IntegCanaryTestDefaultTestDeployAssert3AD5A094.assets" + ], + "metadata": { + "/IntegCanaryTest/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/IntegCanaryTest/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "IntegCanaryTest/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/tree.json b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/tree.json new file mode 100644 index 0000000000000..4cfdc833562e1 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.js.snapshot/tree.json @@ -0,0 +1,1122 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "canary-artifact-s3-encryption": { + "id": "canary-artifact-s3-encryption", + "path": "canary-artifact-s3-encryption", + "children": { + "MyTestBucket": { + "id": "MyTestBucket", + "path": "canary-artifact-s3-encryption/MyTestBucket", + "children": { + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/MyTestBucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "aws-cdk:auto-delete-objects", + "value": "true" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "canary-artifact-s3-encryption/MyTestBucket/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/MyTestBucket/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy", + "aws:cdk:cloudformation:props": { + "bucket": { + "Ref": "MyTestBucket81062429" + }, + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*", + "s3:PutBucketPolicy" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketPolicy", + "version": "0.0.0" + } + }, + "AutoDeleteObjectsCustomResource": { + "id": "AutoDeleteObjectsCustomResource", + "path": "canary-artifact-s3-encryption/MyTestBucket/AutoDeleteObjectsCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "canary-artifact-s3-encryption/MyTestBucket/AutoDeleteObjectsCustomResource/Default", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0" + } + }, + "LatestNodeRuntimeMap": { + "id": "LatestNodeRuntimeMap", + "path": "canary-artifact-s3-encryption/LatestNodeRuntimeMap", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnMapping", + "version": "0.0.0" + } + }, + "Custom::S3AutoDeleteObjectsCustomResourceProvider": { + "id": "Custom::S3AutoDeleteObjectsCustomResourceProvider", + "path": "canary-artifact-s3-encryption/Custom::S3AutoDeleteObjectsCustomResourceProvider", + "children": { + "Staging": { + "id": "Staging", + "path": "canary-artifact-s3-encryption/Custom::S3AutoDeleteObjectsCustomResourceProvider/Staging", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "canary-artifact-s3-encryption/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "canary-artifact-s3-encryption/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResourceProviderBase", + "version": "0.0.0" + } + }, + "CanarySseS3": { + "id": "CanarySseS3", + "path": "canary-artifact-s3-encryption/CanarySseS3", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "canary-artifact-s3-encryption/CanarySseS3/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "canary-artifact-s3-encryption/CanarySseS3/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/CanarySseS3/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "policies": [ + { + "policyName": "canaryPolicy", + "policyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + "/integ/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/CanarySseS3/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Synthetics::Canary", + "aws:cdk:cloudformation:props": { + "artifactConfig": { + "s3Encryption": { + "encryptionMode": "SSE_S3" + } + }, + "artifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "MyTestBucket81062429" + }, + "/integ" + ] + ] + }, + "code": { + "handler": "index.handler", + "script": "\n exports.handler = async () => {\n console.log('hello world');\n };" + }, + "executionRoleArn": { + "Fn::GetAtt": [ + "CanarySseS3ServiceRoleC3DFF4A1", + "Arn" + ] + }, + "name": "canaryartifactseee471", + "runtimeVersion": "syn-nodejs-puppeteer-7.0", + "schedule": { + "durationInSeconds": "0", + "expression": "rate(1 minute)" + }, + "startCanaryAfterCreation": true, + "tags": [ + { + "key": "aws-cdk:auto-delete-underlying-resources", + "value": "true" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_synthetics.CfnCanary", + "version": "0.0.0" + } + }, + "AutoDeleteUnderlyingResourcesCustomResource": { + "id": "AutoDeleteUnderlyingResourcesCustomResource", + "path": "canary-artifact-s3-encryption/CanarySseS3/AutoDeleteUnderlyingResourcesCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "canary-artifact-s3-encryption/CanarySseS3/AutoDeleteUnderlyingResourcesCustomResource/Default", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_synthetics.Canary", + "version": "0.0.0" + } + }, + "Custom::SyntheticsAutoDeleteUnderlyingResourcesCustomResourceProvider": { + "id": "Custom::SyntheticsAutoDeleteUnderlyingResourcesCustomResourceProvider", + "path": "canary-artifact-s3-encryption/Custom::SyntheticsAutoDeleteUnderlyingResourcesCustomResourceProvider", + "children": { + "Staging": { + "id": "Staging", + "path": "canary-artifact-s3-encryption/Custom::SyntheticsAutoDeleteUnderlyingResourcesCustomResourceProvider/Staging", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "canary-artifact-s3-encryption/Custom::SyntheticsAutoDeleteUnderlyingResourcesCustomResourceProvider/Role", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "canary-artifact-s3-encryption/Custom::SyntheticsAutoDeleteUnderlyingResourcesCustomResourceProvider/Handler", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResourceProviderBase", + "version": "0.0.0" + } + }, + "CanarySseKmsWithoutKeySetting": { + "id": "CanarySseKmsWithoutKeySetting", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "policies": [ + { + "policyName": "canaryPolicy", + "policyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + "/integ/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/ServiceRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/ServiceRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "CanarySseKmsWithoutKeySettingKey11BDE817", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "CanarySseKmsWithoutKeySettingServiceRoleDefaultPolicyAC49E578", + "roles": [ + { + "Ref": "CanarySseKmsWithoutKeySettingServiceRole50435BBC" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Key": { + "id": "Key", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/Key", + "children": { + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/Key/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::KMS::Key", + "aws:cdk:cloudformation:props": { + "description": "Created by canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting", + "keyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.CfnKey", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.Key", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Synthetics::Canary", + "aws:cdk:cloudformation:props": { + "artifactConfig": { + "s3Encryption": { + "encryptionMode": "SSE_KMS", + "kmsKeyArn": { + "Fn::GetAtt": [ + "CanarySseKmsWithoutKeySettingKey11BDE817", + "Arn" + ] + } + } + }, + "artifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "MyTestBucket81062429" + }, + "/integ" + ] + ] + }, + "code": { + "handler": "index.handler", + "script": "\n exports.handler = async () => {\n console.log('hello world');\n };" + }, + "executionRoleArn": { + "Fn::GetAtt": [ + "CanarySseKmsWithoutKeySettingServiceRole50435BBC", + "Arn" + ] + }, + "name": "canaryartifactsefa4b6", + "runtimeVersion": "syn-nodejs-puppeteer-7.0", + "schedule": { + "durationInSeconds": "0", + "expression": "rate(1 minute)" + }, + "startCanaryAfterCreation": true, + "tags": [ + { + "key": "aws-cdk:auto-delete-underlying-resources", + "value": "true" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_synthetics.CfnCanary", + "version": "0.0.0" + } + }, + "AutoDeleteUnderlyingResourcesCustomResource": { + "id": "AutoDeleteUnderlyingResourcesCustomResource", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/AutoDeleteUnderlyingResourcesCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "canary-artifact-s3-encryption/CanarySseKmsWithoutKeySetting/AutoDeleteUnderlyingResourcesCustomResource/Default", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_synthetics.Canary", + "version": "0.0.0" + } + }, + "Key": { + "id": "Key", + "path": "canary-artifact-s3-encryption/Key", + "children": { + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/Key/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::KMS::Key", + "aws:cdk:cloudformation:props": { + "keyPolicy": { + "Statement": [ + { + "Action": "kms:*", + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":iam::", + { + "Ref": "AWS::AccountId" + }, + ":root" + ] + ] + } + }, + "Resource": "*" + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.CfnKey", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_kms.Key", + "version": "0.0.0" + } + }, + "CanarySseKmsWith": { + "id": "CanarySseKmsWith", + "path": "canary-artifact-s3-encryption/CanarySseKmsWith", + "children": { + "ServiceRole": { + "id": "ServiceRole", + "path": "canary-artifact-s3-encryption/CanarySseKmsWith/ServiceRole", + "children": { + "ImportServiceRole": { + "id": "ImportServiceRole", + "path": "canary-artifact-s3-encryption/CanarySseKmsWith/ServiceRole/ImportServiceRole", + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/CanarySseKmsWith/ServiceRole/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Role", + "aws:cdk:cloudformation:props": { + "assumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + }, + "policies": [ + { + "policyName": "canaryPolicy", + "policyDocument": { + "Statement": [ + { + "Action": "s3:ListAllMyBuckets", + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": "s3:GetBucketLocation", + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + } + }, + { + "Action": "s3:PutObject", + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "MyTestBucket81062429", + "Arn" + ] + }, + "/integ/*" + ] + ] + } + }, + { + "Action": "cloudwatch:PutMetricData", + "Condition": { + "StringEquals": { + "cloudwatch:namespace": "CloudWatchSynthetics" + } + }, + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "logs:CreateLogGroup", + "logs:CreateLogStream", + "logs:PutLogEvents" + ], + "Effect": "Allow", + "Resource": { + "Fn::Join": [ + "", + [ + "arn:", + { + "Ref": "AWS::Partition" + }, + ":logs:", + { + "Ref": "AWS::Region" + }, + ":", + { + "Ref": "AWS::AccountId" + }, + ":log-group:/aws/lambda/cwsyn-*" + ] + ] + } + } + ], + "Version": "2012-10-17" + } + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnRole", + "version": "0.0.0" + } + }, + "DefaultPolicy": { + "id": "DefaultPolicy", + "path": "canary-artifact-s3-encryption/CanarySseKmsWith/ServiceRole/DefaultPolicy", + "children": { + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/CanarySseKmsWith/ServiceRole/DefaultPolicy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IAM::Policy", + "aws:cdk:cloudformation:props": { + "policyDocument": { + "Statement": [ + { + "Action": [ + "kms:Decrypt", + "kms:Encrypt", + "kms:GenerateDataKey*", + "kms:ReEncrypt*" + ], + "Effect": "Allow", + "Resource": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + } + } + ], + "Version": "2012-10-17" + }, + "policyName": "CanarySseKmsWithServiceRoleDefaultPolicyBD214DF4", + "roles": [ + { + "Ref": "CanarySseKmsWithServiceRoleDE325788" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.CfnPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Policy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_iam.Role", + "version": "0.0.0" + } + }, + "Resource": { + "id": "Resource", + "path": "canary-artifact-s3-encryption/CanarySseKmsWith/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::Synthetics::Canary", + "aws:cdk:cloudformation:props": { + "artifactConfig": { + "s3Encryption": { + "encryptionMode": "SSE_KMS", + "kmsKeyArn": { + "Fn::GetAtt": [ + "Key961B73FD", + "Arn" + ] + } + } + }, + "artifactS3Location": { + "Fn::Join": [ + "", + [ + "s3://", + { + "Ref": "MyTestBucket81062429" + }, + "/integ" + ] + ] + }, + "code": { + "handler": "index.handler", + "script": "\n exports.handler = async () => {\n console.log('hello world');\n };" + }, + "executionRoleArn": { + "Fn::GetAtt": [ + "CanarySseKmsWithServiceRoleDE325788", + "Arn" + ] + }, + "name": "canaryartifacts4881f3", + "runtimeVersion": "syn-nodejs-puppeteer-7.0", + "schedule": { + "durationInSeconds": "0", + "expression": "rate(1 minute)" + }, + "startCanaryAfterCreation": true, + "tags": [ + { + "key": "aws-cdk:auto-delete-underlying-resources", + "value": "true" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_synthetics.CfnCanary", + "version": "0.0.0" + } + }, + "AutoDeleteUnderlyingResourcesCustomResource": { + "id": "AutoDeleteUnderlyingResourcesCustomResource", + "path": "canary-artifact-s3-encryption/CanarySseKmsWith/AutoDeleteUnderlyingResourcesCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "canary-artifact-s3-encryption/CanarySseKmsWith/AutoDeleteUnderlyingResourcesCustomResource/Default", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_synthetics.Canary", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "canary-artifact-s3-encryption/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "canary-artifact-s3-encryption/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "IntegCanaryTest": { + "id": "IntegCanaryTest", + "path": "IntegCanaryTest", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "IntegCanaryTest/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "IntegCanaryTest/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "IntegCanaryTest/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "IntegCanaryTest/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "IntegCanaryTest/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.ts b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.ts new file mode 100644 index 0000000000000..bacc2cc1492b1 --- /dev/null +++ b/packages/@aws-cdk-testing/framework-integ/test/aws-synthetics/test/integ.canary-artifact-s3-encryption.ts @@ -0,0 +1,68 @@ +/// !cdk-integ canary-one +import * as s3 from 'aws-cdk-lib/aws-s3'; +import * as cdk from 'aws-cdk-lib/core'; +import { ArtifactsEncryptionMode, Canary, Cleanup, Code, Runtime, Schedule, Test } from 'aws-cdk-lib/aws-synthetics'; +import { IntegTest } from '@aws-cdk/integ-tests-alpha'; +import { RemovalPolicy } from 'aws-cdk-lib'; +import { Key } from 'aws-cdk-lib/aws-kms'; + +const app = new cdk.App(); +const stack = new cdk.Stack(app, 'canary-artifact-s3-encryption'); + +const bucket = new s3.Bucket(stack, 'MyTestBucket', { + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, +}); +const prefix = 'integ'; + +new Canary(stack, 'CanarySseS3', { + test: Test.custom({ + handler: 'index.handler', + code: Code.fromInline(` + exports.handler = async () => { + console.log(\'hello world\'); + };`), + }), + schedule: Schedule.rate(cdk.Duration.minutes(1)), + artifactsBucketLocation: { bucket, prefix }, + runtime: Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + cleanup: Cleanup.LAMBDA, + artifactS3EncryptionMode: ArtifactsEncryptionMode.S3_MANAGED, +}); + +new Canary(stack, 'CanarySseKmsWithoutKeySetting', { + test: Test.custom({ + handler: 'index.handler', + code: Code.fromInline(` + exports.handler = async () => { + console.log(\'hello world\'); + };`), + }), + schedule: Schedule.rate(cdk.Duration.minutes(1)), + artifactsBucketLocation: { bucket, prefix }, + runtime: Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + cleanup: Cleanup.LAMBDA, + artifactS3EncryptionMode: ArtifactsEncryptionMode.KMS, +}); + +const encryptKey = new Key(stack, 'Key', { removalPolicy: RemovalPolicy.DESTROY }); + +new Canary(stack, 'CanarySseKmsWith', { + test: Test.custom({ + handler: 'index.handler', + code: Code.fromInline(` + exports.handler = async () => { + console.log(\'hello world\'); + };`), + }), + schedule: Schedule.rate(cdk.Duration.minutes(1)), + artifactsBucketLocation: { bucket, prefix }, + runtime: Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + cleanup: Cleanup.LAMBDA, + artifactS3EncryptionMode: ArtifactsEncryptionMode.KMS, + artifactS3KmsKey: encryptKey, +}); + +new IntegTest(app, 'IntegCanaryTest', { + testCases: [stack], +}); diff --git a/packages/aws-cdk-lib/aws-synthetics/README.md b/packages/aws-cdk-lib/aws-synthetics/README.md index 9fc7620f4faae..cbb111433369d 100644 --- a/packages/aws-cdk-lib/aws-synthetics/README.md +++ b/packages/aws-cdk-lib/aws-synthetics/README.md @@ -304,3 +304,29 @@ const canary = new synthetics.Canary(this, 'MyCanary', { }], }); ``` + +Canary artifacts are encrypted at rest using an AWS-managed key by default. + +You can choose the encryption options SSE-S3 or SSE-KMS by setting the `artifactS3EncryptionMode` property. + +When you use SSE-KMS, you can also supply your own external KMS key by specifying the `kmsKey` property. If you don't, a KMS key will be automatically created and associated with the canary. + +```ts +import * as kms from 'aws-cdk-lib/aws-kms'; + +const key = new kms.Key(this, 'myKey'); + +const canary = new synthetics.Canary(this, 'MyCanary', { + schedule: synthetics.Schedule.rate(Duration.minutes(5)), + test: synthetics.Test.custom({ + code: synthetics.Code.fromAsset(path.join(__dirname, 'canary')), + handler: 'index.handler', + }), + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + artifactsBucketLifecycleRules: [{ + expiration: Duration.days(30), + }], + artifactS3EncryptionMode: synthetics.ArtifactsEncryptionMode.KMS, + artifactS3KmsKey: key, +}); +``` \ No newline at end of file diff --git a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts index 530050720f5dc..365b73882f810 100644 --- a/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts +++ b/packages/aws-cdk-lib/aws-synthetics/lib/canary.ts @@ -8,6 +8,7 @@ import { CfnCanary } from './synthetics.generated'; import { Metric, MetricOptions, MetricProps } from '../../aws-cloudwatch'; import * as ec2 from '../../aws-ec2'; import * as iam from '../../aws-iam'; +import * as kms from '../../aws-kms'; import * as s3 from '../../aws-s3'; import * as cdk from '../../core'; import { AutoDeleteUnderlyingResourcesProvider } from '../../custom-resource-handlers/dist/aws-synthetics/auto-delete-underlying-resources-provider.generated'; @@ -262,6 +263,39 @@ export interface CanaryProps { * @default - no rules applied to the generated bucket. */ readonly artifactsBucketLifecycleRules?: Array; + + /** + * Canary Artifacts in S3 encryption mode. + * Artifact encryption is only supported for canaries that use Synthetics runtime + * version `syn-nodejs-puppeteer-3.3` or later. + * + * @default - Artifacts are encrypted at rest using an AWS managed key. `ArtifactsEncryptionMode.KMS` is set if you specify `artifactS3KmsKey`. + * + * @see https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/CloudWatch_Synthetics_artifact_encryption.html + */ + readonly artifactS3EncryptionMode?: ArtifactsEncryptionMode; + + /** + * The KMS key used to encrypt canary artifacts. + * + * @default - no kms key if `artifactS3EncryptionMode` is set to `S3_MANAGED`. A key will be created if one is not provided and `artifactS3EncryptionMode` is set to `KMS`. + */ + readonly artifactS3KmsKey?: kms.IKey; +} + +/** + * Encryption mode for canary artifacts. + */ +export enum ArtifactsEncryptionMode { + /** + * Server-side encryption (SSE) with an Amazon S3-managed key. + */ + S3_MANAGED = 'SSE_S3', + + /** + * Server-side encryption (SSE) with an AWS KMS customer managed key. + */ + KMS = 'SSE_KMS', } /** @@ -341,6 +375,7 @@ export class Canary extends cdk.Resource implements ec2.IConnectable { code: this.createCode(props), runConfig: this.createRunConfig(props), vpcConfig: this.createVpcConfig(props), + artifactConfig: this.createArtifactConfig(props), }); this._resource = resource; @@ -633,6 +668,43 @@ export class Canary extends cdk.Resource implements ec2.IConnectable { }; } + private createArtifactConfig(props: CanaryProps): CfnCanary.ArtifactConfigProperty | undefined { + if (!props.artifactS3EncryptionMode && !props.artifactS3KmsKey) { + return undefined; + } + + const isNodeRuntime = props.runtime.family === RuntimeFamily.NODEJS; + + if ( + props.artifactS3EncryptionMode === ArtifactsEncryptionMode.S3_MANAGED && + props.artifactS3KmsKey + ) { + throw new Error(`A customer-managed KMS key was provided, but the encryption mode is not set to SSE-KMS, got: ${props.artifactS3EncryptionMode}.`); + } + + // Only check runtime family is Node.js because versions prior to `syn-nodejs-puppeteer-3.3` are deprecated and can no longer be configured. + if (!isNodeRuntime && props.artifactS3EncryptionMode) { + throw new Error(`Artifact encryption is only supported for canaries that use Synthetics runtime version \`syn-nodejs-puppeteer-3.3\` or later, got \`${props.runtime.name}\`.`); + } + + const encryptionMode = props.artifactS3EncryptionMode ? props.artifactS3EncryptionMode : + props.artifactS3KmsKey ? ArtifactsEncryptionMode.KMS : undefined; + + let encryptionKey: kms.IKey | undefined; + if (encryptionMode === ArtifactsEncryptionMode.KMS) { + encryptionKey = props.artifactS3KmsKey ?? new kms.Key(this, 'Key', { description: `Created by ${this.node.path}` }); + } + + encryptionKey?.grantEncryptDecrypt(this.role); + + return { + s3Encryption: { + encryptionMode, + kmsKeyArn: encryptionKey?.keyArn, + }, + }; + } + /** * Creates a unique name for the canary. The generated name is the physical ID of the canary. */ diff --git a/packages/aws-cdk-lib/aws-synthetics/test/canary.test.ts b/packages/aws-cdk-lib/aws-synthetics/test/canary.test.ts index f11f08ebe02bd..286997bd98de9 100644 --- a/packages/aws-cdk-lib/aws-synthetics/test/canary.test.ts +++ b/packages/aws-cdk-lib/aws-synthetics/test/canary.test.ts @@ -2,6 +2,7 @@ import * as path from 'path'; import { Match, Template } from '../../assertions'; import * as ec2 from '../../aws-ec2'; import * as iam from '../../aws-iam'; +import * as kms from '../../aws-kms'; import * as s3 from '../../aws-s3'; import { Duration, Lazy, Size, Stack } from '../../core'; import * as synthetics from '../lib'; @@ -933,3 +934,158 @@ describe('handler validation', () => { }).toThrow(/Canary Handler length must be between 1 and 128/); }); }); + +describe('artifact encryption test', () => { + test('SSE_S3 without a key', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline(` + exports.handler = async () => { + console.log(\'hello world\'); + };`), + }), + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + artifactS3EncryptionMode: synthetics.ArtifactsEncryptionMode.S3_MANAGED, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + ArtifactConfig: { + S3Encryption: { + EncryptionMode: 'SSE_S3', + KmsKeyArn: Match.absent(), + }, + }, + }); + }); + + test('auto-creates KMS key if encryption type is SSE_KMS but no key is provided', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + const canary = new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline(` + exports.handler = async () => { + console.log(\'hello world\'); + };`), + }), + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + artifactS3EncryptionMode: synthetics.ArtifactsEncryptionMode.KMS, + }); + + Template.fromStack(stack).hasResourceProperties('AWS::KMS::Key', { + Description: 'Created by Default/Canary', + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + ArtifactConfig: { + S3Encryption: { + EncryptionMode: 'SSE_KMS', + KmsKeyArn: { + 'Fn::GetAtt': [ + 'CanaryKey36A631B4', + 'Arn', + ], + }, + }, + }, + }); + }); + + test('SSE_KMS with a key', () => { + // GIVEN + const stack = new Stack(); + const key = new kms.Key(stack, 'myKey'); + + // WHEN + new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline(` + exports.handler = async () => { + console.log(\'hello world\'); + };`), + }), + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + artifactS3EncryptionMode: synthetics.ArtifactsEncryptionMode.KMS, + artifactS3KmsKey: key, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + ArtifactConfig: { + S3Encryption: { + EncryptionMode: 'SSE_KMS', + KmsKeyArn: stack.resolve(key.keyArn), + }, + }, + }); + }); + + test('No artifactS3EncryptionMode setting with a key is set to SSE_KMS', () => { + // GIVEN + const stack = new Stack(); + const key = new kms.Key(stack, 'myKey'); + + // WHEN + new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('/* Synthetics handler code */'), + }), + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + artifactS3KmsKey: key, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::Synthetics::Canary', { + ArtifactConfig: { + S3Encryption: { + EncryptionMode: 'SSE_KMS', + KmsKeyArn: stack.resolve(key.keyArn), + }, + }, + }); + }); + + test('SSE-S3 with a key throws', () => { + const stack = new Stack(); + const key = new kms.Key(stack, 'myKey'); + + expect(() => { + new synthetics.Canary(stack, 'Canary', { + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('/* Synthetics handler code */'), + }), + runtime: synthetics.Runtime.SYNTHETICS_NODEJS_PUPPETEER_7_0, + artifactS3EncryptionMode: synthetics.ArtifactsEncryptionMode.S3_MANAGED, + artifactS3KmsKey: key, + }); + }).toThrow('A customer-managed KMS key was provided, but the encryption mode is not set to SSE-KMS, got: SSE_S3.'); + }); + + test('Artifact encryption for non-Node.js runtime throws an error', () => { + const stack = new Stack(); + + expect(() => { + new synthetics.Canary(stack, 'Canary', { + runtime: synthetics.Runtime.SYNTHETICS_PYTHON_SELENIUM_3_0, + test: synthetics.Test.custom({ + handler: 'index.handler', + code: synthetics.Code.fromInline('# Synthetics handler code'), + }), + artifactS3EncryptionMode: synthetics.ArtifactsEncryptionMode.S3_MANAGED, + }); + }).toThrow('Artifact encryption is only supported for canaries that use Synthetics runtime version `syn-nodejs-puppeteer-3.3` or later, got `syn-python-selenium-3.0`.'); + }); +}); From dd61d124199ad6fd0fa0bf4d58f144f4694e05f7 Mon Sep 17 00:00:00 2001 From: Grace Luo <54298030+gracelu0@users.noreply.github.com> Date: Wed, 6 Nov 2024 12:29:40 -0800 Subject: [PATCH 20/24] chore(scheduler-alpha): increase unit test coverage (#32033) ### Issue # (if applicable) tracking issue: #31785 ### Reason for this change Satisfy 90% unit test coverage required for developer preview (see https://github.com/cdklabs/team-internal/blob/main/docs/construct-library-lifecycle.md#exit-criteria-1) ### Description of changes Added parameterized unit tests for metric methods Previous coverage summary: ``` =============================== Coverage summary =============================== Statements : 83.06% ( 103/124 ) Branches : 76.19% ( 32/42 ) Functions : 70.9% ( 39/55 ) Lines : 83.47% ( 101/121 ) ================================================================================ ``` Current coverage summary: ``` =============================== Coverage summary =============================== Statements : 95.16% ( 118/124 ) Branches : 83.33% ( 35/42 ) Functions : 96.36% ( 53/55 ) Lines : 95.86% ( 116/121 ) ================================================================================ ``` ### Description of how you validated changes Unit tests pass ### 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/aws-scheduler-alpha/lib/group.ts | 2 +- .../aws-scheduler-alpha/test/group.test.ts | 55 +++++++++++++++++-- .../test/schedule-expression.test.ts | 17 ++++-- .../aws-scheduler-alpha/test/schedule.test.ts | 27 ++++++++- 4 files changed, 87 insertions(+), 14 deletions(-) diff --git a/packages/@aws-cdk/aws-scheduler-alpha/lib/group.ts b/packages/@aws-cdk/aws-scheduler-alpha/lib/group.ts index 3bd5f5809ec13..7f737a91eca2e 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/lib/group.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/lib/group.ts @@ -202,7 +202,7 @@ abstract class GroupBase extends Resource implements IGroup { * * @default - sum over 5 minutes */ - metricSentToDLQ(props?: cloudwatch.MetricOptions): cloudwatch.Metric { + public metricSentToDLQ(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('InvocationsSentToDeadLetterCount', props); } diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts index 3f59a10903c4b..e0de97b02ce30 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/group.test.ts @@ -279,20 +279,33 @@ describe('Schedule Group', () => { }, }); }); +}); - test('Target Error Metrics', () => { +describe('Schedule Group Metrics', () => { + test.each([ + ['metricTargetErrors', 'TargetErrorCount'], + ['metricThrottled', 'InvocationThrottleCount'], + ['metricAttempts', 'InvocationAttemptCount'], + ['metricTargetThrottled', 'TargetErrorThrottledCount'], + ['metricDropped', 'InvocationDroppedCount'], + ['metricSentToDLQ', 'InvocationsSentToDeadLetterCount'], + ['metricSentToDLQTruncated', 'InvocationsSentToDeadLetterCount_Truncated_MessageSizeExceeded'], + ])('calling %s creates alarm for %s metric', (metricMethodName, metricName) => { // GIVEN + const app = new App(); const props: GroupProps = { groupName: 'MyGroup', }; + const stack = new Stack(app, 'Stack', { env: { region: 'us-east-1', account: '123456789012' } }); const group = new Group(stack, 'TestGroup', props); // WHEN - const metricTargetErrors = group.metricTargetErrors({ + const metricMethod = (group as any)[metricMethodName].bind(group); // Get the method dynamically + const metricTargetErrors = metricMethod({ period: Duration.minutes(1), }); - new cw.Alarm(stack, 'GroupTargetErrorAlarm', { + new cw.Alarm(stack, `Group${metricName}Alarm`, { metric: metricTargetErrors, evaluationPeriods: 1, threshold: 1, @@ -306,7 +319,41 @@ describe('Schedule Group', () => { Value: 'MyGroup', }), ]), - MetricName: 'TargetErrorCount', + MetricName: metricName, + Namespace: 'AWS/Scheduler', + }); + }); + + test('Invocations Failed to Deliver to DLQ Metrics', () => { + // GIVEN + const app = new App(); + const props: GroupProps = { + groupName: 'MyGroup', + }; + const stack = new Stack(app, 'Stack', { env: { region: 'us-east-1', account: '123456789012' } }); + const group = new Group(stack, 'TestGroup', props); + const errorCode = '403'; + + // WHEN + const metricFailedToBeSentToDLQ = group.metricFailedToBeSentToDLQ(errorCode, { + period: Duration.minutes(1), + }); + + new cw.Alarm(stack, 'GroupFailedInvocationsToDLQAlarm', { + metric: metricFailedToBeSentToDLQ, + evaluationPeriods: 1, + threshold: 1, + }); + + // THEN + Template.fromStack(stack).hasResourceProperties('AWS::CloudWatch::Alarm', { + Dimensions: Match.arrayWith([ + Match.objectLike({ + Name: 'ScheduleGroup', + Value: 'MyGroup', + }), + ]), + MetricName: `InvocationsFailedToBeSentToDeadLetterCount_${errorCode}`, Namespace: 'AWS/Scheduler', }); }); diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-expression.test.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-expression.test.ts index d12ca608820d3..416679e2667d2 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-expression.test.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule-expression.test.ts @@ -118,7 +118,7 @@ describe('schedule expression', () => { }); test('one-time expression with invalid date throws', () => { - expect(() => ScheduleExpression.at(new Date('13-20-1969'))).toThrowError('Invalid date'); + expect(() => ScheduleExpression.at(new Date('13-20-1969'))).toThrow('Invalid date'); }); }); @@ -130,13 +130,13 @@ describe('fractional minutes checks', () => { }); test('rate cannot be a fractional amount of minutes (defined with minutes)', () => { - expect(()=> { - ScheduleExpression.rate(Duration.minutes(5/3)); + expect(() => { + ScheduleExpression.rate(Duration.minutes(5 / 3)); }).toThrow(/must be a whole number of/); }); test('rate cannot be a fractional amount of minutes (defined with hours)', () => { - expect(()=> { + expect(() => { ScheduleExpression.rate(Duration.hours(1.03)); }).toThrow(/cannot be converted into a whole number of/); }); @@ -149,7 +149,7 @@ describe('fractional minutes checks', () => { test('rate cannot be less than 1 minute (defined with minutes as fractions)', () => { expect(() => { - ScheduleExpression.rate(Duration.minutes(1/2)); + ScheduleExpression.rate(Duration.minutes(1 / 2)); }).toThrow(/must be a whole number of/); }); @@ -164,4 +164,9 @@ describe('fractional minutes checks', () => { ScheduleExpression.rate(Duration.minutes(10)) .expressionString); }); -}); \ No newline at end of file + + test('literal schedule expression', () => { + expect('rate(1 hour)').toEqual( + ScheduleExpression.expression('rate(1 hour)').expressionString); + }); +}); diff --git a/packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts index 92b395f6eaf8e..6f21fa7ca3cbf 100644 --- a/packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts +++ b/packages/@aws-cdk/aws-scheduler-alpha/test/schedule.test.ts @@ -64,13 +64,22 @@ describe('Schedule', () => { }); }); - test('returns metric for delivery of failed invocations to DLQ', () => { + test.each([ + ['metricAllThrottled', 'InvocationThrottleCount'], + ['metricAllErrors', 'TargetErrorCount'], + ['metricAllAttempts', 'InvocationAttemptCount'], + ['metricAllTargetThrottled', 'TargetErrorThrottledCount'], + ['metricAllDropped', 'InvocationDroppedCount'], + ['metricAllSentToDLQ', 'InvocationsSentToDeadLetterCount'], + ['metricAllSentToDLQTruncated', 'InvocationsSentToDeadLetterCount_Truncated_MessageSizeExceeded'], + + ])('returns expected metric for %s', (metricMethodName: string, metricName: string) => { // WHEN - const metric = Schedule.metricAllFailedToBeSentToDLQ(); + const metric = (Schedule as any)[metricMethodName](); // THEN expect(metric.namespace).toEqual('AWS/Scheduler'); - expect(metric.metricName).toEqual('InvocationsFailedToBeSentToDeadLetterCount'); + expect(metric.metricName).toEqual(metricName); expect(metric.dimensions).toBeUndefined(); expect(metric.statistic).toEqual('Sum'); expect(metric.period).toEqual(Duration.minutes(5)); @@ -88,6 +97,18 @@ describe('Schedule', () => { expect(metric.period).toEqual(Duration.minutes(5)); }); + test('returns metric for delivery of failed invocations to DLQ with no error code', () => { + // WHEN + const metric = Schedule.metricAllFailedToBeSentToDLQ(); + + // THEN + expect(metric.namespace).toEqual('AWS/Scheduler'); + expect(metric.metricName).toEqual('InvocationsFailedToBeSentToDeadLetterCount'); + expect(metric.dimensions).toBeUndefined(); + expect(metric.statistic).toEqual('Sum'); + expect(metric.period).toEqual(Duration.minutes(5)); + }); + test('returns metric for all errors with provided statistic and period', () => { // WHEN const metric = Schedule.metricAllErrors({ From 18c19fd49f2b83fee3d1cdb7de8b53ea310729b4 Mon Sep 17 00:00:00 2001 From: Rico Hermans Date: Wed, 6 Nov 2024 22:45:01 +0100 Subject: [PATCH 21/24] fix: deploy-time stack tags cause synthesis to fail (#32041) In https://github.com/aws/aws-cdk/pull/31457, we introduced a change that made synthesis fail if one of the stack tags was a deploy-time value. Since stack tags are assigned outside a CloudFormation context, deploy-time values cannot be evaluated, so the stack ends up with a tag like `{ Key: "my-tag", Value: "${Token[1234]}" }`, which is probably not what is intended. Worse, those tags are automatically propagated to all resources in the stack by CloudFormation, and some may validate the tag value and find that `$` or any of the other characters are not valid tag values. The intent was that customers would be alerted to these kinds of mistakes and apply their tags to resources, or skip stacks when applying tags to large scopes: ```ts Tags.of(this).add('my-tag', Fn.importValue('SomeExport'), { excludeResourceTypes: ['aws:cdk:stack'], }); ``` The previous change was a bit drastic in its attempts. In this one we ignore the unresolved tags and add a warning instead. That way, synthesis still succeeds. Closes #32040. ---- *By submitting this pull request, I confirm that my contribution is made under the terms of the Apache-2.0 license* --- .../core/lib/stack-synthesizers/_shared.ts | 21 ++++++++++++------- packages/aws-cdk-lib/core/test/stack.test.ts | 11 ++++++++-- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts index 46cb62b27c218..686968298475c 100644 --- a/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts +++ b/packages/aws-cdk-lib/core/lib/stack-synthesizers/_shared.ts @@ -26,15 +26,22 @@ export function addStackArtifactToAssembly( // nested stack tags are applied at the AWS::CloudFormation::Stack resource // level and are not needed in the cloud assembly. if (Object.entries(stackTags).length > 0) { - for (const [k, v] of Object.entries(stackTags)) { - if (Token.isUnresolved(k) || Token.isUnresolved(v)) { - throw new Error(`Stack tags may not contain deploy-time values (tag: ${k}=${v}). Apply tags containing deploy-time values to resources only, avoid tagging stacks.`); - } + const resolvedTags = Object.entries(stackTags).filter(([k, v]) => !(Token.isUnresolved(k) || Token.isUnresolved(v))); + const unresolvedTags = Object.entries(stackTags).filter(([k, v]) => Token.isUnresolved(k) || Token.isUnresolved(v)); + + if (unresolvedTags.length > 0) { + const rendered = unresolvedTags.map(([k, v]) => `${Token.isUnresolved(k) ? '': k}=${Token.isUnresolved(v) ? '' : v}`).join(', '); + stack.node.addMetadata( + cxschema.ArtifactMetadataEntryType.WARN, + `Ignoring stack tags that contain deploy-time values (found: ${rendered}). Apply tags containing deploy-time values to resources only, avoid tagging stacks (for example using { excludeResourceTypes: ['aws:cdk:stack'] }).`, + ); } - stack.node.addMetadata( - cxschema.ArtifactMetadataEntryType.STACK_TAGS, - Object.entries(stackTags).map(([key, value]) => ({ Key: key, Value: value }))); + if (resolvedTags.length > 0) { + stack.node.addMetadata( + cxschema.ArtifactMetadataEntryType.STACK_TAGS, + resolvedTags.map(([key, value]) => ({ Key: key, Value: value }))); + } } const deps = [ diff --git a/packages/aws-cdk-lib/core/test/stack.test.ts b/packages/aws-cdk-lib/core/test/stack.test.ts index 0f67d1ad6ac7b..399e355063dfb 100644 --- a/packages/aws-cdk-lib/core/test/stack.test.ts +++ b/packages/aws-cdk-lib/core/test/stack.test.ts @@ -2075,7 +2075,7 @@ describe('stack', () => { expect(asm.getStackArtifact(stack2.artifactId).tags).toEqual(expected); }); - test('stack tags may not contain tokens', () => { + test('warning when stack tags contain tokens', () => { // GIVEN const app = new App({ stackTraces: false, @@ -2087,7 +2087,14 @@ describe('stack', () => { }, }); - expect(() => app.synth()).toThrow(/Stack tags may not contain deploy-time values/); + const asm = app.synth(); + const stackArtifact = asm.stacks[0]; + expect(stackArtifact.manifest.metadata?.['/stack1']).toEqual([ + { + type: 'aws:cdk:warning', + data: expect.stringContaining('Ignoring stack tags that contain deploy-time values'), + }, + ]); }); test('stack notification arns are reflected in the stack artifact properties', () => { From 33985ebf408249e1c2f515047ab99d4932e27a87 Mon Sep 17 00:00:00 2001 From: AWS CDK Team Date: Wed, 6 Nov 2024 21:58:37 +0000 Subject: [PATCH 22/24] chore(release): 2.166.0 --- CHANGELOG.v2.alpha.md | 16 ++++++++++++++++ CHANGELOG.v2.md | 21 +++++++++++++++++++++ version.v2.json | 4 ++-- 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.v2.alpha.md b/CHANGELOG.v2.alpha.md index 05f256ef79507..fa0a5523c2095 100644 --- a/CHANGELOG.v2.alpha.md +++ b/CHANGELOG.v2.alpha.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.166.0-alpha.0](https://github.com/aws/aws-cdk/compare/v2.165.0-alpha.0...v2.166.0-alpha.0) (2024-11-06) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **scheduler-targets-alpha:** Schedule Target will reuse role if target is re-used across schedules. This change triggered replacement of existing roles for Schedule as logical ID of the roles are changed. + +### Features + +* **glue-alpha:** add job run queuing to Glue job ([#31830](https://github.com/aws/aws-cdk/issues/31830)) ([5fca268](https://github.com/aws/aws-cdk/commit/5fca268e455c1ae7c424a4dec01c0c08bec3c16c)), closes [#31826](https://github.com/aws/aws-cdk/issues/31826) + + +### Bug Fixes + +* **scheduler-targets-alpha:** create a role per target instead of singleton schedule target role ([#31895](https://github.com/aws/aws-cdk/issues/31895)) ([aee1b30](https://github.com/aws/aws-cdk/commit/aee1b30adabebe1712720d0d7d27ed4704ac9719)), closes [#31785](https://github.com/aws/aws-cdk/issues/31785) + ## [2.165.0-alpha.0](https://github.com/aws/aws-cdk/compare/v2.164.1-alpha.0...v2.165.0-alpha.0) (2024-10-31) diff --git a/CHANGELOG.v2.md b/CHANGELOG.v2.md index 33e340ebc90e5..e3c1dd8885c5f 100644 --- a/CHANGELOG.v2.md +++ b/CHANGELOG.v2.md @@ -2,6 +2,27 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [2.166.0](https://github.com/aws/aws-cdk/compare/v2.165.0...v2.166.0) (2024-11-06) + + +### Features + +* **cli:** automatically roll back stacks if necessary ([#31920](https://github.com/aws/aws-cdk/issues/31920)) ([2f9fb1e](https://github.com/aws/aws-cdk/commit/2f9fb1e050331efbbe84bb0d5943ff7798cbf3fe)), closes [#30546](https://github.com/aws/aws-cdk/issues/30546) +* **kinesis:** support resource policy for a data stream ([#31909](https://github.com/aws/aws-cdk/issues/31909)) ([18fbd6d](https://github.com/aws/aws-cdk/commit/18fbd6d5a1a3069b0fc1356d87e534a75239e668)), closes [#28814](https://github.com/aws/aws-cdk/issues/28814) +* **rds:** configure `autoMinorVersionUpgrade` for a database cluster ([#31962](https://github.com/aws/aws-cdk/issues/31962)) ([0fb6106](https://github.com/aws/aws-cdk/commit/0fb610678495deb150fad1adebde259fc5fc0993)) +* **route53:** support HTTPS, SSHFP, SVCB, and TLSA DNS resource record ([#31955](https://github.com/aws/aws-cdk/issues/31955)) ([afc2b0d](https://github.com/aws/aws-cdk/commit/afc2b0d1d1d620e6350e96cc464428485fca198e)), closes [/docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-route53-recordset.html#cfn-route53](https://github.com/aws//docs.aws.amazon.com/ja_jp/AWSCloudFormation/latest/UserGuide/aws-resource-route53-recordset.html/issues/cfn-route53) +* **synthetics:** add `artifactS3Encryption` property to the Canary Construct. ([#30197](https://github.com/aws/aws-cdk/issues/30197)) ([1f39cb9](https://github.com/aws/aws-cdk/commit/1f39cb9e0770b5c02302b9b36ac874ee6bf53329)), closes [#30190](https://github.com/aws/aws-cdk/issues/30190) +* update L1 CloudFormation resource definitions ([#32007](https://github.com/aws/aws-cdk/issues/32007)) ([be6a964](https://github.com/aws/aws-cdk/commit/be6a964ec17f22a065aa64511f7d3d31341d0ba5)) + + +### Bug Fixes + +* deploy-time stack tags cause synthesis to fail ([#32041](https://github.com/aws/aws-cdk/issues/32041)) ([18c19fd](https://github.com/aws/aws-cdk/commit/18c19fd49f2b83fee3d1cdb7de8b53ea310729b4)), closes [#32040](https://github.com/aws/aws-cdk/issues/32040) +* **aws_route53:** cannot use CfnParameter.valueAsNumber for L2 RecordSet weight ([#31823](https://github.com/aws/aws-cdk/issues/31823)) ([14561ac](https://github.com/aws/aws-cdk/commit/14561aca0ad9c0d5eef29729dbc97fc3ccd4d171)), closes [#31810](https://github.com/aws/aws-cdk/issues/31810) +* **cli:** asset uploads fail if Object Lock is enabled on access bucket ([#31937](https://github.com/aws/aws-cdk/issues/31937)) ([ab1e91d](https://github.com/aws/aws-cdk/commit/ab1e91d11ec2d3abc1edad318b7ea67ff91c9a88)) +* **dynamoDB:** make TableV2 taggable ([#31867](https://github.com/aws/aws-cdk/issues/31867)) ([796c6d1](https://github.com/aws/aws-cdk/commit/796c6d170a91d9f5fa96f3915a6fb6a7f7bddff3)), closes [#30631](https://github.com/aws/aws-cdk/issues/30631) +* **opensearch:** add I4G to list of OpenSearch nodes not requiring EBS volumes ([#31948](https://github.com/aws/aws-cdk/issues/31948)) ([73378f2](https://github.com/aws/aws-cdk/commit/73378f288e8c422228fbd145d9d41efcb3605857)) + ## [2.165.0](https://github.com/aws/aws-cdk/compare/v2.164.1...v2.165.0) (2024-10-31) diff --git a/version.v2.json b/version.v2.json index 7a6a093413901..2d6903e208cbd 100644 --- a/version.v2.json +++ b/version.v2.json @@ -1,4 +1,4 @@ { - "version": "2.165.0", - "alphaVersion": "2.165.0-alpha.0" + "version": "2.166.0", + "alphaVersion": "2.166.0-alpha.0" } \ No newline at end of file From 722b94c470d5859d8ee5de5026248250d9d0f84d Mon Sep 17 00:00:00 2001 From: "Kenta Goto (k.goto)" <24818752+go-to-k@users.noreply.github.com> Date: Thu, 7 Nov 2024 20:00:31 +0900 Subject: [PATCH 23/24] docs(core): fix typo in `Stack` (#32047) found a very very small typo. ### 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/core/lib/stack.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/aws-cdk-lib/core/lib/stack.ts b/packages/aws-cdk-lib/core/lib/stack.ts index a31610fdfa00c..0ccb560636718 100644 --- a/packages/aws-cdk-lib/core/lib/stack.ts +++ b/packages/aws-cdk-lib/core/lib/stack.ts @@ -924,7 +924,7 @@ export class Stack extends Construct implements ITaggable { } /** - * Adds an arbitary key-value pair, with information you want to record about the stack. + * Adds an arbitrary key-value pair, with information you want to record about the stack. * These get translated to the Metadata section of the generated template. * * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/metadata-section-structure.html From 8a3734d25ce36460b6fee583a7e2049b17f79d87 Mon Sep 17 00:00:00 2001 From: Matsuda Date: Fri, 8 Nov 2024 06:05:50 +0900 Subject: [PATCH 24/24] feat(ivs): support recording configuration for channel (#31899) ### Issue # (if applicable) Closes #31780. ### Reason for this change To use recording configuration for IVS channel. ### Description of changes * Add `RecordingConfiguration` Construct. * Add `recordingConfiguration` property to the Channel. ### Description of how you validated changes Add unit tests and integ 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* --- packages/@aws-cdk/aws-ivs-alpha/README.md | 87 +++++ .../@aws-cdk/aws-ivs-alpha/lib/channel.ts | 9 + packages/@aws-cdk/aws-ivs-alpha/lib/index.ts | 4 + .../lib/recording-configuration.ts | 210 +++++++++++ .../lib/rendition-configuration.ts | 55 +++ .../lib/thumbnail-configuration.ts | 82 ++++ packages/@aws-cdk/aws-ivs-alpha/lib/util.ts | 24 ++ .../aws-ivs-alpha/rosetta/default.ts-fixture | 1 + .../index.js | 1 + ...s-recording-configuration-test.assets.json | 32 ++ ...recording-configuration-test.template.json | 357 ++++++++++++++++++ .../cdk.out | 1 + .../integ.json | 12 + ...efaultTestDeployAssert4AB65A05.assets.json | 19 + ...aultTestDeployAssert4AB65A05.template.json | 36 ++ .../manifest.json | 163 ++++++++ .../tree.json | 341 +++++++++++++++++ .../test/integ.ivs-recording-configuration.ts | 37 ++ .../test/recording-configuration.test.ts | 280 ++++++++++++++ 19 files changed, 1751 insertions(+) create mode 100644 packages/@aws-cdk/aws-ivs-alpha/lib/recording-configuration.ts create mode 100644 packages/@aws-cdk/aws-ivs-alpha/lib/rendition-configuration.ts create mode 100644 packages/@aws-cdk/aws-ivs-alpha/lib/thumbnail-configuration.ts create mode 100644 packages/@aws-cdk/aws-ivs-alpha/lib/util.ts create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.assets.json create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.template.json create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/cdk.out create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/integ.json create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets.json create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/manifest.json create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/tree.json create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.ts create mode 100644 packages/@aws-cdk/aws-ivs-alpha/test/recording-configuration.test.ts diff --git a/packages/@aws-cdk/aws-ivs-alpha/README.md b/packages/@aws-cdk/aws-ivs-alpha/README.md index 5af6031301a0b..f8ea242c9990d 100644 --- a/packages/@aws-cdk/aws-ivs-alpha/README.md +++ b/packages/@aws-cdk/aws-ivs-alpha/README.md @@ -115,3 +115,90 @@ const myChannel = new ivs.Channel(this, 'Channel', { authorized: true, // default value is false }); ``` + +## Recording Configurations + +An Amazon IVS Recording Configuration stores settings that specify how a channel's live streams should be recorded. +You can configure video quality, thumbnail generation, and where recordings are stored in Amazon S3. + +For more information about IVS recording, see [IVS Auto-Record to Amazon S3 | Low-Latency Streaming](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html). + +You can create a recording configuration: + +```ts +// create an S3 bucket for storing recordings +const recordingBucket = new s3.Bucket(this, 'RecordingBucket'); + +// create a basic recording configuration +const recordingConfiguration = new ivs.RecordingConfiguration(this, 'RecordingConfiguration', { + bucket: recordingBucket, +}); +``` + +### Renditions of a Recording + +When you stream content to an Amazon IVS channel, auto-record-to-s3 uses the source video to generate multiple renditions. + +For more information, see [Discovering the Renditions of a Recording](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html#r2s3-recording-renditions). + +```ts +declare const recordingBucket: s3.Bucket; + +const recordingConfiguration= new ivs.RecordingConfiguration(this, 'RecordingConfiguration', { + bucket: recordingBucket, + + // set rendition configuration + renditionConfiguration: ivs.RenditionConfiguration.custom([ivs.Resolution.HD, ivs.Resolution.SD]), +}); +``` + +### Thumbnail Generation + +You can enable or disable the recording of thumbnails for a live session and modify the interval at which thumbnails are generated for the live session. + +Thumbnail intervals may range from 1 second to 60 seconds; by default, thumbnail recording is enabled, at an interval of 60 seconds. + +For more information, see [Thumbnails](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html#r2s3-thumbnails). + +```ts +declare const recordingBucket: s3.Bucket; + +const recordingConfiguration = new ivs.RecordingConfiguration(this, 'RecordingConfiguration', { + bucket: recordingBucket, + + // set thumbnail settings + thumbnailConfiguration: ivs.ThumbnailConfiguration.interval(ivs.Resolution.HD, [ivs.Storage.LATEST, ivs.Storage.SEQUENTIAL], Duration.seconds(30)), +}); +``` + +### Merge Fragmented Streams + +The `recordingReconnectWindow` property allows you to specify a window of time (in seconds) during which, if your stream is interrupted and a new stream is started, Amazon IVS tries to record to the same S3 prefix as the previous stream. + +In other words, if a broadcast disconnects and then reconnects within the specified interval, the multiple streams are considered a single broadcast and merged together. + +For more information, see [Merge Fragmented Streams](https://docs.aws.amazon.com/ivs/latest/LowLatencyUserGuide/record-to-s3.html#r2s3-merge-fragmented-streams). + +```ts +declare const recordingBucket: s3.Bucket; + +const recordingConfiguration= new ivs.RecordingConfiguration(this, 'RecordingConfiguration', { + bucket: recordingBucket, + + // set recording reconnect window + recordingReconnectWindow: Duration.seconds(60), +}); +``` + +### Attaching Recording Configuration to a Channel + +To enable recording for a channel, specify the recording configuration when creating the channel: + +```ts +declare const recordingConfiguration: ivs.RecordingConfiguration; + +const channel = new ivs.Channel(this, 'Channel', { + // set recording configuration + recordingConfiguration: recordingConfiguration, +}); +``` diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/channel.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/channel.ts index 07eda0279f5ca..8bb48712a7375 100644 --- a/packages/@aws-cdk/aws-ivs-alpha/lib/channel.ts +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/channel.ts @@ -3,6 +3,7 @@ import { Lazy, Names } from 'aws-cdk-lib/core'; import { Construct } from 'constructs'; import { CfnChannel } from 'aws-cdk-lib/aws-ivs'; import { StreamKey } from './stream-key'; +import { IRecordingConfiguration } from './recording-configuration'; /** * Represents an IVS Channel @@ -153,6 +154,13 @@ export interface ChannelProps { * @default - Preset.HIGHER_BANDWIDTH_DELIVERY if channelType is ADVANCED_SD or ADVANCED_HD, none otherwise */ readonly preset?: Preset; + + /** + * A recording configuration for the channel. + * + * @default - recording is disabled + */ + readonly recordingConfiguration?: IRecordingConfiguration; } /** @@ -223,6 +231,7 @@ export class Channel extends ChannelBase { name: this.physicalName, type: props.type, preset, + recordingConfigurationArn: props.recordingConfiguration?.recordingConfigurationArn, }); this.channelArn = resource.attrArn; diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/index.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/index.ts index c873eb9662c7b..6a5a36b273cd6 100644 --- a/packages/@aws-cdk/aws-ivs-alpha/lib/index.ts +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/index.ts @@ -1,5 +1,9 @@ export * from './channel'; export * from './playback-key-pair'; +export * from './recording-configuration'; +export * from './rendition-configuration'; export * from './stream-key'; +export * from './thumbnail-configuration'; +export * from './util'; // AWS::IVS CloudFormation Resources: diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/recording-configuration.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/recording-configuration.ts new file mode 100644 index 0000000000000..8864e958233b0 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/recording-configuration.ts @@ -0,0 +1,210 @@ +import { CfnRecordingConfiguration } from 'aws-cdk-lib/aws-ivs'; +import { IBucket } from 'aws-cdk-lib/aws-s3'; +import { Duration, Fn, IResource, Resource, Stack, Token } from 'aws-cdk-lib/core'; +import { Construct } from 'constructs'; +import { RenditionConfiguration } from './rendition-configuration'; +import { ThumbnailConfiguration } from './thumbnail-configuration'; + +/** + * Properties of the IVS Recording configuration + */ +export interface RecordingConfigurationProps { + /** + * S3 bucket where recorded videos will be stored. + */ + readonly bucket: IBucket; + + /** + * The name of the Recording configuration. + * The value does not need to be unique. + * + * @default - auto generate + */ + readonly recordingConfigurationName?: string; + + /** + * If a broadcast disconnects and then reconnects within the specified interval, + * the multiple streams will be considered a single broadcast and merged together. + * + * `recordingReconnectWindow` must be between 0 and 300 seconds + * + * @default - 0 seconds (means disabled) + */ + readonly recordingReconnectWindow?: Duration; + + /** + * A rendition configuration describes which renditions should be recorded for a stream. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-renditionconfiguration.html + * + * @default - no rendition configuration + */ + readonly renditionConfiguration?: RenditionConfiguration; + + /** + * A thumbnail configuration enables/disables the recording of thumbnails for a live session and controls the interval at which thumbnails are generated for the live session. + * + * @see https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-ivs-recordingconfiguration-thumbnailconfiguration.html + * + * @default - no thumbnail configuration + */ + readonly thumbnailConfiguration?:ThumbnailConfiguration; +} + +/** + * Represents the IVS Recording configuration. + */ +export interface IRecordingConfiguration extends IResource { + /** + * The ID of the Recording configuration. + * @attribute + */ + readonly recordingConfigurationId: string; + + /** + * The ARN of the Recording configuration. + * @attribute + */ + readonly recordingConfigurationArn: string; +} + +/** + * The IVS Recording configuration + * + * @resource AWS::IVS::RecordingConfiguration + */ +export class RecordingConfiguration extends Resource implements IRecordingConfiguration { + /** + * Imports an IVS Recording Configuration from attributes. + */ + public static fromRecordingConfigurationId(scope: Construct, id: string, + recordingConfigurationId: string): IRecordingConfiguration { + + class Import extends Resource implements IRecordingConfiguration { + public readonly recordingConfigurationId = recordingConfigurationId; + public readonly recordingConfigurationArn = Stack.of(this).formatArn({ + resource: 'recording-configuration', + service: 'ivs', + resourceName: recordingConfigurationId, + }); + } + + return new Import(scope, id); + } + + /** + * Imports an IVS Recording Configuration from its ARN + */ + public static fromArn(scope: Construct, id: string, recordingConfigurationArn: string): IRecordingConfiguration { + const resourceParts = Fn.split('/', recordingConfigurationArn); + + if (!resourceParts || resourceParts.length < 2) { + throw new Error(`Unexpected ARN format: ${recordingConfigurationArn}`); + } + + const recordingConfigurationId = Fn.select(1, resourceParts); + + class Import extends Resource implements IRecordingConfiguration { + public readonly recordingConfigurationId = recordingConfigurationId; + public readonly recordingConfigurationArn = recordingConfigurationArn; + } + + return new Import(scope, id); + } + + /** + * The ID of the Recording configuration. + * @attribute + */ + readonly recordingConfigurationId: string; + + /** + * The ARN of the Recording configuration. + * @attribute + */ + readonly recordingConfigurationArn: string; + + private readonly props: RecordingConfigurationProps; + + public constructor(scope: Construct, id: string, props: RecordingConfigurationProps) { + super(scope, id, { + physicalName: props.recordingConfigurationName, + }); + + this.props = props; + + this.validateRecordingConfigurationName(); + this.validateRecordingReconnectWindowSeconds(); + + const resource = new CfnRecordingConfiguration(this, 'Resource', { + destinationConfiguration: { + s3: { + bucketName: this.props.bucket.bucketName, + }, + }, + name: this.props.recordingConfigurationName, + recordingReconnectWindowSeconds: this.props.recordingReconnectWindow?.toSeconds(), + renditionConfiguration: this._renderRenditionConfiguration(), + thumbnailConfiguration: this._renderThumbnailConfiguration(), + }); + + this.recordingConfigurationId = resource.ref; + this.recordingConfigurationArn = resource.attrArn; + } + + private _renderRenditionConfiguration(): CfnRecordingConfiguration.RenditionConfigurationProperty | undefined { + if (!this.props.renditionConfiguration) { + return; + } + + return { + renditions: this.props.renditionConfiguration.renditions, + renditionSelection: this.props.renditionConfiguration.renditionSelection, + }; + }; + + private _renderThumbnailConfiguration(): CfnRecordingConfiguration.ThumbnailConfigurationProperty | undefined { + if (!this.props.thumbnailConfiguration) { + return; + } + + return { + recordingMode: this.props.thumbnailConfiguration.recordingMode, + resolution: this.props.thumbnailConfiguration.resolution, + storage: this.props.thumbnailConfiguration.storage, + targetIntervalSeconds: this.props.thumbnailConfiguration.targetInterval?.toSeconds(), + }; + }; + + private validateRecordingConfigurationName(): undefined { + const recordingConfigurationName = this.props.recordingConfigurationName; + + if (recordingConfigurationName == undefined || Token.isUnresolved(recordingConfigurationName)) { + return; + } + + if (!/^[a-zA-Z0-9-_]*$/.test(recordingConfigurationName)) { + throw new Error(`\`recordingConfigurationName\` must consist only of alphanumeric characters, hyphens or underbars, got: ${recordingConfigurationName}.`); + } + + if (recordingConfigurationName.length > 128) { + throw new Error(`\`recordingConfigurationName\` must be less than or equal to 128 characters, got: ${recordingConfigurationName.length} characters.`); + } + }; + + private validateRecordingReconnectWindowSeconds(): undefined { + const recordingReconnectWindow = this.props.recordingReconnectWindow; + + if (recordingReconnectWindow === undefined || Token.isUnresolved(recordingReconnectWindow)) { + return; + } + + if (0 < recordingReconnectWindow.toMilliseconds() && recordingReconnectWindow.toMilliseconds() < Duration.seconds(1).toMilliseconds()) { + throw new Error(`\`recordingReconnectWindow\` must be between 0 and 300 seconds, got ${recordingReconnectWindow.toMilliseconds()} milliseconds.`); + } + + if (recordingReconnectWindow.toSeconds() > 300) { + throw new Error(`\`recordingReconnectWindow\` must be between 0 and 300 seconds, got ${recordingReconnectWindow.toSeconds()} seconds.`); + } + }; +} diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/rendition-configuration.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/rendition-configuration.ts new file mode 100644 index 0000000000000..691fff7dec0bf --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/rendition-configuration.ts @@ -0,0 +1,55 @@ +import { Resolution } from './util'; + +/** + * Rendition selection mode. + */ +export enum RenditionSelection { + /** + * Record all available renditions. + */ + ALL = 'ALL', + + /** + * Does not record any video. This option is useful if you just want to record thumbnails. + */ + NONE = 'NONE', + + /** + * Select a subset of video renditions to record. + */ + CUSTOM = 'CUSTOM', +} + +/** + * Rendition configuration for IVS Recording configuration + */ +export class RenditionConfiguration { + /** + * Record all available renditions. + */ + public static all(): RenditionConfiguration { + return new RenditionConfiguration(RenditionSelection.ALL); + } + + /** + * Does not record any video. + */ + public static none(): RenditionConfiguration { + return new RenditionConfiguration(RenditionSelection.NONE); + } + + /** + * Record a subset of video renditions. + * + * @param renditions A list of which renditions are recorded for a stream. + */ + public static custom(renditions: Resolution[]): RenditionConfiguration { + return new RenditionConfiguration(RenditionSelection.CUSTOM, renditions); + } + + /** + * @param renditionSelection The set of renditions are recorded for a stream. + * @param renditions A list of which renditions are recorded for a stream. If you do not specify this property, no resolution is selected. + */ + private constructor(public readonly renditionSelection: RenditionSelection, public readonly renditions?: Resolution[]) { } +} diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/thumbnail-configuration.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/thumbnail-configuration.ts new file mode 100644 index 0000000000000..ba8d060bdf832 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/thumbnail-configuration.ts @@ -0,0 +1,82 @@ +import { Duration, Token } from 'aws-cdk-lib'; +import { Resolution } from './util'; + +/** + * Thumbnail recording mode. + */ +export enum RecordingMode { + /** + * Use INTERVAL to enable the generation of thumbnails for recorded video at a time interval controlled by the TargetIntervalSeconds property. + */ + INTERVAL = 'INTERVAL', + + /** + * Use DISABLED to disable the generation of thumbnails for recorded video. + */ + DISABLED = 'DISABLED', +} + +/** + * The format in which thumbnails are recorded for a stream. + */ +export enum Storage { + /** + * SEQUENTIAL records all generated thumbnails in a serial manner, to the media/thumbnails directory. + */ + SEQUENTIAL = 'SEQUENTIAL', + + /** + * LATEST saves the latest thumbnail in media/thumbnails/latest/thumb.jpg and overwrites it at the interval specified by thumbnailTargetInterval. + */ + LATEST = 'LATEST', +} + +/** + * Thumbnail configuration for IVS Recording configuration + */ +export class ThumbnailConfiguration { + /** + * Disable the generation of thumbnails for recorded video + */ + public static disable(): ThumbnailConfiguration { + return new ThumbnailConfiguration(RecordingMode.DISABLED); + } + + /** + * Enable the generation of thumbnails for recorded video at a time interval. + * + * @param resolution The desired resolution of recorded thumbnails for a stream. If you do not specify this property, same resolution as Input stream is used. + * @param storage The format in which thumbnails are recorded for a stream. If you do not specify this property, `ThumbnailStorage.SEQUENTIAL` is set. + * @param targetInterval The targeted thumbnail-generation interval. If you do not specify this property, `Duration.seconds(60)` is set. + */ + public static interval(resolution?: Resolution, storage?: Storage[], targetInterval?: Duration): ThumbnailConfiguration { + return new ThumbnailConfiguration(RecordingMode.INTERVAL, resolution, storage, targetInterval); + } + + /** + * @param recordingMode Thumbnail recording mode. If you do not specify this property, `ThumbnailRecordingMode.INTERVAL` is set. + * @param resolution The desired resolution of recorded thumbnails for a stream. If you do not specify this property, same resolution as Input stream is used. + * @param storage The format in which thumbnails are recorded for a stream. If you do not specify this property, `ThumbnailStorage.SEQUENTIAL` is set. + * @param targetInterval The targeted thumbnail-generation interval. Must be between 1 and 60 seconds. If you do not specify this property, `Duration.seconds(60)` is set. + */ + private constructor( + public readonly recordingMode?: RecordingMode, + public readonly resolution?: Resolution, + public readonly storage?: Storage[], + public readonly targetInterval?: Duration, + ) { + + if (targetInterval === undefined || Token.isUnresolved(targetInterval)) { + return; + } + + if (targetInterval.toMilliseconds() < Duration.seconds(1).toMilliseconds()) { + throw new Error(`\`targetInterval\` must be between 1 and 60 seconds, got ${targetInterval.toMilliseconds()} milliseconds.`); + } + + if (targetInterval.toSeconds() > 60) { + throw new Error(`\`targetInterval\` must be between 1 and 60 seconds, got ${targetInterval.toSeconds()} seconds.`); + } + } +} + diff --git a/packages/@aws-cdk/aws-ivs-alpha/lib/util.ts b/packages/@aws-cdk/aws-ivs-alpha/lib/util.ts new file mode 100644 index 0000000000000..dd6f65b5a4bfc --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/lib/util.ts @@ -0,0 +1,24 @@ +/** + * Resolution for rendition + */ +export enum Resolution { + /** + * Full HD (1080p) + */ + FULL_HD = 'FULL_HD', + + /** + * HD (720p) + */ + HD = 'HD', + + /** + * SD (480p) + */ + SD = 'SD', + + /** + * Lowest resolution + */ + LOWEST_RESOLUTION = 'LOWEST_RESOLUTION', +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/rosetta/default.ts-fixture b/packages/@aws-cdk/aws-ivs-alpha/rosetta/default.ts-fixture index 5cde59056e614..b4e8d3356cfdc 100644 --- a/packages/@aws-cdk/aws-ivs-alpha/rosetta/default.ts-fixture +++ b/packages/@aws-cdk/aws-ivs-alpha/rosetta/default.ts-fixture @@ -2,6 +2,7 @@ import { Duration, Stack } from 'aws-cdk-lib'; import { Construct } from 'constructs'; import * as ivs from '@aws-cdk/aws-ivs-alpha'; +import * as s3 from 'aws-cdk-lib/aws-s3'; class Fixture extends Stack { constructor(scope: Construct, id: string) { diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js new file mode 100644 index 0000000000000..1002ba018e9fb --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61/index.js @@ -0,0 +1 @@ +"use strict";var f=Object.create;var i=Object.defineProperty;var I=Object.getOwnPropertyDescriptor;var C=Object.getOwnPropertyNames;var w=Object.getPrototypeOf,P=Object.prototype.hasOwnProperty;var A=(t,e)=>{for(var o in e)i(t,o,{get:e[o],enumerable:!0})},d=(t,e,o,r)=>{if(e&&typeof e=="object"||typeof e=="function")for(let s of C(e))!P.call(t,s)&&s!==o&&i(t,s,{get:()=>e[s],enumerable:!(r=I(e,s))||r.enumerable});return t};var l=(t,e,o)=>(o=t!=null?f(w(t)):{},d(e||!t||!t.__esModule?i(o,"default",{value:t,enumerable:!0}):o,t)),B=t=>d(i({},"__esModule",{value:!0}),t);var q={};A(q,{autoDeleteHandler:()=>S,handler:()=>H});module.exports=B(q);var h=require("@aws-sdk/client-s3");var y=l(require("https")),m=l(require("url")),a={sendHttpRequest:D,log:T,includeStackTraces:!0,userHandlerIndex:"./index"},p="AWSCDK::CustomResourceProviderFramework::CREATE_FAILED",L="AWSCDK::CustomResourceProviderFramework::MISSING_PHYSICAL_ID";function R(t){return async(e,o)=>{let r={...e,ResponseURL:"..."};if(a.log(JSON.stringify(r,void 0,2)),e.RequestType==="Delete"&&e.PhysicalResourceId===p){a.log("ignoring DELETE event caused by a failed CREATE event"),await u("SUCCESS",e);return}try{let s=await t(r,o),n=k(e,s);await u("SUCCESS",n)}catch(s){let n={...e,Reason:a.includeStackTraces?s.stack:s.message};n.PhysicalResourceId||(e.RequestType==="Create"?(a.log("CREATE failed, responding with a marker physical resource id so that the subsequent DELETE will be ignored"),n.PhysicalResourceId=p):a.log(`ERROR: Malformed event. "PhysicalResourceId" is required: ${JSON.stringify(e)}`)),await u("FAILED",n)}}}function k(t,e={}){let o=e.PhysicalResourceId??t.PhysicalResourceId??t.RequestId;if(t.RequestType==="Delete"&&o!==t.PhysicalResourceId)throw new Error(`DELETE: cannot change the physical resource ID from "${t.PhysicalResourceId}" to "${e.PhysicalResourceId}" during deletion`);return{...t,...e,PhysicalResourceId:o}}async function u(t,e){let o={Status:t,Reason:e.Reason??t,StackId:e.StackId,RequestId:e.RequestId,PhysicalResourceId:e.PhysicalResourceId||L,LogicalResourceId:e.LogicalResourceId,NoEcho:e.NoEcho,Data:e.Data},r=m.parse(e.ResponseURL),s=`${r.protocol}//${r.hostname}/${r.pathname}?***`;a.log("submit response to cloudformation",s,o);let n=JSON.stringify(o),E={hostname:r.hostname,path:r.path,method:"PUT",headers:{"content-type":"","content-length":Buffer.byteLength(n,"utf8")}};await O({attempts:5,sleep:1e3},a.sendHttpRequest)(E,n)}async function D(t,e){return new Promise((o,r)=>{try{let s=y.request(t,n=>{n.resume(),!n.statusCode||n.statusCode>=400?r(new Error(`Unsuccessful HTTP response: ${n.statusCode}`)):o()});s.on("error",r),s.write(e),s.end()}catch(s){r(s)}})}function T(t,...e){console.log(t,...e)}function O(t,e){return async(...o)=>{let r=t.attempts,s=t.sleep;for(;;)try{return await e(...o)}catch(n){if(r--<=0)throw n;await b(Math.floor(Math.random()*s)),s*=2}}}async function b(t){return new Promise(e=>setTimeout(e,t))}var g="aws-cdk:auto-delete-objects",x=JSON.stringify({Version:"2012-10-17",Statement:[]}),c=new h.S3({}),H=R(S);async function S(t){switch(t.RequestType){case"Create":return;case"Update":return{PhysicalResourceId:(await F(t)).PhysicalResourceId};case"Delete":return N(t.ResourceProperties?.BucketName)}}async function F(t){let e=t,o=e.OldResourceProperties?.BucketName;return{PhysicalResourceId:e.ResourceProperties?.BucketName??o}}async function _(t){try{let e=(await c.getBucketPolicy({Bucket:t}))?.Policy??x,o=JSON.parse(e);o.Statement.push({Principal:"*",Effect:"Deny",Action:["s3:PutObject"],Resource:[`arn:aws:s3:::${t}/*`]}),await c.putBucketPolicy({Bucket:t,Policy:JSON.stringify(o)})}catch(e){if(e.name==="NoSuchBucket")throw e;console.log(`Could not set new object deny policy on bucket '${t}' prior to deletion.`)}}async function U(t){let e;do{e=await c.listObjectVersions({Bucket:t});let o=[...e.Versions??[],...e.DeleteMarkers??[]];if(o.length===0)return;let r=o.map(s=>({Key:s.Key,VersionId:s.VersionId}));await c.deleteObjects({Bucket:t,Delete:{Objects:r}})}while(e?.IsTruncated)}async function N(t){if(!t)throw new Error("No BucketName was provided.");try{if(!await W(t)){console.log(`Bucket does not have '${g}' tag, skipping cleaning.`);return}await _(t),await U(t)}catch(e){if(e.name==="NoSuchBucket"){console.log(`Bucket '${t}' does not exist.`);return}throw e}}async function W(t){return(await c.getBucketTagging({Bucket:t})).TagSet?.some(o=>o.Key===g&&o.Value==="true")}0&&(module.exports={autoDeleteHandler,handler}); diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.assets.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.assets.json new file mode 100644 index 0000000000000..414a84517ac66 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.assets.json @@ -0,0 +1,32 @@ +{ + "version": "38.0.1", + "files": { + "44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61": { + "source": { + "path": "asset.44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61", + "packaging": "zip" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61.zip", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + }, + "2651be7888f4c0925451bcaab35c5758d4e6cf8b94f1952c1b31f7d99057da7a": { + "source": { + "path": "aws-cdk-ivs-recording-configuration-test.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "2651be7888f4c0925451bcaab35c5758d4e6cf8b94f1952c1b31f7d99057da7a.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.template.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.template.json new file mode 100644 index 0000000000000..a67c3b3a34e6b --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/aws-cdk-ivs-recording-configuration-test.template.json @@ -0,0 +1,357 @@ +{ + "Resources": { + "Bucket83908E77": { + "Type": "AWS::S3::Bucket", + "Properties": { + "Tags": [ + { + "Key": "aws-cdk:auto-delete-objects", + "Value": "true" + } + ] + }, + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "BucketPolicyE9A3008A": { + "Type": "AWS::S3::BucketPolicy", + "Properties": { + "Bucket": { + "Ref": "Bucket83908E77" + }, + "PolicyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*", + "s3:PutBucketPolicy" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "BucketAutoDeleteObjectsCustomResourceBAFD23C2": { + "Type": "Custom::S3AutoDeleteObjects", + "Properties": { + "ServiceToken": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F", + "Arn" + ] + }, + "BucketName": { + "Ref": "Bucket83908E77" + } + }, + "DependsOn": [ + "BucketPolicyE9A3008A" + ], + "UpdateReplacePolicy": "Delete", + "DeletionPolicy": "Delete" + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Version": "2012-10-17", + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "lambda.amazonaws.com" + } + } + ] + }, + "ManagedPolicyArns": [ + { + "Fn::Sub": "arn:${AWS::Partition}:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole" + } + ] + } + }, + "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F": { + "Type": "AWS::Lambda::Function", + "Properties": { + "Code": { + "S3Bucket": { + "Fn::Sub": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}" + }, + "S3Key": "44e9c4d7a5d3fd2d677e1a7e416b2b56f6b0104bd5eff9cac5557b4c65a9dc61.zip" + }, + "Timeout": 900, + "MemorySize": 128, + "Handler": "index.handler", + "Role": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + }, + "Runtime": { + "Fn::FindInMap": [ + "LatestNodeRuntimeMap", + { + "Ref": "AWS::Region" + }, + "value" + ] + }, + "Description": { + "Fn::Join": [ + "", + [ + "Lambda function for auto-deleting objects in ", + { + "Ref": "Bucket83908E77" + }, + " S3 bucket." + ] + ] + } + }, + "DependsOn": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + ] + }, + "RecordingConfigurationA528CBBF": { + "Type": "AWS::IVS::RecordingConfiguration", + "Properties": { + "DestinationConfiguration": { + "S3": { + "BucketName": { + "Ref": "Bucket83908E77" + } + } + }, + "Name": "my-recording-configuration", + "RecordingReconnectWindowSeconds": 10, + "RenditionConfiguration": { + "RenditionSelection": "CUSTOM", + "Renditions": [ + "FULL_HD", + "HD", + "SD", + "LOWEST_RESOLUTION" + ] + }, + "ThumbnailConfiguration": { + "RecordingMode": "INTERVAL", + "Resolution": "FULL_HD", + "Storage": [ + "LATEST", + "SEQUENTIAL" + ], + "TargetIntervalSeconds": 30 + } + } + }, + "Channel4048F119": { + "Type": "AWS::IVS::Channel", + "Properties": { + "Name": "aws-cdk-ivs-recording-configuration-testChannelE0AF024A", + "RecordingConfigurationArn": { + "Fn::GetAtt": [ + "RecordingConfigurationA528CBBF", + "Arn" + ] + }, + "Type": "ADVANCED_SD" + } + } + }, + "Mappings": { + "LatestNodeRuntimeMap": { + "af-south-1": { + "value": "nodejs20.x" + }, + "ap-east-1": { + "value": "nodejs20.x" + }, + "ap-northeast-1": { + "value": "nodejs20.x" + }, + "ap-northeast-2": { + "value": "nodejs20.x" + }, + "ap-northeast-3": { + "value": "nodejs20.x" + }, + "ap-south-1": { + "value": "nodejs20.x" + }, + "ap-south-2": { + "value": "nodejs20.x" + }, + "ap-southeast-1": { + "value": "nodejs20.x" + }, + "ap-southeast-2": { + "value": "nodejs20.x" + }, + "ap-southeast-3": { + "value": "nodejs20.x" + }, + "ap-southeast-4": { + "value": "nodejs20.x" + }, + "ap-southeast-5": { + "value": "nodejs20.x" + }, + "ap-southeast-7": { + "value": "nodejs20.x" + }, + "ca-central-1": { + "value": "nodejs20.x" + }, + "ca-west-1": { + "value": "nodejs20.x" + }, + "cn-north-1": { + "value": "nodejs18.x" + }, + "cn-northwest-1": { + "value": "nodejs18.x" + }, + "eu-central-1": { + "value": "nodejs20.x" + }, + "eu-central-2": { + "value": "nodejs20.x" + }, + "eu-isoe-west-1": { + "value": "nodejs18.x" + }, + "eu-north-1": { + "value": "nodejs20.x" + }, + "eu-south-1": { + "value": "nodejs20.x" + }, + "eu-south-2": { + "value": "nodejs20.x" + }, + "eu-west-1": { + "value": "nodejs20.x" + }, + "eu-west-2": { + "value": "nodejs20.x" + }, + "eu-west-3": { + "value": "nodejs20.x" + }, + "il-central-1": { + "value": "nodejs20.x" + }, + "me-central-1": { + "value": "nodejs20.x" + }, + "me-south-1": { + "value": "nodejs20.x" + }, + "mx-central-1": { + "value": "nodejs20.x" + }, + "sa-east-1": { + "value": "nodejs20.x" + }, + "us-east-1": { + "value": "nodejs20.x" + }, + "us-east-2": { + "value": "nodejs20.x" + }, + "us-gov-east-1": { + "value": "nodejs18.x" + }, + "us-gov-west-1": { + "value": "nodejs18.x" + }, + "us-iso-east-1": { + "value": "nodejs18.x" + }, + "us-iso-west-1": { + "value": "nodejs18.x" + }, + "us-isob-east-1": { + "value": "nodejs18.x" + }, + "us-west-1": { + "value": "nodejs20.x" + }, + "us-west-2": { + "value": "nodejs20.x" + } + } + }, + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/cdk.out b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/cdk.out new file mode 100644 index 0000000000000..c6e612584e352 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/cdk.out @@ -0,0 +1 @@ +{"version":"38.0.1"} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/integ.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/integ.json new file mode 100644 index 0000000000000..6c23c9761b918 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/integ.json @@ -0,0 +1,12 @@ +{ + "version": "38.0.1", + "testCases": { + "ivs-recording-configuration-test/DefaultTest": { + "stacks": [ + "aws-cdk-ivs-recording-configuration-test" + ], + "assertionStack": "ivs-recording-configuration-test/DefaultTest/DeployAssert", + "assertionStackName": "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets.json new file mode 100644 index 0000000000000..09caab92a2fdf --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets.json @@ -0,0 +1,19 @@ +{ + "version": "38.0.1", + "files": { + "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22": { + "source": { + "path": "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json", + "packaging": "file" + }, + "destinations": { + "current_account-current_region": { + "bucketName": "cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}", + "objectKey": "21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-file-publishing-role-${AWS::AccountId}-${AWS::Region}" + } + } + } + }, + "dockerImages": {} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json new file mode 100644 index 0000000000000..ad9d0fb73d1dd --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json @@ -0,0 +1,36 @@ +{ + "Parameters": { + "BootstrapVersion": { + "Type": "AWS::SSM::Parameter::Value", + "Default": "/cdk-bootstrap/hnb659fds/version", + "Description": "Version of the CDK Bootstrap resources in this environment, automatically retrieved from SSM Parameter Store. [cdk:skip]" + } + }, + "Rules": { + "CheckBootstrapVersion": { + "Assertions": [ + { + "Assert": { + "Fn::Not": [ + { + "Fn::Contains": [ + [ + "1", + "2", + "3", + "4", + "5" + ], + { + "Ref": "BootstrapVersion" + } + ] + } + ] + }, + "AssertDescription": "CDK bootstrap stack version 6 required. Please run 'cdk bootstrap' with a recent version of the CDK CLI." + } + ] + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/manifest.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/manifest.json new file mode 100644 index 0000000000000..e206699737663 --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/manifest.json @@ -0,0 +1,163 @@ +{ + "version": "38.0.1", + "artifacts": { + "aws-cdk-ivs-recording-configuration-test.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "aws-cdk-ivs-recording-configuration-test.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "aws-cdk-ivs-recording-configuration-test": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "aws-cdk-ivs-recording-configuration-test.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "notificationArns": [], + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/2651be7888f4c0925451bcaab35c5758d4e6cf8b94f1952c1b31f7d99057da7a.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "aws-cdk-ivs-recording-configuration-test.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "aws-cdk-ivs-recording-configuration-test.assets" + ], + "metadata": { + "/aws-cdk-ivs-recording-configuration-test/Bucket/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Bucket83908E77" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Bucket/Policy/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketPolicyE9A3008A" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Bucket/AutoDeleteObjectsCustomResource/Default": [ + { + "type": "aws:cdk:logicalId", + "data": "BucketAutoDeleteObjectsCustomResourceBAFD23C2" + } + ], + "/aws-cdk-ivs-recording-configuration-test/LatestNodeRuntimeMap": [ + { + "type": "aws:cdk:logicalId", + "data": "LatestNodeRuntimeMap" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider": [ + { + "type": "aws:cdk:is-custom-resource-handler-customResourceProvider", + "data": true + } + ], + "/aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler": [ + { + "type": "aws:cdk:logicalId", + "data": "CustomS3AutoDeleteObjectsCustomResourceProviderHandler9D90184F" + } + ], + "/aws-cdk-ivs-recording-configuration-test/RecordingConfiguration/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "RecordingConfigurationA528CBBF" + } + ], + "/aws-cdk-ivs-recording-configuration-test/Channel/Resource": [ + { + "type": "aws:cdk:logicalId", + "data": "Channel4048F119" + } + ], + "/aws-cdk-ivs-recording-configuration-test/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/aws-cdk-ivs-recording-configuration-test/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "aws-cdk-ivs-recording-configuration-test" + }, + "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets": { + "type": "cdk:asset-manifest", + "properties": { + "file": "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05": { + "type": "aws:cloudformation:stack", + "environment": "aws://unknown-account/unknown-region", + "properties": { + "templateFile": "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.template.json", + "terminationProtection": false, + "validateOnSynth": false, + "notificationArns": [], + "assumeRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-deploy-role-${AWS::AccountId}-${AWS::Region}", + "cloudFormationExecutionRoleArn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-cfn-exec-role-${AWS::AccountId}-${AWS::Region}", + "stackTemplateAssetObjectUrl": "s3://cdk-hnb659fds-assets-${AWS::AccountId}-${AWS::Region}/21fbb51d7b23f6a6c262b46a9caee79d744a3ac019fd45422d988b96d44b2a22.json", + "requiresBootstrapStackVersion": 6, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version", + "additionalDependencies": [ + "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets" + ], + "lookupRole": { + "arn": "arn:${AWS::Partition}:iam::${AWS::AccountId}:role/cdk-hnb659fds-lookup-role-${AWS::AccountId}-${AWS::Region}", + "requiresBootstrapStackVersion": 8, + "bootstrapStackVersionSsmParameter": "/cdk-bootstrap/hnb659fds/version" + } + }, + "dependencies": [ + "ivsrecordingconfigurationtestDefaultTestDeployAssert4AB65A05.assets" + ], + "metadata": { + "/ivs-recording-configuration-test/DefaultTest/DeployAssert/BootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "BootstrapVersion" + } + ], + "/ivs-recording-configuration-test/DefaultTest/DeployAssert/CheckBootstrapVersion": [ + { + "type": "aws:cdk:logicalId", + "data": "CheckBootstrapVersion" + } + ] + }, + "displayName": "ivs-recording-configuration-test/DefaultTest/DeployAssert" + }, + "Tree": { + "type": "cdk:tree", + "properties": { + "file": "tree.json" + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/tree.json b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/tree.json new file mode 100644 index 0000000000000..16364b235964b --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.js.snapshot/tree.json @@ -0,0 +1,341 @@ +{ + "version": "tree-0.1", + "tree": { + "id": "App", + "path": "", + "children": { + "aws-cdk-ivs-recording-configuration-test": { + "id": "aws-cdk-ivs-recording-configuration-test", + "path": "aws-cdk-ivs-recording-configuration-test", + "children": { + "Bucket": { + "id": "Bucket", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::Bucket", + "aws:cdk:cloudformation:props": { + "tags": [ + { + "key": "aws-cdk:auto-delete-objects", + "value": "true" + } + ] + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucket", + "version": "0.0.0" + } + }, + "Policy": { + "id": "Policy", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/Policy", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/Policy/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::S3::BucketPolicy", + "aws:cdk:cloudformation:props": { + "bucket": { + "Ref": "Bucket83908E77" + }, + "policyDocument": { + "Statement": [ + { + "Action": [ + "s3:DeleteObject*", + "s3:GetBucket*", + "s3:List*", + "s3:PutBucketPolicy" + ], + "Effect": "Allow", + "Principal": { + "AWS": { + "Fn::GetAtt": [ + "CustomS3AutoDeleteObjectsCustomResourceProviderRole3B1BD092", + "Arn" + ] + } + }, + "Resource": [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + { + "Fn::Join": [ + "", + [ + { + "Fn::GetAtt": [ + "Bucket83908E77", + "Arn" + ] + }, + "/*" + ] + ] + } + ] + } + ], + "Version": "2012-10-17" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.CfnBucketPolicy", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.BucketPolicy", + "version": "0.0.0" + } + }, + "AutoDeleteObjectsCustomResource": { + "id": "AutoDeleteObjectsCustomResource", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/AutoDeleteObjectsCustomResource", + "children": { + "Default": { + "id": "Default", + "path": "aws-cdk-ivs-recording-configuration-test/Bucket/AutoDeleteObjectsCustomResource/Default", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_s3.Bucket", + "version": "0.0.0" + } + }, + "LatestNodeRuntimeMap": { + "id": "LatestNodeRuntimeMap", + "path": "aws-cdk-ivs-recording-configuration-test/LatestNodeRuntimeMap", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnMapping", + "version": "0.0.0" + } + }, + "Custom::S3AutoDeleteObjectsCustomResourceProvider": { + "id": "Custom::S3AutoDeleteObjectsCustomResourceProvider", + "path": "aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider", + "children": { + "Staging": { + "id": "Staging", + "path": "aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Staging", + "constructInfo": { + "fqn": "aws-cdk-lib.AssetStaging", + "version": "0.0.0" + } + }, + "Role": { + "id": "Role", + "path": "aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Role", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + }, + "Handler": { + "id": "Handler", + "path": "aws-cdk-ivs-recording-configuration-test/Custom::S3AutoDeleteObjectsCustomResourceProvider/Handler", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnResource", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.CustomResourceProviderBase", + "version": "0.0.0" + } + }, + "RecordingConfiguration": { + "id": "RecordingConfiguration", + "path": "aws-cdk-ivs-recording-configuration-test/RecordingConfiguration", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ivs-recording-configuration-test/RecordingConfiguration/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IVS::RecordingConfiguration", + "aws:cdk:cloudformation:props": { + "destinationConfiguration": { + "s3": { + "bucketName": { + "Ref": "Bucket83908E77" + } + } + }, + "name": "my-recording-configuration", + "recordingReconnectWindowSeconds": 10, + "renditionConfiguration": { + "renditions": [ + "FULL_HD", + "HD", + "SD", + "LOWEST_RESOLUTION" + ], + "renditionSelection": "CUSTOM" + }, + "thumbnailConfiguration": { + "recordingMode": "INTERVAL", + "resolution": "FULL_HD", + "storage": [ + "LATEST", + "SEQUENTIAL" + ], + "targetIntervalSeconds": 30 + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ivs.CfnRecordingConfiguration", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "Channel": { + "id": "Channel", + "path": "aws-cdk-ivs-recording-configuration-test/Channel", + "children": { + "Resource": { + "id": "Resource", + "path": "aws-cdk-ivs-recording-configuration-test/Channel/Resource", + "attributes": { + "aws:cdk:cloudformation:type": "AWS::IVS::Channel", + "aws:cdk:cloudformation:props": { + "name": "aws-cdk-ivs-recording-configuration-testChannelE0AF024A", + "recordingConfigurationArn": { + "Fn::GetAtt": [ + "RecordingConfigurationA528CBBF", + "Arn" + ] + }, + "type": "ADVANCED_SD" + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.aws_ivs.CfnChannel", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Resource", + "version": "0.0.0" + } + }, + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "aws-cdk-ivs-recording-configuration-test/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "aws-cdk-ivs-recording-configuration-test/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + }, + "ivs-recording-configuration-test": { + "id": "ivs-recording-configuration-test", + "path": "ivs-recording-configuration-test", + "children": { + "DefaultTest": { + "id": "DefaultTest", + "path": "ivs-recording-configuration-test/DefaultTest", + "children": { + "Default": { + "id": "Default", + "path": "ivs-recording-configuration-test/DefaultTest/Default", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + }, + "DeployAssert": { + "id": "DeployAssert", + "path": "ivs-recording-configuration-test/DefaultTest/DeployAssert", + "children": { + "BootstrapVersion": { + "id": "BootstrapVersion", + "path": "ivs-recording-configuration-test/DefaultTest/DeployAssert/BootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnParameter", + "version": "0.0.0" + } + }, + "CheckBootstrapVersion": { + "id": "CheckBootstrapVersion", + "path": "ivs-recording-configuration-test/DefaultTest/DeployAssert/CheckBootstrapVersion", + "constructInfo": { + "fqn": "aws-cdk-lib.CfnRule", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.Stack", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTestCase", + "version": "0.0.0" + } + } + }, + "constructInfo": { + "fqn": "@aws-cdk/integ-tests-alpha.IntegTest", + "version": "0.0.0" + } + }, + "Tree": { + "id": "Tree", + "path": "Tree", + "constructInfo": { + "fqn": "constructs.Construct", + "version": "10.3.0" + } + } + }, + "constructInfo": { + "fqn": "aws-cdk-lib.App", + "version": "0.0.0" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.ts b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.ts new file mode 100644 index 0000000000000..24a402e622e3a --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/integ.ivs-recording-configuration.ts @@ -0,0 +1,37 @@ +import { App, Duration, RemovalPolicy, Stack } from 'aws-cdk-lib'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { Channel, ChannelType, RecordingConfiguration, Resolution } from '../lib'; +import * as integ from '@aws-cdk/integ-tests-alpha'; +import { RenditionConfiguration } from '../lib/rendition-configuration'; +import { Storage, ThumbnailConfiguration } from '../lib/thumbnail-configuration'; + +const app = new App(); + +const stack = new Stack(app, 'aws-cdk-ivs-recording-configuration-test'); + +const bucket = new Bucket(stack, 'Bucket', { + removalPolicy: RemovalPolicy.DESTROY, + autoDeleteObjects: true, +}); + +const recordingConfiguration = new RecordingConfiguration(stack, 'RecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + recordingReconnectWindow: Duration.seconds(10), + renditionConfiguration: RenditionConfiguration.custom([ + Resolution.FULL_HD, + Resolution.HD, + Resolution.SD, + Resolution.LOWEST_RESOLUTION, + ]), + thumbnailConfiguration: ThumbnailConfiguration.interval(Resolution.FULL_HD, [Storage.LATEST, Storage.SEQUENTIAL], Duration.seconds(30)), +}); + +new Channel(stack, 'Channel', { + type: ChannelType.ADVANCED_SD, + recordingConfiguration, +}); + +new integ.IntegTest(app, 'ivs-recording-configuration-test', { + testCases: [stack], +}); diff --git a/packages/@aws-cdk/aws-ivs-alpha/test/recording-configuration.test.ts b/packages/@aws-cdk/aws-ivs-alpha/test/recording-configuration.test.ts new file mode 100644 index 0000000000000..28b92265bff3c --- /dev/null +++ b/packages/@aws-cdk/aws-ivs-alpha/test/recording-configuration.test.ts @@ -0,0 +1,280 @@ +import { App, Duration, Stack } from 'aws-cdk-lib'; +import { Template } from 'aws-cdk-lib/assertions'; +import { Bucket } from 'aws-cdk-lib/aws-s3'; +import { IRecordingConfiguration, RecordingConfiguration, Resolution } from '../lib'; +import { Storage, ThumbnailConfiguration } from '../lib/thumbnail-configuration'; +import { RenditionConfiguration } from '../lib/rendition-configuration'; + +describe('IVS Recording Configuration', () => { + let app: App; + let stack: Stack; + let bucket: Bucket; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'TestStack', {}); + bucket = new Bucket(stack, 'Bucket', {}); + }); + + test('creates a recording configuration with minimum properties', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + Name: 'my-recording-configuration', + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + }); + }); + + test('set recordingReconnectWindowSeconds', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + recordingReconnectWindow: Duration.seconds(30), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + Name: 'my-recording-configuration', + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + RecordingReconnectWindowSeconds: 30, + }); + }); + + describe('test rendition configuration', () => { + test('set rendition all', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + renditionConfiguration: RenditionConfiguration.all(), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + RenditionConfiguration: { + RenditionSelection: 'ALL', + }, + }); + }); + + test('set rendition none', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + renditionConfiguration: RenditionConfiguration.none(), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + RenditionConfiguration: { + RenditionSelection: 'NONE', + }, + }); + }); + + test('set rendition custom', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + renditionConfiguration: RenditionConfiguration.custom([Resolution.HD, Resolution.SD]), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + RenditionConfiguration: { + RenditionSelection: 'CUSTOM', + Renditions: ['HD', 'SD'], + }, + }); + }); + }); + + describe('test thumbnail configuration', () => { + test('set thumbnail disable', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + thumbnailConfiguration: ThumbnailConfiguration.disable(), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + Name: 'my-recording-configuration', + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + ThumbnailConfiguration: { + RecordingMode: 'DISABLED', + }, + }); + }); + + test('set thumbnail interval', () => { + // WHEN + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + recordingConfigurationName: 'my-recording-configuration', + bucket, + thumbnailConfiguration: ThumbnailConfiguration.interval(Resolution.HD, [Storage.LATEST, Storage.SEQUENTIAL], Duration.seconds(30)), + }); + + // THEN + const template = Template.fromStack(stack); + template.hasResourceProperties('AWS::IVS::RecordingConfiguration', { + Name: 'my-recording-configuration', + DestinationConfiguration: { + S3: { + BucketName: stack.resolve(bucket.bucketName), + }, + }, + ThumbnailConfiguration: { + RecordingMode: 'INTERVAL', + Resolution: 'HD', + Storage: ['LATEST', 'SEQUENTIAL'], + TargetIntervalSeconds: 30, + }, + }); + }); + }); + + describe('fromRecordingConfigurationId method test', () => { + let importRecordingConfiguration: IRecordingConfiguration; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'TestStack'); + importRecordingConfiguration = RecordingConfiguration.fromRecordingConfigurationId(stack, 'ImportedRecordingConfiguration', 'my-record-configuration'); + }); + + test('should correctly set recordingConfigurationId', () => { + expect(importRecordingConfiguration.recordingConfigurationId).toEqual('my-record-configuration'); + }); + + test('should correctly format recordingConfigurationArn', () => { + expect(importRecordingConfiguration.recordingConfigurationArn).toEqual( + Stack.of(stack).formatArn({ + service: 'ivs', + resource: 'recording-configuration', + resourceName: 'my-record-configuration', + }), + ); + }); + }); + + describe('fromArn method test', () => { + let importRecordingConfiguration: IRecordingConfiguration; + + beforeEach(() => { + app = new App(); + stack = new Stack(app, 'TestStack'); + importRecordingConfiguration = RecordingConfiguration.fromArn(stack, 'ImportedRecordingConfiguration', 'arn:aws:ivs:us-east-1:012345678912:recording-configuration/my-record-configuration'); + }); + + test('should correctly set recordingConfigurationId', () => { + expect(importRecordingConfiguration.recordingConfigurationId).toEqual('my-record-configuration'); + }); + + test('should correctly format recordingConfigurationArn', () => { + expect(importRecordingConfiguration.recordingConfigurationArn).toEqual('arn:aws:ivs:us-east-1:012345678912:recording-configuration/my-record-configuration'); + }); + }); + + describe('validateRecordingConfigurationName test', () => { + test('throws when recordingConfigurationName include invalid characters.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + recordingConfigurationName: 'invalid name', + }); + }).toThrow('\`recordingConfigurationName\` must consist only of alphanumeric characters, hyphens or underbars, got: invalid name.'); + }, + ); + + test('throws when recordingConfigurationName length is invalid.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + recordingConfigurationName: 'a'.repeat(129), + }); + }).toThrow('\`recordingConfigurationName\` must be less than or equal to 128 characters, got: 129 characters.'); + }, + ); + }); + + describe('validateRecordingReconnectWindowSeconds test', () => { + test('throws when recordingReconnectWindow is smaller than 1 second.', () => { + + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + recordingReconnectWindow: Duration.millis(1), + }); + }).toThrow('\`recordingReconnectWindow\` must be between 0 and 300 seconds, got 1 milliseconds.'); + }); + + test('throws when recordingReconnectWindow is invalid seconds.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + recordingReconnectWindow: Duration.seconds(301), + }); + }).toThrow('\`recordingReconnectWindow\` must be between 0 and 300 seconds, got 301 seconds.'); + }); + }); + + describe('validate thumbnailConfiguraion test', () => { + test('throws when targetInterval is smaller than 1 second.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + thumbnailConfiguration: ThumbnailConfiguration.interval(Resolution.HD, [Storage.LATEST], Duration.millis(1)), + }); + }).toThrow('\`targetInterval\` must be between 1 and 60 seconds, got 1 milliseconds.'); + }); + + test('throws when targetInterval is invalid seconds.', () => { + expect(() => { + new RecordingConfiguration(stack, 'MyRecordingConfiguration', { + bucket, + thumbnailConfiguration: ThumbnailConfiguration.interval(Resolution.HD, [Storage.LATEST], Duration.seconds(61)), + }); + }).toThrow('\`targetInterval\` must be between 1 and 60 seconds, got 61 seconds.'); + }); + }); +}); \ No newline at end of file