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"