diff --git a/.gitallowed b/.gitallowed index 7542be9216573..972728e4b4a7e 100644 --- a/.gitallowed +++ b/.gitallowed @@ -21,3 +21,4 @@ account: '123456789012' account: '772975370895' account: '856666278305' account: '840364872350' +account: '422531588944' diff --git a/.github/ISSUE_TEMPLATE/bug.md b/.github/ISSUE_TEMPLATE/bug.md index 2ad0217714a88..b12a80d025350 100644 --- a/.github/ISSUE_TEMPLATE/bug.md +++ b/.github/ISSUE_TEMPLATE/bug.md @@ -33,7 +33,7 @@ What is the unexpected behavior you were seeing? If you got an error, paste it h ### Environment - - **CLI Version :** + - **CDK CLI Version :** - **Framework Version:** - **Node.js Version:** - **OS :** diff --git a/.gitignore b/.gitignore index 28ed33d0064b2..03b7512a00c05 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,5 @@ yarn-error.log # Cloud9 .c9 + +/.versionrc.json diff --git a/.versionrc.json b/.versionrc.json deleted file mode 100644 index 3178955551057..0000000000000 --- a/.versionrc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "skip": { "tag": true }, - "packageFiles": [ { "filename": "lerna.json", "type": "json" } ], - "bumpFiles": [ { "filename": "lerna.json", "type": "json" } ] -} diff --git a/CHANGELOG.md b/CHANGELOG.md index 0bb4d99fb0643..9bcb4c89cfe00 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,88 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.72.0](https://github.com/aws/aws-cdk/compare/v1.71.0...v1.72.0) (2020-11-06) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **rds:** Serverless cluster `enableHttpEndpoint` renamed to `enableDataApi` +* **stepfunctions-tasks:** type of `outputLocation` in the experimental Athena `StartQueryExecution` has been changed to `s3.Location` from `string` + +### Features + +* **apigatewayv2:** http api - endpoint url ([#11092](https://github.com/aws/aws-cdk/issues/11092)) ([c200413](https://github.com/aws/aws-cdk/commit/c20041356940c5569c00e82f9e6bee794002929b)), closes [#10651](https://github.com/aws/aws-cdk/issues/10651) +* **apigatewayv2:** vpc link and private integrations ([#11198](https://github.com/aws/aws-cdk/issues/11198)) ([e87a6a3](https://github.com/aws/aws-cdk/commit/e87a6a333c06d157f6d9074e05f251505525d0d5)), closes [#10531](https://github.com/aws/aws-cdk/issues/10531) [#10119](https://github.com/aws/aws-cdk/issues/10119) [aws/jsii#1947](https://github.com/aws/jsii/issues/1947) +* **appmesh:** add Virtual Gateways and Gateway Routes ([#10879](https://github.com/aws/aws-cdk/issues/10879)) ([79200e7](https://github.com/aws/aws-cdk/commit/79200e75b2468ccdee46154d049f3ceb30bb51e1)) +* **appsync:** add RDS datasource ([#9258](https://github.com/aws/aws-cdk/issues/9258)) ([23d0943](https://github.com/aws/aws-cdk/commit/23d0943216df76bea395b319deb21282e4c57a7c)), closes [#9152](https://github.com/aws/aws-cdk/issues/9152) +* **appsync:** support custom cloudWatchLogsRoleArn for GraphqlApi ([#10357](https://github.com/aws/aws-cdk/issues/10357)) ([bed89a5](https://github.com/aws/aws-cdk/commit/bed89a5d0aabe7d9a25ad7fac74a38f03b92e4c9)), closes [#9441](https://github.com/aws/aws-cdk/issues/9441) +* **ec2:** Add Lambda interface endpoint ([#11260](https://github.com/aws/aws-cdk/issues/11260)) ([9d0c935](https://github.com/aws/aws-cdk/commit/9d0c935fc62f325105598473e39b47b247437146)), closes [#11259](https://github.com/aws/aws-cdk/issues/11259) +* intro "Names.uniqueId()" instead of the deprecated "node.uniqueId" ([#11166](https://github.com/aws/aws-cdk/issues/11166)) ([5e433b1](https://github.com/aws/aws-cdk/commit/5e433b1d52470c3ecf5a460f79e4b8103542c35c)), closes [aws/constructs#314](https://github.com/aws/constructs/issues/314) +* **ecs-patterns:** add option to create cname instead of alias record ([#10812](https://github.com/aws/aws-cdk/issues/10812)) ([89a5055](https://github.com/aws/aws-cdk/commit/89a505524ae3fe1c726d6988df07da6167493480)) +* **ecs-service-extensions:** create an `Environment` from attributes ([#10932](https://github.com/aws/aws-cdk/issues/10932)) ([d395b5e](https://github.com/aws/aws-cdk/commit/d395b5e618fc423c46c65b9be40d0c1423e2b578)), closes [#10931](https://github.com/aws/aws-cdk/issues/10931) +* **rds:** add grant method for Data API ([#10748](https://github.com/aws/aws-cdk/issues/10748)) ([884539b](https://github.com/aws/aws-cdk/commit/884539b231245c893c456b2c619fe661cd39960f)), closes [#10744](https://github.com/aws/aws-cdk/issues/10744) + + +### Bug Fixes + +* **apigateway:** changes to gateway response does not trigger auto deployment ([#11068](https://github.com/aws/aws-cdk/issues/11068)) ([0c8264a](https://github.com/aws/aws-cdk/commit/0c8264adf782d1adbfe8d538186a71093d9c8834)), closes [#10963](https://github.com/aws/aws-cdk/issues/10963) +* **cfnspec:** incorrect Route 53 health check configuration properties in CloudFormation specification ([#11280](https://github.com/aws/aws-cdk/issues/11280)) ([f3c8b50](https://github.com/aws/aws-cdk/commit/f3c8b5034eb7ad1ccd9eecb4a929c8f11a2336d0)), closes [#issuecomment-717435271](https://github.com/aws/aws-cdk/issues/11096#issuecomment-717435271) [#11096](https://github.com/aws/aws-cdk/issues/11096) +* **cli:** `--no-previous-parameters` incorrectly skips updates ([#11288](https://github.com/aws/aws-cdk/issues/11288)) ([1bfc649](https://github.com/aws/aws-cdk/commit/1bfc64948b6ac63f93f030c5a2064b3ac4376289)) +* **core:** many nested stacks make NodeJS run out of memory ([#11250](https://github.com/aws/aws-cdk/issues/11250)) ([c124886](https://github.com/aws/aws-cdk/commit/c124886fbcabea166f34250cad84f7526e05b1bf)) +* **core:** multiple library copies lead to 'Assets must be defined within Stage or App' error ([#11113](https://github.com/aws/aws-cdk/issues/11113)) ([fcfed39](https://github.com/aws/aws-cdk/commit/fcfed39e3524eef66d3638896bf4ca86697f1718)), closes [#10314](https://github.com/aws/aws-cdk/issues/10314) +* **core:** support docker engine v20.10.0-beta1 ([#11124](https://github.com/aws/aws-cdk/issues/11124)) ([87887a3](https://github.com/aws/aws-cdk/commit/87887a3faf24f5fde608135429585c6521637764)) +* **dynamodb:** Misconfigured metrics causing empty graphs ([#11283](https://github.com/aws/aws-cdk/issues/11283)) ([9968669](https://github.com/aws/aws-cdk/commit/9968669e4f4602a03de67e12bc5636a4f4bb1fd7)) +* **ecs:** redirect config should honor openListener flag ([#11115](https://github.com/aws/aws-cdk/issues/11115)) ([ed6e7ed](https://github.com/aws/aws-cdk/commit/ed6e7ed9ebee7dc8932c35885698fc72e2052085)) +* **event-targets:** circular dependency when the lambda target is in a different stack ([#11217](https://github.com/aws/aws-cdk/issues/11217)) ([e21f249](https://github.com/aws/aws-cdk/commit/e21f249f7b9c78ed5948d63e7650ee7b8d5b3f8b)), closes [#10942](https://github.com/aws/aws-cdk/issues/10942) +* **pipelines:** asset stage can't support more than 50 assets ([#11284](https://github.com/aws/aws-cdk/issues/11284)) ([5db8e80](https://github.com/aws/aws-cdk/commit/5db8e8018d2b8304025b7e61178c7a747c778a78)), closes [#9353](https://github.com/aws/aws-cdk/issues/9353) +* **secretsmanager:** can't export secret name from Secret ([#11202](https://github.com/aws/aws-cdk/issues/11202)) ([5dcdecb](https://github.com/aws/aws-cdk/commit/5dcdecb2c5d6ce19517af66090cfacabed88025b)), closes [#10914](https://github.com/aws/aws-cdk/issues/10914) +* **secretsmanager:** Secret.fromSecretName doesn't work with ECS ([#11042](https://github.com/aws/aws-cdk/issues/11042)) ([fe1ce73](https://github.com/aws/aws-cdk/commit/fe1ce73ec59fc3ad9d8b138ba2122303e77c0531)), closes [#10309](https://github.com/aws/aws-cdk/issues/10309) [#10519](https://github.com/aws/aws-cdk/issues/10519) +* **stepfunctions:** stack overflow when referenced json path finding encounters a circular object graph ([#11225](https://github.com/aws/aws-cdk/issues/11225)) ([f14d823](https://github.com/aws/aws-cdk/commit/f14d823279e4dbb6ac90ab21d219257b22b81278)), closes [#9319](https://github.com/aws/aws-cdk/issues/9319) +* **stepfunctions-tasks:** Athena* APIs have incorrect supported integration patterns ([#11188](https://github.com/aws/aws-cdk/issues/11188)) ([0f66833](https://github.com/aws/aws-cdk/commit/0f6683394fa6f96d6839b2c107f3dab8045509b4)), closes [#11045](https://github.com/aws/aws-cdk/issues/11045) [#11246](https://github.com/aws/aws-cdk/issues/11246) +* **stepfunctions-tasks:** incorrect S3 permissions for AthenaStartQueryExecution ([#11203](https://github.com/aws/aws-cdk/issues/11203)) ([b35c423](https://github.com/aws/aws-cdk/commit/b35c423644fbd8f20705c16c0809a9fb93e6d6f3)) +* explicitly set the 'ImagePullPrincipalType' of image ([#11264](https://github.com/aws/aws-cdk/issues/11264)) ([29aa223](https://github.com/aws/aws-cdk/commit/29aa223f05b5f012b42b662e7a9fcc8fe82167a7)), closes [#10569](https://github.com/aws/aws-cdk/issues/10569) + +## [1.71.0](https://github.com/aws/aws-cdk/compare/v1.70.0...v1.71.0) (2020-10-29) + + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **synthetics:** `runtime` is now a required property. + +### ⚠ BREAKING CHANGES TO EXPERIMENTAL FEATURES + +* **core:** Creation stack traces for `Lazy` values are no longer +captured by default. The `CDK_DEBUG=true` environment variable must be +set in order to capture stack traces (this is also achieved by using the +`--debug` option of the `cdk` CLI). Users should not need those stack +traces most of the time, and should only enable creation stack trace +captures when tyring to troubleshoot a resolution error that they are +otherwise unable to trace back. + +### Features + +* **autoscaling:** CloudFormation init for ASGs ([#9674](https://github.com/aws/aws-cdk/issues/9674)) ([bdf1d30](https://github.com/aws/aws-cdk/commit/bdf1d30a08c034703ca05eebe8e9d0cc5e070949)), closes [#9065](https://github.com/aws/aws-cdk/issues/9065) [#9664](https://github.com/aws/aws-cdk/issues/9664) +* **cli:** `--all` flag to select all stacks ([#10745](https://github.com/aws/aws-cdk/issues/10745)) ([bcd9d0a](https://github.com/aws/aws-cdk/commit/bcd9d0aa900aceb32e50031ea1a8f8a21e07a963)), closes [#3222](https://github.com/aws/aws-cdk/issues/3222) +* **cli:** change virtualenv directory to `.venv` to comply with python recommendation ([#10995](https://github.com/aws/aws-cdk/issues/10995)) ([a4a41b5](https://github.com/aws/aws-cdk/commit/a4a41b5e006110304b51ee55c34e91cc3f129281)), closes [#9134](https://github.com/aws/aws-cdk/issues/9134) +* **cli:** disable version check ([#10975](https://github.com/aws/aws-cdk/issues/10975)) ([575e47e](https://github.com/aws/aws-cdk/commit/575e47e4d6e8b89b4402ddc4b7bdea985b1e6edf)), closes [#10974](https://github.com/aws/aws-cdk/issues/10974) +* **core:** make creationStack collection for Lazy opt-in ([#11170](https://github.com/aws/aws-cdk/issues/11170)) ([a3fae02](https://github.com/aws/aws-cdk/commit/a3fae02a5256a25fca011bab2a2aa9be58121c6e)) +* **init-templates:** Java init template tests updated to JUnit 5 ([#11101](https://github.com/aws/aws-cdk/issues/11101)) ([e0c00a1](https://github.com/aws/aws-cdk/commit/e0c00a1aafe82d390fd1859090e0bbe1ac249043)), closes [#10694](https://github.com/aws/aws-cdk/issues/10694) +* upgrade "constructs" to 3.2.0 ([#11145](https://github.com/aws/aws-cdk/issues/11145)) ([d85e3ed](https://github.com/aws/aws-cdk/commit/d85e3eda8a0d97d60d178922bf9db33a31f4abe9)) +* **redshift:** add publiclyAccessible prop ([#11162](https://github.com/aws/aws-cdk/issues/11162)) ([9f8a6de](https://github.com/aws/aws-cdk/commit/9f8a6dee36105f7bbf7f433075881d5068fb5779)), closes [#11161](https://github.com/aws/aws-cdk/issues/11161) +* **stepfunctions-tasks:** Support for Athena APIs: StartQueryExecution, StopQueryExeuction, GetQueryResults and GetQueryExecution ([#11045](https://github.com/aws/aws-cdk/issues/11045)) ([19180cc](https://github.com/aws/aws-cdk/commit/19180cc7dd2e3cfbbcc82ef2b45f3a8f60894f8c)) +* **synthetics:** The CloudWatch Synthetics Construct Library is now in Developer Preview ([#11180](https://github.com/aws/aws-cdk/issues/11180)) ([b3b5f48](https://github.com/aws/aws-cdk/commit/b3b5f48ba457d382b6289997f164444ac6dfed0a)) + + +### Bug Fixes + +* **aws-rds/aws-secretmanager:** `credentials.fromSecret` does not access `secretsmanager.ISecret` ([#11033](https://github.com/aws/aws-cdk/issues/11033)) ([35ad608](https://github.com/aws/aws-cdk/commit/35ad608fb0c9801756b0557b460e3587684b7110)), closes [#11015](https://github.com/aws/aws-cdk/issues/11015) +* **bootstrap:** same-account modern bootstrapping still requires policy ARNs ([#9867](https://github.com/aws/aws-cdk/issues/9867)) ([f5ab374](https://github.com/aws/aws-cdk/commit/f5ab374eafeafff02f386be445d10863717b51ed)), closes [#8571](https://github.com/aws/aws-cdk/issues/8571) +* **codebuild:** ReportGroup name is ignored ([#11080](https://github.com/aws/aws-cdk/issues/11080)) ([1e2250a](https://github.com/aws/aws-cdk/commit/1e2250aa8345ee9fe22ed2a7395ba28994fe8ff1)), closes [#11052](https://github.com/aws/aws-cdk/issues/11052) +* **core:** assets are duplicated between nested Cloud Assemblies ([#11008](https://github.com/aws/aws-cdk/issues/11008)) ([c84217f](https://github.com/aws/aws-cdk/commit/c84217f94cf66cae800d434350b3b3d7676a03b3)), closes [#10877](https://github.com/aws/aws-cdk/issues/10877) [#9627](https://github.com/aws/aws-cdk/issues/9627) [#9917](https://github.com/aws/aws-cdk/issues/9917) +* **ec2:** `CfnInit` cannot be used with custom constructs ([#11167](https://github.com/aws/aws-cdk/issues/11167)) ([01c52c8](https://github.com/aws/aws-cdk/commit/01c52c84118b101de9aaca3091673b16d6871386)) +* **region-info:** incorrect S3 static website endpoint for us-gov-west-1 ([#10920](https://github.com/aws/aws-cdk/issues/10920)) ([dde9c55](https://github.com/aws/aws-cdk/commit/dde9c5530478e9371726278ef34b82da19624a4b)), closes [40aws-cdk/region-info/build-tools/generate-static-data.ts#L47-L49](https://github.com/40aws-cdk/region-info/build-tools/generate-static-data.ts/issues/L47-L49) + + ## [1.70.0](https://github.com/aws/aws-cdk/compare/v1.69.0...v1.70.0) (2020-10-23) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 0c5137c77a4a5..364042c28ae18 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,7 @@ and let us know if it's not up-to-date (even better, submit a PR with your corr - [API Compatibility Checks](#api-compatibility-checks) - [Examples](#examples) - [Feature Flags](#feature-flags) - - [Versioning](#versioning) + - [Versioning and Release](#versioning-and-release) - [Troubleshooting](#troubleshooting) - [Debugging](#debugging) - [Connecting the VS Code Debugger](#connecting-the-vs-code-debugger) @@ -779,24 +779,59 @@ CDK](https://github.com/aws/aws-cdk/issues/3398) we will either remove the legacy behavior or flip the logic for all these features and then reset the `FEATURE_FLAGS` map for the next cycle. -### Versioning +### Versioning and Release -All `package.json` files in this repo use a stable marker version of `0.0.0`. -This means that when you declare dependencies, you should always use `0.0.0`. -This makes it easier for us to bump a new version (the `bump.sh` script will -just update the central version and create a CHANGELOG entry) and also reduces -the chance of merge conflicts after a new version is released. +The `release.json` file at the root of the repo determines which release line +this branch belongs to. -Additional scripts that take part in the versioning mechanism: +```js +{ + "majorVersion": 1 | 2, + "releaseType": "stable" | "alpha" | "rc" +} +``` + +To reduce merge conflicts in automatic merges between version branches, the +current version number is stored under `version.vNN.json` (where `NN` is +`majorVersion`) and changelogs are stored under `CHANGELOG.NN.md` (for +historical reasons, the changelog for 1.x is under `CHANGELOG.md`). When we +fork to a new release branch (e.g. `v2-main`), we will update `release.json` in +this branch to reflect the new version line, and this information will be used +to determine how releases are cut. + +The actual `version` field in all `package.json` files should always be `0.0.0`. +This means that local development builds will use version `0.0.0` instead of the +official version from the version file. + +#### `./bump.sh` + +This script uses [standard-version] to update the version in `version.vNN.json` +to the next version. By default it will perform a **minor** bump, but `./bump.sh +patch` can be used to perform a patch release if that's needed. + +This script will also update the relevant changelog file. + +[standard-version]: https://github.com/conventional-changelog/standard-version + +#### `scripts/resolve-version.js` + +The script evaluates evaluates the configuration in `release.json` and exports an +object like this: + +```js +{ + version: '2.0.0-alpha.1', // the current version + versionFile: 'version.v2.json', // the version file + changelogFile: 'CHANGELOG.v2.md', // changelog file name + prerelease: 'alpha', // prerelease tag (undefined for stable) + marker: '0.0.0' // version marker in package.json files +} +``` + +#### scripts/align-version.sh -- `scripts/get-version.js` can be used to obtain the actual version of the repo. - You can use either from JavaScript code by `require('./scripts/get-version')` - or from a shell script `node -p "require('./scripts/get-version')"`. -- `scripts/get-version-marker.js` returns `0.0.0` and used to DRY the version - marker. -- `scripts/align-version.sh` and `scripts/align-version.js` are used to align - all package.json files in the repo to the official version. This script is - invoked in CI builds and should not be used inside a development environment. +In official builds, the `scripts/align-version.sh` is used to update all +`package.json` files based on the version from `version.vNN.json`. ## Troubleshooting diff --git a/allowed-breaking-changes.txt b/allowed-breaking-changes.txt index 2e53fd5b537fa..9120903b01912 100644 --- a/allowed-breaking-changes.txt +++ b/allowed-breaking-changes.txt @@ -3,6 +3,10 @@ # and that won't typecheck if Manifest.load() adds a union arm and now returns A | B | C. change-return-type:@aws-cdk/cloud-assembly-schema.Manifest.load +# Adding any new context queries will add to the ContextQueryProperties type, +# which changes the signature of MissingContext. +weakened:@aws-cdk/cloud-assembly-schema.MissingContext + removed:@aws-cdk/core.BootstraplessSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN removed:@aws-cdk/core.DefaultStackSynthesizer.DEFAULT_ASSET_PUBLISHING_ROLE_ARN removed:@aws-cdk/core.DefaultStackSynthesizerProps.assetPublishingExternalId @@ -47,4 +51,4 @@ incompatible-argument:@aws-cdk/aws-ecs.Ec2TaskDefinition.addVolume incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition. incompatible-argument:@aws-cdk/aws-ecs.FargateTaskDefinition.addVolume incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition. -incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume \ No newline at end of file +incompatible-argument:@aws-cdk/aws-ecs.TaskDefinition.addVolume diff --git a/bump.sh b/bump.sh index 028779e5a748d..73954148de7c6 100755 --- a/bump.sh +++ b/bump.sh @@ -13,21 +13,7 @@ # # -------------------------------------------------------------------------------------------------- set -euo pipefail -version=${1:-minor} - -echo "Starting ${version} version bump" - -# /bin/bash ./install.sh - -# Generate CHANGELOG and create a commit (see .versionrc.json) -npx standard-version --release-as ${version} - -# I am sorry. -# -# I've gone diving through the code of `conventional-changelog` to see if there -# was a way to configure the string and ultimately I decided that a 'sed' was the simpler -# way to go. -sed -i.tmp -e 's/BREAKING CHANGES$/BREAKING CHANGES TO EXPERIMENTAL FEATURES/' CHANGELOG.md -rm CHANGELOG.md.tmp -git add CHANGELOG.md -git commit --amend --no-edit +scriptdir=$(cd $(dirname $0) && pwd) +cd ${scriptdir} +yarn --frozen-lockfile +${scriptdir}/scripts/bump.js ${1:-minor} diff --git a/lerna.json b/lerna.json index a530390f60415..0fcae573a32ae 100644 --- a/lerna.json +++ b/lerna.json @@ -8,8 +8,9 @@ "packages/@aws-cdk-containers/*", "packages/@monocdk-experiment/*", "packages/@aws-cdk/*/lambda-packages/*", - "tools/*" + "tools/*", + "scripts/script-tests" ], "rejectCycles": "true", - "version": "1.70.0" + "version": "0.0.0" } diff --git a/pack.sh b/pack.sh index 02b901f141273..a61a461ce9f3e 100755 --- a/pack.sh +++ b/pack.sh @@ -7,6 +7,11 @@ export PATH=$PWD/node_modules/.bin:$PATH export NODE_OPTIONS="--max-old-space-size=4096 ${NODE_OPTIONS:-}" root=$PWD +# Get version and changelog file name (these require that .versionrc.json would have been generated) +version=$(node -p "require('./scripts/resolve-version').version") +changelog_file=$(node -p "require('./scripts/resolve-version').changelogFile") +marker=$(node -p "require('./scripts/resolve-version').marker") + PACMAK=${PACMAK:-jsii-pacmak} ROSETTA=${ROSETTA:-jsii-rosetta} TMPDIR=${TMPDIR:-$(dirname $(mktemp -u))} @@ -57,15 +62,6 @@ done # Remove a JSII aggregate POM that may have snuk past rm -rf dist/java/software/amazon/jsii -# Get version -version="$(node -p "require('./scripts/get-version')")" - -# Ensure we don't publish anything beyond 1.x for now -if [[ ! "${version}" == "1."* ]]; then - echo "ERROR: accidentally releasing a major version? Expecting repo version to start with '1.' but got '${version}'" - exit 1 -fi - # Get commit from CodePipeline (or git, if we are in CodeBuild) # If CODEBUILD_RESOLVED_SOURCE_VERSION is not defined (i.e. local # build or CodePipeline build), use the HEAD commit hash). @@ -83,12 +79,11 @@ cat > ${distdir}/build.json < Please submit a pull request so that we can review your service extension and +> list it here. diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/environment.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/environment.ts index 2a5e215d7571e..dcff0d28960b4 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/environment.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/environment.ts @@ -28,13 +28,50 @@ export interface EnvironmentProps { readonly capacityType?: EnvironmentCapacityType } +/** + * An environment into which to deploy a service. + */ +export interface IEnvironment { + /** + * The name of this environment. + */ + readonly id: string; + + /** + * The VPC into which environment services should be placed. + */ + readonly vpc: ec2.IVpc; + + /** + * The cluster that is providing capacity for this service. + */ + readonly cluster: ecs.ICluster; + + /** + * The capacity type used by the service's cluster. + */ + readonly capacityType: EnvironmentCapacityType; + + /** + * Add a default cloudmap namespace to the environment's cluster. + */ + addDefaultCloudMapNamespace(options: ecs.CloudMapNamespaceOptions): void; +} + /** * An environment into which to deploy a service. This environment * can either be instantiated with a preexisting AWS VPC and ECS cluster, * or it can create it's own VPC and cluster. By default it will create * a cluster with Fargate capacity. */ -export class Environment extends cdk.Construct { +export class Environment extends cdk.Construct implements IEnvironment { + /** + * Import an existing environment from its attributes. + */ + public static fromEnvironmentAttributes(scope: cdk.Construct, id: string, attrs: EnvironmentAttributes): IEnvironment { + return new ImportedEnvironment(scope, id, attrs); + } + /** * The name of this environment. */ @@ -81,4 +118,47 @@ export class Environment extends cdk.Construct { this.capacityType = EnvironmentCapacityType.FARGATE; } } + + /** + * Add a default cloudmap namespace to the environment's cluster. + */ + addDefaultCloudMapNamespace(options: ecs.CloudMapNamespaceOptions) { + this.cluster.addDefaultCloudMapNamespace(options); + } +} + +export interface EnvironmentAttributes { + /** + * The capacity type used by the service's cluster. + */ + capacityType: EnvironmentCapacityType; + + /** + * The cluster that is providing capacity for this service. + */ + cluster: ecs.ICluster; } + +export class ImportedEnvironment extends cdk.Construct implements IEnvironment { + public readonly capacityType: EnvironmentCapacityType; + public readonly cluster: ecs.ICluster; + public readonly id: string; + public readonly vpc: ec2.IVpc; + + constructor(scope: cdk.Construct, id: string, props: EnvironmentAttributes) { + super(scope, id); + + this.id = id; + this.capacityType = props.capacityType; + this.cluster = props.cluster; + this.vpc = props.cluster.vpc; + } + + /** + * Refuses to add a default cloudmap namespace to the cluster as we don't + * own it. + */ + addDefaultCloudMapNamespace(_options: ecs.CloudMapNamespaceOptions) { + throw new Error('the cluster environment is immutable when imported'); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts index dcf3f7ac73e56..583ca06435c09 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/extensions/appmesh.ts @@ -10,7 +10,7 @@ import { Container } from './container'; import { ServiceExtension, ServiceBuild } from './extension-interfaces'; // The version of the App Mesh envoy sidecar to add to the task. -const APP_MESH_ENVOY_SIDECAR_VERSION = 'v1.15.0.0-prod'; +const APP_MESH_ENVOY_SIDECAR_VERSION = 'v1.15.1.0-prod'; /** * The settings for the App Mesh extension. @@ -70,7 +70,7 @@ export class AppMeshExtension extends ServiceExtension { // Make sure that the parent cluster for this service has // a namespace attached. if (!this.parentService.cluster.defaultCloudMapNamespace) { - this.parentService.cluster.addDefaultCloudMapNamespace({ + this.parentService.environment.addDefaultCloudMapNamespace({ // Name the namespace after the environment name. // Service DNS will be like . name: this.parentService.environment.id, @@ -276,12 +276,9 @@ export class AppMeshExtension extends ServiceExtension { // and other similar behaviors. this.virtualRouter = new appmesh.VirtualRouter(this.scope, `${this.parentService.id}-virtual-router`, { mesh: this.mesh, - listener: { - portMapping: { - port: containerextension.trafficPort, - protocol: this.protocol, - }, - }, + listeners: [ + this.virtualRouterListener(containerextension.trafficPort), + ], virtualRouterName: `${this.parentService.id}`, }); @@ -331,4 +328,13 @@ export class AppMeshExtension extends ServiceExtension { // nodes from the other service. this.virtualNode.addBackends(otherAppMesh.virtualService); } + + private virtualRouterListener(port: number): appmesh.VirtualRouterListener { + switch (this.protocol) { + case appmesh.Protocol.HTTP: return appmesh.VirtualRouterListener.http(port); + case appmesh.Protocol.HTTP2: return appmesh.VirtualRouterListener.http2(port); + case appmesh.Protocol.GRPC: return appmesh.VirtualRouterListener.grpc(port); + case appmesh.Protocol.TCP: return appmesh.VirtualRouterListener.tcp(port); + } + } } diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts b/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts index 0b46f782a64d8..29134a8c83260 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/lib/service.ts @@ -1,7 +1,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as cdk from '@aws-cdk/core'; -import { Environment } from './environment'; +import { IEnvironment } from './environment'; import { EnvironmentCapacityType, ServiceBuild } from './extensions/extension-interfaces'; import { ServiceDescription } from './service-description'; @@ -17,7 +17,7 @@ export interface ServiceProps { /** * The environment to launch the service in */ - readonly environment: Environment + readonly environment: IEnvironment } /** @@ -44,7 +44,7 @@ export class Service extends cdk.Construct { * The cluster that is providing capacity for this service * [disable-awslint:ref-via-interface] */ - public readonly cluster: ecs.Cluster; + public readonly cluster: ecs.ICluster; /** * The capacity type that this service will use @@ -59,7 +59,7 @@ export class Service extends cdk.Construct { /** * The environment this service was launched in */ - public readonly environment: Environment; + public readonly environment: IEnvironment; /** * The generated task definition for this service, is only diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/package.json b/packages/@aws-cdk-containers/ecs-service-extensions/package.json index 6969fcd980fb1..4b1da71dd86fe 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/package.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/package.json @@ -40,7 +40,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json index 6ec717eee8cbe..f9d42c3927b60 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.all-service-addons.expected.json @@ -727,7 +727,7 @@ { "Ref": "AWS::URLSuffix" }, - "/aws-appmesh-envoy:v1.15.0.0-prod" + "/aws-appmesh-envoy:v1.15.1.0-prod" ] ] }, @@ -1596,7 +1596,7 @@ { "Ref": "AWS::URLSuffix" }, - "/aws-appmesh-envoy:v1.15.0.0-prod" + "/aws-appmesh-envoy:v1.15.1.0-prod" ] ] }, @@ -2584,7 +2584,7 @@ { "Ref": "AWS::URLSuffix" }, - "/aws-appmesh-envoy:v1.15.0.0-prod" + "/aws-appmesh-envoy:v1.15.1.0-prod" ] ] }, @@ -3323,7 +3323,7 @@ "ecrRepo": "840364872350" }, "eu-south-1": { - "ecrRepo": "840364872350" + "ecrRepo": "422531588944" }, "eu-west-1": { "ecrRepo": "840364872350" @@ -3382,7 +3382,7 @@ "ecrRepo": "840364872350" }, "eu-south-1": { - "ecrRepo": "840364872350" + "ecrRepo": "422531588944" }, "eu-west-1": { "ecrRepo": "840364872350" @@ -3441,7 +3441,7 @@ "ecrRepo": "840364872350" }, "eu-south-1": { - "ecrRepo": "840364872350" + "ecrRepo": "422531588944" }, "eu-west-1": { "ecrRepo": "840364872350" @@ -3491,4 +3491,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.expected.json new file mode 100644 index 0000000000000..80156c0ed0d2d --- /dev/null +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.expected.json @@ -0,0 +1,372 @@ +{ + "Resources": { + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60": { + "Type": "AWS::CloudFormation::Stack", + "Properties": { + "TemplateURL": { + "Fn::Join": [ + "", + [ + "https://s3.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/", + { + "Ref": "AssetParametersb537a3d3462b62d59d4661ff456403d270823b5c9974ea4f4264ff95683a8ba8S3Bucket60C7B412" + }, + "/", + { + "Fn::Select": [ + 0, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb537a3d3462b62d59d4661ff456403d270823b5c9974ea4f4264ff95683a8ba8S3VersionKey6900DC52" + } + ] + } + ] + }, + { + "Fn::Select": [ + 1, + { + "Fn::Split": [ + "||", + { + "Ref": "AssetParametersb537a3d3462b62d59d4661ff456403d270823b5c9974ea4f4264ff95683a8ba8S3VersionKey6900DC52" + } + ] + } + ] + } + ] + ] + } + } + }, + "ServiceloadbalancerD5D60894": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [ + { + "Key": "deletion_protection.enabled", + "Value": "false" + } + ], + "Scheme": "internet-facing", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ServiceloadbalancerSecurityGroup2DA3E8D6", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentvpcPublicSubnet1Subnet0D15D382Ref" + ] + }, + { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentvpcPublicSubnet2Subnet68645ACARef" + ] + }, + { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentvpcPublicSubnet3Subnet408F449FRef" + ] + } + ], + "Type": "application" + } + }, + "ServiceloadbalancerSecurityGroup2DA3E8D6": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB importedenvironmentintegServiceloadbalancerFAE8A5FA", + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentvpcDC34D4D3Ref" + ] + } + } + }, + "ServiceloadbalancerSecurityGrouptoimportedenvironmentintegServiceserviceSecurityGroup2BE90F7480B17EB7CA": { + "Type": "AWS::EC2::SecurityGroupEgress", + "Properties": { + "GroupId": { + "Fn::GetAtt": [ + "ServiceloadbalancerSecurityGroup2DA3E8D6", + "GroupId" + ] + }, + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "DestinationSecurityGroupId": { + "Fn::GetAtt": [ + "ServiceserviceSecurityGroup1915660F", + "GroupId" + ] + }, + "FromPort": 80, + "ToPort": 80 + } + }, + "ServiceloadbalancerServicelistenerC862F722": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "ServiceloadbalancerServicelistenerServiceGroup844B51E6" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "ServiceloadbalancerD5D60894" + }, + "Port": 80, + "Protocol": "HTTP" + } + }, + "ServiceloadbalancerServicelistenerServiceGroup844B51E6": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "TargetGroupAttributes": [ + { + "Key": "deregistration_delay.timeout_seconds", + "Value": "10" + } + ], + "TargetType": "ip", + "VpcId": { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentvpcDC34D4D3Ref" + ] + } + } + }, + "ServicetaskdefinitionTaskRole5B4B60A4": { + "Type": "AWS::IAM::Role", + "Properties": { + "AssumeRolePolicyDocument": { + "Statement": [ + { + "Action": "sts:AssumeRole", + "Effect": "Allow", + "Principal": { + "Service": "ecs-tasks.amazonaws.com" + } + } + ], + "Version": "2012-10-17" + } + } + }, + "Servicetaskdefinition0CEAD834": { + "Type": "AWS::ECS::TaskDefinition", + "Properties": { + "ContainerDefinitions": [ + { + "Cpu": 256, + "Environment": [ + { + "Name": "PORT", + "Value": "80" + } + ], + "Essential": true, + "Image": "nathanpeck/name", + "Memory": 512, + "Name": "app", + "PortMappings": [ + { + "ContainerPort": 80, + "Protocol": "tcp" + } + ], + "Ulimits": [ + { + "HardLimit": 1024000, + "Name": "nofile", + "SoftLimit": 1024000 + } + ] + } + ], + "Cpu": "256", + "Family": "importedenvironmentintegServicetaskdefinition63936B87", + "Memory": "512", + "NetworkMode": "awsvpc", + "RequiresCompatibilities": [ + "EC2", + "FARGATE" + ], + "TaskRoleArn": { + "Fn::GetAtt": [ + "ServicetaskdefinitionTaskRole5B4B60A4", + "Arn" + ] + } + } + }, + "ServiceserviceService6A153CB8": { + "Type": "AWS::ECS::Service", + "Properties": { + "Cluster": { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentcluster594A3DCARef" + ] + }, + "DeploymentConfiguration": { + "MaximumPercent": 200, + "MinimumHealthyPercent": 100 + }, + "DesiredCount": 1, + "EnableECSManagedTags": false, + "HealthCheckGracePeriodSeconds": 60, + "LaunchType": "FARGATE", + "LoadBalancers": [ + { + "ContainerName": "app", + "ContainerPort": 80, + "TargetGroupArn": { + "Ref": "ServiceloadbalancerServicelistenerServiceGroup844B51E6" + } + } + ], + "NetworkConfiguration": { + "AwsvpcConfiguration": { + "AssignPublicIp": "DISABLED", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "ServiceserviceSecurityGroup1915660F", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentvpcPrivateSubnet1Subnet7309E967Ref" + ] + }, + { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentvpcPrivateSubnet2Subnet55014443Ref" + ] + }, + { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentvpcPrivateSubnet3Subnet6CF08327Ref" + ] + } + ] + } + }, + "TaskDefinition": { + "Ref": "Servicetaskdefinition0CEAD834" + } + }, + "DependsOn": [ + "ServiceloadbalancerServicelistenerC862F722", + "ServiceloadbalancerServicelistenerServiceGroup844B51E6" + ] + }, + "ServiceserviceSecurityGroup1915660F": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "imported-environment-integ/Service-service/SecurityGroup", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Fn::GetAtt": [ + "ResourcesNestedStackResourcesNestedStackResourceCDA26E60", + "Outputs.importedenvironmentintegResourcesEnvironmentenvironmentvpcDC34D4D3Ref" + ] + } + } + }, + "ServiceserviceSecurityGroupfromimportedenvironmentintegServiceloadbalancerSecurityGroup68EE533C8070FCF629": { + "Type": "AWS::EC2::SecurityGroupIngress", + "Properties": { + "IpProtocol": "tcp", + "Description": "Load balancer to target", + "FromPort": 80, + "GroupId": { + "Fn::GetAtt": [ + "ServiceserviceSecurityGroup1915660F", + "GroupId" + ] + }, + "SourceSecurityGroupId": { + "Fn::GetAtt": [ + "ServiceloadbalancerSecurityGroup2DA3E8D6", + "GroupId" + ] + }, + "ToPort": 80 + } + } + }, + "Outputs": { + "Serviceloadbalancerdnsoutput": { + "Value": { + "Fn::GetAtt": [ + "ServiceloadbalancerD5D60894", + "DNSName" + ] + } + } + }, + "Parameters": { + "AssetParametersb537a3d3462b62d59d4661ff456403d270823b5c9974ea4f4264ff95683a8ba8S3Bucket60C7B412": { + "Type": "String", + "Description": "S3 bucket for asset \"b537a3d3462b62d59d4661ff456403d270823b5c9974ea4f4264ff95683a8ba8\"" + }, + "AssetParametersb537a3d3462b62d59d4661ff456403d270823b5c9974ea4f4264ff95683a8ba8S3VersionKey6900DC52": { + "Type": "String", + "Description": "S3 key for asset version \"b537a3d3462b62d59d4661ff456403d270823b5c9974ea4f4264ff95683a8ba8\"" + }, + "AssetParametersb537a3d3462b62d59d4661ff456403d270823b5c9974ea4f4264ff95683a8ba8ArtifactHash5EEB924C": { + "Type": "String", + "Description": "Artifact hash for asset \"b537a3d3462b62d59d4661ff456403d270823b5c9974ea4f4264ff95683a8ba8\"" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.ts new file mode 100644 index 0000000000000..899d9e4a4f7c4 --- /dev/null +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.imported-environment.ts @@ -0,0 +1,99 @@ +import { Vpc } from '@aws-cdk/aws-ec2'; +import { Cluster, ContainerImage } from '@aws-cdk/aws-ecs'; +import { App, NestedStack, Stack } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { + Container, + Environment, + EnvironmentCapacityType, + HttpLoadBalancerExtension, + Service, + ServiceDescription, +} from '../lib'; + +class ResourceStack extends NestedStack { + public readonly clusterName: string; + public readonly vpcId: string; + public readonly publicSubnetIds: string[]; + public readonly privateSubnetIds: string[]; + + constructor(scope: Construct, id: string) { + super(scope, id); + + const environment = new Environment(this, 'Environment'); + + this.clusterName = environment.cluster.clusterName; + this.vpcId = environment.vpc.vpcId; + this.privateSubnetIds = environment.vpc.privateSubnets.map(m => m.subnetId); + this.publicSubnetIds = environment.vpc.publicSubnets.map(m => m.subnetId); + } +} + +class TestStack extends Stack { + constructor(scope: Construct, id: string) { + super(scope, id); + // Create a nested stack with the shared resources + const resourceStack = new ResourceStack(this, 'Resources'); + + // Import the vpc from the nested stack + const vpc = Vpc.fromVpcAttributes(this, 'Vpc', { + availabilityZones: resourceStack.availabilityZones, + vpcId: resourceStack.vpcId, + privateSubnetIds: resourceStack.privateSubnetIds, + publicSubnetIds: resourceStack.publicSubnetIds, + }); + + // Import the cluster from the nested stack + const cluster = Cluster.fromClusterAttributes(this, 'Cluster', { + clusterName: resourceStack.clusterName, + securityGroups: [], + vpc: vpc, + }); + + // Create the environment from attributes. + const environment = Environment.fromEnvironmentAttributes(this, 'Environment', { + cluster, + capacityType: EnvironmentCapacityType.FARGATE, + }); + + // Add a workload. + const serviceDescription = new ServiceDescription(); + serviceDescription.add(new Container({ + cpu: 256, + memoryMiB: 512, + trafficPort: 80, + image: ContainerImage.fromRegistry('nathanpeck/name'), + environment: { + PORT: '80', + }, + })); + serviceDescription.add(new HttpLoadBalancerExtension()); + + new Service(this, 'Service', { + environment, + serviceDescription, + }); + } +} + +const app = new App(); +new TestStack(app, 'imported-environment-integ'); + +/** + * Expect this stack to deploy and show a load balancer DNS address. When you + * request the address with curl, you should see the name container's output. + * The load balancer may response 503 Service Temporarily Unavailable for a + * short while, before you can see the container output. + * + * Example: + * ``` + * $ cdk --app 'node integ.imported-environment.js' deploy + * ... + * Outputs: + * shared-cluster-integ.Serviceloadbalancerdnsoutput = share-Servi-6JALU1FDE36L-2093347098.us-east-1.elb.amazonaws.com + * ... + * + * $ curl share-Servi-6JALU1FDE36L-2093347098.us-east-1.elb.amazonaws.com + * Keira (ip-10-0-153-44.ec2.internal) + * ``` + */ diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json index baffae9541166..1d41a6913794e 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/integ.multiple-environments.expected.json @@ -1186,7 +1186,7 @@ { "Ref": "AWS::URLSuffix" }, - "/aws-appmesh-envoy:v1.15.0.0-prod" + "/aws-appmesh-envoy:v1.15.1.0-prod" ] ] }, @@ -1710,7 +1710,7 @@ { "Ref": "AWS::URLSuffix" }, - "/aws-appmesh-envoy:v1.15.0.0-prod" + "/aws-appmesh-envoy:v1.15.1.0-prod" ] ] }, @@ -2142,7 +2142,7 @@ "ecrRepo": "840364872350" }, "eu-south-1": { - "ecrRepo": "840364872350" + "ecrRepo": "422531588944" }, "eu-west-1": { "ecrRepo": "840364872350" @@ -2201,7 +2201,7 @@ "ecrRepo": "840364872350" }, "eu-south-1": { - "ecrRepo": "840364872350" + "ecrRepo": "422531588944" }, "eu-west-1": { "ecrRepo": "840364872350" @@ -2235,4 +2235,4 @@ } } } -} \ No newline at end of file +} diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.appmesh.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.appmesh.ts index 3c34c99ff8f18..a221b9d31933e 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.appmesh.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.appmesh.ts @@ -132,7 +132,7 @@ export = { { Ref: 'AWS::URLSuffix', }, - '/aws-appmesh-envoy:v1.15.0.0-prod', + '/aws-appmesh-envoy:v1.15.1.0-prod', ], ], }, diff --git a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.environment.ts b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.environment.ts index cb19e71de1d82..d029f81e34bc6 100644 --- a/packages/@aws-cdk-containers/ecs-service-extensions/test/test.environment.ts +++ b/packages/@aws-cdk-containers/ecs-service-extensions/test/test.environment.ts @@ -217,4 +217,28 @@ export = { test.done(); }, -}; \ No newline at end of file + 'should be able to create an environment from attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + cluster.addCapacity('DefaultAutoScalingGroup', { + instanceType: new ec2.InstanceType('t2.micro'), + }); + + // WHEN + const environment = Environment.fromEnvironmentAttributes(stack, 'Environment', { + capacityType: EnvironmentCapacityType.EC2, + cluster: cluster, + }); + + // THEN + test.equal(environment.capacityType, EnvironmentCapacityType.EC2); + test.equal(environment.cluster, cluster); + test.equal(environment.vpc, vpc); + test.equal(environment.id, 'Environment'); + + test.done(); + }, +}; diff --git a/packages/@aws-cdk/app-delivery/package.json b/packages/@aws-cdk/app-delivery/package.json index 80f78b742787a..e0a95c03879eb 100644 --- a/packages/@aws-cdk/app-delivery/package.json +++ b/packages/@aws-cdk/app-delivery/package.json @@ -63,7 +63,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^2.6.0", + "fast-check": "^2.6.1", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/assert/package.json b/packages/@aws-cdk/assert/package.json index 5e320f5b393de..cb651d3ba12df 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -23,9 +23,9 @@ "devDependencies": { "@types/jest": "^26.0.15", "cdk-build-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.4.3" + "ts-jest": "^26.4.4" }, "dependencies": { "@aws-cdk/cloud-assembly-schema": "0.0.0", @@ -37,7 +37,7 @@ "peerDependencies": { "@aws-cdk/core": "0.0.0", "constructs": "^3.2.0", - "jest": "^26.6.1" + "jest": "^26.6.3" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts index 84722c8811864..f48a01193385f 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/api-key.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/api-key.ts @@ -3,7 +3,7 @@ import { IResource as IResourceBase, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApiKey } from './apigateway.generated'; import { ResourceOptions } from './resource'; -import { RestApi } from './restapi'; +import { IRestApi } from './restapi'; import { QuotaSettings, ThrottleSettings, UsagePlan, UsagePlanPerApiStage } from './usage-plan'; /** @@ -47,11 +47,10 @@ export interface ApiKeyOptions extends ResourceOptions { */ export interface ApiKeyProps extends ApiKeyOptions { /** - * [disable-awslint:ref-via-interface] * A list of resources this api key is associated with. * @default none */ - readonly resources?: RestApi[]; + readonly resources?: IRestApi[]; /** * An AWS Marketplace customer identifier to use when integrating with the AWS SaaS Marketplace. @@ -183,12 +182,12 @@ export class ApiKey extends ApiKeyBase { }); } - private renderStageKeys(resources: RestApi[] | undefined): CfnApiKey.StageKeyProperty[] | undefined { + private renderStageKeys(resources: IRestApi[] | undefined): CfnApiKey.StageKeyProperty[] | undefined { if (!resources) { return undefined; } - return resources.map((resource: RestApi) => { + return resources.map((resource: IRestApi) => { const restApi = resource; const restApiId = restApi.restApiId; const stageName = restApi.deploymentStage!.stageName.toString(); diff --git a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts index 1235b3e0e32fb..82f7696fe2fa2 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/authorizers/lambda.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Duration, Lazy, Stack } from '@aws-cdk/core'; +import { Duration, Lazy, Names, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnAuthorizer } from '../apigateway.generated'; import { Authorizer, IAuthorizer } from '../authorizer'; @@ -13,7 +13,7 @@ export interface LambdaAuthorizerProps { /** * An optional human friendly name for the authorizer. Note that, this is not the primary identifier of the authorizer. * - * @default this.node.uniqueId + * @default - the unique construcrt ID */ readonly authorizerName?: string; @@ -97,7 +97,7 @@ abstract class LambdaAuthorizer extends Authorizer implements IAuthorizer { */ protected setupPermissions() { if (!this.role) { - this.handler.addPermission(`${this.node.uniqueId}:Permissions`, { + this.handler.addPermission(`${Names.uniqueId(this)}:Permissions`, { principal: new iam.ServicePrincipal('apigateway.amazonaws.com'), sourceArn: this.authorizerArn, }); @@ -168,7 +168,7 @@ export class TokenAuthorizer extends LambdaAuthorizer { const restApiId = this.lazyRestApiId(); const resource = new CfnAuthorizer(this, 'Resource', { - name: props.authorizerName ?? this.node.uniqueId, + name: props.authorizerName ?? Names.uniqueId(this), restApiId, type: 'TOKEN', authorizerUri: lambdaAuthorizerArn(props.handler), @@ -230,7 +230,7 @@ export class RequestAuthorizer extends LambdaAuthorizer { const restApiId = this.lazyRestApiId(); const resource = new CfnAuthorizer(this, 'Resource', { - name: props.authorizerName ?? this.node.uniqueId, + name: props.authorizerName ?? Names.uniqueId(this), restApiId, type: 'REQUEST', authorizerUri: lambdaAuthorizerArn(props.handler), diff --git a/packages/@aws-cdk/aws-apigateway/lib/domain-name.ts b/packages/@aws-cdk/aws-apigateway/lib/domain-name.ts index 3252bb1691307..0437b986fdc74 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/domain-name.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/domain-name.ts @@ -1,6 +1,6 @@ import * as acm from '@aws-cdk/aws-certificatemanager'; import { IBucket } from '@aws-cdk/aws-s3'; -import { IResource, Resource, Token } from '@aws-cdk/core'; +import { IResource, Names, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDomainName } from './apigateway.generated'; import { BasePathMapping, BasePathMappingOptions } from './base-path-mapping'; @@ -147,7 +147,7 @@ export class DomainName extends Resource implements IDomainName { */ public addBasePathMapping(targetApi: IRestApi, options: BasePathMappingOptions = { }) { const basePath = options.basePath || '/'; - const id = `Map:${basePath}=>${targetApi.node.uniqueId}`; + const id = `Map:${basePath}=>${Names.nodeUniqueId(targetApi.node)}`; return new BasePathMapping(this, id, { domainName: this, restApi: targetApi, diff --git a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts index 91c1c9da97d64..e0d6953707e82 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/integrations/lambda.ts @@ -1,6 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Lazy, Token } from '@aws-cdk/core'; +import { Lazy, Names, Token } from '@aws-cdk/core'; import { IntegrationConfig, IntegrationOptions } from '../integration'; import { Method } from '../method'; import { AwsIntegration } from './aws'; @@ -56,7 +56,7 @@ export class LambdaIntegration extends AwsIntegration { const bindResult = super.bind(method); const principal = new iam.ServicePrincipal('apigateway.amazonaws.com'); - const desc = `${method.api.node.uniqueId}.${method.httpMethod}.${method.resource.path.replace(/\//g, '.')}`; + const desc = `${Names.nodeUniqueId(method.api.node)}.${method.httpMethod}.${method.resource.path.replace(/\//g, '.')}`; this.handler.addPermission(`ApiPermission.${desc}`, { principal, diff --git a/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts b/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts index b320031edf499..66b9c1b026203 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/json-schema.ts @@ -35,6 +35,12 @@ export interface JsonSchema { readonly title?: string; readonly description?: string; readonly 'enum'?: any[]; + /** + * The default value if you use an enum. + * + * @default - not set + */ + readonly default?: any; readonly format?: string; readonly definitions?: { [name: string]: JsonSchema }; diff --git a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts index a4b8f6e2ee389..af2e0b2b3da01 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/restapi.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/restapi.ts @@ -378,6 +378,16 @@ export abstract class RestApiBase extends Resource implements IRestApi { }); } + /** + * Add an ApiKey + */ + public addApiKey(id: string, options?: ApiKeyOptions): IApiKey { + return new ApiKey(this, id, { + resources: [this], + ...options, + }); + } + /** * Returns the given named metric for this API */ @@ -706,16 +716,6 @@ export class RestApi extends RestApiBase { return this.urlForPath(); } - /** - * Add an ApiKey - */ - public addApiKey(id: string, options?: ApiKeyOptions): IApiKey { - return new ApiKey(this, id, { - resources: [this], - ...options, - }); - } - /** * Adds a new model. */ diff --git a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts index 6e1c5a4266a9e..e39efd410fe80 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/usage-plan.ts @@ -1,4 +1,4 @@ -import { Lazy, Resource, Token } from '@aws-cdk/core'; +import { Lazy, Names, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IApiKey } from './api-key'; import { CfnUsagePlan, CfnUsagePlanKey } from './apigateway.generated'; @@ -182,7 +182,7 @@ export class UsagePlan extends Resource { const prefix = 'UsagePlanKeyResource'; // Postfixing apikey id only from the 2nd child, to keep physicalIds of UsagePlanKey for existing CDK apps unmodifed. - const id = this.node.tryFindChild(prefix) ? `${prefix}:${apiKey.node.uniqueId}` : prefix; + const id = this.node.tryFindChild(prefix) ? `${prefix}:${Names.nodeUniqueId(apiKey.node)}` : prefix; new CfnUsagePlanKey(this, id, { keyId: apiKey.keyId, diff --git a/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts index 700d22c137cc1..dc7576b22961c 100644 --- a/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts +++ b/packages/@aws-cdk/aws-apigateway/lib/vpc-link.ts @@ -1,5 +1,5 @@ import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; -import { IResource, Lazy, Resource } from '@aws-cdk/core'; +import { IResource, Lazy, Names, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVpcLink } from './apigateway.generated'; @@ -66,7 +66,7 @@ export class VpcLink extends Resource implements IVpcLink { constructor(scope: Construct, id: string, props: VpcLinkProps = {}) { super(scope, id, { physicalName: props.vpcLinkName || - Lazy.stringValue({ produce: () => this.node.uniqueId }), + Lazy.stringValue({ produce: () => Names.nodeUniqueId(this.node) }), }); const cfnResource = new CfnVpcLink(this, 'Resource', { diff --git a/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts b/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts index 0617ae6f8eeaf..d8d0fdbcfd030 100644 --- a/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/restapi.test.ts @@ -918,6 +918,34 @@ describe('restapi', () => { }, }); }); + + test('addApiKey is supported', () => { + // GIVEN + const stack = new Stack(); + const api = new apigw.SpecRestApi(stack, 'myapi', { + apiDefinition: apigw.ApiDefinition.fromInline({ foo: 'bar' }), + }); + api.root.addMethod('OPTIONS'); + + // WHEN + api.addApiKey('myapikey', { + apiKeyName: 'myApiKey1', + value: '01234567890ABCDEFabcdef', + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGateway::ApiKey', { + Enabled: true, + Name: 'myApiKey1', + StageKeys: [ + { + RestApiId: { Ref: 'myapi162F20B8' }, + StageName: { Ref: 'myapiDeploymentStageprod329F21FF' }, + }, + ], + Value: '01234567890ABCDEFabcdef', + }); + }); }); describe('Metrics', () => { diff --git a/packages/@aws-cdk/aws-apigateway/test/util.test.ts b/packages/@aws-cdk/aws-apigateway/test/util.test.ts index f879c63698733..18352d21f5b0f 100644 --- a/packages/@aws-cdk/aws-apigateway/test/util.test.ts +++ b/packages/@aws-cdk/aws-apigateway/test/util.test.ts @@ -121,5 +121,21 @@ describe('util', () => { required: ['ref'], }); }); + + test('"default" for enum', () => { + const schema: JsonSchema = { + type: JsonSchemaType.STRING, + enum: ['green', 'blue', 'red'], + default: 'blue', + }; + + const actual = JsonSchemaMapper.toCfnJsonSchema(schema); + expect(actual).toEqual({ + $schema: 'http://json-schema.org/draft-04/schema#', + type: 'string', + enum: ['green', 'blue', 'red'], + default: 'blue', + }); + }); }); }); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json index 06d7c8376c1ba..3cca63ba579e2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json @@ -91,7 +91,7 @@ "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", "@aws-cdk/aws-servicediscovery": "0.0.0", "@aws-cdk/core": "0.0.0", - "constructs": "^3.0.4" + "constructs": "^3.2.0" }, "engines": { "node": ">= 10.13.0 <13 || >=13.7.0" diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index 208271df2a732..c25027fc81c39 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -59,6 +59,8 @@ integrations](https://docs.aws.amazon.com/apigateway/latest/developerguide/http- The code snippet below configures a route `GET /books` with an HTTP proxy integration and uses the `ANY` method to proxy all other HTTP method calls to `/books` to a lambda proxy. +The URL to the endpoint can be retrieved via the `apiEndpoint` attribute. + ```ts const getBooksIntegration = new HttpProxyIntegration({ url: 'https://get-books-proxy.myproxy.internal', diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index 8ea35ab4f1312..3bc5f47676339 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -255,6 +255,12 @@ export class HttpApi extends HttpApiBase { public readonly httpApiId: string; + /** + * The default endpoint for an API + * @attribute + */ + public readonly apiEndpoint: string; + /** * default stage of the api resource */ @@ -298,6 +304,7 @@ export class HttpApi extends HttpApiBase { const resource = new CfnApi(this, 'Resource', apiProps); this.httpApiId = resource.ref; + this.apiEndpoint = resource.attrApiEndpoint; if (props?.defaultIntegration) { new HttpRoute(this, 'DefaultRoute', { diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts index 30973a10b2ca0..9f0d4f89ee6f6 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integrations/lambda.ts @@ -1,6 +1,6 @@ import { ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; -import { Stack } from '@aws-cdk/core'; +import { Names, Stack } from '@aws-cdk/core'; import { HttpIntegrationType, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, IHttpRouteIntegration, PayloadFormatVersion } from '../integration'; /** @@ -30,7 +30,7 @@ export class LambdaProxyIntegration implements IHttpRouteIntegration { public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { const route = options.route; - this.props.handler.addPermission(`${route.node.uniqueId}-Permission`, { + this.props.handler.addPermission(`${Names.nodeUniqueId(route.node)}-Permission`, { scope: options.scope, principal: new ServicePrincipal('apigateway.amazonaws.com'), sourceArn: Stack.of(route).formatArn({ diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index c62cf72f5d918..07b768f848bdd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -108,8 +108,7 @@ "props-physical-name-type:@aws-cdk/aws-apigatewayv2.HttpStageProps.stageName", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpApiMappingProps", "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpIntegrationProps", - "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps", - "resource-attribute:@aws-cdk/aws-apigatewayv2.HttpApi.apiEndpoint" + "props-physical-name:@aws-cdk/aws-apigatewayv2.HttpRouteProps" ] }, "stability": "experimental", diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts index bfc9c9102a011..96c747985be04 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -273,4 +273,11 @@ describe('HttpApi', () => { Name: 'Link-2', }); }); + + test('apiEndpoint is exported', () => { + const stack = new Stack(); + const api = new HttpApi(stack, 'api'); + + expect(api.apiEndpoint).toBeDefined(); + }); }); diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-action.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-action.ts index 44eb88e9475a5..95242ed9e8cdf 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-action.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/step-scaling-action.ts @@ -82,7 +82,7 @@ export class StepScalingAction extends cdk.Construct { // properties, or the ScalingTargetId property, but not both. // https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-resource-applicationautoscaling-scalingpolicy.html const resource = new CfnScalingPolicy(this, 'Resource', { - policyName: props.policyName || this.node.uniqueId, + policyName: props.policyName || cdk.Names.uniqueId(this), policyType: 'StepScaling', scalingTargetId: props.scalingTarget.scalableTargetId, stepScalingPolicyConfiguration: { diff --git a/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts b/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts index 5270177629f2e..65146b754757b 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts +++ b/packages/@aws-cdk/aws-applicationautoscaling/lib/target-tracking-scaling-policy.ts @@ -133,7 +133,7 @@ export class TargetTrackingScalingPolicy extends cdk.Construct { super(scope, id); const resource = new CfnScalingPolicy(this, 'Resource', { - policyName: props.policyName || this.node.uniqueId, + policyName: props.policyName || cdk.Names.uniqueId(this), policyType: 'TargetTrackingScaling', scalingTargetId: props.scalingTarget.scalableTargetId, targetTrackingScalingPolicyConfiguration: { diff --git a/packages/@aws-cdk/aws-applicationautoscaling/package.json b/packages/@aws-cdk/aws-applicationautoscaling/package.json index 2bbdff68e0c78..3072fda41400d 100644 --- a/packages/@aws-cdk/aws-applicationautoscaling/package.json +++ b/packages/@aws-cdk/aws-applicationautoscaling/package.json @@ -75,7 +75,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^2.6.0", + "fast-check": "^2.6.1", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index e38a8ea49a9e9..9c0a4b7f1da2e 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -53,22 +53,21 @@ const mesh = new Mesh(stack, 'AppMesh', { ## Adding VirtualRouters -The `Mesh` needs `VirtualRouters` as logical units to route to `VirtualNodes`. +The _Mesh_ needs _VirtualRouters_ as logical units to route requests to _VirtualNodes_. -Virtual routers handle traffic for one or more virtual services within your mesh. After you create a virtual router, you can create and associate routes for your virtual router that direct incoming requests to different virtual nodes. +Virtual routers handle traffic for one or more virtual services within your mesh. +After you create a virtual router, you can create and associate routes to your virtual router that direct incoming requests to different virtual nodes. ```typescript const router = mesh.addVirtualRouter('router', { - listener: { - portMapping: { - port: 8081, - protocol: Protocol.HTTP, - } - } + listeners: [ appmesh.VirtualRouterListener.http(8080) ], }); ``` -The router can also be created using the constructor and passing in the mesh instead of calling the addVirtualRouter() method for the mesh. +The router can also be created using the constructor and passing in the mesh instead of calling the `addVirtualRouter()` method for the mesh. +The same pattern applies to all constructs within the appmesh library, for any mesh.addXZY method, a new constuctor can also be used. +This is particularly useful for cross stack resources are required. +Where creating the `mesh` as part of an infrastructure stack and creating the `resources` such as `nodes` is more useful to keep in the application stack. ```typescript const mesh = new Mesh(stack, 'AppMesh', { @@ -78,18 +77,15 @@ const mesh = new Mesh(stack, 'AppMesh', { const router = new VirtualRouter(stack, 'router', { mesh, // notice that mesh is a required property when creating a router with a new statement - listener: { - portMapping: { - port: 8081, - protocol: Protocol.HTTP, - } + listeners: [ appmesh.VirtualRouterListener.http(8081) ] } }); ``` -The listener protocol can be either `HTTP` or `TCP`. - -The same pattern applies to all constructs within the appmesh library, for any mesh.addXZY method, a new constuctor can also be used. This is particularly useful for cross stack resources are required. Where creating the `mesh` as part of an infrastructure stack and creating the `resources` such as `nodes` is more useful to keep in the application stack. +The _VirtualRouterListener_ class provides an easy interface for defining new protocol specific listeners. +The `http()`, `http2()`, `grpc()` and `tcp()` methods are available for use. +They accept a single port parameter, that is used to define what port to match requests on. +The port parameter can be omitted, and it will default to port 8080. ## Adding VirtualService @@ -307,3 +303,32 @@ gateway.addGatewayRoute('gateway-route-grpc', { }), }); ``` + +## Importing Resources + +Each mesh resource comes with two static methods for importing a reference to an existing App Mesh resource. +These imported resources can be used as references for other resources in your mesh. +There are two static methods, `fromArn` and `fromAttributes` where the `` is replaced with the resource name. + +```typescript +const arn = "arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh/virtualNode/testNode"; +appmesh.VirtualNode.fromVirtualNodeArn(stack, 'importedVirtualNode', arn); +``` + +```typescript +appmesh.VirtualNode.fromVirtualNodeAttributes(stack, 'imported-virtual-node', { + mesh: appmesh.Mesh.fromMeshName(stack, 'Mesh', 'testMesh'), + virtualNodeName: virtualNodeName, +}); +``` + +To import a mesh, there are two static methods, `fromMeshArn` and `fromMeshName`. + +```typescript +const arn = 'arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh'; +appmesh.Mesh.fromMeshArn(stack, 'imported-mesh', arn); +``` + +```typescript +appmesh.Mesh.fromMeshName(stack, 'imported-mesh', 'abc'); +``` diff --git a/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts b/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts index cc00c0f632ac3..5bfb3b0ca21cd 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts @@ -65,7 +65,26 @@ export class GatewayRoute extends cdk.Resource implements IGatewayRoute { * Import an existing GatewayRoute given an ARN */ public static fromGatewayRouteArn(scope: Construct, id: string, gatewayRouteArn: string): IGatewayRoute { - return new ImportedGatewayRoute(scope, id, { gatewayRouteArn }); + return new class extends cdk.Resource implements IGatewayRoute { + readonly gatewayRouteArn = gatewayRouteArn; + readonly gatewayRouteName = cdk.Fn.select(4, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(gatewayRouteArn).resourceName!)); + readonly virtualGateway = VirtualGateway.fromVirtualGatewayArn(this, 'virtualGateway', gatewayRouteArn); + }(scope, id); + } + + /** + * Import an existing GatewayRoute given attributes + */ + public static fromGatewayRouteAttributes(scope: Construct, id: string, attrs: GatewayRouteAttributes): IGatewayRoute { + return new class extends cdk.Resource implements IGatewayRoute { + readonly gatewayRouteName = attrs.gatewayRouteName; + readonly gatewayRouteArn = cdk.Stack.of(scope).formatArn({ + service: 'appmesh', + resource: `mesh/${attrs.virtualGateway.mesh.meshName}/virtualGateway/${attrs.virtualGateway.virtualGatewayName}/gatewayRoute`, + resourceName: this.gatewayRouteName, + }); + readonly virtualGateway = attrs.virtualGateway; + }(scope, id); } /** @@ -114,55 +133,14 @@ export class GatewayRoute extends cdk.Resource implements IGatewayRoute { /** * Interface with properties necessary to import a reusable GatewayRoute */ -interface GatewayRouteAttributes { +export interface GatewayRouteAttributes { /** * The name of the GatewayRoute */ - readonly gatewayRouteName?: string; + readonly gatewayRouteName: string; /** - * The Amazon Resource Name (ARN) for the GatewayRoute + * The VirtualGateway this GatewayRoute is associated with. */ - readonly gatewayRouteArn?: string; - - /** - * The name of the mesh this GatewayRoute is associated with - */ - readonly meshName?: string; - - /** - * The name of the Virtual Gateway this GatewayRoute is associated with - */ - readonly virtualGateway?: IVirtualGateway; -} - -/** - * Represents an imported IGatewayRoute - */ -class ImportedGatewayRoute extends cdk.Resource implements IGatewayRoute { - /** - * The name of the GatewayRoute - */ - public gatewayRouteName: string; - - /** - * The Amazon Resource Name (ARN) for the GatewayRoute - */ - public gatewayRouteArn: string; - - /** - * The VirtualGateway the GatewayRoute belongs to - */ - public virtualGateway: IVirtualGateway; - - constructor(scope: Construct, id: string, props: GatewayRouteAttributes) { - super(scope, id); - if (props.gatewayRouteArn) { - this.gatewayRouteArn = props.gatewayRouteArn; - this.gatewayRouteName = cdk.Fn.select(4, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.gatewayRouteArn).resourceName!)); - this.virtualGateway = VirtualGateway.fromVirtualGatewayArn(this, 'virtualGateway', props.gatewayRouteArn); - } else { - throw new Error('Need gatewayRouteArn'); - } - } + readonly virtualGateway: IVirtualGateway; } diff --git a/packages/@aws-cdk/aws-appmesh/lib/index.ts b/packages/@aws-cdk/aws-appmesh/lib/index.ts index 69cdfd8d2e3c6..d95c017c2071e 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/index.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/index.ts @@ -5,6 +5,7 @@ export * from './route'; export * from './shared-interfaces'; export * from './virtual-node'; export * from './virtual-router'; +export * from './virtual-router-listener'; export * from './virtual-service'; export * from './virtual-gateway'; export * from './virtual-gateway-listener'; diff --git a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts index 961128151b537..869d2198fdd0c 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts @@ -186,7 +186,7 @@ export class Mesh extends MeshBase { constructor(scope: Construct, id: string, props: MeshProps = {}) { super(scope, id, { - physicalName: props.meshName || cdk.Lazy.stringValue({ produce: () => this.node.uniqueId }), + physicalName: props.meshName || cdk.Lazy.stringValue({ produce: () => cdk.Names.uniqueId(this) }), }); const mesh = new CfnMesh(this, 'Resource', { diff --git a/packages/@aws-cdk/aws-appmesh/lib/route.ts b/packages/@aws-cdk/aws-appmesh/lib/route.ts index 0dfeac897375a..cfa0e8e6d54c1 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route.ts @@ -3,7 +3,7 @@ import { Construct } from 'constructs'; import { CfnRoute } from './appmesh.generated'; import { IMesh } from './mesh'; import { IVirtualNode } from './virtual-node'; -import { IVirtualRouter } from './virtual-router'; +import { IVirtualRouter, VirtualRouter } from './virtual-router'; /** * Interface for which all Route based classes MUST implement @@ -22,6 +22,11 @@ export interface IRoute extends cdk.IResource { * @attribute */ readonly routeArn: string; + + /** + * The VirtualRouter the Route belongs to + */ + readonly virtualRouter: IVirtualRouter; } /** @@ -99,7 +104,7 @@ export interface RouteProps extends RouteBaseProps { readonly mesh: IMesh; /** - * The virtual router in which to define the route + * The VirtualRouter the Route belongs to */ readonly virtualRouter: IVirtualRouter; } @@ -111,21 +116,33 @@ export interface RouteProps extends RouteBaseProps { */ export class Route extends cdk.Resource implements IRoute { /** - * Import an existing route given an ARN + * Import an existing Route given an ARN */ public static fromRouteArn(scope: Construct, id: string, routeArn: string): IRoute { - return new ImportedRoute(scope, id, { routeArn }); + return new class extends cdk.Resource implements IRoute { + readonly routeArn = routeArn; + readonly virtualRouter = VirtualRouter.fromVirtualRouterArn(this, 'VirtualRouter', routeArn); + readonly routeName = cdk.Fn.select(4, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(routeArn).resourceName!)); + }(scope, id); } /** - * Import an existing route given its name + * Import an existing Route given attributes */ - public static fromRouteName(scope: Construct, id: string, meshName: string, virtualRouterName: string, routeName: string): IRoute { - return new ImportedRoute(scope, id, { meshName, virtualRouterName, routeName }); + public static fromRouteAttributes(scope: Construct, id: string, attrs: RouteAttributes): IRoute { + return new class extends cdk.Resource implements IRoute { + readonly routeName = attrs.routeName; + readonly virtualRouter = attrs.virtualRouter; + readonly routeArn = cdk.Stack.of(this).formatArn({ + service: 'appmesh', + resource: `mesh/${attrs.virtualRouter.mesh.meshName}/virtualRouter/${attrs.virtualRouter.virtualRouterName}/route`, + resourceName: this.routeName, + }); + }(scope, id); } /** - * The name of the route + * The name of the Route */ public readonly routeName: string; @@ -135,7 +152,7 @@ export class Route extends cdk.Resource implements IRoute { public readonly routeArn: string; /** - * The virtual router this route is a part of + * The VirtualRouter the Route belongs to */ public readonly virtualRouter: IVirtualRouter; @@ -145,7 +162,7 @@ export class Route extends cdk.Resource implements IRoute { constructor(scope: Construct, id: string, props: RouteProps) { super(scope, id, { - physicalName: props.routeName || cdk.Lazy.stringValue({ produce: () => this.node.uniqueId }), + physicalName: props.routeName || cdk.Lazy.stringValue({ produce: () => cdk.Names.uniqueId(this) }), }); this.virtualRouter = props.virtualRouter; @@ -215,57 +232,14 @@ export class Route extends cdk.Resource implements IRoute { /** * Interface with properties ncecessary to import a reusable Route */ -interface RouteAttributes { - /** - * The name of the route - */ - readonly routeName?: string; - - /** - * The Amazon Resource Name (ARN) for the route - */ - readonly routeArn?: string; - - /** - * The name of the mesh this route is associated with - */ - readonly meshName?: string; - - /** - * The name of the virtual router this route is associated with - */ - readonly virtualRouterName?: string; -} - -/** - * Represents and imported IRoute - */ -class ImportedRoute extends cdk.Resource implements IRoute { +export interface RouteAttributes { /** - * The name of the route + * The name of the Route */ - public readonly routeName: string; + readonly routeName: string; /** - * The Amazon Resource Name (ARN) for the route + * The VirtualRouter the Route belongs to */ - public readonly routeArn: string; - - constructor(scope: Construct, id: string, props: RouteAttributes) { - super(scope, id); - - if (props.routeArn) { - this.routeArn = props.routeArn; - this.routeName = cdk.Fn.select(4, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.routeArn).resourceName!)); - } else if (props.routeName && props.meshName && props.virtualRouterName) { - this.routeName = props.routeName; - this.routeArn = cdk.Stack.of(this).formatArn({ - service: 'appmesh', - resource: `mesh/${props.meshName}/virtualRouter/${props.virtualRouterName}/route`, - resourceName: this.routeName, - }); - } else { - throw new Error('Need either arn or three names'); - } - } + readonly virtualRouter: IVirtualRouter; } diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts index 51950f82dbb13..337807c9153da 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts @@ -26,7 +26,7 @@ export interface IVirtualGateway extends cdk.IResource { readonly virtualGatewayArn: string; /** - * The mesh which the VirtualGateway belongs to + * The Mesh which the VirtualGateway belongs to */ readonly mesh: IMesh; @@ -67,7 +67,7 @@ export interface VirtualGatewayBaseProps { */ export interface VirtualGatewayProps extends VirtualGatewayBaseProps { /** - * The mesh which the VirtualGateway belongs to + * The Mesh which the VirtualGateway belongs to */ readonly mesh: IMesh; } @@ -84,7 +84,7 @@ abstract class VirtualGatewayBase extends cdk.Resource implements IVirtualGatewa public abstract readonly virtualGatewayArn: string; /** - * The name of the mesh which the VirtualGateway belongs to + * The Mesh which the VirtualGateway belongs to */ public abstract readonly mesh: IMesh; @@ -112,7 +112,27 @@ export class VirtualGateway extends VirtualGatewayBase { * Import an existing VirtualGateway given an ARN */ public static fromVirtualGatewayArn(scope: Construct, id: string, virtualGatewayArn: string): IVirtualGateway { - return new ImportedVirtualGateway(scope, id, { virtualGatewayArn }); + return new class extends VirtualGatewayBase { + private readonly parsedArn = cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(virtualGatewayArn).resourceName!); + readonly mesh = Mesh.fromMeshName(this, 'Mesh', cdk.Fn.select(0, this.parsedArn)); + readonly virtualGatewayArn = virtualGatewayArn; + readonly virtualGatewayName = cdk.Fn.select(2, this.parsedArn); + }(scope, id); + } + + /** + * Import an existing VirtualGateway given its attributes + */ + public static fromVirtualGatewayAttributes(scope: Construct, id: string, attrs: VirtualGatewayAttributes): IVirtualGateway { + return new class extends VirtualGatewayBase { + readonly mesh = attrs.mesh; + readonly virtualGatewayName = attrs.virtualGatewayName; + readonly virtualGatewayArn = cdk.Stack.of(this).formatArn({ + service: 'appmesh', + resource: `mesh/${attrs.mesh.meshName}/virtualGateway`, + resourceName: this.virtualGatewayName, + }); + }(scope, id); } /** @@ -171,56 +191,14 @@ export class VirtualGateway extends VirtualGatewayBase { /** * Unterface with properties necessary to import a reusable VirtualGateway */ -interface VirtualGatewayAttributes { - /** - * The name of the VirtualGateway - */ - readonly virtualGatewayName?: string; - - /** - * The Amazon Resource Name (ARN) belonging to the VirtualGateway - */ - readonly virtualGatewayArn?: string; - - /** - * The Mesh that the VirtualGateway belongs to - */ - readonly mesh?: IMesh; - - /** - * The name of the mesh that the VirtualGateway belongs to - */ - readonly meshName?: string; -} - -/** - * Used to import a VirtualGateway and read its properties - */ -class ImportedVirtualGateway extends VirtualGatewayBase { +export interface VirtualGatewayAttributes { /** * The name of the VirtualGateway */ - public readonly virtualGatewayName: string; - - /** - * The Amazon Resource Name (ARN) belonging to the VirtualGateway - */ - public readonly virtualGatewayArn: string; + readonly virtualGatewayName: string; /** * The Mesh that the VirtualGateway belongs to */ - public readonly mesh: IMesh; - - constructor(scope: Construct, id: string, props: VirtualGatewayAttributes) { - super(scope, id); - if (props.virtualGatewayArn) { - const meshName = cdk.Fn.select(0, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.virtualGatewayArn).resourceName!)); - this.mesh = Mesh.fromMeshName(this, 'Mesh', meshName); - this.virtualGatewayArn = props.virtualGatewayArn; - this.virtualGatewayName = cdk.Fn.select(2, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.virtualGatewayArn).resourceName!)); - } else { - throw new Error('Need virtualGatewayArn'); - } - } + readonly mesh: IMesh; } diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts index bfad388fa91ee..b8b9da2026715 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts @@ -2,7 +2,7 @@ import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualNode } from './appmesh.generated'; -import { IMesh } from './mesh'; +import { IMesh, Mesh } from './mesh'; import { validateHealthChecks } from './private/utils'; import { AccessLog, HealthCheck, PortMapping, Protocol, VirtualNodeListener } from './shared-interfaces'; import { IVirtualService } from './virtual-service'; @@ -19,7 +19,7 @@ export interface IVirtualNode extends cdk.IResource { readonly virtualNodeName: string; /** - * The Amazon Resource Name belonging to the VirtualNdoe + * The Amazon Resource Name belonging to the VirtualNode * * Set this value as the APPMESH_VIRTUAL_NODE_NAME environment variable for * your task group's Envoy proxy container in your task definition or pod @@ -29,6 +29,11 @@ export interface IVirtualNode extends cdk.IResource { */ readonly virtualNodeArn: string; + /** + * The Mesh which the VirtualNode belongs to + */ + readonly mesh: IMesh; + /** * Utility method to add backends for existing or new VirtualNodes */ @@ -105,7 +110,7 @@ export interface VirtualNodeBaseProps { */ export interface VirtualNodeProps extends VirtualNodeBaseProps { /** - * The name of the AppMesh which the virtual node belongs to + * The Mesh which the VirtualNode belongs to */ readonly mesh: IMesh; } @@ -117,15 +122,20 @@ abstract class VirtualNodeBase extends cdk.Resource implements IVirtualNode { public abstract readonly virtualNodeName: string; /** - * The Amazon Resource Name belonging to the VirtualNdoe + * The Amazon Resource Name belonging to the VirtualNode */ public abstract readonly virtualNodeArn: string; + /** + * The Mesh which the VirtualNode belongs to + */ + public abstract readonly mesh: IMesh; + protected readonly backends = new Array(); protected readonly listeners = new Array(); /** - * Add a Virtual Services that this node is expected to send outbound traffic to + * Add a VirtualServices that this node is expected to send outbound traffic to */ public addBackends(...props: IVirtualService[]) { for (const s of props) { @@ -197,17 +207,27 @@ export class VirtualNode extends VirtualNodeBase { * Import an existing VirtualNode given an ARN */ public static fromVirtualNodeArn(scope: Construct, id: string, virtualNodeArn: string): IVirtualNode { - return new ImportedVirtualNode(scope, id, { virtualNodeArn }); + return new class extends VirtualNodeBase { + readonly virtualNodeArn = virtualNodeArn; + private readonly parsedArn = cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(virtualNodeArn).resourceName!); + readonly mesh = Mesh.fromMeshName(this, 'Mesh', cdk.Fn.select(0, this.parsedArn)); + readonly virtualNodeName = cdk.Fn.select(2, this.parsedArn); + }(scope, id); } /** * Import an existing VirtualNode given its name */ - public static fromVirtualNodeName(scope: Construct, id: string, meshName: string, virtualNodeName: string): IVirtualNode { - return new ImportedVirtualNode(scope, id, { - meshName, - virtualNodeName, - }); + public static fromVirtualNodeAttributes(scope: Construct, id: string, attrs: VirtualNodeAttributes): IVirtualNode { + return new class extends VirtualNodeBase { + readonly mesh = attrs.mesh; + readonly virtualNodeName = attrs.virtualNodeName; + readonly virtualNodeArn = cdk.Stack.of(this).formatArn({ + service: 'appmesh', + resource: `mesh/${attrs.mesh.meshName}/virtualNode`, + resourceName: this.virtualNodeName, + }); + }(scope, id); } /** @@ -221,13 +241,13 @@ export class VirtualNode extends VirtualNodeBase { public readonly virtualNodeArn: string; /** - * The service mesh that the virtual node resides in + * The Mesh which the VirtualNode belongs to */ public readonly mesh: IMesh; constructor(scope: Construct, id: string, props: VirtualNodeProps) { super(scope, id, { - physicalName: props.virtualNodeName || cdk.Lazy.stringValue({ produce: () => this.node.uniqueId }), + physicalName: props.virtualNodeName || cdk.Lazy.stringValue({ produce: () => cdk.Names.uniqueId(this) }), }); this.mesh = props.mesh; @@ -271,54 +291,16 @@ function renderAttributes(attrs?: {[key: string]: string}) { } /** - * Interface with properties ncecessary to import a reusable VirtualNode - */ -interface VirtualNodeAttributes { - /** - * The name of the VirtualNode - */ - readonly virtualNodeName?: string; - - /** - * The Amazon Resource Name belonging to the VirtualNdoe - */ - readonly virtualNodeArn?: string; - - /** - * The service mesh that the virtual node resides in - */ - readonly meshName?: string; -} - -/** - * Used to import a VirtualNode and read its properties + * Interface with properties necessary to import a reusable VirtualNode */ -class ImportedVirtualNode extends VirtualNodeBase { +export interface VirtualNodeAttributes { /** * The name of the VirtualNode */ - public readonly virtualNodeName: string; + readonly virtualNodeName: string; /** - * The Amazon Resource Name belonging to the VirtualNdoe + * The Mesh that the VirtualNode belongs to */ - public readonly virtualNodeArn: string; - - constructor(scope: Construct, id: string, props: VirtualNodeAttributes) { - super(scope, id); - - if (props.virtualNodeArn) { - this.virtualNodeArn = props.virtualNodeArn; - this.virtualNodeName = cdk.Fn.select(2, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.virtualNodeArn).resourceName!)); - } else if (props.virtualNodeName && props.meshName) { - this.virtualNodeName = props.virtualNodeName; - this.virtualNodeArn = cdk.Stack.of(this).formatArn({ - service: 'appmesh', - resource: `mesh/${props.meshName}/virtualNode`, - resourceName: this.virtualNodeName, - }); - } else { - throw new Error('Need either arn or both names'); - } - } + readonly mesh: IMesh; } diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-router-listener.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-router-listener.ts new file mode 100644 index 0000000000000..7a5e867d0b7c9 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-router-listener.ts @@ -0,0 +1,82 @@ +import * as cdk from '@aws-cdk/core'; +import { CfnVirtualRouter } from './appmesh.generated'; +import { Protocol } from './shared-interfaces'; + +/** + * Properties for a VirtualRouter listener + */ +export interface VirtualRouterListenerConfig { + /** + * Single listener config for a VirtualRouter + */ + readonly listener: CfnVirtualRouter.VirtualRouterListenerProperty; +} + +/** + * Represents the properties needed to define listeners for a VirtualRouter + */ +export abstract class VirtualRouterListener { + /** + * Returns an HTTP Listener for a VirtualRouter + * + * @param port the optional port of the listener, 8080 by default + */ + public static http(port?: number): VirtualRouterListener { + return new VirtualRouterListenerImpl(Protocol.HTTP, port); + } + + /** + * Returns an HTTP2 Listener for a VirtualRouter + * + * @param port the optional port of the listener, 8080 by default + */ + public static http2(port?: number): VirtualRouterListener { + return new VirtualRouterListenerImpl(Protocol.HTTP2, port); + } + + /** + * Returns a GRPC Listener for a VirtualRouter + * + * @param port the optional port of the listener, 8080 by default + */ + public static grpc(port?: number): VirtualRouterListener { + return new VirtualRouterListenerImpl(Protocol.GRPC, port); + } + + /** + * Returns a TCP Listener for a VirtualRouter + * + * @param port the optional port of the listener, 8080 by default + */ + public static tcp(port?: number): VirtualRouterListener { + return new VirtualRouterListenerImpl(Protocol.TCP, port); + } + + /** + * Called when the VirtualRouterListener type is initialized. Can be used to enforce + * mutual exclusivity + */ + public abstract bind(scope: cdk.Construct): VirtualRouterListenerConfig; +} + +class VirtualRouterListenerImpl extends VirtualRouterListener { + private readonly protocol: Protocol; + private readonly port: number; + + constructor(protocol: Protocol, port?: number) { + super(); + this.protocol = protocol; + this.port = port ?? 8080; + } + + bind(_scope: cdk.Construct): VirtualRouterListenerConfig { + return { + listener: { + portMapping: { + port: this.port, + protocol: this.protocol, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts index 05fcbb29d5f71..65c3921a844fd 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts @@ -3,7 +3,7 @@ import { Construct } from 'constructs'; import { CfnVirtualRouter } from './appmesh.generated'; import { IMesh, Mesh } from './mesh'; import { Route, RouteBaseProps } from './route'; -import { PortMapping, Protocol } from './shared-interfaces'; +import { VirtualRouterListener } from './virtual-router-listener'; /** * Interface which all VirtualRouter based classes MUST implement @@ -24,7 +24,7 @@ export interface IVirtualRouter extends cdk.IResource { readonly virtualRouterArn: string; /** - * The service mesh that the virtual router resides in + * The Mesh which the VirtualRouter belongs to */ readonly mesh: IMesh; @@ -39,11 +39,11 @@ export interface IVirtualRouter extends cdk.IResource { */ export interface VirtualRouterBaseProps { /** - * Listener specification for the virtual router + * Listener specification for the VirtualRouter * * @default - A listener on HTTP port 8080 */ - readonly listener?: Listener; + readonly listeners?: VirtualRouterListener[]; /** * The name of the VirtualRouter @@ -53,16 +53,6 @@ export interface VirtualRouterBaseProps { readonly virtualRouterName?: string; } -/** - * A single listener for - */ -export interface Listener { - /** - * Listener port for the virtual router - */ - readonly portMapping: PortMapping; -} - abstract class VirtualRouterBase extends cdk.Resource implements IVirtualRouter { /** * The name of the VirtualRouter @@ -75,7 +65,7 @@ abstract class VirtualRouterBase extends cdk.Resource implements IVirtualRouter public abstract readonly virtualRouterArn: string; /** - * The AppMesh mesh the VirtualRouter belongs to + * The Mesh which the VirtualRouter belongs to */ public abstract readonly mesh: IMesh; @@ -95,11 +85,11 @@ abstract class VirtualRouterBase extends cdk.Resource implements IVirtualRouter } /** - * The properties used when creating a new VritualRouter + * The properties used when creating a new VirtualRouter */ export interface VirtualRouterProps extends VirtualRouterBaseProps { /** - * The AppMesh mesh the VirtualRouter belongs to + * The Mesh which the VirtualRouter belongs to */ readonly mesh: IMesh; } @@ -109,21 +99,27 @@ export class VirtualRouter extends VirtualRouterBase { * Import an existing VirtualRouter given an ARN */ public static fromVirtualRouterArn(scope: Construct, id: string, virtualRouterArn: string): IVirtualRouter { - return new ImportedVirtualRouter(scope, id, { virtualRouterArn }); + return new class extends VirtualRouterBase { + readonly virtualRouterArn = virtualRouterArn; + private readonly parsedArn = cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(virtualRouterArn).resourceName!); + readonly virtualRouterName = cdk.Fn.select(2, this.parsedArn); + readonly mesh = Mesh.fromMeshName(this, 'Mesh', cdk.Fn.select(0, this.parsedArn)); + }(scope, id); } /** - * Import an existing VirtualRouter given names - */ - public static fromVirtualRouterName(scope: Construct, id: string, meshName: string, virtualRouterName: string): IVirtualRouter { - return new ImportedVirtualRouter(scope, id, { meshName, virtualRouterName }); - } - - /** - * Import an existing virtual router given attributes + * Import an existing VirtualRouter given attributes */ public static fromVirtualRouterAttributes(scope: Construct, id: string, attrs: VirtualRouterAttributes): IVirtualRouter { - return new ImportedVirtualRouter(scope, id, attrs); + return new class extends VirtualRouterBase { + readonly virtualRouterName = attrs.virtualRouterName; + readonly mesh = attrs.mesh; + readonly virtualRouterArn = cdk.Stack.of(this).formatArn({ + service: 'appmesh', + resource: `mesh/${attrs.mesh.meshName}/virtualRouter`, + resourceName: this.virtualRouterName, + }); + }(scope, id); } /** @@ -137,7 +133,7 @@ export class VirtualRouter extends VirtualRouterBase { public readonly virtualRouterArn: string; /** - * The AppMesh mesh the VirtualRouter belongs to + * The Mesh which the VirtualRouter belongs to */ public readonly mesh: IMesh; @@ -145,12 +141,15 @@ export class VirtualRouter extends VirtualRouterBase { constructor(scope: Construct, id: string, props: VirtualRouterProps) { super(scope, id, { - physicalName: props.virtualRouterName || cdk.Lazy.stringValue({ produce: () => this.node.uniqueId }), + physicalName: props.virtualRouterName || cdk.Lazy.stringValue({ produce: () => cdk.Names.uniqueId(this) }), }); this.mesh = props.mesh; - - this.addListener(props.listener || { portMapping: { port: 8080, protocol: Protocol.HTTP } }); + if (props.listeners && props.listeners.length) { + props.listeners.forEach(listener => this.addListener(listener)); + } else { + this.addListener(VirtualRouterListener.http()); + } const router = new CfnVirtualRouter(this, 'Resource', { virtualRouterName: this.physicalName, @@ -171,10 +170,8 @@ export class VirtualRouter extends VirtualRouterBase { /** * Add port mappings to the router */ - private addListener(listener: Listener) { - this.listeners.push({ - portMapping: listener.portMapping, - }); + private addListener(listener: VirtualRouterListener) { + this.listeners.push(listener.bind(this).listener); } } @@ -185,75 +182,10 @@ export interface VirtualRouterAttributes { /** * The name of the VirtualRouter */ - readonly virtualRouterName?: string; - - /** - * The Amazon Resource Name (ARN) for the VirtualRouter - */ - readonly virtualRouterArn?: string; - - /** - * The AppMesh mesh the VirtualRouter belongs to - */ - readonly mesh?: IMesh; - - /** - * The name of the AppMesh mesh the VirtualRouter belongs to - */ - readonly meshName?: string; -} - -/** - * Used to import a VirtualRouter and perform actions or read properties from - */ -class ImportedVirtualRouter extends VirtualRouterBase { - /** - * The name of the VirtualRouter - */ - public readonly virtualRouterName: string; - - /** - * The Amazon Resource Name (ARN) for the VirtualRouter - */ - public readonly virtualRouterArn: string; - - private _mesh?: IMesh; - - constructor(scope: Construct, id: string, props: VirtualRouterAttributes) { - super(scope, id); - - if (props.mesh) { - this._mesh = props.mesh; - } - if (props.meshName) { - if (props.mesh) { - throw new Error('Supply either \'mesh\' or \'meshName\', but not both'); - } - this._mesh = Mesh.fromMeshName(this, 'Mesh', props.meshName); - } - - if (props.virtualRouterArn) { - this.virtualRouterArn = props.virtualRouterArn; - this.virtualRouterName = cdk.Fn.select(2, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.virtualRouterArn).resourceName!)); - } else if (props.virtualRouterName && props.meshName) { - this.virtualRouterName = props.virtualRouterName; - this.virtualRouterArn = cdk.Stack.of(this).formatArn({ - service: 'appmesh', - resource: `mesh/${props.meshName}/virtualRouter`, - resourceName: this.virtualRouterName, - }); - } else { - throw new Error('Need either arn or both names'); - } - } + readonly virtualRouterName: string; /** - * The AppMesh mesh the VirtualRouter belongs to + * The Mesh which the VirtualRouter belongs to */ - public get mesh(): IMesh { - if (!this._mesh) { - throw new Error('Please supply either \'mesh\' or \'meshName\' when calling \'fromVirtualRouterAttributes\''); - } - return this._mesh; - } + readonly mesh: IMesh; } diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts index 00b48f2d39d80..374d342040784 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts @@ -1,7 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualService } from './appmesh.generated'; -import { IMesh } from './mesh'; +import { IMesh, Mesh } from './mesh'; import { IVirtualNode } from './virtual-node'; import { IVirtualRouter } from './virtual-router'; @@ -22,6 +22,11 @@ export interface IVirtualService extends cdk.IResource { * @attribute */ readonly virtualServiceArn: string; + + /** + * The Mesh which the VirtualService belongs to + */ + readonly mesh: IMesh; } /** @@ -59,7 +64,7 @@ export interface VirtualServiceBaseProps { */ export interface VirtualServiceProps extends VirtualServiceBaseProps { /** - * The AppMesh mesh name for which the VirtualService belongs to + * The Mesh which the VirtualService belongs to */ readonly mesh: IMesh; } @@ -76,19 +81,27 @@ export class VirtualService extends cdk.Resource implements IVirtualService { * Import an existing VirtualService given an ARN */ public static fromVirtualServiceArn(scope: Construct, id: string, virtualServiceArn: string): IVirtualService { - return new ImportedVirtualService(scope, id, { - virtualServiceArn, - }); + return new class extends cdk.Resource implements IVirtualService { + readonly virtualServiceArn = virtualServiceArn; + private readonly parsedArn = cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(virtualServiceArn).resourceName!); + readonly virtualServiceName = cdk.Fn.select(2, this.parsedArn); + readonly mesh = Mesh.fromMeshName(this, 'Mesh', cdk.Fn.select(0, this.parsedArn)); + }(scope, id); } /** - * Import an existing VirtualService given mesh and service names + * Import an existing VirtualService given its attributes */ - public static fromVirtualServiceName(scope: Construct, id: string, meshName: string, virtualServiceName: string): IVirtualService { - return new ImportedVirtualService(scope, id, { - meshName, - virtualServiceName, - }); + public static fromVirtualServiceAttributes(scope: Construct, id: string, attrs: VirtualServiceAttributes): IVirtualService { + return new class extends cdk.Resource implements IVirtualService { + readonly virtualServiceName = attrs.virtualServiceName; + readonly mesh = attrs.mesh; + readonly virtualServiceArn = cdk.Stack.of(this).formatArn({ + service: 'appmesh', + resource: `mesh/${attrs.mesh.meshName}/virtualService`, + resourceName: this.virtualServiceName, + }); + }(scope, id); } /** @@ -101,12 +114,16 @@ export class VirtualService extends cdk.Resource implements IVirtualService { */ public readonly virtualServiceArn: string; + /** + * The Mesh which the VirtualService belongs to + */ + public readonly mesh: IMesh; + private readonly virtualServiceProvider?: CfnVirtualService.VirtualServiceProviderProperty; - private readonly mesh: IMesh; constructor(scope: Construct, id: string, props: VirtualServiceProps) { super(scope, id, { - physicalName: props.virtualServiceName || cdk.Lazy.stringValue({ produce: () => this.node.uniqueId }), + physicalName: props.virtualServiceName || cdk.Lazy.stringValue({ produce: () => cdk.Names.uniqueId(this) }), }); if (props.virtualNode && props.virtualRouter) { @@ -159,60 +176,14 @@ export class VirtualService extends cdk.Resource implements IVirtualService { /** * Interface with properties ncecessary to import a reusable VirtualService */ -interface VirtualServiceAttributes { - /** - * The Amazon Resource Name (ARN) for the virtual service - * - * @default - Required if virtualServiceName and virtualMeshName are not supplied. - */ - readonly virtualServiceArn?: string; - +export interface VirtualServiceAttributes { /** * The name of the VirtualService, it is recommended this follows the fully-qualified domain name format. - * - * @default - Required if virtualServiceArn is not supplied. */ - readonly virtualServiceName?: string; - - /** - * The name of the service mesh that the virtual service resides in - * - * Used to derive ARN from mesh name if ARN not provided - * - * @default - Required if virtualServiceArn is not supplied. - */ - readonly meshName?: string; -} + readonly virtualServiceName: string; -/** - * Returns properties that allows a VirtualService to be imported - */ -class ImportedVirtualService extends cdk.Resource implements IVirtualService { /** - * The name of the VirtualService, it is recommended this follows the fully-qualified domain name format. + * The Mesh which the VirtualService belongs to */ - public readonly virtualServiceName: string; - - /** - * The Amazon Resource Name (ARN) for the virtual service - */ - public readonly virtualServiceArn: string; - - constructor(scope: Construct, id: string, props: VirtualServiceAttributes) { - super(scope, id); - - if (props.virtualServiceArn) { - this.virtualServiceArn = props.virtualServiceArn; - this.virtualServiceName = cdk.Fn.select(2, cdk.Fn.split('/', cdk.Stack.of(scope).parseArn(props.virtualServiceArn).resourceName!)); - } else if (props.virtualServiceName && props.meshName) { - this.virtualServiceName = props.virtualServiceName; - this.virtualServiceArn = cdk.Stack.of(this).formatArn({ - service: 'appmesh', - resource: `mesh/${props.meshName}/virtualService`, - resourceName: this.virtualServiceName, - }); - } else { - throw new Error('Need either arn or both names'); - } - } + readonly mesh: IMesh; } diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index 322ae1cc608d6..3de6eb20c77d2 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -19,12 +19,9 @@ const namespace = new cloudmap.PrivateDnsNamespace(stack, 'test-namespace', { const mesh = new appmesh.Mesh(stack, 'mesh'); const router = mesh.addVirtualRouter('router', { - listener: { - portMapping: { - port: 8080, - protocol: appmesh.Protocol.HTTP, - }, - }, + listeners: [ + appmesh.VirtualRouterListener.http(), + ], }); const virtualService = mesh.addVirtualService('service', { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts index 842e553dfa20c..2f08e58bb873a 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts @@ -133,20 +133,47 @@ export = { }, }, - 'Can export and import GatewayRoutes and perform actions'(test: Test) { + 'Can import Gateway Routes using an ARN'(test: Test) { const app = new cdk.App(); // GIVEN const stack = new cdk.Stack(app, 'Imports', { env: { account: '123456789012', region: 'us-east-1' }, }); + const meshName = 'test-mesh'; + const virtualGatewayName = 'test-gateway'; + const gatewayRouteName = 'test-gateway-route'; + const arn = `arn:aws:appmesh:us-east-1:123456789012:mesh/${meshName}/virtualGateway/${virtualGatewayName}/gatewayRoute/${gatewayRouteName}`; // WHEN - const gatewayRoute2 = appmesh.GatewayRoute.fromGatewayRouteArn( - stack, 'importedGatewayRoute2', 'arn:aws:appmesh:us-east-1:123456789012:mesh/test-mesh/virtualGateway/test-gateway/gatewayRoute/test-gateway-route'); + const gatewayRoute = appmesh.GatewayRoute.fromGatewayRouteArn(stack, 'importedGatewayRoute', arn); // THEN - test.equal(gatewayRoute2.gatewayRouteName, 'test-gateway-route'); - test.equal(gatewayRoute2.virtualGateway.virtualGatewayName, 'test-gateway'); - test.equal(gatewayRoute2.virtualGateway.mesh.meshName, 'test-mesh'); + test.equal(gatewayRoute.gatewayRouteName, gatewayRouteName); + test.equal(gatewayRoute.virtualGateway.virtualGatewayName, virtualGatewayName); + test.equal(gatewayRoute.virtualGateway.mesh.meshName, meshName); + test.done(); + }, + 'Can import Gateway Routes using attributes'(test: Test) { + const app = new cdk.App(); + // GIVEN + const stack = new cdk.Stack(app, 'Imports', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + const meshName = 'test-mesh'; + const virtualGatewayName = 'test-gateway'; + const gatewayRouteName = 'test-gateway-route'; + + // WHEN + const mesh = appmesh.Mesh.fromMeshName(stack, 'Mesh', meshName); + const gateway = mesh.addVirtualGateway('VirtualGateway', { + virtualGatewayName: virtualGatewayName, + }); + const gatewayRoute = appmesh.GatewayRoute.fromGatewayRouteAttributes(stack, 'importedGatewayRoute', { + gatewayRouteName: gatewayRouteName, + virtualGateway: gateway, + }); + // THEN + test.equal(gatewayRoute.gatewayRouteName, gatewayRouteName); + test.equal(gatewayRoute.virtualGateway.mesh.meshName, meshName); test.done(); }, }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts index f8e6ebcbb716d..690dce4a08ddc 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.mesh.ts @@ -66,14 +66,7 @@ export = { meshName: 'test-mesh', }); - mesh.addVirtualRouter('router', { - listener: { - portMapping: { - port: 8080, - protocol: appmesh.Protocol.HTTP, - }, - }, - }); + mesh.addVirtualRouter('router'); // THEN expect(stack).to( @@ -147,12 +140,9 @@ export = { }); const testRouter = mesh.addVirtualRouter('router', { - listener: { - portMapping: { - port: 8080, - protocol: appmesh.Protocol.HTTP, - }, - }, + listeners: [ + appmesh.VirtualRouterListener.http(), + ], }); // THEN @@ -178,12 +168,9 @@ export = { }); const testRouter = mesh.addVirtualRouter('test-router', { - listener: { - portMapping: { - port: 8080, - protocol: appmesh.Protocol.HTTP, - }, - }, + listeners: [ + appmesh.VirtualRouterListener.http(), + ], }); mesh.addVirtualService('service', { diff --git a/packages/@aws-cdk/aws-appmesh/test/test.route.ts b/packages/@aws-cdk/aws-appmesh/test/test.route.ts index 706a29a3552fc..3f24bdf4be470 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.route.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.route.ts @@ -4,54 +4,44 @@ import { Test } from 'nodeunit'; import * as appmesh from '../lib'; export = { - 'Can export existing route and re-import'(test: Test) { + 'Can import Routes using an ARN'(test: Test) { + const app = new cdk.App(); // GIVEN - const stack = new cdk.Stack(); - - // WHEN - const mesh = new appmesh.Mesh(stack, 'mesh', { - meshName: 'test-mesh', - }); - - const router = new appmesh.VirtualRouter(stack, 'router', { - mesh, + const stack = new cdk.Stack(app, 'Imports', { + env: { account: '123456789012', region: 'us-east-1' }, }); + const meshName = 'test-mesh'; + const virtualRouterName = 'test-virtual-router'; + const routeName = 'test-route'; + const arn = `arn:aws:appmesh:us-east-1:123456789012:mesh/${meshName}/virtualRouter/${virtualRouterName}/gatewayRoute/${routeName}`; - const service1 = new appmesh.VirtualService(stack, 'service-1', { - virtualServiceName: 'service1.domain.local', - mesh, - }); - - const node = mesh.addVirtualNode('test-node', { - dnsHostName: 'test', - listener: { - portMapping: - { - port: 8080, - protocol: appmesh.Protocol.HTTP, - }, - }, - backends: [ - service1, - ], + // WHEN + const route = appmesh.Route.fromRouteArn(stack, 'importedRoute', arn); + // THEN + test.equal(route.routeName, routeName); + test.equal(route.virtualRouter.virtualRouterName, virtualRouterName); + test.equal(route.virtualRouter.mesh.meshName, meshName); + test.done(); + }, + 'Can import Routes using ARN and attributes'(test: Test) { + const app = new cdk.App(); + // GIVEN + const stack = new cdk.Stack(app, 'Imports', { + env: { account: '123456789012', region: 'us-east-1' }, }); + const meshName = 'test-mesh'; + const virtualRouterName = 'test-virtual-router'; + const routeName = 'test-route'; - const route = new appmesh.Route(stack, 'route-1', { - mesh, - virtualRouter: router, - routeTargets: [ - { - virtualNode: node, - weight: 50, - }, - ], - prefix: '/', + // WHEN + const mesh = appmesh.Mesh.fromMeshName(stack, 'Mesh', meshName); + const virtualRouter = mesh.addVirtualRouter('VirtualGateway', { + virtualRouterName: virtualRouterName, }); - - const stack2 = new cdk.Stack(); - appmesh.Route.fromRouteName(stack2, 'imported-route', mesh.meshName, router.virtualRouterName, route.routeName); - - // Nothing to do with imported route yet + const route = appmesh.Route.fromRouteAttributes(stack, 'importedRoute', { routeName, virtualRouter }); + // THEN + test.equal(route.routeName, routeName); + test.equal(route.virtualRouter.mesh.meshName, meshName); test.done(); }, diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts index 1dd75b938ab04..f5c020426c9b9 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts @@ -238,21 +238,42 @@ export = { test.done(); }, }, - - 'Can export and import VirtualGateway and perform actions'(test: Test) { + 'Can import VirtualGateways using an ARN'(test: Test) { const app = new cdk.App(); // GIVEN const stack = new cdk.Stack(app, 'Imports', { env: { account: '123456789012', region: 'us-east-1' }, }); + const meshName = 'testMesh'; + const virtualGatewayName = 'test-gateway'; + const arn = `arn:aws:appmesh:us-east-1:123456789012:mesh/${meshName}/virtualGateway/${virtualGatewayName}`; // WHEN - const virtualGateway2 = appmesh.VirtualGateway.fromVirtualGatewayArn( - stack, 'importedGateway2', 'arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh/virtualGateway/test-gateway'); + const virtualGateway = appmesh.VirtualGateway.fromVirtualGatewayArn( + stack, 'importedGateway', arn); + // THEN + test.equal(virtualGateway.mesh.meshName, meshName); + test.equal(virtualGateway.virtualGatewayName, virtualGatewayName); + test.done(); + }, + 'Can import VirtualGateways using attributes'(test: Test) { + const app = new cdk.App(); + // GIVEN + const stack = new cdk.Stack(app, 'Imports', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + const meshName = 'testMesh'; + const virtualGatewayName = 'test-gateway'; + // WHEN + const virtualGateway = appmesh.VirtualGateway.fromVirtualGatewayAttributes(stack, 'importedGateway', { + mesh: appmesh.Mesh.fromMeshName(stack, 'Mesh', meshName), + virtualGatewayName: virtualGatewayName, + }); // THEN - test.equal(virtualGateway2.mesh.meshName, 'testMesh'); - test.equal(virtualGateway2.virtualGatewayName, 'test-gateway'); + test.equal(virtualGateway.mesh.meshName, meshName); + test.equal(virtualGateway.virtualGatewayName, virtualGatewayName); + test.done(); }, }; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts index 82589e2c52ac5..06928a4a25351 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-node.ts @@ -102,55 +102,36 @@ export = { }, }, }, - 'Can export and import VirtualNode and perform actions'(test: Test) { + 'Can import Virtual Nodes using an ARN'(test: Test) { // GIVEN const stack = new cdk.Stack(); - // WHEN - const mesh = new appmesh.Mesh(stack, 'mesh', { - meshName: 'test-mesh', - }); - - const node = mesh.addVirtualNode('test-node', { - dnsHostName: 'test.domain.local', - listener: {}, - }); - - const stack2 = new cdk.Stack(); + const meshName = 'testMesh'; + const virtualNodeName = 'test-node'; + const arn = `arn:aws:appmesh:us-east-1:123456789012:mesh/${meshName}/virtualNode/${virtualNodeName}`; - const node2 = appmesh.VirtualNode.fromVirtualNodeName(stack2, 'imported-node', mesh.meshName, node.virtualNodeName); + // WHEN + const virtualNode = appmesh.VirtualNode.fromVirtualNodeArn( + stack, 'importedVirtualNode', arn); + // THEN + test.equal(virtualNode.mesh.meshName, meshName); + test.equal(virtualNode.virtualNodeName, virtualNodeName); - node2.addListeners({ - portMapping: { - port: 8081, - protocol: appmesh.Protocol.TCP, - }, + test.done(); + }, + 'Can import Virtual Nodes using attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const meshName = 'testMesh'; + const virtualNodeName = 'test-node'; + // WHEN + const virtualNode = appmesh.VirtualNode.fromVirtualNodeAttributes(stack, 'importedVirtualNode', { + mesh: appmesh.Mesh.fromMeshName(stack, 'Mesh', meshName), + virtualNodeName: virtualNodeName, }); - // THEN - expect(stack).to( - haveResourceLike('AWS::AppMesh::VirtualNode', { - MeshName: { - 'Fn::GetAtt': ['meshACDFE68E', 'MeshName'], - }, - Spec: { - Listeners: [ - { - PortMapping: { - Port: 8080, - Protocol: 'http', - }, - }, - ], - ServiceDiscovery: { - DNS: { - Hostname: 'test.domain.local', - }, - }, - }, - VirtualNodeName: 'meshtestnode428A9479', - }), - ); + test.equal(virtualNode.mesh.meshName, meshName); + test.equal(virtualNode.virtualNodeName, virtualNodeName); test.done(); }, diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts index 47b59e0e39962..72510c83c4de2 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-router.ts @@ -5,6 +5,88 @@ import { Test } from 'nodeunit'; import * as appmesh from '../lib'; export = { + 'When creating a VirtualRouter': { + 'should have appropriate defaults'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + // WHEN + mesh.addVirtualRouter('http-router-listener'); + + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualRouter', { + VirtualRouterName: 'meshhttprouterlistenerF57BCB2F', + Spec: { + Listeners: [ + { + PortMapping: { + Port: 8080, + Protocol: appmesh.Protocol.HTTP, + }, + }, + ], + }, + })); + test.done(); + }, + 'should have protocol variant listeners'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + // WHEN + mesh.addVirtualRouter('http-router-listener', { + listeners: [ + appmesh.VirtualRouterListener.http(), + ], + virtualRouterName: 'http-router-listener', + }); + + mesh.addVirtualRouter('http2-router-listener', { + listeners: [ + appmesh.VirtualRouterListener.http2(), + ], + virtualRouterName: 'http2-router-listener', + }); + + mesh.addVirtualRouter('grpc-router-listener', { + listeners: [ + appmesh.VirtualRouterListener.grpc(), + ], + virtualRouterName: 'grpc-router-listener', + }); + + mesh.addVirtualRouter('tcp-router-listener', { + listeners: [ + appmesh.VirtualRouterListener.tcp(), + ], + virtualRouterName: 'tcp-router-listener', + }); + + // THEN + const expectedPorts = [appmesh.Protocol.HTTP, appmesh.Protocol.HTTP2, appmesh.Protocol.GRPC, appmesh.Protocol.TCP]; + expectedPorts.forEach(protocol => { + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualRouter', { + VirtualRouterName: `${protocol}-router-listener`, + Spec: { + Listeners: [ + { + PortMapping: { + Port: 8080, + Protocol: protocol, + }, + }, + ], + }, + })); + }); + + test.done(); + }, + }, + 'When adding route to existing VirtualRouter': { 'should create route resource'(test: Test) { // GIVEN @@ -304,15 +386,38 @@ export = { }, }, - 'can import a virtual router'(test: Test) { + 'Can import Virtual Routers using an ARN'(test: Test) { // GIVEN const stack = new cdk.Stack(); + const meshName = 'testMesh'; + const virtualRouterName = 'virtual-router'; + const arn = `arn:aws:appmesh:us-east-1:123456789012:mesh/${meshName}/virtualRouter/${virtualRouterName}`; + // WHEN - const vr = appmesh.VirtualRouter.fromVirtualRouterName(stack, 'Router', 'MyMesh', 'MyRouter'); + const virtualRouter = appmesh.VirtualRouter.fromVirtualRouterArn( + stack, 'importedVirtualRouter', arn); + // THEN + test.equal(virtualRouter.mesh.meshName, meshName); + test.equal(virtualRouter.virtualRouterName, virtualRouterName); + + test.done(); + }, + 'Can import Virtual Routers using attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const meshName = 'testMesh'; + const virtualRouterName = 'virtual-router'; + // WHEN + const virtualRouter1 = appmesh.VirtualRouter.fromVirtualRouterAttributes(stack, 'importVirtualRouter', { + mesh: appmesh.Mesh.fromMeshName(stack, 'Mesh', meshName), + virtualRouterName: virtualRouterName, + }); // THEN - test.ok(vr.mesh !== undefined); + test.equal(virtualRouter1.mesh.meshName, meshName); + test.equal(virtualRouter1.virtualRouterName, virtualRouterName); test.done(); }, diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts index aa582c9376f19..c09c156ac75ea 100644 --- a/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-service.ts @@ -1,39 +1,39 @@ -import * as ec2 from '@aws-cdk/aws-ec2'; -import * as cloudmap from '@aws-cdk/aws-servicediscovery'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as appmesh from '../lib'; export = { - 'Can export existing virtual-service and re-import'(test: Test) { + 'Can import Virtual Services using an ARN'(test: Test) { // GIVEN const stack = new cdk.Stack(); + const meshName = 'testMesh'; + const virtualServiceName = 'virtual-service'; + const arn = `arn:aws:appmesh:us-east-1:123456789012:mesh/${meshName}/virtualService/${virtualServiceName}`; // WHEN - const mesh = new appmesh.Mesh(stack, 'mesh', { - meshName: 'test-mesh', - }); + const virtualRouter = appmesh.VirtualRouter.fromVirtualRouterArn(stack, 'importedVirtualRouter', arn); + // THEN + test.equal(virtualRouter.mesh.meshName, meshName); + test.equal(virtualRouter.virtualRouterName, virtualServiceName); - const router = new appmesh.VirtualRouter(stack, 'router', { mesh }); + test.done(); + }, + 'Can import Virtual Services using attributes'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); - const vpc = new ec2.Vpc(stack, 'vpc'); - const namespace = new cloudmap.PrivateDnsNamespace(stack, 'test-namespace', { - vpc, - name: 'domain.local', - }); + const meshName = 'testMesh'; + const virtualServiceName = 'virtual-service'; - const service = new appmesh.VirtualService(stack, 'service-1', { - mesh, - virtualServiceName: `service.${namespace.namespaceName}`, - virtualRouter: router, + // WHEN + const virtualService = appmesh.VirtualService.fromVirtualServiceAttributes(stack, 'importedVirtualService', { + mesh: appmesh.Mesh.fromMeshName(stack, 'Mesh', meshName), + virtualServiceName: virtualServiceName, }); - - const stack2 = new cdk.Stack(); - appmesh.VirtualService.fromVirtualServiceName(stack2, 'imported-virtual-service', mesh.meshName, service.virtualServiceName); - - // Nothing to do with imported virtual service yet - + // THEN + test.equal(virtualService.mesh.meshName, meshName); + test.equal(virtualService.virtualServiceName, virtualServiceName); test.done(); }, }; diff --git a/packages/@aws-cdk/aws-appsync/README.md b/packages/@aws-cdk/aws-appsync/README.md index 6095e3657ef9d..56c4cf6e0504a 100644 --- a/packages/@aws-cdk/aws-appsync/README.md +++ b/packages/@aws-cdk/aws-appsync/README.md @@ -31,7 +31,7 @@ type demo { version: String! } type Query { - getDemos: [ test! ] + getDemos: [ demo! ] } input DemoInput { version: String! @@ -84,6 +84,69 @@ demoDS.createResolver({ }); ``` +## Aurora Serverless + +AppSync provides a data source for executing SQL commands against Amazon Aurora +Serverless clusters. You can use AppSync resolvers to execute SQL statements +against the Data API with GraphQL queries, mutations, and subscriptions. + +```ts +// Create username and password secret for DB Cluster +const secret = new rds.DatabaseSecret(stack, 'AuroraSecret', { + username: 'clusteradmin', +}); + +// Create the DB cluster, provide all values needed to customise the database. +const cluster = new rds.DatabaseCluster(stack, 'AuroraCluster', { + engine: rds.DatabaseClusterEngine.auroraMysql({ version: rds.AuroraMysqlEngineVersion.VER_2_07_1 }), + credentials: { username: 'clusteradmin' }, + clusterIdentifier: 'db-endpoint-test', + defaultDatabaseName: 'demos', +}); + +// Build a data source for AppSync to access the database. +const rdsDS = api.addRdsDataSource('rds', 'The rds data source', cluster, secret); + +// Set up a resolver for an RDS query. +rdsDS.createResolver({ + typeName: 'Query', + fieldName: 'getDemosRds', + requestMappingTemplate: MappingTemplate.fromString(` + { + "version": "2018-05-29", + "statements": [ + "SELECT * FROM demos" + ] + } + `), + responseMappingTemplate: MappingTemplate.fromString(` + $util.rds.toJsonObject($ctx.result) + `), +}); + +// Set up a resolver for an RDS mutation. +rdsDS.createResolver({ + typeName: 'Mutation', + fieldName: 'addDemoRds', + requestMappingTemplate: MappingTemplate.fromString(` + { + "version": "2018-05-29", + "statements": [ + "INSERT INTO demos VALUES (:id, :version)", + "SELECT * WHERE id = :id" + ], + "variableMap": { + ":id": $util.toJson($util.autoId()), + ":version": $util.toJson($ctx.args.version) + } + } + `), + responseMappingTemplate: MappingTemplate.fromString(` + $util.rds.toJsonObject($ctx.result) + `), +}); +``` + #### HTTP Endpoints GraphQL schema file `schema.graphql`: diff --git a/packages/@aws-cdk/aws-appsync/lib/data-source.ts b/packages/@aws-cdk/aws-appsync/lib/data-source.ts index babde4be0a3fb..e13136090b56c 100644 --- a/packages/@aws-cdk/aws-appsync/lib/data-source.ts +++ b/packages/@aws-cdk/aws-appsync/lib/data-source.ts @@ -1,7 +1,9 @@ import { ITable } from '@aws-cdk/aws-dynamodb'; -import { IGrantable, IPrincipal, IRole, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; +import { Grant, IGrantable, IPrincipal, IRole, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import { IFunction } from '@aws-cdk/aws-lambda'; -import { IResolvable } from '@aws-cdk/core'; +import { IDatabaseCluster } from '@aws-cdk/aws-rds'; +import { ISecret } from '@aws-cdk/aws-secretsmanager'; +import { IResolvable, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnDataSource } from './appsync.generated'; import { IGraphqlApi } from './graphqlapi-base'; @@ -283,4 +285,56 @@ export class LambdaDataSource extends BackedDataSource { }); props.lambdaFunction.grantInvoke(this); } +} + +/** + * Properties for an AppSync RDS datasource + */ +export interface RdsDataSourceProps extends BackedDataSourceProps { + /** + * The database cluster to call to interact with this data source + */ + readonly databaseCluster: IDatabaseCluster; + /** + * The secret containing the credentials for the database + */ + readonly secretStore: ISecret; +} + +/** + * An AppSync datasource backed by RDS + */ +export class RdsDataSource extends BackedDataSource { + constructor(scope: Construct, id: string, props: RdsDataSourceProps) { + super(scope, id, props, { + type: 'RELATIONAL_DATABASE', + relationalDatabaseConfig: { + rdsHttpEndpointConfig: { + awsRegion: props.databaseCluster.stack.region, + dbClusterIdentifier: props.databaseCluster.clusterIdentifier, + awsSecretStoreArn: props.secretStore.secretArn, + }, + relationalDatabaseSourceType: 'RDS_HTTP_ENDPOINT', + }, + }); + props.secretStore.grantRead(this); + const clusterArn = Stack.of(this).formatArn({ + service: 'rds', + resource: `cluster:${props.databaseCluster.clusterIdentifier}`, + }); + // Change to grant with RDS grant becomes implemented + Grant.addToPrincipal({ + grantee: this, + actions: [ + 'rds-data:DeleteItems', + 'rds-data:ExecuteSql', + 'rds-data:ExecuteStatement', + 'rds-data:GetItems', + 'rds-data:InsertItems', + 'rds-data:UpdateItems', + ], + resourceArns: [clusterArn, `${clusterArn}:*`], + scope: this, + }); + } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi-base.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi-base.ts index 0525b51340fcd..288384c6454a5 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi-base.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi-base.ts @@ -1,7 +1,9 @@ import { ITable } from '@aws-cdk/aws-dynamodb'; import { IFunction } from '@aws-cdk/aws-lambda'; +import { IDatabaseCluster } from '@aws-cdk/aws-rds'; +import { ISecret } from '@aws-cdk/aws-secretsmanager'; import { CfnResource, IResource, Resource } from '@aws-cdk/core'; -import { DynamoDbDataSource, HttpDataSource, LambdaDataSource, NoneDataSource, AwsIamConfig } from './data-source'; +import { DynamoDbDataSource, HttpDataSource, LambdaDataSource, NoneDataSource, RdsDataSource, AwsIamConfig } from './data-source'; /** * Optional configuration for data sources @@ -90,6 +92,21 @@ export interface IGraphqlApi extends IResource { */ addLambdaDataSource(id: string, lambdaFunction: IFunction, options?: DataSourceOptions): LambdaDataSource; + /** + * add a new Rds data source to this API + * + * @param id The data source's id + * @param databaseCluster The database cluster to interact with this data source + * @param secretStore The secret store that contains the username and password for the database cluster + * @param options The optional configuration for this data source + */ + addRdsDataSource( + id: string, + databaseCluster: IDatabaseCluster, + secretStore: ISecret, + options?: DataSourceOptions + ): RdsDataSource; + /** * Add schema dependency if not imported * @@ -178,6 +195,28 @@ export abstract class GraphqlApiBase extends Resource implements IGraphqlApi { }); } + /** + * add a new Rds data source to this API + * @param id The data source's id + * @param databaseCluster The database cluster to interact with this data source + * @param secretStore The secret store that contains the username and password for the database cluster + * @param options The optional configuration for this data source + */ + public addRdsDataSource( + id: string, + databaseCluster: IDatabaseCluster, + secretStore: ISecret, + options?: DataSourceOptions, + ): RdsDataSource { + return new RdsDataSource(this, id, { + api: this, + name: options?.name, + description: options?.description, + databaseCluster, + secretStore, + }); + } + /** * Add schema dependency if not imported * diff --git a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts index 7ec0a8b555686..2d9addb93cf24 100644 --- a/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts +++ b/packages/@aws-cdk/aws-appsync/lib/graphqlapi.ts @@ -1,5 +1,5 @@ import { IUserPool } from '@aws-cdk/aws-cognito'; -import { ManagedPolicy, Role, ServicePrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam'; +import { ManagedPolicy, Role, IRole, ServicePrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam'; import { CfnResource, Duration, Expiration, IResolvable, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnApiKey, CfnGraphQLApi, CfnGraphQLSchema } from './appsync.generated'; @@ -203,6 +203,13 @@ export interface LogConfig { * @default - Use AppSync default */ readonly fieldLogLevel?: FieldLogLevel; + + /** + * The role for CloudWatch Logs + * + * @default - None + */ + readonly role?: IRole; } /** @@ -512,15 +519,14 @@ export class GraphqlApi extends GraphqlApiBase { private setupLogConfig(config?: LogConfig) { if (!config) return undefined; - const role = new Role(this, 'ApiLogsRole', { + const logsRoleArn: string = config.role?.roleArn ?? new Role(this, 'ApiLogsRole', { assumedBy: new ServicePrincipal('appsync.amazonaws.com'), managedPolicies: [ - ManagedPolicy.fromAwsManagedPolicyName( - 'service-role/AWSAppSyncPushToCloudWatchLogs'), + ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppSyncPushToCloudWatchLogs'), ], - }); + }).roleArn; return { - cloudWatchLogsRoleArn: role.roleArn, + cloudWatchLogsRoleArn: logsRoleArn, excludeVerboseContent: config.excludeVerboseContent, fieldLogLevel: config.fieldLogLevel, }; diff --git a/packages/@aws-cdk/aws-appsync/package.json b/packages/@aws-cdk/aws-appsync/package.json index adbec0f587e77..699aec7a504c5 100644 --- a/packages/@aws-cdk/aws-appsync/package.json +++ b/packages/@aws-cdk/aws-appsync/package.json @@ -76,13 +76,16 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-rds": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", @@ -92,10 +95,13 @@ "peerDependencies": { "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-dynamodb": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3-assets": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/aws-rds": "0.0.0", + "@aws-cdk/aws-secretsmanager": "0.0.0", "constructs": "^3.2.0" }, "engines": { diff --git a/packages/@aws-cdk/aws-appsync/test/appsync-rds.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync-rds.test.ts new file mode 100644 index 0000000000000..ba0d80d00037b --- /dev/null +++ b/packages/@aws-cdk/aws-appsync/test/appsync-rds.test.ts @@ -0,0 +1,206 @@ +import '@aws-cdk/assert/jest'; +import * as path from 'path'; +import { Vpc, SecurityGroup, SubnetType, InstanceType, InstanceClass, InstanceSize } from '@aws-cdk/aws-ec2'; +import { DatabaseSecret, DatabaseCluster, DatabaseClusterEngine, AuroraMysqlEngineVersion } from '@aws-cdk/aws-rds'; +import * as cdk from '@aws-cdk/core'; +import * as appsync from '../lib'; + +// GLOBAL GIVEN +let stack: cdk.Stack; +let api: appsync.GraphqlApi; +beforeEach(() => { + stack = new cdk.Stack(); + api = new appsync.GraphqlApi(stack, 'baseApi', { + name: 'api', + schema: new appsync.Schema({ + filePath: path.join(__dirname, 'appsync.test.graphql'), + }), + }); +}); + +describe('Rds Data Source configuration', () => { + // GIVEN + let secret: DatabaseSecret; + let cluster: DatabaseCluster; + beforeEach(() => { + const vpc = new Vpc(stack, 'Vpc', { maxAzs: 2 }); + const securityGroup = new SecurityGroup(stack, 'AuroraSecurityGroup', { + vpc, + allowAllOutbound: true, + }); + secret = new DatabaseSecret(stack, 'AuroraSecret', { + username: 'clusteradmin', + }); + cluster = new DatabaseCluster(stack, 'AuroraCluster', { + engine: DatabaseClusterEngine.auroraMysql({ version: AuroraMysqlEngineVersion.VER_2_07_1 }), + credentials: { username: 'clusteradmin' }, + clusterIdentifier: 'db-endpoint-test', + instanceProps: { + instanceType: InstanceType.of(InstanceClass.BURSTABLE2, InstanceSize.SMALL), + vpcSubnets: { subnetType: SubnetType.PRIVATE }, + vpc, + securityGroups: [securityGroup], + }, + defaultDatabaseName: 'Animals', + }); + }); + + test('appsync creates correct policy', () => { + // WHEN + api.addRdsDataSource('ds', cluster, secret); + + // THEN + expect(stack).toHaveResourceLike('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { Ref: 'AuroraSecret41E6E877' }, + }, + { + Action: [ + 'rds-data:DeleteItems', + 'rds-data:ExecuteSql', + 'rds-data:ExecuteStatement', + 'rds-data:GetItems', + 'rds-data:InsertItems', + 'rds-data:UpdateItems', + ], + Effect: 'Allow', + Resource: [{ + 'Fn::Join': ['', ['arn:', + { Ref: 'AWS::Partition' }, + ':rds:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':cluster:', + { Ref: 'AuroraCluster23D869C0' }]], + }, + { + 'Fn::Join': ['', ['arn:', + { Ref: 'AWS::Partition' }, + ':rds:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':cluster:', + { Ref: 'AuroraCluster23D869C0' }, + ':*']], + }], + }], + }, + }); + }); + + test('default configuration produces name identical to the id', () => { + // WHEN + api.addRdsDataSource('ds', cluster, secret); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'RELATIONAL_DATABASE', + Name: 'ds', + }); + }); + + test('appsync configures name correctly', () => { + // WHEN + api.addRdsDataSource('ds', cluster, secret, { + name: 'custom', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'RELATIONAL_DATABASE', + Name: 'custom', + }); + }); + + test('appsync configures name and description correctly', () => { + // WHEN + api.addRdsDataSource('ds', cluster, secret, { + name: 'custom', + description: 'custom description', + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'RELATIONAL_DATABASE', + Name: 'custom', + Description: 'custom description', + }); + }); + + test('appsync errors when creating multiple rds data sources with no configuration', () => { + // WHEN + const when = () => { + api.addRdsDataSource('ds', cluster, secret); + api.addRdsDataSource('ds', cluster, secret); + }; + + // THEN + expect(when).toThrow('There is already a Construct with name \'ds\' in GraphqlApi [baseApi]'); + }); +}); + +describe('adding rds data source from imported api', () => { + // GIVEN + let secret: DatabaseSecret; + let cluster: DatabaseCluster; + beforeEach(() => { + const vpc = new Vpc(stack, 'Vpc', { maxAzs: 2 }); + const securityGroup = new SecurityGroup(stack, 'AuroraSecurityGroup', { + vpc, + allowAllOutbound: true, + }); + secret = new DatabaseSecret(stack, 'AuroraSecret', { + username: 'clusteradmin', + }); + cluster = new DatabaseCluster(stack, 'AuroraCluster', { + engine: DatabaseClusterEngine.auroraMysql({ version: AuroraMysqlEngineVersion.VER_2_07_1 }), + credentials: { username: 'clusteradmin' }, + clusterIdentifier: 'db-endpoint-test', + instanceProps: { + instanceType: InstanceType.of(InstanceClass.BURSTABLE2, InstanceSize.SMALL), + vpcSubnets: { subnetType: SubnetType.PRIVATE }, + vpc, + securityGroups: [securityGroup], + }, + defaultDatabaseName: 'Animals', + }); + }); + + test('imported api can add RdsDbDataSource from id', () => { + // WHEN + const importedApi = appsync.GraphqlApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + }); + importedApi.addRdsDataSource('ds', cluster, secret); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'RELATIONAL_DATABASE', + ApiId: { 'Fn::GetAtt': ['baseApiCDA4D43A', 'ApiId'] }, + }); + }); + + test('imported api can add RdsDataSource from attributes', () => { + // WHEN + const importedApi = appsync.GraphqlApi.fromGraphqlApiAttributes(stack, 'importedApi', { + graphqlApiId: api.apiId, + graphqlApiArn: api.arn, + }); + importedApi.addRdsDataSource('ds', cluster, secret); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::DataSource', { + Type: 'RELATIONAL_DATABASE', + ApiId: { 'Fn::GetAtt': ['baseApiCDA4D43A', 'ApiId'] }, + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts index debdfa71ce4f0..252051de6f0c6 100644 --- a/packages/@aws-cdk/aws-appsync/test/appsync.test.ts +++ b/packages/@aws-cdk/aws-appsync/test/appsync.test.ts @@ -1,5 +1,6 @@ import * as path from 'path'; import '@aws-cdk/assert/jest'; +import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; import * as appsync from '../lib'; @@ -11,6 +12,7 @@ beforeEach(() => { authorizationConfig: {}, name: 'api', schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), + logConfig: {}, }); }); @@ -73,3 +75,51 @@ test('when xray is enabled should not throw an Error', () => { XrayEnabled: true, }); }); + +test('appsync GraphqlApi should be configured with custom CloudWatch Logs role when specified', () => { + // GIVEN + const cloudWatchLogRole: iam.Role = new iam.Role(stack, 'CloudWatchLogRole', { + assumedBy: new iam.ServicePrincipal('appsync.amazonaws.com'), + managedPolicies: [ + iam.ManagedPolicy.fromAwsManagedPolicyName('service-role/AWSAppSyncPushToCloudWatchLogs'), + ], + }); + + // WHEN + new appsync.GraphqlApi(stack, 'api-custom-cw-logs-role', { + authorizationConfig: {}, + name: 'apiWithCustomRole', + schema: appsync.Schema.fromAsset(path.join(__dirname, 'appsync.test.graphql')), + logConfig: { + role: cloudWatchLogRole, + }, + }); + + // THEN + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLApi', { + Name: 'apiWithCustomRole', + LogConfig: { + CloudWatchLogsRoleArn: { + 'Fn::GetAtt': [ + 'CloudWatchLogRoleE3242F1C', + 'Arn', + ], + }, + }, + }); +}); + +test('appsync GraphqlApi should not use custom role for CW Logs when not specified', () => { + // EXPECT + expect(stack).toHaveResourceLike('AWS::AppSync::GraphQLApi', { + Name: 'api', + LogConfig: { + CloudWatchLogsRoleArn: { + 'Fn::GetAtt': [ + 'apiApiLogsRole56BEE3F1', + 'Arn', + ], + }, + }, + }); +}); diff --git a/packages/@aws-cdk/aws-autoscaling-common/package.json b/packages/@aws-cdk/aws-autoscaling-common/package.json index ae3e29534c59b..805460b6ef2ba 100644 --- a/packages/@aws-cdk/aws-autoscaling-common/package.json +++ b/packages/@aws-cdk/aws-autoscaling-common/package.json @@ -67,7 +67,7 @@ "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "fast-check": "^2.6.0", + "fast-check": "^2.6.1", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json index 7321dff3ad7f3..fa2b3589acd92 100644 --- a/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json +++ b/packages/@aws-cdk/aws-autoscaling-hooktargets/package.json @@ -68,7 +68,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-backup/lib/vault.ts b/packages/@aws-cdk/aws-backup/lib/vault.ts index e74b73b13d453..cfee7f4c6ffa7 100644 --- a/packages/@aws-cdk/aws-backup/lib/vault.ts +++ b/packages/@aws-cdk/aws-backup/lib/vault.ts @@ -1,7 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as sns from '@aws-cdk/aws-sns'; -import { IResource, RemovalPolicy, Resource } from '@aws-cdk/core'; +import { IResource, Names, RemovalPolicy, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnBackupVault } from './backup.generated'; @@ -161,7 +161,7 @@ export class BackupVault extends Resource implements IBackupVault { private uniqueVaultName() { // Max length of 50 chars, get the last 50 chars - const id = this.node.uniqueId; + const id = Names.uniqueId(this); return id.substring(Math.max(id.length - 50, 0), id.length); } } diff --git a/packages/@aws-cdk/aws-batch/package.json b/packages/@aws-cdk/aws-batch/package.json index da76870a75613..e9a73189f0716 100644 --- a/packages/@aws-cdk/aws-batch/package.json +++ b/packages/@aws-cdk/aws-batch/package.json @@ -76,7 +76,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts b/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts index 42c958473f1c3..f069ab3a5a080 100644 --- a/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts +++ b/packages/@aws-cdk/aws-batch/test/compute-environment.test.ts @@ -1,10 +1,10 @@ +import { throws } from 'assert'; import { expect, haveResource, haveResourceLike, ResourcePart } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as ecs from '@aws-cdk/aws-ecs'; import * as iam from '@aws-cdk/aws-iam'; import * as cdk from '@aws-cdk/core'; -import { throws } from 'assert'; import * as batch from '../lib'; describe('Batch Compute Evironment', () => { @@ -240,10 +240,10 @@ describe('Batch Compute Evironment', () => { ], Subnets: [ { - Ref: `${vpc.node.uniqueId}PrivateSubnet1Subnet865FB50A`, + Ref: `${cdk.Names.uniqueId(vpc)}PrivateSubnet1Subnet865FB50A`, }, { - Ref: `${vpc.node.uniqueId}PrivateSubnet2Subnet23D3396F`, + Ref: `${cdk.Names.uniqueId(vpc)}PrivateSubnet2Subnet23D3396F`, }, ], Tags: { diff --git a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json index 9e4e9abd7f9ce..9f25bfc9d6683 100644 --- a/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json +++ b/packages/@aws-cdk/aws-certificatemanager/lambda-packages/dns_validated_certificate_handler/package.json @@ -35,7 +35,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.2", - "jest": "^26.6.1", + "jest": "^26.6.3", "lambda-tester": "^3.6.0", "nock": "^13.0.4", "ts-jest": "^26.4.3" diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts index f133d07bea8c9..b64a4d488e956 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi-refs.ts @@ -1,3 +1,4 @@ +/// !cdk-integ pragma:ignore-assets import * as sns from '@aws-cdk/aws-sns'; import { App, Fn, Stack } from '@aws-cdk/core'; import { NestedStack } from '../lib'; @@ -28,4 +29,4 @@ app.synth(); // Stack1-NestedUnderStack1NestedStackNestedUnderStack1NestedStackResourceF616305B-EM64TEGA04J9-TopicInNestedUnderStack115E329C4-HEO7NLYC1AFL function shortName(topicName: string) { return Fn.select(1, Fn.split('-', topicName)); -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi.ts b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi.ts index b3e51a8c049f1..109b151138b19 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/integ.nested-stacks-multi.ts @@ -1,3 +1,4 @@ +/// !cdk-integ pragma:ignore-assets import * as sns from '@aws-cdk/aws-sns'; import { App, Construct, Stack } from '@aws-cdk/core'; import * as cfn from '../lib'; diff --git a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts index de102b4b1cf51..056849c1dc12b 100644 --- a/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts +++ b/packages/@aws-cdk/aws-cloudformation/test/test.nested-stack.ts @@ -3,7 +3,7 @@ import * as path from 'path'; import { expect, haveResource, matchTemplate, SynthUtils } from '@aws-cdk/assert'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; import * as sns from '@aws-cdk/aws-sns'; -import { App, CfnParameter, CfnResource, Construct, ContextProvider, Stack } from '@aws-cdk/core'; +import { App, CfnParameter, CfnResource, Construct, ContextProvider, LegacyStackSynthesizer, Names, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { NestedStack } from '../lib/nested-stack'; @@ -63,7 +63,7 @@ export = { const assembly = app.synth(); // THEN - const template = JSON.parse(fs.readFileSync(path.join(assembly.directory, `${nested.node.uniqueId}.nested.template.json`), 'utf-8')); + const template = JSON.parse(fs.readFileSync(path.join(assembly.directory, `${Names.uniqueId(nested)}.nested.template.json`), 'utf-8')); test.deepEqual(template, { Resources: { ResourceInNestedStack: { @@ -714,6 +714,8 @@ export = { }, }); + const middleStackHash = 'b2670b4c0c3fdf1d8fd9b9272bb8bf8173d18c0f67a888ba165cc569a248a84f'; + // nested1 wires the nested2 template through parameters, so we expect those expect(nested1).to(haveResource('Resource::1')); const nested2Template = SynthUtils.toCloudFormation(nested1); @@ -729,9 +731,9 @@ export = { AssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3BucketDE3B88D6: { Type: 'String', Description: 'S3 bucket for asset "8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235c"' }, AssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cS3VersionKey3A62EFEA: { Type: 'String', Description: 'S3 key for asset version "8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235c"' }, AssetParameters8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235cArtifactHash7DC546E0: { Type: 'String', Description: 'Artifact hash for asset "8169c6f8aaeaf5e2e8620f5f895ffe2099202ccb4b6889df48fe0967a894235c"' }, - AssetParameters8b50795a950cca6b01352f162c45d9d274dee6bc409f2f2b2ed029ad6828b3bfS3Bucket76ACFB38: { Type: 'String', Description: 'S3 bucket for asset "8b50795a950cca6b01352f162c45d9d274dee6bc409f2f2b2ed029ad6828b3bf"' }, - AssetParameters8b50795a950cca6b01352f162c45d9d274dee6bc409f2f2b2ed029ad6828b3bfS3VersionKey04162EF1: { Type: 'String', Description: 'S3 key for asset version "8b50795a950cca6b01352f162c45d9d274dee6bc409f2f2b2ed029ad6828b3bf"' }, - AssetParameters8b50795a950cca6b01352f162c45d9d274dee6bc409f2f2b2ed029ad6828b3bfArtifactHashF227ADD3: { Type: 'String', Description: 'Artifact hash for asset "8b50795a950cca6b01352f162c45d9d274dee6bc409f2f2b2ed029ad6828b3bf"' }, + [`AssetParameters${middleStackHash}S3Bucket3DB431CB`]: { Type: 'String', Description: `S3 bucket for asset "${middleStackHash}"` }, + [`AssetParameters${middleStackHash}S3VersionKeyBFFDABE9`]: { Type: 'String', Description: `S3 key for asset version "${middleStackHash}"` }, + [`AssetParameters${middleStackHash}ArtifactHash8EA52875`]: { Type: 'String', Description: `Artifact hash for asset "${middleStackHash}"` }, }); // proxy asset params to nested stack @@ -935,6 +937,37 @@ export = { test.done(); }, + '3-level stacks: legacy synthesizer parameters are added to the middle-level stack'(test: Test) { + // GIVEN + const app = new App(); + const top = new Stack(app, 'stack', { + synthesizer: new LegacyStackSynthesizer(), + }); + const middle = new NestedStack(top, 'nested1'); + const bottom = new NestedStack(middle, 'nested2'); + + // WHEN + new CfnResource(bottom, 'Something', { + type: 'BottomLevel', + }); + + // THEN + const asm = app.synth(); + const middleTemplate = JSON.parse(fs.readFileSync(path.join(asm.directory, middle.templateFile), { encoding: 'utf-8' })); + + const hash = 'bc3c51e4d3545ee0a0069401e5a32c37b66d044b983f12de416ba1576ecaf0a4'; + test.deepEqual(middleTemplate.Parameters ?? {}, { + [`referencetostackAssetParameters${hash}S3BucketD7C30435Ref`]: { + Type: 'String', + }, + [`referencetostackAssetParameters${hash}S3VersionKeyB667DBE1Ref`]: { + Type: 'String', + }, + }); + + test.done(); + }, + 'references to a resource from a deeply nested stack'(test: Test) { // GIVEN const app = new App(); diff --git a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts index c6e0f83d0c15a..50fe044d3a484 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts +++ b/packages/@aws-cdk/aws-cloudfront-origins/lib/s3-origin.ts @@ -74,7 +74,7 @@ class S3BucketOrigin extends cloudfront.OriginBase { const bucketStack = cdk.Stack.of(this.bucket); const bucketInDifferentStack = bucketStack !== cdk.Stack.of(scope); const oaiScope = bucketInDifferentStack ? bucketStack : scope; - const oaiId = bucketInDifferentStack ? `${scope.node.uniqueId}S3Origin` : 'S3Origin'; + const oaiId = bucketInDifferentStack ? `${cdk.Names.uniqueId(scope)}S3Origin` : 'S3Origin'; this.originAccessIdentity = new cloudfront.OriginAccessIdentity(oaiScope, oaiId, { comment: `Identity for ${options.originId}`, diff --git a/packages/@aws-cdk/aws-cloudfront-origins/package.json b/packages/@aws-cdk/aws-cloudfront-origins/package.json index 11b4fe7c62c40..f785f93e01873 100644 --- a/packages/@aws-cdk/aws-cloudfront-origins/package.json +++ b/packages/@aws-cdk/aws-cloudfront-origins/package.json @@ -72,7 +72,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "pkglint": "0.0.0" diff --git a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts index ac3075d5f9aa4..30d3e6b97db74 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/cache-policy.ts @@ -1,4 +1,4 @@ -import { Duration, Resource, Token } from '@aws-cdk/core'; +import { Duration, Names, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnCachePolicy } from './cloudfront.generated'; @@ -128,7 +128,7 @@ export class CachePolicy extends Resource implements ICachePolicy { physicalName: props.cachePolicyName, }); - const cachePolicyName = props.cachePolicyName ?? this.node.uniqueId; + const cachePolicyName = props.cachePolicyName ?? Names.uniqueId(this); if (!Token.isUnresolved(cachePolicyName) && !cachePolicyName.match(/^[\w-]+$/i)) { throw new Error(`'cachePolicyName' can only include '-', '_', and alphanumeric characters, got: '${props.cachePolicyName}'`); } diff --git a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts index 806bf2a36c44b..43be41d7ae6bc 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/distribution.ts @@ -1,8 +1,8 @@ import * as acm from '@aws-cdk/aws-certificatemanager'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; -import { IResource, Lazy, Resource, Stack, Token, Duration } from '@aws-cdk/core'; -import { Construct, Node } from 'constructs'; +import { IResource, Lazy, Resource, Stack, Token, Duration, Names } from '@aws-cdk/core'; +import { Construct } from 'constructs'; import { ICachePolicy } from './cache-policy'; import { CfnDistribution } from './cloudfront.generated'; import { GeoRestriction } from './geo-restriction'; @@ -322,7 +322,7 @@ export class Distribution extends Resource implements IDistribution { } else { const originIndex = this.boundOrigins.length + 1; const scope = new CoreConstruct(this, `Origin${originIndex}`); - const originId = Node.of(scope).uniqueId; + const originId = Names.uniqueId(scope); const originBindConfig = origin.bind(scope, { originId }); if (!originBindConfig.failoverConfig) { this.boundOrigins.push({ origin, originId, ...originBindConfig }); @@ -331,7 +331,7 @@ export class Distribution extends Resource implements IDistribution { throw new Error('An Origin cannot use an Origin with its own failover configuration as its fallback origin!'); } const groupIndex = this.originGroups.length + 1; - const originGroupId = Node.of(new CoreConstruct(this, `OriginGroup${groupIndex}`)).uniqueId; + const originGroupId = Names.uniqueId(new CoreConstruct(this, `OriginGroup${groupIndex}`)); this.boundOrigins.push({ origin, originId, originGroupId, ...originBindConfig }); const failoverOriginId = this.addOrigin(originBindConfig.failoverConfig.failoverOrigin, true); diff --git a/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts b/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts index 225de62cb12ec..8f279c7ddb213 100644 --- a/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts +++ b/packages/@aws-cdk/aws-cloudfront/lib/origin-request-policy.ts @@ -1,4 +1,4 @@ -import { Resource, Token } from '@aws-cdk/core'; +import { Names, Resource, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnOriginRequestPolicy } from './cloudfront.generated'; @@ -91,7 +91,7 @@ export class OriginRequestPolicy extends Resource implements IOriginRequestPolic physicalName: props.originRequestPolicyName, }); - const originRequestPolicyName = props.originRequestPolicyName ?? this.node.uniqueId; + const originRequestPolicyName = props.originRequestPolicyName ?? Names.uniqueId(this); if (!Token.isUnresolved(originRequestPolicyName) && !originRequestPolicyName.match(/^[\w-]+$/i)) { throw new Error(`'originRequestPolicyName' can only include '-', '_', and alphanumeric characters, got: '${props.originRequestPolicyName}'`); } diff --git a/packages/@aws-cdk/aws-cloudfront/package.json b/packages/@aws-cdk/aws-cloudfront/package.json index 5094429042dbd..9a84b3f57a115 100644 --- a/packages/@aws-cdk/aws-cloudfront/package.json +++ b/packages/@aws-cdk/aws-cloudfront/package.json @@ -73,7 +73,7 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-cloudtrail/package.json b/packages/@aws-cdk/aws-cloudtrail/package.json index fd4627f69bf53..f7285169ae2b7 100644 --- a/packages/@aws-cdk/aws-cloudtrail/package.json +++ b/packages/@aws-cdk/aws-cloudtrail/package.json @@ -73,12 +73,12 @@ "license": "Apache-2.0", "devDependencies": { "@aws-cdk/assert": "0.0.0", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", "colors": "^1.4.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-cloudwatch-actions/package.json b/packages/@aws-cdk/aws-cloudwatch-actions/package.json index f831bf93c58b1..8d845afd8eff9 100644 --- a/packages/@aws-cdk/aws-cloudwatch-actions/package.json +++ b/packages/@aws-cdk/aws-cloudwatch-actions/package.json @@ -68,7 +68,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts index 94268ae282fcb..ae8fd29424786 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/alarm.ts @@ -273,11 +273,18 @@ export class Alarm extends AlarmBase { }; }, withExpression(expr, conf) { + + const hasSubmetrics = mathExprHasSubmetrics(expr); + + if (hasSubmetrics) { + assertSubmetricsCount(expr); + } + return { expression: expr.expression, id: entry.id || uniqueMetricId(), label: conf.renderingProperties?.label, - period: mathExprHasSubmetrics(expr) ? undefined : expr.period, + period: hasSubmetrics ? undefined : expr.period, returnData: entry.tag ? undefined : false, // Tag stores "primary" attribute, default is "true" }; }, @@ -344,4 +351,11 @@ function mathExprHasSubmetrics(expr: MetricExpressionConfig) { return Object.keys(expr.usingMetrics).length > 0; } +function assertSubmetricsCount(expr: MetricExpressionConfig) { + if (Object.keys(expr.usingMetrics).length > 10) { + // https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/AlarmThatSendsEmail.html#alarms-on-metric-math-expressions + throw new Error('Alarms on math expressions cannot contain more than 10 individual metrics'); + }; +} + type Writeable = { -readonly [P in keyof T]: T[P] }; diff --git a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts index 6f889b5762b58..e8f8b95db3850 100644 --- a/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/lib/composite-alarm.ts @@ -1,4 +1,4 @@ -import { Lazy, Stack } from '@aws-cdk/core'; +import { Lazy, Names, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { AlarmBase, IAlarm, IAlarmRule } from './alarm-base'; import { CfnCompositeAlarm } from './cloudwatch.generated'; @@ -120,7 +120,7 @@ export class CompositeAlarm extends AlarmBase { } private generateUniqueId(): string { - const name = this.node.uniqueId; + const name = Names.uniqueId(this); if (name.length > 240) { return name.substring(0, 120) + name.substring(name.length - 120); } diff --git a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts index 6b863af88b1cd..c7c0f647c2e58 100644 --- a/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts +++ b/packages/@aws-cdk/aws-cloudwatch/test/test.alarm.ts @@ -2,7 +2,7 @@ import { ABSENT, expect, haveResource } from '@aws-cdk/assert'; import { Duration, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Test } from 'nodeunit'; -import { Alarm, IAlarm, IAlarmAction, Metric } from '../lib'; +import { Alarm, IAlarm, IAlarmAction, Metric, MathExpression, IMetric } from '../lib'; const testMetric = new Metric({ namespace: 'CDK/Test', @@ -10,6 +10,38 @@ const testMetric = new Metric({ }); export = { + + 'alarm does not accept a math expression with more than 10 metrics'(test: Test) { + + const stack = new Stack(); + + const usingMetrics: Record = {}; + + for (const i of [...Array(15).keys()]) { + const metricName = `metric${i}`; + usingMetrics[metricName] = new Metric({ + namespace: 'CDK/Test', + metricName: metricName, + }); + } + + const math = new MathExpression({ + expression: 'a', + usingMetrics, + }); + + test.throws(() => { + + new Alarm(stack, 'Alarm', { + metric: math, + threshold: 1000, + evaluationPeriods: 3, + }); + + }, /Alarms on math expressions cannot contain more than 10 individual metrics/); + + test.done(); + }, 'can make simple alarm'(test: Test) { // GIVEN const stack = new Stack(); diff --git a/packages/@aws-cdk/aws-codebuild/lib/project.ts b/packages/@aws-cdk/aws-codebuild/lib/project.ts index bcbb85a9ba588..8bfc8f38e09cf 100644 --- a/packages/@aws-cdk/aws-codebuild/lib/project.ts +++ b/packages/@aws-cdk/aws-codebuild/lib/project.ts @@ -7,7 +7,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Aws, Duration, IResource, Lazy, PhysicalName, Resource, Stack } from '@aws-cdk/core'; +import { Aws, Duration, IResource, Lazy, Names, PhysicalName, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IArtifacts } from './artifacts'; import { BuildSpec } from './build-spec'; @@ -938,7 +938,7 @@ export class Project extends ProjectBase { } const imagePullPrincipalType = this.buildImage.imagePullPrincipalType === ImagePullPrincipalType.CODEBUILD - ? undefined + ? ImagePullPrincipalType.CODEBUILD : ImagePullPrincipalType.SERVICE_ROLE; if (this.buildImage.repository) { if (imagePullPrincipalType === ImagePullPrincipalType.SERVICE_ROLE) { @@ -956,14 +956,17 @@ export class Project extends ProjectBase { this.buildImage.secretsManagerCredentials?.grantRead(this); } + const secret = this.buildImage.secretsManagerCredentials; return { type: this.buildImage.type, image: this.buildImage.imageId, imagePullCredentialsType: imagePullPrincipalType, - registryCredential: this.buildImage.secretsManagerCredentials + registryCredential: secret ? { credentialProvider: 'SECRETS_MANAGER', - credential: this.buildImage.secretsManagerCredentials.secretArn, + // Secrets must be referenced by either the full ARN (with SecretsManager suffix), or by name. + // "Partial" ARNs (without the suffix) will fail a validation regex at deploy-time. + credential: secret.secretFullArn ?? secret.secretName, } : undefined, privilegedMode: env.privileged || false, @@ -1019,7 +1022,7 @@ export class Project extends ProjectBase { } else { const securityGroup = new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc, - description: 'Automatic generated security group for CodeBuild ' + this.node.uniqueId, + description: 'Automatic generated security group for CodeBuild ' + Names.uniqueId(this), allowAllOutbound: props.allowAllOutbound, }); securityGroups = [securityGroup]; diff --git a/packages/@aws-cdk/aws-codebuild/package.json b/packages/@aws-cdk/aws-codebuild/package.json index 6ce66c00f98b9..e8eeec89bed79 100644 --- a/packages/@aws-cdk/aws-codebuild/package.json +++ b/packages/@aws-cdk/aws-codebuild/package.json @@ -79,7 +79,7 @@ "@aws-cdk/aws-sns": "0.0.0", "@aws-cdk/aws-sqs": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.caching.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.caching.expected.json index 79f9c50c2e5a7..248d2e31c9ac8 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.caching.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.caching.expected.json @@ -137,6 +137,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.expected.json index 75491ab8fa653..d9e429b2d694c 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.defaults.lit.expected.json @@ -133,6 +133,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.github.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.github.expected.json index 222a29feeeb8a..12c37d9da5b3b 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.github.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.github.expected.json @@ -99,6 +99,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-bucket.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-bucket.expected.json index 0ba33dae84b91..f8f5aa2e67aad 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-bucket.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-bucket.expected.json @@ -134,6 +134,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_LARGE", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-buildspec-artifacts.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-buildspec-artifacts.expected.json index 13ff8f1e054c0..b2101c7135f2f 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-buildspec-artifacts.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-buildspec-artifacts.expected.json @@ -145,6 +145,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json index 696a9b7b065ee..492e78820dc47 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-file-system-location.expected.json @@ -366,6 +366,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": true, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json index f7261ca4b21ae..7d6a3f783c899 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-secondary-sources-artifacts.expected.json @@ -167,6 +167,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json index a5d874bf04830..cb3561326a2a4 100644 --- a/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json +++ b/packages/@aws-cdk/aws-codebuild/test/integ.project-vpc.expected.json @@ -366,6 +366,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts index ea1c8eb87b021..66a4ccd09e8db 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.codebuild.ts @@ -152,6 +152,7 @@ export = { 'Type': 'LINUX_CONTAINER', 'PrivilegedMode': false, 'Image': 'aws/codebuild/standard:1.0', + 'ImagePullCredentialsType': 'CODEBUILD', 'ComputeType': 'BUILD_GENERAL1_SMALL', }, 'EncryptionKey': 'alias/aws/s3', @@ -315,6 +316,7 @@ export = { 'Environment': { 'ComputeType': 'BUILD_GENERAL1_SMALL', 'Image': 'aws/codebuild/standard:1.0', + 'ImagePullCredentialsType': 'CODEBUILD', 'PrivilegedMode': false, 'Type': 'LINUX_CONTAINER', }, @@ -514,6 +516,7 @@ export = { 'Environment': { 'ComputeType': 'BUILD_GENERAL1_MEDIUM', 'Image': 'aws/codebuild/windows-base:2.0', + 'ImagePullCredentialsType': 'CODEBUILD', 'PrivilegedMode': false, 'Type': 'WINDOWS_CONTAINER', }, @@ -1205,6 +1208,7 @@ export = { 'Type': 'LINUX_CONTAINER', 'PrivilegedMode': false, 'Image': 'aws/codebuild/standard:1.0', + 'ImagePullCredentialsType': 'CODEBUILD', 'ComputeType': 'BUILD_GENERAL1_SMALL', }, })); @@ -1449,6 +1453,7 @@ export = { ], 'PrivilegedMode': false, 'Image': 'aws/codebuild/standard:1.0', + 'ImagePullCredentialsType': 'CODEBUILD', 'ComputeType': 'BUILD_GENERAL1_SMALL', }, })); diff --git a/packages/@aws-cdk/aws-codebuild/test/test.project.ts b/packages/@aws-cdk/aws-codebuild/test/test.project.ts index 0714a400acc25..f892f8b30eac2 100644 --- a/packages/@aws-cdk/aws-codebuild/test/test.project.ts +++ b/packages/@aws-cdk/aws-codebuild/test/test.project.ts @@ -1,7 +1,8 @@ -import { countResources, expect, haveResource, haveResourceLike, not, ResourcePart } from '@aws-cdk/assert'; +import { countResources, expect, haveResource, haveResourceLike, objectLike, not, ResourcePart } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; +import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as codebuild from '../lib'; @@ -540,4 +541,64 @@ export = { test.done(); }, }, + + 'Environment': { + 'build image - can use secret to access build image'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const secret = new secretsmanager.Secret(stack, 'Secret'); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.s3({ + bucket: new s3.Bucket(stack, 'Bucket'), + path: 'path', + }), + environment: { + buildImage: codebuild.LinuxBuildImage.fromDockerRegistry('myimage', { secretsManagerCredentials: secret }), + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + Environment: objectLike({ + RegistryCredential: { + CredentialProvider: 'SECRETS_MANAGER', + Credential: { 'Ref': 'SecretA720EF05' }, + }, + }), + })); + + test.done(); + }, + + 'build image - can use imported secret by name'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const secret = secretsmanager.Secret.fromSecretNameV2(stack, 'Secret', 'MySecretName'); + + // WHEN + new codebuild.Project(stack, 'Project', { + source: codebuild.Source.s3({ + bucket: new s3.Bucket(stack, 'Bucket'), + path: 'path', + }), + environment: { + buildImage: codebuild.LinuxBuildImage.fromDockerRegistry('myimage', { secretsManagerCredentials: secret }), + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::CodeBuild::Project', { + Environment: objectLike({ + RegistryCredential: { + CredentialProvider: 'SECRETS_MANAGER', + Credential: 'MySecretName', + }, + }), + })); + + test.done(); + }, + }, }; diff --git a/packages/@aws-cdk/aws-codecommit/package.json b/packages/@aws-cdk/aws-codecommit/package.json index 475decd1ad916..dadc9d4e53590 100644 --- a/packages/@aws-cdk/aws-codecommit/package.json +++ b/packages/@aws-cdk/aws-codecommit/package.json @@ -79,7 +79,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-sns": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-codedeploy/lib/lambda/custom-deployment-config.ts b/packages/@aws-cdk/aws-codedeploy/lib/lambda/custom-deployment-config.ts index 870937f046c32..20822a0d2356b 100644 --- a/packages/@aws-cdk/aws-codedeploy/lib/lambda/custom-deployment-config.ts +++ b/packages/@aws-cdk/aws-codedeploy/lib/lambda/custom-deployment-config.ts @@ -1,4 +1,4 @@ -import { Duration, Resource } from '@aws-cdk/core'; +import { Duration, Names, Resource } from '@aws-cdk/core'; import { AwsCustomResource, AwsCustomResourcePolicy, PhysicalResourceId } from '@aws-cdk/custom-resources'; import { Construct } from 'constructs'; import { arnForDeploymentConfig } from '../utils'; @@ -101,7 +101,7 @@ export class CustomLambdaDeploymentConfig extends Resource implements ILambdaDep // Unless the user provides an explicit name this.deploymentConfigName = props.deploymentConfigName !== undefined ? props.deploymentConfigName - : `${this.node.uniqueId}.Lambda${props.type}${props.percentage}Percent${props.type === CustomLambdaDeploymentConfigType.LINEAR + : `${Names.uniqueId(this)}.Lambda${props.type}${props.percentage}Percent${props.type === CustomLambdaDeploymentConfigType.LINEAR ? 'Every' : ''}${props.interval.toMinutes()}Minutes`; this.deploymentConfigArn = arnForDeploymentConfig(this.deploymentConfigName); diff --git a/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts b/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts index 70d159fe6fd8c..d47c994c283e4 100644 --- a/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts +++ b/packages/@aws-cdk/aws-codeguruprofiler/lib/profiling-group.ts @@ -1,5 +1,5 @@ import { Grant, IGrantable } from '@aws-cdk/aws-iam'; -import { IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { IResource, Lazy, Names, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnProfilingGroup } from './codeguruprofiler.generated'; @@ -195,7 +195,7 @@ export class ProfilingGroup extends ProfilingGroupBase { } private generateUniqueId(): string { - const name = this.node.uniqueId; + const name = Names.uniqueId(this); if (name.length > 240) { return name.substring(0, 120) + name.substring(name.length - 120); } diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts index 1982b0b8336bf..1ba14d469120d 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/cloudformation/pipeline-actions.ts @@ -270,7 +270,7 @@ abstract class CloudFormationDeployAction extends CloudFormationAction { // pass role is not allowed for cross-account access - so, // create the deployment Role in the other account! this._deploymentRole = new iam.Role(roleStack, - `${stage.pipeline.node.uniqueId}-${stage.stageName}-${this.actionProperties.actionName}-DeploymentRole`, { + `${cdk.Names.nodeUniqueId(stage.pipeline.node)}-${stage.stageName}-${this.actionProperties.actionName}-DeploymentRole`, { assumedBy: new iam.ServicePrincipal('cloudformation.amazonaws.com'), roleName: cdk.PhysicalName.GENERATE_IF_NEEDED, }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts index 8abca963a1943..9935bcb5be4d7 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/codecommit/source-action.ts @@ -2,7 +2,7 @@ import * as codecommit from '@aws-cdk/aws-codecommit'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as targets from '@aws-cdk/aws-events-targets'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct, Token } from '@aws-cdk/core'; +import { Construct, Names, Token } from '@aws-cdk/core'; import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; @@ -165,7 +165,7 @@ export class CodeCommitSourceAction extends Action { } private generateEventId(stage: codepipeline.IStage): string { - const baseId = stage.pipeline.node.uniqueId; + const baseId = Names.nodeUniqueId(stage.pipeline.node); if (Token.isUnresolved(this.branch)) { let candidate = ''; let counter = 0; diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts index b9261cf34d878..f788c60d6aeea 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/ecr/source-action.ts @@ -2,7 +2,7 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ecr from '@aws-cdk/aws-ecr'; import * as targets from '@aws-cdk/aws-events-targets'; import * as iam from '@aws-cdk/aws-iam'; -import { Construct } from '@aws-cdk/core'; +import { Construct, Names } from '@aws-cdk/core'; import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; @@ -89,7 +89,7 @@ export class EcrSourceAction extends Action { resources: [this.props.repository.repositoryArn], })); - this.props.repository.onCloudTrailImagePushed(stage.pipeline.node.uniqueId + 'SourceEventRule', { + this.props.repository.onCloudTrailImagePushed(Names.nodeUniqueId(stage.pipeline.node) + 'SourceEventRule', { target: new targets.CodePipeline(stage.pipeline), imageTag: this.props.imageTag, }); diff --git a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts index 0d80b576227d9..2470ea6cca25f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts +++ b/packages/@aws-cdk/aws-codepipeline-actions/lib/s3/source-action.ts @@ -1,7 +1,7 @@ import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as targets from '@aws-cdk/aws-events-targets'; import * as s3 from '@aws-cdk/aws-s3'; -import { Construct, Token } from '@aws-cdk/core'; +import { Construct, Names, Token } from '@aws-cdk/core'; import { Action } from '../action'; import { sourceArtifactBounds } from '../common'; @@ -134,7 +134,7 @@ export class S3SourceAction extends Action { private generateEventId(stage: codepipeline.IStage): string { let ret: string; - const baseId = stage.pipeline.node.uniqueId + 'SourceEventRule'; + const baseId = Names.nodeUniqueId(stage.pipeline.node) + 'SourceEventRule'; if (Token.isUnresolved(this.props.bucketKey)) { // If bucketKey is a Token, don't include it in the ID. diff --git a/packages/@aws-cdk/aws-codepipeline-actions/package.json b/packages/@aws-cdk/aws-codepipeline-actions/package.json index fde49ea10ae3f..6cf31932d7c31 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/package.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/package.json @@ -69,7 +69,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-cloudtrail": "0.0.0", - "@types/lodash": "^4.14.163", + "@types/lodash": "^4.14.165", "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json index 2e48bb1667495..023573c87412f 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.lambda-deployed-through-codepipeline.lit.expected.json @@ -1559,6 +1559,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/nodejs:10.1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, @@ -1777,6 +1778,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/nodejs:10.1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json index d2d59b1fe8884..ce11844d4a671 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-build-multiple-inputs-outputs.expected.json @@ -603,6 +603,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json index d5b7c25c88505..7fe649e0c2f8c 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-code-commit-build.expected.json @@ -212,6 +212,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json index 5570acefe6336..2d6b137036065 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-ecs-deploy.expected.json @@ -528,6 +528,7 @@ } ], "Image": "aws/codebuild/docker:17.09.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": true, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json index ba0872c38c31b..d2031a00a8252 100644 --- a/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json +++ b/packages/@aws-cdk/aws-codepipeline-actions/test/integ.pipeline-events.expected.json @@ -882,6 +882,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts index b3d18091322b1..e59de17b454b3 100644 --- a/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts +++ b/packages/@aws-cdk/aws-codepipeline/lib/pipeline.ts @@ -4,7 +4,7 @@ import * as kms from '@aws-cdk/aws-kms'; import * as s3 from '@aws-cdk/aws-s3'; import { App, BootstraplessSynthesizer, Construct as CoreConstruct, DefaultStackSynthesizer, - IStackSynthesizer, Lazy, PhysicalName, RemovalPolicy, Resource, Stack, Token, + IStackSynthesizer, Lazy, Names, PhysicalName, RemovalPolicy, Resource, Stack, Token, } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { ActionCategory, IAction, IPipeline, IStage } from './action'; @@ -552,7 +552,7 @@ export class Pipeline extends PipelineBase { private generateNameForDefaultBucketKeyAlias(): string { const prefix = 'alias/codepipeline-'; const maxAliasLength = 256; - const uniqueId = this.node.uniqueId; + const uniqueId = Names.uniqueId(this); // take the last 256 - (prefix length) characters of uniqueId const startIndex = Math.max(0, uniqueId.length - (maxAliasLength - prefix.length)); return prefix + uniqueId.substring(startIndex).toLowerCase(); @@ -639,7 +639,7 @@ export class Pipeline extends PipelineBase { // generate a role in the other stack, that the Pipeline will assume for executing this action const ret = new iam.Role(otherAccountStack, - `${this.node.uniqueId}-${stage.stageName}-${action.actionProperties.actionName}-ActionRole`, { + `${Names.uniqueId(this)}-${stage.stageName}-${action.actionProperties.actionName}-ActionRole`, { assumedBy: new iam.AccountPrincipal(pipelineStack.account), roleName: PhysicalName.GENERATE_IF_NEEDED, }); diff --git a/packages/@aws-cdk/aws-cognito/lib/index.ts b/packages/@aws-cdk/aws-cognito/lib/index.ts index 9aaccf45bf47e..2da1e6121b69b 100644 --- a/packages/@aws-cdk/aws-cognito/lib/index.ts +++ b/packages/@aws-cdk/aws-cognito/lib/index.ts @@ -4,4 +4,5 @@ export * from './user-pool'; export * from './user-pool-attr'; export * from './user-pool-client'; export * from './user-pool-domain'; -export * from './user-pool-idp'; \ No newline at end of file +export * from './user-pool-idp'; +export * from './user-pool-idps'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts index 1cc5ccbea23b1..c569ccd778fd8 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idp.ts @@ -1,8 +1,5 @@ import { IResource, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { CfnUserPoolIdentityProvider } from './cognito.generated'; -import { StandardAttributeNames } from './private/attr-names'; -import { IUserPool } from './user-pool'; /** * Represents a UserPoolIdentityProvider @@ -32,388 +29,4 @@ export class UserPoolIdentityProvider { } private constructor() {} -} - -/** - * An attribute available from a third party identity provider. - */ -export class ProviderAttribute { - /** The user id attribute provided by Amazon */ - public static readonly AMAZON_USER_ID = new ProviderAttribute('user_id'); - /** The email attribute provided by Amazon */ - public static readonly AMAZON_EMAIL = new ProviderAttribute('email'); - /** The name attribute provided by Amazon */ - public static readonly AMAZON_NAME = new ProviderAttribute('name'); - /** The postal code attribute provided by Amazon */ - public static readonly AMAZON_POSTAL_CODE = new ProviderAttribute('postal_code'); - - /** The user id attribute provided by Facebook */ - public static readonly FACEBOOK_ID = new ProviderAttribute('id'); - /** The birthday attribute provided by Facebook */ - public static readonly FACEBOOK_BIRTHDAY = new ProviderAttribute('birthday'); - /** The email attribute provided by Facebook */ - public static readonly FACEBOOK_EMAIL = new ProviderAttribute('email'); - /** The name attribute provided by Facebook */ - public static readonly FACEBOOK_NAME = new ProviderAttribute('name'); - /** The first name attribute provided by Facebook */ - public static readonly FACEBOOK_FIRST_NAME = new ProviderAttribute('first_name'); - /** The last name attribute provided by Facebook */ - public static readonly FACEBOOK_LAST_NAME = new ProviderAttribute('last_name'); - /** The middle name attribute provided by Facebook */ - public static readonly FACEBOOK_MIDDLE_NAME = new ProviderAttribute('middle_name'); - /** The gender attribute provided by Facebook */ - public static readonly FACEBOOK_GENDER = new ProviderAttribute('gender'); - /** The locale attribute provided by Facebook */ - public static readonly FACEBOOK_LOCALE = new ProviderAttribute('locale'); - - /** The name attribute provided by Google */ - public static readonly GOOGLE_NAMES = new ProviderAttribute('names'); - /** The gender attribute provided by Google */ - public static readonly GOOGLE_GENDER = new ProviderAttribute('gender'); - /** The birthday attribute provided by Google */ - public static readonly GOOGLE_BIRTHDAYS = new ProviderAttribute('birthdays'); - /** The birthday attribute provided by Google */ - public static readonly GOOGLE_PHONE_NUMBERS = new ProviderAttribute('phoneNumbers'); - /** The email attribute provided by Google */ - public static readonly GOOGLE_EMAIL = new ProviderAttribute('email'); - /** The name attribute provided by Google */ - public static readonly GOOGLE_NAME = new ProviderAttribute('name'); - /** The email attribute provided by Google */ - public static readonly GOOGLE_PICTURE = new ProviderAttribute('picture'); - /** The email attribute provided by Google */ - public static readonly GOOGLE_GIVEN_NAME = new ProviderAttribute('given_name'); - /** The email attribute provided by Google */ - public static readonly GOOGLE_FAMILY_NAME = new ProviderAttribute('family_name'); - - /** - * Use this to specify an attribute from the identity provider that is not pre-defined in the CDK. - * @param attributeName the attribute value string as recognized by the provider - */ - public static other(attributeName: string): ProviderAttribute { - return new ProviderAttribute(attributeName); - } - - /** The attribute value string as recognized by the provider. */ - public readonly attributeName: string; - - private constructor(attributeName: string) { - this.attributeName = attributeName; - } -} - -/** - * The mapping of user pool attributes to the attributes provided by the identity providers. - */ -export interface AttributeMapping { - /** - * The user's postal address is a required attribute. - * @default - not mapped - */ - readonly address?: ProviderAttribute; - - /** - * The user's birthday. - * @default - not mapped - */ - readonly birthdate?: ProviderAttribute; - - /** - * The user's e-mail address. - * @default - not mapped - */ - readonly email?: ProviderAttribute; - - /** - * The surname or last name of user. - * @default - not mapped - */ - readonly familyName?: ProviderAttribute; - - /** - * The user's gender. - * @default - not mapped - */ - readonly gender?: ProviderAttribute; - - /** - * The user's first name or give name. - * @default - not mapped - */ - readonly givenName?: ProviderAttribute; - - /** - * The user's locale. - * @default - not mapped - */ - readonly locale?: ProviderAttribute; - - /** - * The user's middle name. - * @default - not mapped - */ - readonly middleName?: ProviderAttribute; - - /** - * The user's full name in displayable form. - * @default - not mapped - */ - readonly fullname?: ProviderAttribute; - - /** - * The user's nickname or casual name. - * @default - not mapped - */ - readonly nickname?: ProviderAttribute; - - /** - * The user's telephone number. - * @default - not mapped - */ - readonly phoneNumber?: ProviderAttribute; - - /** - * The URL to the user's profile picture. - * @default - not mapped - */ - readonly profilePicture?: ProviderAttribute; - - /** - * The user's preferred username. - * @default - not mapped - */ - readonly preferredUsername?: ProviderAttribute; - - /** - * The URL to the user's profile page. - * @default - not mapped - */ - readonly profilePage?: ProviderAttribute; - - /** - * The user's time zone. - * @default - not mapped - */ - readonly timezone?: ProviderAttribute; - - /** - * Time, the user's information was last updated. - * @default - not mapped - */ - readonly lastUpdateTime?: ProviderAttribute; - - /** - * The URL to the user's web page or blog. - * @default - not mapped - */ - readonly website?: ProviderAttribute; - - /** - * Specify custom attribute mapping here and mapping for any standard attributes not supported yet. - * @default - no custom attribute mapping - */ - readonly custom?: { [key: string]: ProviderAttribute }; -} - -/** - * Properties to create a new instance of UserPoolIdentityProvider - */ -export interface UserPoolIdentityProviderProps { - /** - * The user pool to which this construct provides identities. - */ - readonly userPool: IUserPool; - - /** - * Mapping attributes from the identity provider to standard and custom attributes of the user pool. - * @default - no attribute mapping - */ - readonly attributeMapping?: AttributeMapping; -} - -/** - * Options to integrate with the various social identity providers. - */ -abstract class UserPoolIdentityProviderBase extends Resource implements IUserPoolIdentityProvider { - public abstract readonly providerName: string; - - public constructor(scope: Construct, id: string, private readonly props: UserPoolIdentityProviderProps) { - super(scope, id); - props.userPool.registerIdentityProvider(this); - } - - protected configureAttributeMapping(): any { - if (!this.props.attributeMapping) { - return undefined; - } - type SansCustom = Omit; - let mapping: { [key: string]: string } = {}; - mapping = Object.entries(this.props.attributeMapping) - .filter(([k, _]) => k !== 'custom') // 'custom' handled later separately - .reduce((agg, [k, v]) => { - return { ...agg, [StandardAttributeNames[k as keyof SansCustom]]: v.attributeName }; - }, mapping); - if (this.props.attributeMapping.custom) { - mapping = Object.entries(this.props.attributeMapping.custom).reduce((agg, [k, v]) => { - return { ...agg, [k]: v.attributeName }; - }, mapping); - } - if (Object.keys(mapping).length === 0) { return undefined; } - return mapping; - } -} - -/** - * Properties to initialize UserPoolAmazonIdentityProvider - */ -export interface UserPoolIdentityProviderAmazonProps extends UserPoolIdentityProviderProps { - /** - * The client id recognized by 'Login with Amazon' APIs. - * @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier - */ - readonly clientId: string; - /** - * The client secret to be accompanied with clientId for 'Login with Amazon' APIs to authenticate the client. - * @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier - */ - readonly clientSecret: string; - /** - * The types of user profile data to obtain for the Amazon profile. - * @see https://developer.amazon.com/docs/login-with-amazon/customer-profile.html - * @default [ profile ] - */ - readonly scopes?: string[]; -} - -/** - * Represents a identity provider that integrates with 'Login with Amazon' - * @resource AWS::Cognito::UserPoolIdentityProvider - */ -export class UserPoolIdentityProviderAmazon extends UserPoolIdentityProviderBase { - public readonly providerName: string; - - constructor(scope: Construct, id: string, props: UserPoolIdentityProviderAmazonProps) { - super(scope, id, props); - - const scopes = props.scopes ?? ['profile']; - - const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { - userPoolId: props.userPool.userPoolId, - providerName: 'LoginWithAmazon', // must be 'LoginWithAmazon' when the type is 'LoginWithAmazon' - providerType: 'LoginWithAmazon', - providerDetails: { - client_id: props.clientId, - client_secret: props.clientSecret, - authorize_scopes: scopes.join(' '), - }, - attributeMapping: super.configureAttributeMapping(), - }); - - this.providerName = super.getResourceNameAttribute(resource.ref); - } -} - -/** - * Properties to initialize UserPoolFacebookIdentityProvider - */ -export interface UserPoolIdentityProviderFacebookProps extends UserPoolIdentityProviderProps { - /** - * The client id recognized by Facebook APIs. - */ - readonly clientId: string; - /** - * The client secret to be accompanied with clientUd for Facebook to authenticate the client. - * @see https://developers.facebook.com/docs/facebook-login/security#appsecret - */ - readonly clientSecret: string; - /** - * The list of facebook permissions to obtain for getting access to the Facebook profile. - * @see https://developers.facebook.com/docs/facebook-login/permissions - * @default [ public_profile ] - */ - readonly scopes?: string[]; - /** - * The Facebook API version to use - * @default - to the oldest version supported by Facebook - */ - readonly apiVersion?: string; -} - -/** -* Represents a identity provider that integrates with 'Facebook Login' -* @resource AWS::Cognito::UserPoolIdentityProvider -*/ -export class UserPoolIdentityProviderFacebook extends UserPoolIdentityProviderBase { - public readonly providerName: string; - - constructor(scope: Construct, id: string, props: UserPoolIdentityProviderFacebookProps) { - super(scope, id, props); - - const scopes = props.scopes ?? ['public_profile']; - - const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { - userPoolId: props.userPool.userPoolId, - providerName: 'Facebook', // must be 'Facebook' when the type is 'Facebook' - providerType: 'Facebook', - providerDetails: { - client_id: props.clientId, - client_secret: props.clientSecret, - authorize_scopes: scopes.join(','), - api_version: props.apiVersion, - }, - attributeMapping: super.configureAttributeMapping(), - }); - - this.providerName = super.getResourceNameAttribute(resource.ref); - } -} - - -/** - * Properties to initialize UserPoolGoogleIdentityProvider - */ -export interface UserPoolIdentityProviderGoogleProps extends UserPoolIdentityProviderProps { - /** - * The client id recognized by Google APIs. - * @see https://developers.google.com/identity/sign-in/web/sign-in#specify_your_apps_client_id - */ - readonly clientId: string; - /** - * The client secret to be accompanied with clientId for Google APIs to authenticate the client. - * @see https://developers.google.com/identity/sign-in/web/sign-in - */ - readonly clientSecret: string; - /** - * The list of google permissions to obtain for getting access to the google profile - * @see https://developers.google.com/identity/sign-in/web/sign-in - * @default [ profile ] - */ - readonly scopes?: string[]; -} - -/** - * Represents a identity provider that integrates with 'Google' - * @resource AWS::Cognito::UserPoolIdentityProvider - */ -export class UserPoolIdentityProviderGoogle extends UserPoolIdentityProviderBase { - public readonly providerName: string; - - constructor(scope: Construct, id: string, props: UserPoolIdentityProviderGoogleProps) { - super(scope, id, props); - - const scopes = props.scopes ?? ['profile']; - - const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { - userPoolId: props.userPool.userPoolId, - providerName: 'Google', // must be 'Google' when the type is 'Google' - providerType: 'Google', - providerDetails: { - client_id: props.clientId, - client_secret: props.clientSecret, - authorize_scopes: scopes.join(' '), - }, - attributeMapping: super.configureAttributeMapping(), - }); - - this.providerName = super.getResourceNameAttribute(resource.ref); - } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts new file mode 100644 index 0000000000000..c94cd5583a9d9 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/amazon.ts @@ -0,0 +1,54 @@ +import { Construct } from 'constructs'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderProps } from './base'; +import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base'; + +/** + * Properties to initialize UserPoolAmazonIdentityProvider + */ +export interface UserPoolIdentityProviderAmazonProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by 'Login with Amazon' APIs. + * @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier + */ + readonly clientId: string; + /** + * The client secret to be accompanied with clientId for 'Login with Amazon' APIs to authenticate the client. + * @see https://developer.amazon.com/docs/login-with-amazon/security-profile.html#client-identifier + */ + readonly clientSecret: string; + /** + * The types of user profile data to obtain for the Amazon profile. + * @see https://developer.amazon.com/docs/login-with-amazon/customer-profile.html + * @default [ profile ] + */ + readonly scopes?: string[]; +} + +/** + * Represents a identity provider that integrates with 'Login with Amazon' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderAmazon extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderAmazonProps) { + super(scope, id, props); + + const scopes = props.scopes ?? ['profile']; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'LoginWithAmazon', // must be 'LoginWithAmazon' when the type is 'LoginWithAmazon' + providerType: 'LoginWithAmazon', + providerDetails: { + client_id: props.clientId, + client_secret: props.clientSecret, + authorize_scopes: scopes.join(' '), + }, + attributeMapping: super.configureAttributeMapping(), + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts new file mode 100644 index 0000000000000..27ee88777d885 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/base.ts @@ -0,0 +1,198 @@ +import { IUserPool } from '../user-pool'; + +/** + * An attribute available from a third party identity provider. + */ +export class ProviderAttribute { + /** The user id attribute provided by Amazon */ + public static readonly AMAZON_USER_ID = new ProviderAttribute('user_id'); + /** The email attribute provided by Amazon */ + public static readonly AMAZON_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Amazon */ + public static readonly AMAZON_NAME = new ProviderAttribute('name'); + /** The postal code attribute provided by Amazon */ + public static readonly AMAZON_POSTAL_CODE = new ProviderAttribute('postal_code'); + + /** The user id attribute provided by Facebook */ + public static readonly FACEBOOK_ID = new ProviderAttribute('id'); + /** The birthday attribute provided by Facebook */ + public static readonly FACEBOOK_BIRTHDAY = new ProviderAttribute('birthday'); + /** The email attribute provided by Facebook */ + public static readonly FACEBOOK_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Facebook */ + public static readonly FACEBOOK_NAME = new ProviderAttribute('name'); + /** The first name attribute provided by Facebook */ + public static readonly FACEBOOK_FIRST_NAME = new ProviderAttribute('first_name'); + /** The last name attribute provided by Facebook */ + public static readonly FACEBOOK_LAST_NAME = new ProviderAttribute('last_name'); + /** The middle name attribute provided by Facebook */ + public static readonly FACEBOOK_MIDDLE_NAME = new ProviderAttribute('middle_name'); + /** The gender attribute provided by Facebook */ + public static readonly FACEBOOK_GENDER = new ProviderAttribute('gender'); + /** The locale attribute provided by Facebook */ + public static readonly FACEBOOK_LOCALE = new ProviderAttribute('locale'); + + /** The name attribute provided by Google */ + public static readonly GOOGLE_NAMES = new ProviderAttribute('names'); + /** The gender attribute provided by Google */ + public static readonly GOOGLE_GENDER = new ProviderAttribute('gender'); + /** The birthday attribute provided by Google */ + public static readonly GOOGLE_BIRTHDAYS = new ProviderAttribute('birthdays'); + /** The birthday attribute provided by Google */ + public static readonly GOOGLE_PHONE_NUMBERS = new ProviderAttribute('phoneNumbers'); + /** The email attribute provided by Google */ + public static readonly GOOGLE_EMAIL = new ProviderAttribute('email'); + /** The name attribute provided by Google */ + public static readonly GOOGLE_NAME = new ProviderAttribute('name'); + /** The email attribute provided by Google */ + public static readonly GOOGLE_PICTURE = new ProviderAttribute('picture'); + /** The email attribute provided by Google */ + public static readonly GOOGLE_GIVEN_NAME = new ProviderAttribute('given_name'); + /** The email attribute provided by Google */ + public static readonly GOOGLE_FAMILY_NAME = new ProviderAttribute('family_name'); + + /** + * Use this to specify an attribute from the identity provider that is not pre-defined in the CDK. + * @param attributeName the attribute value string as recognized by the provider + */ + public static other(attributeName: string): ProviderAttribute { + return new ProviderAttribute(attributeName); + } + + /** The attribute value string as recognized by the provider. */ + public readonly attributeName: string; + + private constructor(attributeName: string) { + this.attributeName = attributeName; + } +} + +/** + * The mapping of user pool attributes to the attributes provided by the identity providers. + */ +export interface AttributeMapping { + /** + * The user's postal address is a required attribute. + * @default - not mapped + */ + readonly address?: ProviderAttribute; + + /** + * The user's birthday. + * @default - not mapped + */ + readonly birthdate?: ProviderAttribute; + + /** + * The user's e-mail address. + * @default - not mapped + */ + readonly email?: ProviderAttribute; + + /** + * The surname or last name of user. + * @default - not mapped + */ + readonly familyName?: ProviderAttribute; + + /** + * The user's gender. + * @default - not mapped + */ + readonly gender?: ProviderAttribute; + + /** + * The user's first name or give name. + * @default - not mapped + */ + readonly givenName?: ProviderAttribute; + + /** + * The user's locale. + * @default - not mapped + */ + readonly locale?: ProviderAttribute; + + /** + * The user's middle name. + * @default - not mapped + */ + readonly middleName?: ProviderAttribute; + + /** + * The user's full name in displayable form. + * @default - not mapped + */ + readonly fullname?: ProviderAttribute; + + /** + * The user's nickname or casual name. + * @default - not mapped + */ + readonly nickname?: ProviderAttribute; + + /** + * The user's telephone number. + * @default - not mapped + */ + readonly phoneNumber?: ProviderAttribute; + + /** + * The URL to the user's profile picture. + * @default - not mapped + */ + readonly profilePicture?: ProviderAttribute; + + /** + * The user's preferred username. + * @default - not mapped + */ + readonly preferredUsername?: ProviderAttribute; + + /** + * The URL to the user's profile page. + * @default - not mapped + */ + readonly profilePage?: ProviderAttribute; + + /** + * The user's time zone. + * @default - not mapped + */ + readonly timezone?: ProviderAttribute; + + /** + * Time, the user's information was last updated. + * @default - not mapped + */ + readonly lastUpdateTime?: ProviderAttribute; + + /** + * The URL to the user's web page or blog. + * @default - not mapped + */ + readonly website?: ProviderAttribute; + + /** + * Specify custom attribute mapping here and mapping for any standard attributes not supported yet. + * @default - no custom attribute mapping + */ + readonly custom?: { [key: string]: ProviderAttribute }; +} + +/** + * Properties to create a new instance of UserPoolIdentityProvider + * + */ +export interface UserPoolIdentityProviderProps { + /** + * The user pool to which this construct provides identities. + */ + readonly userPool: IUserPool; + + /** + * Mapping attributes from the identity provider to standard and custom attributes of the user pool. + * @default - no attribute mapping + */ + readonly attributeMapping?: AttributeMapping; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts new file mode 100644 index 0000000000000..81b7a37ac3d11 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/facebook.ts @@ -0,0 +1,59 @@ +import { Construct } from 'constructs'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderProps } from './base'; +import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base'; + +/** + * Properties to initialize UserPoolFacebookIdentityProvider + */ +export interface UserPoolIdentityProviderFacebookProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by Facebook APIs. + */ + readonly clientId: string; + /** + * The client secret to be accompanied with clientUd for Facebook to authenticate the client. + * @see https://developers.facebook.com/docs/facebook-login/security#appsecret + */ + readonly clientSecret: string; + /** + * The list of facebook permissions to obtain for getting access to the Facebook profile. + * @see https://developers.facebook.com/docs/facebook-login/permissions + * @default [ public_profile ] + */ + readonly scopes?: string[]; + /** + * The Facebook API version to use + * @default - to the oldest version supported by Facebook + */ + readonly apiVersion?: string; +} + +/** + * Represents a identity provider that integrates with 'Facebook Login' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderFacebook extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderFacebookProps) { + super(scope, id, props); + + const scopes = props.scopes ?? ['public_profile']; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'Facebook', // must be 'Facebook' when the type is 'Facebook' + providerType: 'Facebook', + providerDetails: { + client_id: props.clientId, + client_secret: props.clientSecret, + authorize_scopes: scopes.join(','), + api_version: props.apiVersion, + }, + attributeMapping: super.configureAttributeMapping(), + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/google.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/google.ts new file mode 100644 index 0000000000000..7d74dc9eb30fe --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/google.ts @@ -0,0 +1,54 @@ +import { Construct } from 'constructs'; +import { CfnUserPoolIdentityProvider } from '../cognito.generated'; +import { UserPoolIdentityProviderProps } from './base'; +import { UserPoolIdentityProviderBase } from './private/user-pool-idp-base'; + +/** + * Properties to initialize UserPoolGoogleIdentityProvider + */ +export interface UserPoolIdentityProviderGoogleProps extends UserPoolIdentityProviderProps { + /** + * The client id recognized by Google APIs. + * @see https://developers.google.com/identity/sign-in/web/sign-in#specify_your_apps_client_id + */ + readonly clientId: string; + /** + * The client secret to be accompanied with clientId for Google APIs to authenticate the client. + * @see https://developers.google.com/identity/sign-in/web/sign-in + */ + readonly clientSecret: string; + /** + * The list of google permissions to obtain for getting access to the google profile + * @see https://developers.google.com/identity/sign-in/web/sign-in + * @default [ profile ] + */ + readonly scopes?: string[]; +} + +/** + * Represents a identity provider that integrates with 'Google' + * @resource AWS::Cognito::UserPoolIdentityProvider + */ +export class UserPoolIdentityProviderGoogle extends UserPoolIdentityProviderBase { + public readonly providerName: string; + + constructor(scope: Construct, id: string, props: UserPoolIdentityProviderGoogleProps) { + super(scope, id, props); + + const scopes = props.scopes ?? ['profile']; + + const resource = new CfnUserPoolIdentityProvider(this, 'Resource', { + userPoolId: props.userPool.userPoolId, + providerName: 'Google', // must be 'Google' when the type is 'Google' + providerType: 'Google', + providerDetails: { + client_id: props.clientId, + client_secret: props.clientSecret, + authorize_scopes: scopes.join(' '), + }, + attributeMapping: super.configureAttributeMapping(), + }); + + this.providerName = super.getResourceNameAttribute(resource.ref); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts new file mode 100644 index 0000000000000..dbc63a9854f37 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/index.ts @@ -0,0 +1,4 @@ +export * from './base'; +export * from './amazon'; +export * from './facebook'; +export * from './google'; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/private/user-pool-idp-base.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/private/user-pool-idp-base.ts new file mode 100644 index 0000000000000..633972f4b82f3 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool-idps/private/user-pool-idp-base.ts @@ -0,0 +1,39 @@ +import { Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { StandardAttributeNames } from '../../private/attr-names'; +import { IUserPoolIdentityProvider } from '../../user-pool-idp'; +import { UserPoolIdentityProviderProps, AttributeMapping } from '../base'; + +/** + * Options to integrate with the various social identity providers. + * + * @internal + */ +export abstract class UserPoolIdentityProviderBase extends Resource implements IUserPoolIdentityProvider { + public abstract readonly providerName: string; + + public constructor(scope: Construct, id: string, private readonly props: UserPoolIdentityProviderProps) { + super(scope, id); + props.userPool.registerIdentityProvider(this); + } + + protected configureAttributeMapping(): any { + if (!this.props.attributeMapping) { + return undefined; + } + type SansCustom = Omit; + let mapping: { [key: string]: string } = {}; + mapping = Object.entries(this.props.attributeMapping) + .filter(([k, _]) => k !== 'custom') // 'custom' handled later separately + .reduce((agg, [k, v]) => { + return { ...agg, [StandardAttributeNames[k as keyof SansCustom]]: v.attributeName }; + }, mapping); + if (this.props.attributeMapping.custom) { + mapping = Object.entries(this.props.attributeMapping.custom).reduce((agg, [k, v]) => { + return { ...agg, [k]: v.attributeName }; + }, mapping); + } + if (Object.keys(mapping).length === 0) { return undefined; } + return mapping; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts index 1f3feba34c669..91d62af406489 100644 --- a/packages/@aws-cdk/aws-cognito/lib/user-pool.ts +++ b/packages/@aws-cdk/aws-cognito/lib/user-pool.ts @@ -1,6 +1,6 @@ import { IRole, PolicyDocument, PolicyStatement, Role, ServicePrincipal } from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Duration, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import { Duration, IResource, Lazy, Names, Resource, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnUserPool } from './cognito.generated'; import { StandardAttributeNames } from './private/attr-names'; @@ -866,7 +866,7 @@ export class UserPool extends UserPoolBase { return undefined; } - const smsRoleExternalId = this.node.uniqueId.substr(0, 1223); // sts:ExternalId max length of 1224 + const smsRoleExternalId = Names.uniqueId(this).substr(0, 1223); // sts:ExternalId max length of 1224 const smsRole = props.smsRole ?? new Role(this, 'smsRole', { assumedBy: new ServicePrincipal('cognito-idp.amazonaws.com', { conditions: { diff --git a/packages/@aws-cdk/aws-cognito/package.json b/packages/@aws-cdk/aws-cognito/package.json index 3d554308d1bff..82884c63ec340 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/package.json @@ -76,7 +76,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idp.test.ts deleted file mode 100644 index c997fa9d61190..0000000000000 --- a/packages/@aws-cdk/aws-cognito/test/user-pool-idp.test.ts +++ /dev/null @@ -1,295 +0,0 @@ -import '@aws-cdk/assert/jest'; -import { Stack } from '@aws-cdk/core'; -import { ProviderAttribute, UserPool, UserPoolIdentityProviderAmazon, UserPoolIdentityProviderFacebook, UserPoolIdentityProviderGoogle } from '../lib'; - -describe('UserPoolIdentityProvider', () => { - describe('amazon', () => { - test('defaults', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { - userPool: pool, - clientId: 'amzn-client-id', - clientSecret: 'amzn-client-secret', - }); - - expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'LoginWithAmazon', - ProviderType: 'LoginWithAmazon', - ProviderDetails: { - client_id: 'amzn-client-id', - client_secret: 'amzn-client-secret', - authorize_scopes: 'profile', - }, - }); - }); - - test('scopes', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { - userPool: pool, - clientId: 'amzn-client-id', - clientSecret: 'amzn-client-secret', - scopes: ['scope1', 'scope2'], - }); - - expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'LoginWithAmazon', - ProviderType: 'LoginWithAmazon', - ProviderDetails: { - client_id: 'amzn-client-id', - client_secret: 'amzn-client-secret', - authorize_scopes: 'scope1 scope2', - }, - }); - }); - - test('registered with user pool', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - const provider = new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { - userPool: pool, - clientId: 'amzn-client-id', - clientSecret: 'amzn-client-secret', - }); - - // THEN - expect(pool.identityProviders).toContain(provider); - }); - - test('attribute mapping', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { - userPool: pool, - clientId: 'amazn-client-id', - clientSecret: 'amzn-client-secret', - attributeMapping: { - givenName: ProviderAttribute.AMAZON_NAME, - address: ProviderAttribute.other('amzn-address'), - custom: { - customAttr1: ProviderAttribute.AMAZON_EMAIL, - customAttr2: ProviderAttribute.other('amzn-custom-attr'), - }, - }, - }); - - // THEN - expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { - AttributeMapping: { - given_name: 'name', - address: 'amzn-address', - customAttr1: 'email', - customAttr2: 'amzn-custom-attr', - }, - }); - }); - }); - - describe('facebook', () => { - test('defaults', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { - userPool: pool, - clientId: 'fb-client-id', - clientSecret: 'fb-client-secret', - }); - - expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'Facebook', - ProviderType: 'Facebook', - ProviderDetails: { - client_id: 'fb-client-id', - client_secret: 'fb-client-secret', - authorize_scopes: 'public_profile', - }, - }); - }); - - test('scopes & api_version', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { - userPool: pool, - clientId: 'fb-client-id', - clientSecret: 'fb-client-secret', - scopes: ['scope1', 'scope2'], - apiVersion: 'version1', - }); - - expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'Facebook', - ProviderType: 'Facebook', - ProviderDetails: { - client_id: 'fb-client-id', - client_secret: 'fb-client-secret', - authorize_scopes: 'scope1,scope2', - api_version: 'version1', - }, - }); - }); - - test('registered with user pool', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - const provider = new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { - userPool: pool, - clientId: 'fb-client-id', - clientSecret: 'fb-client-secret', - }); - - // THEN - expect(pool.identityProviders).toContain(provider); - }); - - test('attribute mapping', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { - userPool: pool, - clientId: 'fb-client-id', - clientSecret: 'fb-client-secret', - attributeMapping: { - givenName: ProviderAttribute.FACEBOOK_NAME, - address: ProviderAttribute.other('fb-address'), - custom: { - customAttr1: ProviderAttribute.FACEBOOK_EMAIL, - customAttr2: ProviderAttribute.other('fb-custom-attr'), - }, - }, - }); - - // THEN - expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { - AttributeMapping: { - given_name: 'name', - address: 'fb-address', - customAttr1: 'email', - customAttr2: 'fb-custom-attr', - }, - }); - }); - }); - - describe('google', () => { - test('defaults', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { - userPool: pool, - clientId: 'google-client-id', - clientSecret: 'google-client-secret', - }); - - expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'Google', - ProviderType: 'Google', - ProviderDetails: { - client_id: 'google-client-id', - client_secret: 'google-client-secret', - authorize_scopes: 'profile', - }, - }); - }); - - test('scopes', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { - userPool: pool, - clientId: 'google-client-id', - clientSecret: 'google-client-secret', - scopes: ['scope1', 'scope2'], - }); - - expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { - ProviderName: 'Google', - ProviderType: 'Google', - ProviderDetails: { - client_id: 'google-client-id', - client_secret: 'google-client-secret', - authorize_scopes: 'scope1 scope2', - }, - }); - }); - - test('registered with user pool', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - const provider = new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { - userPool: pool, - clientId: 'google-client-id', - clientSecret: 'google-client-secret', - }); - - // THEN - expect(pool.identityProviders).toContain(provider); - }); - - test('attribute mapping', () => { - // GIVEN - const stack = new Stack(); - const pool = new UserPool(stack, 'userpool'); - - // WHEN - new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { - userPool: pool, - clientId: 'google-client-id', - clientSecret: 'google-client-secret', - attributeMapping: { - givenName: ProviderAttribute.GOOGLE_NAME, - address: ProviderAttribute.other('google-address'), - custom: { - customAttr1: ProviderAttribute.GOOGLE_EMAIL, - customAttr2: ProviderAttribute.other('google-custom-attr'), - }, - }, - }); - - // THEN - expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { - AttributeMapping: { - given_name: 'name', - address: 'google-address', - customAttr1: 'email', - customAttr2: 'google-custom-attr', - }, - }); - }); - }); -}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts new file mode 100644 index 0000000000000..a6995367a3ded --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/amazon.test.ts @@ -0,0 +1,101 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderAmazon } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('amazon', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'LoginWithAmazon', + ProviderType: 'LoginWithAmazon', + ProviderDetails: { + client_id: 'amzn-client-id', + client_secret: 'amzn-client-secret', + authorize_scopes: 'profile', + }, + }); + }); + + test('scopes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + scopes: ['scope1', 'scope2'], + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'LoginWithAmazon', + ProviderType: 'LoginWithAmazon', + ProviderDetails: { + client_id: 'amzn-client-id', + client_secret: 'amzn-client-secret', + authorize_scopes: 'scope1 scope2', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amzn-client-id', + clientSecret: 'amzn-client-secret', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderAmazon(stack, 'userpoolidp', { + userPool: pool, + clientId: 'amazn-client-id', + clientSecret: 'amzn-client-secret', + attributeMapping: { + givenName: ProviderAttribute.AMAZON_NAME, + address: ProviderAttribute.other('amzn-address'), + custom: { + customAttr1: ProviderAttribute.AMAZON_EMAIL, + customAttr2: ProviderAttribute.other('amzn-custom-attr'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + given_name: 'name', + address: 'amzn-address', + customAttr1: 'email', + customAttr2: 'amzn-custom-attr', + }, + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts new file mode 100644 index 0000000000000..2bbac71068439 --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/base.test.ts @@ -0,0 +1,95 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool } from '../../lib'; +import { UserPoolIdentityProviderBase } from '../../lib/user-pool-idps/private/user-pool-idp-base'; + +class MyIdp extends UserPoolIdentityProviderBase { + public readonly providerName = 'MyProvider'; + public readonly mapping = this.configureAttributeMapping(); +} + +describe('UserPoolIdentityProvider', () => { + describe('attribute mapping', () => { + test('absent or empty', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp1 = new MyIdp(stack, 'MyIdp1', { + userPool: pool, + }); + const idp2 = new MyIdp(stack, 'MyIdp2', { + userPool: pool, + attributeMapping: {}, + }); + + // THEN + expect(idp1.mapping).toBeUndefined(); + expect(idp2.mapping).toBeUndefined(); + }); + + test('standard attributes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp = new MyIdp(stack, 'MyIdp', { + userPool: pool, + attributeMapping: { + givenName: ProviderAttribute.FACEBOOK_NAME, + birthdate: ProviderAttribute.FACEBOOK_BIRTHDAY, + }, + }); + + // THEN + expect(idp.mapping).toStrictEqual({ + given_name: 'name', + birthdate: 'birthday', + }); + }); + + test('custom', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp = new MyIdp(stack, 'MyIdp', { + userPool: pool, + attributeMapping: { + custom: { + 'custom-attr-1': ProviderAttribute.AMAZON_EMAIL, + 'custom-attr-2': ProviderAttribute.AMAZON_NAME, + }, + }, + }); + + // THEN + expect(idp.mapping).toStrictEqual({ + 'custom-attr-1': 'email', + 'custom-attr-2': 'name', + }); + }); + + test('custom provider attribute', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'UserPool'); + + // WHEN + const idp = new MyIdp(stack, 'MyIdp', { + userPool: pool, + attributeMapping: { + address: ProviderAttribute.other('custom-provider-attr'), + }, + }); + + // THEN + expect(idp.mapping).toStrictEqual({ + address: 'custom-provider-attr', + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts new file mode 100644 index 0000000000000..3020bd117221f --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/facebook.test.ts @@ -0,0 +1,103 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderFacebook } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('facebook', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Facebook', + ProviderType: 'Facebook', + ProviderDetails: { + client_id: 'fb-client-id', + client_secret: 'fb-client-secret', + authorize_scopes: 'public_profile', + }, + }); + }); + + test('scopes & api_version', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + scopes: ['scope1', 'scope2'], + apiVersion: 'version1', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Facebook', + ProviderType: 'Facebook', + ProviderDetails: { + client_id: 'fb-client-id', + client_secret: 'fb-client-secret', + authorize_scopes: 'scope1,scope2', + api_version: 'version1', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderFacebook(stack, 'userpoolidp', { + userPool: pool, + clientId: 'fb-client-id', + clientSecret: 'fb-client-secret', + attributeMapping: { + givenName: ProviderAttribute.FACEBOOK_NAME, + address: ProviderAttribute.other('fb-address'), + custom: { + customAttr1: ProviderAttribute.FACEBOOK_EMAIL, + customAttr2: ProviderAttribute.other('fb-custom-attr'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + given_name: 'name', + address: 'fb-address', + customAttr1: 'email', + customAttr2: 'fb-custom-attr', + }, + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-cognito/test/user-pool-idps/google.test.ts b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/google.test.ts new file mode 100644 index 0000000000000..41700abe1c92d --- /dev/null +++ b/packages/@aws-cdk/aws-cognito/test/user-pool-idps/google.test.ts @@ -0,0 +1,101 @@ +import '@aws-cdk/assert/jest'; +import { Stack } from '@aws-cdk/core'; +import { ProviderAttribute, UserPool, UserPoolIdentityProviderGoogle } from '../../lib'; + +describe('UserPoolIdentityProvider', () => { + describe('google', () => { + test('defaults', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { + userPool: pool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Google', + ProviderType: 'Google', + ProviderDetails: { + client_id: 'google-client-id', + client_secret: 'google-client-secret', + authorize_scopes: 'profile', + }, + }); + }); + + test('scopes', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { + userPool: pool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + scopes: ['scope1', 'scope2'], + }); + + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + ProviderName: 'Google', + ProviderType: 'Google', + ProviderDetails: { + client_id: 'google-client-id', + client_secret: 'google-client-secret', + authorize_scopes: 'scope1 scope2', + }, + }); + }); + + test('registered with user pool', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + const provider = new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { + userPool: pool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + }); + + // THEN + expect(pool.identityProviders).toContain(provider); + }); + + test('attribute mapping', () => { + // GIVEN + const stack = new Stack(); + const pool = new UserPool(stack, 'userpool'); + + // WHEN + new UserPoolIdentityProviderGoogle(stack, 'userpoolidp', { + userPool: pool, + clientId: 'google-client-id', + clientSecret: 'google-client-secret', + attributeMapping: { + givenName: ProviderAttribute.GOOGLE_NAME, + address: ProviderAttribute.other('google-address'), + custom: { + customAttr1: ProviderAttribute.GOOGLE_EMAIL, + customAttr2: ProviderAttribute.other('google-custom-attr'), + }, + }, + }); + + // THEN + expect(stack).toHaveResource('AWS::Cognito::UserPoolIdentityProvider', { + AttributeMapping: { + given_name: 'name', + address: 'google-address', + customAttr1: 'email', + customAttr2: 'google-custom-attr', + }, + }); + }); + }); +}); \ No newline at end of file diff --git a/packages/@aws-cdk/aws-docdb/package.json b/packages/@aws-cdk/aws-docdb/package.json index 2e1d464346269..7cf1e49c2755d 100644 --- a/packages/@aws-cdk/aws-docdb/package.json +++ b/packages/@aws-cdk/aws-docdb/package.json @@ -101,6 +101,7 @@ }, "awslint": { "exclude": [ + "attribute-tag:@aws-cdk/aws-docdb.DatabaseSecret.secretFullArn", "attribute-tag:@aws-cdk/aws-docdb.DatabaseSecret.secretName" ] }, diff --git a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json index fcfbb22fdcc06..e19b1fe502b2d 100644 --- a/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json +++ b/packages/@aws-cdk/aws-dynamodb-global/lambda-packages/aws-global-table-coordinator/package.json @@ -35,7 +35,7 @@ "eslint-plugin-node": "^11.1.0", "eslint-plugin-promise": "^4.2.1", "eslint-plugin-standard": "^4.0.2", - "jest": "^26.6.1", + "jest": "^26.6.3", "lambda-tester": "^3.6.0", "nock": "^13.0.4" } diff --git a/packages/@aws-cdk/aws-dynamodb/lib/table.ts b/packages/@aws-cdk/aws-dynamodb/lib/table.ts index d1c6836acd133..f67175c62c5b9 100644 --- a/packages/@aws-cdk/aws-dynamodb/lib/table.ts +++ b/packages/@aws-cdk/aws-dynamodb/lib/table.ts @@ -4,7 +4,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import { Aws, CfnCondition, CfnCustomResource, Construct as CoreConstruct, CustomResource, Fn, - IResource, Lazy, RemovalPolicy, Resource, Stack, Token, + IResource, Lazy, Names, RemovalPolicy, Resource, Stack, Token, } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnTable, CfnTableProps } from './dynamodb.generated'; @@ -19,6 +19,54 @@ const RANGE_KEY_TYPE = 'RANGE'; // https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/Limits.html#limits-secondary-indexes const MAX_LOCAL_SECONDARY_INDEX_COUNT = 5; +/** + * Options for configuring a system errors metric that considers multiple operations. + */ +export interface SystemErrorsForOperationsMetricOptions extends cloudwatch.MetricOptions { + + /** + * The operations to apply the metric to. + * + * @default - All operations available by DynamoDB tables will be considered. + */ + readonly operations?: Operation[]; + +} + +/** + * Supported DynamoDB table operations. + */ +export enum Operation { + + /** GetItem */ + GET_ITEM = 'GetItem', + + /** BatchGetItem */ + BATCH_GET_ITEM = 'BatchGetItem', + + /** Scan */ + SCAN = 'Scan', + + /** Query */ + QUERY = 'Query', + + /** GetRecords */ + GET_RECORDS = 'GetRecords', + + /** PutItem */ + PUT_ITEM = 'PutItem', + + /** DeleteItem */ + DELETE_ITEM = 'DeleteItem', + + /** UpdateItem */ + UPDATE_ITEM = 'UpdateItem', + + /** BatchWriteItem */ + BATCH_WRITE_ITEM = 'BatchWriteItem', + +} + /** * Represents an attribute for describing the key schema for the table * and indexes. @@ -385,6 +433,8 @@ export interface ITable extends IResource { * Metric for the system errors * * @param props properties of a metric + * + * @deprecated use `metricSystemErrorsForOperations` */ metricSystemErrors(props?: cloudwatch.MetricOptions): cloudwatch.Metric; @@ -406,8 +456,10 @@ export interface ITable extends IResource { * Metric for the successful request latency * * @param props properties of a metric + * */ metricSuccessfulRequestLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric; + } /** @@ -628,6 +680,9 @@ abstract class TableBase extends Resource implements ITable { /** * Return the given named metric for this Table + * + * By default, the metric will be calculated as a sum over a period of 5 minutes. + * You can customize this by using the `statistic` and `period` properties. */ public metric(metricName: string, props?: cloudwatch.MetricOptions): cloudwatch.Metric { return new cloudwatch.Metric({ @@ -643,7 +698,8 @@ abstract class TableBase extends Resource implements ITable { /** * Metric for the consumed read capacity units this table * - * @default sum over a minute + * By default, the metric will be calculated as a sum over a period of 5 minutes. + * You can customize this by using the `statistic` and `period` properties. */ public metricConsumedReadCapacityUnits(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('ConsumedReadCapacityUnits', { statistic: 'sum', ...props }); @@ -652,7 +708,8 @@ abstract class TableBase extends Resource implements ITable { /** * Metric for the consumed write capacity units this table * - * @default sum over a minute + * By default, the metric will be calculated as a sum over a period of 5 minutes. + * You can customize this by using the `statistic` and `period` properties. */ public metricConsumedWriteCapacityUnits(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('ConsumedWriteCapacityUnits', { statistic: 'sum', ...props }); @@ -661,37 +718,145 @@ abstract class TableBase extends Resource implements ITable { /** * Metric for the system errors this table * - * @default sum over a minute + * @deprecated use `metricSystemErrorsForOperations`. */ public metricSystemErrors(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('SystemErrors', { statistic: 'sum', ...props }); + + if (!props?.dimensions?.Operation) { + // 'Operation' must be passed because its an operational metric. + throw new Error("'Operation' dimension must be passed for the 'SystemErrors' metric."); + } + + const dimensions = { + TableName: this.tableName, + ...props?.dimensions ?? {}, + }; + + return this.metric('SystemErrors', { statistic: 'sum', ...props, dimensions }); } /** - * Metric for the user errors this table + * Metric for the user errors. Note that this metric reports user errors across all + * the tables in the account and region the table resides in. * - * @default sum over a minute + * By default, the metric will be calculated as a sum over a period of 5 minutes. + * You can customize this by using the `statistic` and `period` properties. */ public metricUserErrors(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('UserErrors', { statistic: 'sum', ...props }); + + if (props?.dimensions) { + throw new Error("'dimensions' is not supported for the 'UserErrors' metric"); + } + + // overriding 'dimensions' here because this metric is an account metric. + // see 'UserErrors' in https://docs.aws.amazon.com/amazondynamodb/latest/developerguide/metrics-dimensions.html + return this.metric('UserErrors', { statistic: 'sum', ...props, dimensions: {} }); } /** * Metric for the conditional check failed requests this table * - * @default sum over a minute + * By default, the metric will be calculated as a sum over a period of 5 minutes. + * You can customize this by using the `statistic` and `period` properties. */ public metricConditionalCheckFailedRequests(props?: cloudwatch.MetricOptions): cloudwatch.Metric { return this.metric('ConditionalCheckFailedRequests', { statistic: 'sum', ...props }); } /** - * Metric for the successful request latency this table + * Metric for the successful request latency this table. + * + * By default, the metric will be calculated as an average over a period of 5 minutes. + * You can customize this by using the `statistic` and `period` properties. * - * @default avg over a minute */ public metricSuccessfulRequestLatency(props?: cloudwatch.MetricOptions): cloudwatch.Metric { - return this.metric('SuccessfulRequestLatency', { statistic: 'avg', ...props }); + + if (!props?.dimensions?.Operation) { + throw new Error("'Operation' dimension must be passed for the 'SuccessfulRequestLatency' metric."); + } + + const dimensions = { + TableName: this.tableName, + ...props?.dimensions ?? {}, + }; + + return this.metric('SuccessfulRequestLatency', { statistic: 'avg', ...props, dimensions }); + } + + /** + * Metric for the system errors this table. + * + * This will sum errors across all possible operations. + * Note that by default, each individual metric will be calculated as a sum over a period of 5 minutes. + * You can customize this by using the `statistic` and `period` properties. + */ + public metricSystemErrorsForOperations(props?: SystemErrorsForOperationsMetricOptions): cloudwatch.IMetric { + + if (props?.dimensions?.Operation) { + throw new Error("The Operation dimension is not supported. Use the 'operations' property."); + } + + const operations = props?.operations ?? Object.values(Operation); + + const values = this.createMetricsForOperations('SystemErrors', operations, { statistic: 'sum', ...props }); + + const sum = new cloudwatch.MathExpression({ + expression: `${Object.keys(values).join(' + ')}`, + usingMetrics: { ...values }, + color: props?.color, + label: 'Sum of errors across all operations', + period: props?.period, + }); + + return sum; + } + + /** + * Create a map of metrics that can be used in a math expression. + * + * Using the return value of this function as the `usingMetrics` property in `cloudwatch.MathExpression` allows you to + * use the keys of this map as metric names inside you expression. + * + * @param metricName The metric name. + * @param operations The list of operations to create metrics for. + * @param props Properties for the individual metrics. + * @param metricNameMapper Mapper function to allow controlling the individual metric name per operation. + */ + private createMetricsForOperations(metricName: string, operations: Operation[], + props?: cloudwatch.MetricOptions, metricNameMapper?: (op: Operation) => string): Record { + + const metrics: Record = {}; + + const mapper = metricNameMapper ?? (op => op.toLowerCase()); + + if (props?.dimensions?.Operation) { + throw new Error('Invalid properties. Operation dimension is not supported when calculating operational metrics'); + } + + for (const operation of operations) { + + const metric = this.metric(metricName, { + ...props, + dimensions: { + TableName: this.tableName, + Operation: operation, + ...props?.dimensions, + }, + }); + + const operationMetricName = mapper(operation); + const firstChar = operationMetricName.charAt(0); + + if (firstChar === firstChar.toUpperCase()) { + // https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/using-metric-math.html#metric-math-syntax + throw new Error(`Mapper generated an illegal operation metric name: ${operationMetricName}. Must start with a lowercase letter`); + } + + metrics[operationMetricName] = metric; + } + + return metrics; } protected abstract get hasIndex(): boolean; @@ -1454,7 +1619,7 @@ class SourceTableAttachedPolicy extends CoreConstruct implements iam.IGrantable public readonly policy: iam.IPolicy; public constructor(sourceTable: Table, role: iam.IRole) { - super(sourceTable, `SourceTableAttachedPolicy-${role.node.uniqueId}`); + super(sourceTable, `SourceTableAttachedPolicy-${Names.nodeUniqueId(role.node)}`); const policy = new iam.Policy(this, 'Resource', { roles: [role] }); this.policy = policy; diff --git a/packages/@aws-cdk/aws-dynamodb/package.json b/packages/@aws-cdk/aws-dynamodb/package.json index a634e0435fd3e..2a4b972b025c5 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -74,15 +74,15 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/jest": "^26.0.15", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0", "sinon": "^9.2.1", - "ts-jest": "^26.4.3" + "ts-jest": "^26.4.4" }, "dependencies": { "@aws-cdk/aws-applicationautoscaling": "0.0.0", diff --git a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts index 4d0ab34e5a757..3a4dad70f1463 100644 --- a/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts +++ b/packages/@aws-cdk/aws-dynamodb/test/dynamodb.test.ts @@ -14,6 +14,7 @@ import { StreamViewType, Table, TableEncryption, + Operation, } from '../lib'; /* eslint-disable quote-props */ @@ -1565,6 +1566,96 @@ describe('metrics', () => { }); }); + test('Using metricSystemErrorsForOperations with no operations will default to all', () => { + + const stack = new Stack(); + const table = new Table(stack, 'Table', { + partitionKey: { name: 'id', type: AttributeType.STRING }, + }); + + expect(Object.keys(table.metricSystemErrorsForOperations().toMetricConfig().mathExpression!.usingMetrics)).toEqual([ + 'getitem', + 'batchgetitem', + 'scan', + 'query', + 'getrecords', + 'putitem', + 'deleteitem', + 'updateitem', + 'batchwriteitem', + ]); + + }); + + test('Can use metricSystemErrors without the TableName dimension', () => { + + const stack = new Stack(); + const table = new Table(stack, 'Table', { + partitionKey: { name: 'id', type: AttributeType.STRING }, + }); + + expect(table.metricSystemErrors({ dimensions: { Operation: 'GetItem' } }).dimensions).toEqual({ + TableName: table.tableName, + Operation: 'GetItem', + }); + + }); + + test('Using metricSystemErrors without the Operation dimension will fail', () => { + + const stack = new Stack(); + const table = new Table(stack, 'Table', { + partitionKey: { name: 'id', type: AttributeType.STRING }, + }); + + expect(() => table.metricSystemErrors({ dimensions: { TableName: table.tableName } })) + .toThrow(/'Operation' dimension must be passed for the 'SystemErrors' metric./); + + }); + + test('Can use metricSystemErrorsForOperations on a Dynamodb Table', () => { + + // GIVEN + const stack = new Stack(); + const table = new Table(stack, 'Table', { + partitionKey: { name: 'id', type: AttributeType.STRING }, + }); + + // THEN + expect(stack.resolve(table.metricSystemErrorsForOperations({ operations: [Operation.GET_ITEM, Operation.PUT_ITEM] }))).toEqual({ + expression: 'getitem + putitem', + label: 'Sum of errors across all operations', + period: Duration.minutes(5), + usingMetrics: { + getitem: { + dimensions: { + Operation: 'GetItem', + TableName: { + Ref: 'TableCD117FA1', + }, + }, + metricName: 'SystemErrors', + namespace: 'AWS/DynamoDB', + period: Duration.minutes(5), + statistic: 'Sum', + }, + putitem: { + dimensions: { + Operation: 'PutItem', + TableName: { + Ref: 'TableCD117FA1', + }, + }, + metricName: 'SystemErrors', + namespace: 'AWS/DynamoDB', + period: Duration.minutes(5), + statistic: 'Sum', + }, + }, + }); + + }); + test('Can use metricSystemErrors on a Dynamodb Table', () => { // GIVEN const stack = new Stack(); @@ -1573,15 +1664,26 @@ describe('metrics', () => { }); // THEN - expect(stack.resolve(table.metricSystemErrors())).toEqual({ + expect(stack.resolve(table.metricSystemErrors({ dimensions: { TableName: table.tableName, Operation: 'GetItem' } }))).toEqual({ period: Duration.minutes(5), - dimensions: { TableName: { Ref: 'TableCD117FA1' } }, + dimensions: { TableName: { Ref: 'TableCD117FA1' }, Operation: 'GetItem' }, namespace: 'AWS/DynamoDB', metricName: 'SystemErrors', statistic: 'Sum', }); }); + test('Using metricUserErrors with dimensions will fail', () => { + // GIVEN + const stack = new Stack(); + const table = new Table(stack, 'Table', { + partitionKey: { name: 'id', type: AttributeType.STRING }, + }); + + expect(() => table.metricUserErrors({ dimensions: { TableName: table.tableName } })).toThrow(/'dimensions' is not supported for the 'UserErrors' metric/); + + }); + test('Can use metricUserErrors on a Dynamodb Table', () => { // GIVEN const stack = new Stack(); @@ -1592,7 +1694,7 @@ describe('metrics', () => { // THEN expect(stack.resolve(table.metricUserErrors())).toEqual({ period: Duration.minutes(5), - dimensions: { TableName: { Ref: 'TableCD117FA1' } }, + dimensions: {}, namespace: 'AWS/DynamoDB', metricName: 'UserErrors', statistic: 'Sum', @@ -1616,6 +1718,32 @@ describe('metrics', () => { }); }); + test('Can use metricSuccessfulRequestLatency without the TableName dimension', () => { + + const stack = new Stack(); + const table = new Table(stack, 'Table', { + partitionKey: { name: 'id', type: AttributeType.STRING }, + }); + + expect(table.metricSuccessfulRequestLatency({ dimensions: { Operation: 'GetItem' } }).dimensions).toEqual({ + TableName: table.tableName, + Operation: 'GetItem', + }); + + }); + + test('Using metricSuccessfulRequestLatency without the Operation dimension will fail', () => { + + const stack = new Stack(); + const table = new Table(stack, 'Table', { + partitionKey: { name: 'id', type: AttributeType.STRING }, + }); + + expect(() => table.metricSuccessfulRequestLatency({ dimensions: { TableName: table.tableName } })) + .toThrow(/'Operation' dimension must be passed for the 'SuccessfulRequestLatency' metric./); + + }); + test('Can use metricSuccessfulRequestLatency on a Dynamodb Table', () => { // GIVEN const stack = new Stack(); @@ -1624,9 +1752,14 @@ describe('metrics', () => { }); // THEN - expect(stack.resolve(table.metricSuccessfulRequestLatency())).toEqual({ + expect(stack.resolve(table.metricSuccessfulRequestLatency({ + dimensions: { + TableName: table.tableName, + Operation: 'GetItem', + }, + }))).toEqual({ period: Duration.minutes(5), - dimensions: { TableName: { Ref: 'TableCD117FA1' } }, + dimensions: { TableName: { Ref: 'TableCD117FA1' }, Operation: 'GetItem' }, namespace: 'AWS/DynamoDB', metricName: 'SuccessfulRequestLatency', statistic: 'Average', diff --git a/packages/@aws-cdk/aws-ec2/lib/security-group.ts b/packages/@aws-cdk/aws-ec2/lib/security-group.ts index 3681c61f686dc..db733fcb816d4 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -1,4 +1,6 @@ -import { Annotations, IResource, Lazy, Resource, ResourceProps, Stack, Token } from '@aws-cdk/core'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { Annotations, ContextProvider, IResource, Lazy, Names, Resource, ResourceProps, Stack, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { Connections } from './connections'; import { CfnSecurityGroup, CfnSecurityGroupEgress, CfnSecurityGroupIngress } from './ec2.generated'; @@ -71,7 +73,7 @@ abstract class SecurityGroupBase extends Resource implements ISecurityGroup { } public get uniqueId() { - return this.node.uniqueId; + return Names.nodeUniqueId(this.node); } public addIngressRule(peer: IPeer, connection: Port, description?: string, remoteRule?: boolean) { @@ -294,6 +296,28 @@ export interface SecurityGroupImportOptions { * ``` */ export class SecurityGroup extends SecurityGroupBase { + /** + * Look up a security group by id. + */ + public static fromLookup(scope: Construct, id: string, securityGroupId: string) { + if (Token.isUnresolved(securityGroupId)) { + throw new Error('All arguments to look up a security group must be concrete (no Tokens)'); + } + + const attributes: cxapi.SecurityGroupContextResponse = ContextProvider.getValue(scope, { + provider: cxschema.ContextProvider.SECURITY_GROUP_PROVIDER, + props: { securityGroupId }, + dummyValue: { + securityGroupId: 'sg-12345', + allowAllOutbound: true, + } as cxapi.SecurityGroupContextResponse, + }).value; + + return SecurityGroup.fromSecurityGroupId(scope, id, attributes.securityGroupId, { + allowAllOutbound: attributes.allowAllOutbound, + mutable: true, + }); + } /** * Import an existing security group into this app. diff --git a/packages/@aws-cdk/aws-ec2/lib/volume.ts b/packages/@aws-cdk/aws-ec2/lib/volume.ts index 9270f6d81addb..3f9c8f3cac0dd 100644 --- a/packages/@aws-cdk/aws-ec2/lib/volume.ts +++ b/packages/@aws-cdk/aws-ec2/lib/volume.ts @@ -2,8 +2,8 @@ import * as crypto from 'crypto'; import { AccountRootPrincipal, Grant, IGrantable } from '@aws-cdk/aws-iam'; import { IKey, ViaServicePrincipal } from '@aws-cdk/aws-kms'; -import { Annotations, IResource, Resource, Size, SizeRoundingBehavior, Stack, Token, Tags } from '@aws-cdk/core'; -import { Construct, Node } from 'constructs'; +import { Annotations, IResource, Resource, Size, SizeRoundingBehavior, Stack, Token, Tags, Names } from '@aws-cdk/core'; +import { Construct } from 'constructs'; import { CfnInstance, CfnVolume } from './ec2.generated'; import { IInstance } from './instance'; @@ -564,7 +564,7 @@ abstract class VolumeBase extends Resource implements IVolume { private calculateResourceTagValue(constructs: Construct[]): string { const md5 = crypto.createHash('md5'); - constructs.forEach(construct => md5.update(Node.of(construct).uniqueId)); + constructs.forEach(construct => md5.update(Names.uniqueId(construct))); return md5.digest('hex'); } } diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts index d612641eaa929..f66045e9ffd3c 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc-endpoint.ts @@ -300,6 +300,7 @@ export class InterfaceVpcEndpointAwsService implements IInterfaceVpcEndpointServ public static readonly REKOGNITION = new InterfaceVpcEndpointAwsService('rekognition'); public static readonly REKOGNITION_FIPS = new InterfaceVpcEndpointAwsService('rekognition-fips'); public static readonly STEP_FUNCTIONS = new InterfaceVpcEndpointAwsService('states'); + public static readonly LAMBDA = new InterfaceVpcEndpointAwsService('lambda'); /** * The name of the service. diff --git a/packages/@aws-cdk/aws-ec2/lib/vpc.ts b/packages/@aws-cdk/aws-ec2/lib/vpc.ts index 3e6d5731d8baa..f73a3b4c08c2a 100644 --- a/packages/@aws-cdk/aws-ec2/lib/vpc.ts +++ b/packages/@aws-cdk/aws-ec2/lib/vpc.ts @@ -1,7 +1,7 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Annotations, ConcreteDependable, ContextProvider, DependableTrait, IConstruct, - IDependable, IResource, Lazy, Resource, Stack, Token, Tags, + IDependable, IResource, Lazy, Resource, Stack, Token, Tags, Names, } from '@aws-cdk/core'; import * as cxapi from '@aws-cdk/cx-api'; import { Construct, Node } from 'constructs'; @@ -1617,7 +1617,7 @@ export class Subnet extends Resource implements ISubnet { const scope = CoreConstruct.isConstruct(networkAcl) ? networkAcl : this; const other = CoreConstruct.isConstruct(networkAcl) ? this : networkAcl; - new SubnetNetworkAclAssociation(scope, id + other.node.uniqueId, { + new SubnetNetworkAclAssociation(scope, id + Names.nodeUniqueId(other.node), { networkAcl, subnet: this, }); @@ -1955,7 +1955,7 @@ class ImportedSubnet extends Resource implements ISubnet, IPublicSubnet, IPrivat public associateNetworkAcl(id: string, networkAcl: INetworkAcl): void { const scope = CoreConstruct.isConstruct(networkAcl) ? networkAcl : this; const other = CoreConstruct.isConstruct(networkAcl) ? this : networkAcl; - new SubnetNetworkAclAssociation(scope, id + other.node.uniqueId, { + new SubnetNetworkAclAssociation(scope, id + Names.nodeUniqueId(other.node), { networkAcl, subnet: this, }); diff --git a/packages/@aws-cdk/aws-ec2/package.json b/packages/@aws-cdk/aws-ec2/package.json index de96c387e2f37..c12df7e9f0688 100644 --- a/packages/@aws-cdk/aws-ec2/package.json +++ b/packages/@aws-cdk/aws-ec2/package.json @@ -248,6 +248,7 @@ "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.REKOGNITION", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.REKOGNITION_FIPS", "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.STEP_FUNCTIONS", + "docs-public-apis:@aws-cdk/aws-ec2.InterfaceVpcEndpointAwsService.LAMBDA", "docs-public-apis:@aws-cdk/aws-ec2.Port.toString", "docs-public-apis:@aws-cdk/aws-ec2.PrivateSubnet.fromPrivateSubnetAttributes", "docs-public-apis:@aws-cdk/aws-ec2.PublicSubnet.fromPublicSubnetAttributes", diff --git a/packages/@aws-cdk/aws-ec2/test/integ.instance-init.expected.json b/packages/@aws-cdk/aws-ec2/test/integ.instance-init.expected.json index e9f87b528f4b1..e9be6df6f6c76 100644 --- a/packages/@aws-cdk/aws-ec2/test/integ.instance-init.expected.json +++ b/packages/@aws-cdk/aws-ec2/test/integ.instance-init.expected.json @@ -130,7 +130,7 @@ ] } }, - "Instance255F352658b696f54f846988d": { + "Instance255F352658ec7e53daf15a1c6": { "Type": "AWS::EC2::Instance", "Properties": { "AvailabilityZone": "us-east-1a", @@ -169,7 +169,7 @@ { "Ref": "AWS::StackName" }, - " --resource Instance255F352658b696f54f846988d -c default\n /opt/aws/bin/cfn-signal -e $? --region ", + " --resource Instance255F352658ec7e53daf15a1c6 -c default\n /opt/aws/bin/cfn-signal -e $? --region ", { "Ref": "AWS::Region" }, @@ -177,7 +177,7 @@ { "Ref": "AWS::StackName" }, - " --resource Instance255F352658b696f54f846988d\n cat /var/log/cfn-init.log >&2\n)" + " --resource Instance255F352658ec7e53daf15a1c6\n cat /var/log/cfn-init.log >&2\n)" ] ] } diff --git a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts index 918c8a79ceb89..1a72a6a715d2c 100644 --- a/packages/@aws-cdk/aws-ec2/test/security-group.test.ts +++ b/packages/@aws-cdk/aws-ec2/test/security-group.test.ts @@ -1,5 +1,5 @@ import { expect, haveResource, not } from '@aws-cdk/assert'; -import { Intrinsic, Lazy, Stack, Token } from '@aws-cdk/core'; +import { App, Intrinsic, Lazy, Stack, Token } from '@aws-cdk/core'; import { nodeunitShim, Test } from 'nodeunit-shim'; import { Peer, Port, SecurityGroup, Vpc } from '../lib'; @@ -293,4 +293,21 @@ nodeunitShim({ test.done(); }, }, + + 'can look up a security group'(test: Test) { + const app = new App(); + const stack = new Stack(app, 'stack', { + env: { + account: '1234', + region: 'us-east-1', + }, + }); + + const securityGroup = SecurityGroup.fromLookup(stack, 'stack', 'sg-1234'); + + test.equal(securityGroup.securityGroupId, 'sg-12345'); + test.equal(securityGroup.allowAllOutbound, true); + + test.done(); + }, }); diff --git a/packages/@aws-cdk/aws-ecs-patterns/README.md b/packages/@aws-cdk/aws-ecs-patterns/README.md index cd88ea5133292..57905649401cd 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/README.md +++ b/packages/@aws-cdk/aws-ecs-patterns/README.md @@ -66,6 +66,8 @@ Fargate services use the default VPC Security Group unless one or more are provi By setting `redirectHTTP` to true, CDK will automatically create a listener on port 80 that redirects HTTP traffic to the HTTPS port. +If you specify the option `recordType` you can decide if you want the construct to use CNAME or Route53-Aliases as record sets. + Additionally, if more than one application target group are needed, instantiate one of the following: * `ApplicationMultipleTargetGroupsEc2Service` @@ -153,6 +155,8 @@ The CDK will create a new Amazon ECS cluster if you specify a VPC and omit `clus If `cluster` and `vpc` are omitted, the CDK creates a new VPC with subnets in two Availability Zones and a cluster within this VPC. +If you specify the option `recordType` you can decide if you want the construct to use CNAME or Route53-Aliases as record sets. + Additionally, if more than one network target group is needed, instantiate one of the following: * NetworkMultipleTargetGroupsEc2Service diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts index cc509d5e6b932..24e2aa98fd430 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/application-load-balanced-service-base.ts @@ -6,11 +6,29 @@ import { IApplicationLoadBalancer, ListenerCertificate, ListenerAction, } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; -import { ARecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; +import { ARecord, IHostedZone, RecordTarget, CnameRecord } from '@aws-cdk/aws-route53'; import { LoadBalancerTarget } from '@aws-cdk/aws-route53-targets'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; +/** + * Describes the type of DNS record the service should create + */ +export enum ApplicationLoadBalancedServiceRecordType { + /** + * Create Route53 A Alias record + */ + ALIAS, + /** + * Create a CNAME record + */ + CNAME, + /** + * Do not create any DNS records + */ + NONE +} + /** * The properties for the base ApplicationLoadBalancedEc2Service or ApplicationLoadBalancedFargateService service. */ @@ -177,6 +195,14 @@ export interface ApplicationLoadBalancedServiceBaseProps { * @default false */ readonly redirectHTTP?: boolean; + + /** + * Specifies whether the Route53 record should be a CNAME, an A record using the Alias feature or no record at all. + * This is useful if you need to work with DNS systems that do not support alias records. + * + * @default ApplicationLoadBalancedServiceRecordType.ALIAS + */ + readonly recordType?: ApplicationLoadBalancedServiceRecordType; } export interface ApplicationLoadBalancedTaskImageOptions { @@ -375,7 +401,7 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct { this.redirectListener = loadBalancer.addListener('PublicRedirectListener', { protocol: ApplicationProtocol.HTTP, port: 80, - open: true, + open: props.openListener ?? true, defaultAction: ListenerAction.redirect({ port: props.listenerPort?.toString() || '443', protocol: ApplicationProtocol.HTTPS, @@ -390,13 +416,27 @@ export abstract class ApplicationLoadBalancedServiceBase extends cdk.Construct { throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); } - const record = new ARecord(this, 'DNS', { - zone: props.domainZone, - recordName: props.domainName, - target: RecordTarget.fromAlias(new LoadBalancerTarget(loadBalancer)), - }); - - domainName = record.domainName; + switch (props.recordType ?? ApplicationLoadBalancedServiceRecordType.ALIAS) { + case ApplicationLoadBalancedServiceRecordType.ALIAS: + let aliasRecord = new ARecord(this, 'DNS', { + zone: props.domainZone, + recordName: props.domainName, + target: RecordTarget.fromAlias(new LoadBalancerTarget(loadBalancer)), + }); + domainName = aliasRecord.domainName; + break; + case ApplicationLoadBalancedServiceRecordType.CNAME: + let cnameRecord = new CnameRecord(this, 'DNS', { + zone: props.domainZone, + recordName: props.domainName, + domainName: loadBalancer.loadBalancerDnsName, + }); + domainName = cnameRecord.domainName; + break; + case ApplicationLoadBalancedServiceRecordType.NONE: + // Do not create a DNS record + break; + } } if (loadBalancer instanceof ApplicationLoadBalancer) { diff --git a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts index e02c402b94867..5143cf18525fe 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/lib/base/network-load-balanced-service-base.ts @@ -2,11 +2,29 @@ import { IVpc } from '@aws-cdk/aws-ec2'; import { AwsLogDriver, BaseService, CloudMapOptions, Cluster, ContainerImage, ICluster, LogDriver, PropagatedTagSource, Secret } from '@aws-cdk/aws-ecs'; import { INetworkLoadBalancer, NetworkListener, NetworkLoadBalancer, NetworkTargetGroup } from '@aws-cdk/aws-elasticloadbalancingv2'; import { IRole } from '@aws-cdk/aws-iam'; -import { ARecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; +import { ARecord, CnameRecord, IHostedZone, RecordTarget } from '@aws-cdk/aws-route53'; import { LoadBalancerTarget } from '@aws-cdk/aws-route53-targets'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; +/** + * Describes the type of DNS record the service should create + */ +export enum NetworkLoadBalancedServiceRecordType { + /** + * Create Route53 A Alias record + */ + ALIAS, + /** + * Create a CNAME record + */ + CNAME, + /** + * Do not create any DNS records + */ + NONE +} + /** * The properties for the base NetworkLoadBalancedEc2Service or NetworkLoadBalancedFargateService service. */ @@ -136,6 +154,14 @@ export interface NetworkLoadBalancedServiceBaseProps { * @default - AWS Cloud Map service discovery is not enabled. */ readonly cloudMapOptions?: CloudMapOptions; + + /** + * Specifies whether the Route53 record should be a CNAME, an A record using the Alias feature or no record at all. + * This is useful if you need to work with DNS systems that do not support alias records. + * + * @default NetworkLoadBalancedServiceRecordType.ALIAS + */ + readonly recordType?: NetworkLoadBalancedServiceRecordType; } export interface NetworkLoadBalancedTaskImageOptions { @@ -294,11 +320,25 @@ export abstract class NetworkLoadBalancedServiceBase extends cdk.Construct { throw new Error('A Route53 hosted domain zone name is required to configure the specified domain name'); } - new ARecord(this, 'DNS', { - zone: props.domainZone, - recordName: props.domainName, - target: RecordTarget.fromAlias(new LoadBalancerTarget(loadBalancer)), - }); + switch (props.recordType ?? NetworkLoadBalancedServiceRecordType.ALIAS) { + case NetworkLoadBalancedServiceRecordType.ALIAS: + new ARecord(this, 'DNS', { + zone: props.domainZone, + recordName: props.domainName, + target: RecordTarget.fromAlias(new LoadBalancerTarget(loadBalancer)), + }); + break; + case NetworkLoadBalancedServiceRecordType.CNAME: + new CnameRecord(this, 'DNS', { + zone: props.domainZone, + recordName: props.domainName, + domainName: loadBalancer.loadBalancerDnsName, + }); + break; + case NetworkLoadBalancedServiceRecordType.NONE: + // Do not create a DNS record + break; + } } if (loadBalancer instanceof NetworkLoadBalancer) { diff --git a/packages/@aws-cdk/aws-ecs-patterns/package.json b/packages/@aws-cdk/aws-ecs-patterns/package.json index 0e560b1bc890e..c18883a851c19 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/package.json +++ b/packages/@aws-cdk/aws-ecs-patterns/package.json @@ -69,7 +69,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "nodeunit": "^0.11.3", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts index 82dff62e0700c..16ace6a20c4a0 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/ec2/test.l3s.ts @@ -1127,6 +1127,7 @@ export = { const vpc = new ec2.Vpc(stack, 'VPC'); const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); cluster.addCapacity('DefaultAutoScalingGroup', { instanceType: new ec2.InstanceType('t2.micro') }); + const zone = new PublicHostedZone(stack, 'HostedZone', { zoneName: 'example.com' }); // WHEN new ecsPatterns.ApplicationLoadBalancedEc2Service(stack, 'Service', { @@ -1135,7 +1136,11 @@ export = { taskImageOptions: { image: ecs.ContainerImage.fromRegistry('test'), }, + domainName: 'api.example.com', + domainZone: zone, + certificate: Certificate.fromCertificateArn(stack, 'Cert', 'helloworld'), openListener: false, + redirectHTTP: true, }); // THEN - Stack contains no ingress security group rules diff --git a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts index 12b3adea9dcc1..4c8152482ed06 100644 --- a/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts +++ b/packages/@aws-cdk/aws-ecs-patterns/test/fargate/test.load-balanced-fargate-service.ts @@ -424,6 +424,123 @@ export = { test.done(); }, + 'setting ALB cname option correctly sets the recordset type'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'FargateAlbService', { + cluster, + protocol: ApplicationProtocol.HTTPS, + domainName: 'test.domain.com', + domainZone: route53.HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { + hostedZoneId: 'fakeId', + zoneName: 'domain.com.', + }), + recordType: ecsPatterns.ApplicationLoadBalancedServiceRecordType.CNAME, + taskImageOptions: { + containerPort: 2015, + image: ecs.ContainerImage.fromRegistry('abiosoft/caddy'), + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Route53::RecordSet', { + Name: 'test.domain.com.', + Type: 'CNAME', + })); + + test.done(); + }, + + 'setting ALB record type to NONE correctly omits the recordset'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.ApplicationLoadBalancedFargateService(stack, 'FargateAlbService', { + cluster, + protocol: ApplicationProtocol.HTTPS, + domainName: 'test.domain.com', + domainZone: route53.HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { + hostedZoneId: 'fakeId', + zoneName: 'domain.com.', + }), + recordType: ecsPatterns.ApplicationLoadBalancedServiceRecordType.NONE, + taskImageOptions: { + containerPort: 2015, + image: ecs.ContainerImage.fromRegistry('abiosoft/caddy'), + }, + }); + + // THEN + expect(stack).notTo(haveResource('AWS::Route53::RecordSet')); + + test.done(); + }, + + + 'setting NLB cname option correctly sets the recordset type'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedFargateService(stack, 'FargateNlbService', { + cluster, + domainName: 'test.domain.com', + domainZone: route53.HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { + hostedZoneId: 'fakeId', + zoneName: 'domain.com.', + }), + recordType: ecsPatterns.NetworkLoadBalancedServiceRecordType.CNAME, + taskImageOptions: { + containerPort: 2015, + image: ecs.ContainerImage.fromRegistry('abiosoft/caddy'), + }, + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::Route53::RecordSet', { + Name: 'test.domain.com.', + Type: 'CNAME', + })); + + test.done(); + }, + + 'setting NLB record type to NONE correctly omits the recordset'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const cluster = new ecs.Cluster(stack, 'Cluster', { vpc }); + + // WHEN + new ecsPatterns.NetworkLoadBalancedFargateService(stack, 'FargateNlbService', { + cluster, + domainName: 'test.domain.com', + domainZone: route53.HostedZone.fromHostedZoneAttributes(stack, 'HostedZone', { + hostedZoneId: 'fakeId', + zoneName: 'domain.com.', + }), + recordType: ecsPatterns.NetworkLoadBalancedServiceRecordType.NONE, + taskImageOptions: { + containerPort: 2015, + image: ecs.ContainerImage.fromRegistry('abiosoft/caddy'), + }, + }); + + // THEN + expect(stack).notTo(haveResource('AWS::Route53::RecordSet')); + + test.done(); + }, + 'setting ALB HTTP protocol to create the listener on 80'(test: Test) { // GIVEN const stack = new cdk.Stack(); diff --git a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts index 6cdafa2c65ee4..948ce4ea8ae90 100644 --- a/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts +++ b/packages/@aws-cdk/aws-ecs/lib/base/task-definition.ts @@ -1,6 +1,6 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; -import { IResource, Lazy, Resource } from '@aws-cdk/core'; +import { IResource, Lazy, Names, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { ContainerDefinition, ContainerDefinitionOptions, PortMapping, Protocol } from '../container-definition'; import { CfnTaskDefinition } from '../ecs.generated'; @@ -267,7 +267,7 @@ export class TaskDefinition extends TaskDefinitionBase { constructor(scope: Construct, id: string, props: TaskDefinitionProps) { super(scope, id); - this.family = props.family || this.node.uniqueId; + this.family = props.family || Names.uniqueId(this); this.compatibility = props.compatibility; if (props.volumes) { diff --git a/packages/@aws-cdk/aws-eks-legacy/README.md b/packages/@aws-cdk/aws-eks-legacy/README.md index bb839de949c7e..73d34d7a9b624 100644 --- a/packages/@aws-cdk/aws-eks-legacy/README.md +++ b/packages/@aws-cdk/aws-eks-legacy/README.md @@ -1,4 +1,5 @@ ## Amazon EKS Construct Library + --- @@ -11,7 +12,7 @@ **This module is available for backwards compatibility purposes only ([details](https://github.com/aws/aws-cdk/pull/5540)). It will no longer be released with the CDK starting March 1st, 2020. See [issue -#5544](https://github.com/aws/aws-cdk/issues/5544) for upgrade instructions.** +# 5544](https://github.com/aws/aws-cdk/issues/5544) for upgrade instructions.** --- @@ -120,7 +121,6 @@ When adding capacity, you can specify options for which is responsible for associating the node to the EKS cluster. For example, you can use `kubeletExtraArgs` to add custom node labels or taints. - ```ts // up to ten spot instances cluster.addCapacity('spot', { @@ -417,8 +417,8 @@ This means that if the chart is deleted from your code (or the stack is deleted), the next `cdk deploy` will issue a `helm uninstall` command and the Helm chart will be deleted. -When there is no `release` defined, the chart will be installed using the `node.uniqueId`, -which will be lower cassed and truncated to the last 63 characters. +When there is no `release` defined, the chart will be installed with a unique name allocated +based on the construct path. ### Roadmap diff --git a/packages/@aws-cdk/aws-eks-legacy/lib/helm-chart.ts b/packages/@aws-cdk/aws-eks-legacy/lib/helm-chart.ts index 23db12ce9aa55..9be42e435123c 100644 --- a/packages/@aws-cdk/aws-eks-legacy/lib/helm-chart.ts +++ b/packages/@aws-cdk/aws-eks-legacy/lib/helm-chart.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import { CustomResource, CustomResourceProvider } from '@aws-cdk/aws-cloudformation'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, Duration, Stack } from '@aws-cdk/core'; +import { Construct, Duration, Names, Stack } from '@aws-cdk/core'; import { Cluster } from './cluster'; import { KubectlLayer } from './kubectl-layer'; @@ -84,7 +84,7 @@ export class HelmChart extends Construct { provider: CustomResourceProvider.lambda(handler), resourceType: HelmChart.RESOURCE_TYPE, properties: { - Release: props.release || this.node.uniqueId.slice(-63).toLowerCase(), // Helm has a 63 character limit for the name + Release: props.release || Names.uniqueId(this).slice(-63).toLowerCase(), // Helm has a 63 character limit for the name Chart: props.chart, Version: props.version, Values: (props.values ? stack.toJsonString(props.values) : undefined), diff --git a/packages/@aws-cdk/aws-eks/README.md b/packages/@aws-cdk/aws-eks/README.md index 895c954a9b692..de599ac1614ec 100644 --- a/packages/@aws-cdk/aws-eks/README.md +++ b/packages/@aws-cdk/aws-eks/README.md @@ -1,4 +1,5 @@ ## Amazon EKS Construct Library + --- @@ -23,20 +24,20 @@ Table Of Contents * [API Reference](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-eks-readme.html) * [Architectural Overview](#architectural-overview) * [Provisioning clusters](#provisioning-clusters) - * [Managed node groups](#managed-node-groups) - * [Fargate Profiles](#fargate-profiles) - * [Self-managed nodes](#self-managed-nodes) - * [Endpoint Access](#endpoint-access) - * [VPC Support](#vpc-support) - * [Kubectl Support](#kubectl-support) - * [ARM64 Support](#arm64-support) - * [Masters Role](#masters-role) - * [Encryption](#encryption) + * [Managed node groups](#managed-node-groups) + * [Fargate Profiles](#fargate-profiles) + * [Self-managed nodes](#self-managed-nodes) + * [Endpoint Access](#endpoint-access) + * [VPC Support](#vpc-support) + * [Kubectl Support](#kubectl-support) + * [ARM64 Support](#arm64-support) + * [Masters Role](#masters-role) + * [Encryption](#encryption) * [Permissions and Security](#permissions-and-security) * [Applying Kubernetes Resources](#applying-kubernetes-resources) - * [Kubernetes Manifests](#kubernetes-manifests) - * [Helm Charts](#helm-charts) - * [CDK8s Charts](#cdk8s-charts) + * [Kubernetes Manifests](#kubernetes-manifests) + * [Helm Charts](#helm-charts) + * [CDK8s Charts](#cdk8s-charts) * [Patching Kuberentes Resources](#patching-kubernetes-resources) * [Querying Kubernetes Resources](#querying-kubernetes-resources) * [Using existing clusters](#using-existing-clusters) @@ -80,7 +81,7 @@ Outputs: ClusterConfigCommand43AAE40F = aws eks update-kubeconfig --name cluster-xxxxx --role-arn arn:aws:iam::112233445566:role/yyyyy ``` -Execute the `aws eks update-kubeconfig ... ` command in your terminal to create or update a local kubeconfig context: +Execute the `aws eks update-kubeconfig ...` command in your terminal to create or update a local kubeconfig context: ```console $ aws eks update-kubeconfig --name cluster-xxxxx --role-arn arn:aws:iam::112233445566:role/yyyyy @@ -157,7 +158,7 @@ new eks.FargateCluster(this, 'HelloEKS', { }); ``` -> **NOTE: Only 1 cluster per stack is supported.** If you have a use-case for multiple clusters per stack, or would like to understand more about this limitation, see https://github.com/aws/aws-cdk/issues/10073. +> **NOTE: Only 1 cluster per stack is supported.** If you have a use-case for multiple clusters per stack, or would like to understand more about this limitation, see . Below you'll find a few important cluster configuration options. First of which is Capacity. Capacity is the amount and the type of worker nodes that are available to the cluster for deploying resources. Amazon EKS offers 3 ways of configuring capacity, which you can combine as you like: @@ -640,7 +641,7 @@ new cdk.CfnOutput(this, 'ServiceAccountIamRole', { value: sa.role.roleArn }) ``` Note that using `sa.serviceAccountName` above **does not** translate into a resource dependency. -This is why an explicit dependency is needed. See https://github.com/aws/aws-cdk/issues/9910 for more details. +This is why an explicit dependency is needed. See for more details. ## Applying Kubernetes Resources @@ -798,8 +799,8 @@ This means that if the chart is deleted from your code (or the stack is deleted), the next `cdk deploy` will issue a `helm uninstall` command and the Helm chart will be deleted. -When there is no `release` defined, the chart will be installed using the `node.uniqueId`, -which will be lower cased and truncated to the last 63 characters. +When there is no `release` defined, a unique ID will be allocated for the release based +on the construct path. By default, all Helm charts will be installed concurrently. In some cases, this could cause race conditions where two Helm charts attempt to deploy the same @@ -846,6 +847,7 @@ Notice that the chart must accept a `constructs.Construct` type as its scope, no For this reason, to avoid possible confusion, we will create the chart in a separate file: `+ my-chart.ts` + ```ts import * as s3 from '@aws-cdk/aws-s3'; import * as constructs from 'constructs'; diff --git a/packages/@aws-cdk/aws-eks/lib/aws-auth.ts b/packages/@aws-cdk/aws-eks/lib/aws-auth.ts index bc3adfee6442f..1d0317758f705 100644 --- a/packages/@aws-cdk/aws-eks/lib/aws-auth.ts +++ b/packages/@aws-cdk/aws-eks/lib/aws-auth.ts @@ -110,7 +110,7 @@ export class AwsAuth extends CoreConstruct { // a dependency on the cluster, allowing those resources to be in a different stack, // will create a circular dependency. granted, it won't always be the case, // but we opted for the more causious and restrictive approach for now. - throw new Error(`${construct.node.uniqueId} should be defined in the scope of the ${thisStack.stackName} stack to prevent circular dependencies`); + throw new Error(`${construct.node.path} should be defined in the scope of the ${thisStack.stackName} stack to prevent circular dependencies`); } } diff --git a/packages/@aws-cdk/aws-eks/lib/helm-chart.ts b/packages/@aws-cdk/aws-eks/lib/helm-chart.ts index 83ed8fc652780..9e981bf449793 100644 --- a/packages/@aws-cdk/aws-eks/lib/helm-chart.ts +++ b/packages/@aws-cdk/aws-eks/lib/helm-chart.ts @@ -1,4 +1,4 @@ -import { CustomResource, Duration, Stack } from '@aws-cdk/core'; +import { CustomResource, Duration, Names, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { ICluster } from './cluster'; import { KubectlProvider } from './kubectl-provider'; @@ -113,7 +113,7 @@ export class HelmChart extends CoreConstruct { properties: { ClusterName: props.cluster.clusterName, RoleArn: provider.roleArn, // TODO: bake into the provider's environment - Release: props.release ?? this.node.uniqueId.slice(-53).toLowerCase(), // Helm has a 53 character limit for the name + Release: props.release ?? Names.uniqueId(this).slice(-53).toLowerCase(), // Helm has a 53 character limit for the name Chart: props.chart, Version: props.version, Wait: wait || undefined, // props are stringified so we encode “false” as undefined diff --git a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts index 15f8229d325ca..be23ac355c42e 100644 --- a/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts +++ b/packages/@aws-cdk/aws-eks/lib/kubectl-provider.ts @@ -1,7 +1,7 @@ import * as path from 'path'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Duration, Stack, NestedStack } from '@aws-cdk/core'; +import { Duration, Stack, NestedStack, Names } from '@aws-cdk/core'; import * as cr from '@aws-cdk/custom-resources'; import { Construct } from 'constructs'; import { ICluster, Cluster } from './cluster'; @@ -28,7 +28,7 @@ export class KubectlProvider extends NestedStack { // if this is an imported cluster, we need to provision a custom resource provider in this stack // we will define one per stack for each cluster based on the cluster uniqueid - const uid = `${cluster.node.uniqueId}-KubectlProvider`; + const uid = `${Names.nodeUniqueId(cluster.node)}-KubectlProvider`; const stack = Stack.of(scope); let provider = stack.node.tryFindChild(uid) as KubectlProvider; if (!provider) { diff --git a/packages/@aws-cdk/aws-eks/lib/service-account.ts b/packages/@aws-cdk/aws-eks/lib/service-account.ts index e6a0e3489204f..17907d7f1685a 100644 --- a/packages/@aws-cdk/aws-eks/lib/service-account.ts +++ b/packages/@aws-cdk/aws-eks/lib/service-account.ts @@ -1,5 +1,5 @@ import { AddToPrincipalPolicyResult, IPrincipal, IRole, OpenIdConnectPrincipal, PolicyStatement, PrincipalPolicyFragment, Role } from '@aws-cdk/aws-iam'; -import { CfnJson } from '@aws-cdk/core'; +import { CfnJson, Names } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Cluster } from './cluster'; import { KubernetesManifest } from './k8s-manifest'; @@ -63,7 +63,7 @@ export class ServiceAccount extends CoreConstruct implements IPrincipal { super(scope, id); const { cluster } = props; - this.serviceAccountName = props.name ?? this.node.uniqueId.toLowerCase(); + this.serviceAccountName = props.name ?? Names.uniqueId(this).toLowerCase(); this.serviceAccountNamespace = props.namespace ?? 'default'; /* Add conditions to the role to improve security. This prevents other pods in the same namespace to assume the role. diff --git a/packages/@aws-cdk/aws-eks/package.json b/packages/@aws-cdk/aws-eks/package.json index 428c5681a732a..f5cbb43f1287b 100644 --- a/packages/@aws-cdk/aws-eks/package.json +++ b/packages/@aws-cdk/aws-eks/package.json @@ -74,7 +74,7 @@ "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", "@types/yaml": "1.9.6", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-eks/test/test.awsauth.ts b/packages/@aws-cdk/aws-eks/test/test.awsauth.ts index 519f2c1415b06..631622aedc836 100644 --- a/packages/@aws-cdk/aws-eks/test/test.awsauth.ts +++ b/packages/@aws-cdk/aws-eks/test/test.awsauth.ts @@ -26,7 +26,7 @@ export = { awsAuth.addRoleMapping(role, { groups: ['group'] }); test.ok(false, 'expected error'); } catch (err) { - test.equal(err.message, 'RoleStackRole6729D0A7 should be defined in the scope of the ClusterStack stack to prevent circular dependencies'); + test.equal(err.message, 'RoleStack/Role should be defined in the scope of the ClusterStack stack to prevent circular dependencies'); } test.done(); @@ -47,7 +47,7 @@ export = { awsAuth.addUserMapping(user, { groups: ['group'] }); test.ok(false, 'expected error'); } catch (err) { - test.equal(err.message, 'UserStackUser0406F94E should be defined in the scope of the ClusterStack stack to prevent circular dependencies'); + test.equal(err.message, 'UserStack/User should be defined in the scope of the ClusterStack stack to prevent circular dependencies'); } test.done(); diff --git a/packages/@aws-cdk/aws-eks/test/test.cluster.ts b/packages/@aws-cdk/aws-eks/test/test.cluster.ts index 10909690f99af..170273d0485fe 100644 --- a/packages/@aws-cdk/aws-eks/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-eks/test/test.cluster.ts @@ -319,7 +319,7 @@ export = { clusterStack.eksCluster.connectAutoScalingGroupCapacity(capacityStack.group, {}); test.ok(false, 'expected error'); } catch (err) { - test.equal(err.message, 'CapacityStackautoScalingInstanceRoleF041EB53 should be defined in the scope of the ClusterStack stack to prevent circular dependencies'); + test.equal(err.message, 'CapacityStack/autoScaling/InstanceRole should be defined in the scope of the ClusterStack stack to prevent circular dependencies'); } test.done(); diff --git a/packages/@aws-cdk/aws-eks/test/test.k8s-patch.ts b/packages/@aws-cdk/aws-eks/test/test.k8s-patch.ts index c4defdf107606..24dabbbf0c1a2 100644 --- a/packages/@aws-cdk/aws-eks/test/test.k8s-patch.ts +++ b/packages/@aws-cdk/aws-eks/test/test.k8s-patch.ts @@ -1,5 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { Stack } from '@aws-cdk/core'; +import { Names, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import * as eks from '../lib'; import { KubernetesPatch, PatchType } from '../lib/k8s-patch'; @@ -44,7 +44,7 @@ export = { })); // also make sure a dependency on the barrier is added to the patch construct. - test.deepEqual(patch.node.dependencies.map(d => d.target.node.uniqueId), ['MyClusterKubectlReadyBarrier7547948A']); + test.deepEqual(patch.node.dependencies.map(d => Names.nodeUniqueId(d.target.node)), ['MyClusterKubectlReadyBarrier7547948A']); test.done(); }, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json index 0f40a2524d758..3d2ad11bd8a9f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-actions/package.json @@ -67,7 +67,7 @@ "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json index ed0c1fc557364..242f294ad02f1 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2-targets/package.json @@ -67,7 +67,7 @@ "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md index 93cbb9948b449..49cfd5dc5db81 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/README.md @@ -356,3 +356,91 @@ case for ECS Services for example), take a resource dependency on // has been associated with the LoadBalancer, before 'resource' is created. resourced.addDependency(targetGroup.loadBalancerDependency()); ``` + +## Looking up Load Balancers and Listeners + +You may look up load balancers and load balancer listeners by using one of the +following lookup methods: + +- `ApplicationLoadBalancer.fromlookup(options)` - Look up an application load + balancer. +- `ApplicationListener.fromLookup(options)` - Look up an application load + balancer listener. +- `NetworkLoadBalancer.fromLookup(options)` - Look up a network load balancer. +- `NetworkListener.fromLookup(options)` - Look up a network load balancer + listener. + +### Load Balancer lookup options + +You may look up a load balancer by ARN or by associated tags. When you look a +load balancer up by ARN, that load balancer will be returned unless CDK detects +that the load balancer is of the wrong type. When you look up a load balancer by +tags, CDK will return the load balancer matching all specified tags. If more +than one load balancer matches, CDK will throw an error requesting that you +provide more specific criteria. + +**Look up a Application Load Balancer by ARN** +```ts +const loadBalancer = ApplicationLoadBalancer.fromLookup(stack, 'ALB', { + loadBalancerArn: YOUR_ALB_ARN, +}); +``` + +**Look up an Application Load Balancer by tags** +```ts +const loadBalancer = ApplicationLoadBalancer.fromLookup(stack, 'ALB', { + loadBalancerTags: { + // Finds a load balancer matching all tags. + some: 'tag', + someother: 'tag', + }, +}); +``` + +## Load Balancer Listener lookup options + +You may look up a load balancer listener by the following criteria: + +- Associated load balancer ARN +- Associated load balancer tags +- Listener ARN +- Listener port +- Listener protocol + +The lookup method will return the matching listener. If more than one listener +matches, CDK will throw an error requesting that you specify additional +criteria. + +**Look up a Listener by associated Load Balancer, Port, and Protocol** + +```ts +const listener = ApplicationListener.fromLookup(stack, 'ALBListener', { + loadBalancerArn: YOUR_ALB_ARN, + listenerProtocol: ApplicationProtocol.HTTPS, + listenerPort: 443, +}); +``` + +**Look up a Listener by associated Load Balancer Tag, Port, and Protocol** + +```ts +const listener = ApplicationListener.fromLookup(stack, 'ALBListener', { + loadBalancerTags: { + Cluster: 'MyClusterName', + }, + listenerProtocol: ApplicationProtocol.HTTPS, + listenerPort: 443, +}); +``` + +**Look up a Network Listener by associated Load Balancer Tag, Port, and Protocol** + +```ts +const listener = NetworkListener.fromLookup(stack, 'ALBListener', { + loadBalancerTags: { + Cluster: 'MyClusterName', + }, + listenerProtocol: Protocol.TCP, + listenerPort: 12345, +}); +``` diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts index d088008e7ce23..3a055b5fce4be 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-listener.ts @@ -1,7 +1,9 @@ import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Duration, IResource, Lazy, Resource, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { BaseListener } from '../shared/base-listener'; +import { BaseListener, BaseListenerLookupOptions } from '../shared/base-listener'; import { HealthCheck } from '../shared/base-target-group'; import { ApplicationProtocol, IpAddressType, SslPolicy } from '../shared/enums'; import { IListenerCertificate, ListenerCertificate } from '../shared/listener-certificate'; @@ -106,12 +108,53 @@ export interface ApplicationListenerProps extends BaseApplicationListenerProps { readonly loadBalancer: IApplicationLoadBalancer; } +/** + * Options for ApplicationListener lookup + */ +export interface ApplicationListenerLookupOptions extends BaseListenerLookupOptions { + /** + * ARN of the listener to look up + * @default - does not filter by listener arn + */ + readonly listenerArn?: string; + + /** + * Filter listeners by listener protocol + * @default - does not filter by listener protocol + */ + readonly listenerProtocol?: ApplicationProtocol; +} + /** * Define an ApplicationListener * * @resource AWS::ElasticLoadBalancingV2::Listener */ export class ApplicationListener extends BaseListener implements IApplicationListener { + /** + * Look up an ApplicationListener. + */ + public static fromLookup(scope: Construct, id: string, options: ApplicationListenerLookupOptions): IApplicationListener { + if (Token.isUnresolved(options.listenerArn)) { + throw new Error('All arguments to look up a load balancer listener must be concrete (no Tokens)'); + } + + let listenerProtocol: cxschema.LoadBalancerListenerProtocol | undefined; + switch (options.listenerProtocol) { + case ApplicationProtocol.HTTP: listenerProtocol = cxschema.LoadBalancerListenerProtocol.HTTP; break; + case ApplicationProtocol.HTTPS: listenerProtocol = cxschema.LoadBalancerListenerProtocol.HTTPS; break; + } + + const props = BaseListener._queryContextProvider(scope, { + userOptions: options, + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + listenerArn: options.listenerArn, + listenerProtocol, + }); + + return new LookedUpApplicationListener(scope, id, props); + } + /** * Import an existing listener */ @@ -517,36 +560,28 @@ export interface ApplicationListenerAttributes { readonly securityGroupAllowsAllOutbound?: boolean; } -class ImportedApplicationListener extends Resource implements IApplicationListener { - public readonly connections: ec2.Connections; +abstract class ExternalApplicationListener extends Resource implements IApplicationListener { + /** + * Connections object. + */ + public abstract readonly connections: ec2.Connections; /** * ARN of the listener */ - public readonly listenerArn: string; + public abstract readonly listenerArn: string; - constructor(scope: Construct, id: string, props: ApplicationListenerAttributes) { + constructor(scope: Construct, id: string) { super(scope, id); + } - this.listenerArn = props.listenerArn; - - const defaultPort = props.defaultPort !== undefined ? ec2.Port.tcp(props.defaultPort) : undefined; - - let securityGroup: ec2.ISecurityGroup; - if (props.securityGroup) { - securityGroup = props.securityGroup; - } else if (props.securityGroupId) { - securityGroup = ec2.SecurityGroup.fromSecurityGroupId(scope, 'SecurityGroup', props.securityGroupId, { - allowAllOutbound: props.securityGroupAllowsAllOutbound, - }); - } else { - throw new Error('Either `securityGroup` or `securityGroupId` must be specified to import an application listener.'); - } - - this.connections = new ec2.Connections({ - securityGroups: [securityGroup], - defaultPort, - }); + /** + * Register that a connectable that has been added to this load balancer. + * + * Don't call this directly. It is called by ApplicationTargetGroup. + */ + public registerConnectable(connectable: ec2.IConnectable, portRange: ec2.Port): void { + this.connections.allowTo(connectable, portRange, 'Load balancer to target'); } /** @@ -599,14 +634,55 @@ class ImportedApplicationListener extends Resource implements IApplicationListen // eslint-disable-next-line max-len throw new Error('Can only call addTargets() when using a constructed ApplicationListener; construct a new TargetGroup and use addTargetGroup.'); } +} - /** - * Register that a connectable that has been added to this load balancer. - * - * Don't call this directly. It is called by ApplicationTargetGroup. - */ - public registerConnectable(connectable: ec2.IConnectable, portRange: ec2.Port): void { - this.connections.allowTo(connectable, portRange, 'Load balancer to target'); +/** + * An imported application listener. + */ +class ImportedApplicationListener extends ExternalApplicationListener { + public readonly listenerArn: string; + public readonly connections: ec2.Connections; + + constructor(scope: Construct, id: string, props: ApplicationListenerAttributes) { + super(scope, id); + + this.listenerArn = props.listenerArn; + const defaultPort = props.defaultPort !== undefined ? ec2.Port.tcp(props.defaultPort) : undefined; + + let securityGroup: ec2.ISecurityGroup; + if (props.securityGroup) { + securityGroup = props.securityGroup; + } else if (props.securityGroupId) { + securityGroup = ec2.SecurityGroup.fromSecurityGroupId(scope, 'SecurityGroup', props.securityGroupId, { + allowAllOutbound: props.securityGroupAllowsAllOutbound, + }); + } else { + throw new Error('Either `securityGroup` or `securityGroupId` must be specified to import an application listener.'); + } + + this.connections = new ec2.Connections({ + securityGroups: [securityGroup], + defaultPort, + }); + } +} + +class LookedUpApplicationListener extends ExternalApplicationListener { + public readonly listenerArn: string; + public readonly connections: ec2.Connections; + + constructor(scope: Construct, id: string, props: cxapi.LoadBalancerListenerContextResponse) { + super(scope, id); + + this.listenerArn = props.listenerArn; + this.connections = new ec2.Connections({ + defaultPort: ec2.Port.tcp(props.listenerPort), + }); + + for (const securityGroupId of props.securityGroupIds) { + const securityGroup = ec2.SecurityGroup.fromLookup(this, `SecurityGroup-${securityGroupId}`, securityGroupId); + this.connections.addSecurityGroup(securityGroup); + } } } diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 278cd8a5fbc44..0b6c9d76a7d8f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts @@ -1,8 +1,10 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; -import { Duration, Lazy, Resource } from '@aws-cdk/core'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { Duration, Lazy, Names, Resource } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { BaseLoadBalancer, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; +import { BaseLoadBalancer, BaseLoadBalancerLookupOptions, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; import { IpAddressType, ApplicationProtocol } from '../shared/enums'; import { ApplicationListener, BaseApplicationListenerProps } from './application-listener'; import { ListenerAction } from './application-listener-action'; @@ -42,12 +44,30 @@ export interface ApplicationLoadBalancerProps extends BaseLoadBalancerProps { readonly idleTimeout?: Duration; } +/** + * Options for looking up an ApplicationLoadBalancer + */ +export interface ApplicationLoadBalancerLookupOptions extends BaseLoadBalancerLookupOptions { +} + /** * Define an Application Load Balancer * * @resource AWS::ElasticLoadBalancingV2::LoadBalancer */ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplicationLoadBalancer { + /** + * Look up an application load balancer. + */ + public static fromLookup(scope: Construct, id: string, options: ApplicationLoadBalancerLookupOptions): IApplicationLoadBalancer { + const props = BaseLoadBalancer._queryContextProvider(scope, { + userOptions: options, + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + }); + + return new LookedUpApplicationLoadBalancer(scope, id, props); + } + /** * Import an existing Application Load Balancer */ @@ -70,7 +90,7 @@ export class ApplicationLoadBalancer extends BaseLoadBalancer implements IApplic this.ipAddressType = props.ipAddressType ?? IpAddressType.IPV4; const securityGroups = [props.securityGroup || new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc, - description: `Automatically created Security Group for ELB ${this.node.uniqueId}`, + description: `Automatically created Security Group for ELB ${Names.uniqueId(this)}`, allowAllOutbound: false, })]; this.connections = new ec2.Connections({ securityGroups }); @@ -598,6 +618,46 @@ class ImportedApplicationLoadBalancer extends Resource implements IApplicationLo } } +class LookedUpApplicationLoadBalancer extends Resource implements IApplicationLoadBalancer { + public readonly loadBalancerArn: string; + public readonly loadBalancerCanonicalHostedZoneId: string; + public readonly loadBalancerDnsName: string; + public readonly ipAddressType?: IpAddressType; + public readonly connections: ec2.Connections; + public readonly vpc?: ec2.IVpc; + + constructor(scope: Construct, id: string, props: cxapi.LoadBalancerContextResponse) { + super(scope, id); + + this.loadBalancerArn = props.loadBalancerArn; + this.loadBalancerCanonicalHostedZoneId = props.loadBalancerCanonicalHostedZoneId; + this.loadBalancerDnsName = props.loadBalancerDnsName; + + if (props.ipAddressType === cxapi.LoadBalancerIpAddressType.IPV4) { + this.ipAddressType = IpAddressType.IPV4; + } else if (props.ipAddressType === cxapi.LoadBalancerIpAddressType.DUAL_STACK) { + this.ipAddressType = IpAddressType.DUAL_STACK; + } + + this.vpc = ec2.Vpc.fromLookup(this, 'Vpc', { + vpcId: props.vpcId, + }); + + this.connections = new ec2.Connections(); + for (const securityGroupId of props.securityGroupIds) { + const securityGroup = ec2.SecurityGroup.fromLookup(this, `SecurityGroup-${securityGroupId}`, securityGroupId); + this.connections.addSecurityGroup(securityGroup); + } + } + + public addListener(id: string, props: BaseApplicationListenerProps): ApplicationListener { + return new ApplicationListener(this, id, { + ...props, + loadBalancer: this, + }); + } +} + /** * Properties for a redirection config */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts index cf98f4b7cbbd4..8d0312977e092 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -1,6 +1,7 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Duration, IResource, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; -import { BaseListener } from '../shared/base-listener'; +import { BaseListener, BaseListenerLookupOptions } from '../shared/base-listener'; import { HealthCheck } from '../shared/base-target-group'; import { Protocol, SslPolicy } from '../shared/enums'; import { IListenerCertificate } from '../shared/listener-certificate'; @@ -86,12 +87,52 @@ export interface NetworkListenerProps extends BaseNetworkListenerProps { readonly loadBalancer: INetworkLoadBalancer; } +/** + * Options for looking up a network listener. + */ +export interface NetworkListenerLookupOptions extends BaseListenerLookupOptions { + /** + * Protocol of the listener port + * @default - listener is not filtered by protocol + */ + readonly listenerProtocol?: Protocol; +} + /** * Define a Network Listener * * @resource AWS::ElasticLoadBalancingV2::Listener */ export class NetworkListener extends BaseListener implements INetworkListener { + /** + * Looks up a network listener + */ + public static fromLookup(scope: Construct, id: string, options: NetworkListenerLookupOptions): INetworkListener { + let listenerProtocol: cxschema.LoadBalancerListenerProtocol | undefined; + if (options.listenerProtocol) { + validateNetworkProtocol(options.listenerProtocol); + + switch (options.listenerProtocol) { + case Protocol.TCP: listenerProtocol = cxschema.LoadBalancerListenerProtocol.TCP; break; + case Protocol.UDP: listenerProtocol = cxschema.LoadBalancerListenerProtocol.UDP; break; + case Protocol.TCP_UDP: listenerProtocol = cxschema.LoadBalancerListenerProtocol.TCP_UDP; break; + case Protocol.TLS: listenerProtocol = cxschema.LoadBalancerListenerProtocol.TLS; break; + } + } + + const props = BaseListener._queryContextProvider(scope, { + userOptions: options, + listenerProtocol: listenerProtocol, + loadBalancerType: cxschema.LoadBalancerType.NETWORK, + }); + + class LookedUp extends Resource implements INetworkListener { + public listenerArn = props.listenerArn; + } + + return new LookedUp(scope, id); + } + /** * Import an existing listener */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts index d8ba0a8b77506..74dd1f060509f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-load-balancer.ts @@ -2,9 +2,11 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as ec2 from '@aws-cdk/aws-ec2'; import { PolicyStatement, ServicePrincipal } from '@aws-cdk/aws-iam'; import { IBucket } from '@aws-cdk/aws-s3'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { Resource } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; -import { BaseLoadBalancer, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; +import { BaseLoadBalancer, BaseLoadBalancerLookupOptions, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; import { BaseNetworkListenerProps, NetworkListener } from './network-listener'; /** @@ -51,12 +53,30 @@ export interface NetworkLoadBalancerAttributes { readonly vpc?: ec2.IVpc; } +/** + * Options for looking up an NetworkLoadBalancer + */ +export interface NetworkLoadBalancerLookupOptions extends BaseLoadBalancerLookupOptions { +} + /** * Define a new network load balancer * * @resource AWS::ElasticLoadBalancingV2::LoadBalancer */ export class NetworkLoadBalancer extends BaseLoadBalancer implements INetworkLoadBalancer { + /** + * Looks up the network load balancer. + */ + public static fromLookup(scope: Construct, id: string, options: NetworkLoadBalancerLookupOptions): INetworkLoadBalancer { + const props = BaseLoadBalancer._queryContextProvider(scope, { + userOptions: options, + loadBalancerType: cxschema.LoadBalancerType.NETWORK, + }); + + return new LookedUpNetworkLoadBalancer(scope, id, props); + } + public static fromNetworkLoadBalancerAttributes(scope: Construct, id: string, attrs: NetworkLoadBalancerAttributes): INetworkLoadBalancer { class Import extends Resource implements INetworkLoadBalancer { public readonly loadBalancerArn = attrs.loadBalancerArn; @@ -289,3 +309,29 @@ export interface INetworkLoadBalancer extends ILoadBalancerV2, ec2.IVpcEndpointS */ addListener(id: string, props: BaseNetworkListenerProps): NetworkListener; } + +class LookedUpNetworkLoadBalancer extends Resource implements INetworkLoadBalancer { + public readonly loadBalancerCanonicalHostedZoneId: string; + public readonly loadBalancerDnsName: string; + public readonly loadBalancerArn: string; + public readonly vpc?: ec2.IVpc; + + constructor(scope: Construct, id: string, props: cxapi.LoadBalancerContextResponse) { + super(scope, id); + + this.loadBalancerArn = props.loadBalancerArn; + this.loadBalancerCanonicalHostedZoneId = props.loadBalancerCanonicalHostedZoneId; + this.loadBalancerDnsName = props.loadBalancerDnsName; + + this.vpc = ec2.Vpc.fromLookup(this, 'Vpc', { + vpcId: props.vpcId, + }); + } + + public addListener(lid: string, props: BaseNetworkListenerProps): NetworkListener { + return new NetworkListener(this, lid, { + loadBalancer: this, + ...props, + }); + } +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts index 7866e522e0375..b735d6e375870 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-listener.ts @@ -1,12 +1,101 @@ -import { Annotations, Lazy, Resource } from '@aws-cdk/core'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { Annotations, ContextProvider, Lazy, Resource, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { Construct } from 'constructs'; import { CfnListener } from '../elasticloadbalancingv2.generated'; import { IListenerAction } from './listener-action'; +import { mapTagMapToCxschema } from './util'; + +/** + * Options for listener lookup + */ +export interface BaseListenerLookupOptions { + /** + * Filter listeners by associated load balancer arn + * @default - does not filter by load balancer arn + */ + readonly loadBalancerArn?: string; + + /** + * Filter listeners by associated load balancer tags + * @default - does not filter by load balancer tags + */ + readonly loadBalancerTags?: Record; + + /** + * Filter listeners by listener port + * @default - does not filter by listener port + */ + readonly listenerPort?: number; +} + +/** + * Options for querying the load balancer listener context provider + * @internal + */ +export interface ListenerQueryContextProviderOptions { + /** + * User's provided options + */ + readonly userOptions: BaseListenerLookupOptions; + + /** + * Type of load balancer expected + */ + readonly loadBalancerType: cxschema.LoadBalancerType; + + /** + * ARN of the listener to look up + * @default - does not filter by listener arn + */ + readonly listenerArn?: string; + + /** + * Optional protocol of the listener to look up + */ + readonly listenerProtocol?: cxschema.LoadBalancerListenerProtocol; +} /** * Base class for listeners */ export abstract class BaseListener extends Resource { + /** + * Queries the load balancer listener context provider for load balancer + * listener info. + * @internal + */ + protected static _queryContextProvider(scope: Construct, options: ListenerQueryContextProviderOptions) { + if (Token.isUnresolved(options.userOptions.loadBalancerArn) + || Object.values(options.userOptions.loadBalancerTags ?? {}).some(Token.isUnresolved) + || Token.isUnresolved(options.userOptions.listenerPort)) { + throw new Error('All arguments to look up a load balancer listener must be concrete (no Tokens)'); + } + + let cxschemaTags: cxschema.Tag[] | undefined; + if (options.userOptions.loadBalancerTags) { + cxschemaTags = mapTagMapToCxschema(options.userOptions.loadBalancerTags); + } + + const props: cxapi.LoadBalancerListenerContextResponse = ContextProvider.getValue(scope, { + provider: cxschema.ContextProvider.LOAD_BALANCER_LISTENER_PROVIDER, + props: { + listenerArn: options.listenerArn, + listenerPort: options.userOptions.listenerPort, + listenerProtocol: options.listenerProtocol, + loadBalancerArn: options.userOptions.loadBalancerArn, + loadBalancerTags: cxschemaTags, + loadBalancerType: options.loadBalancerType, + } as cxschema.LoadBalancerListenerContextQuery, + dummyValue: { + listenerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2`, + listenerPort: 80, + securityGroupIds: ['sg-123456789012'], + } as cxapi.LoadBalancerListenerContextResponse, + }).value; + + return props; + } /** * @attribute */ diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts index 1ca544187038a..46526b9376f68 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/base-load-balancer.ts @@ -1,11 +1,13 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; -import { IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import { ContextProvider, IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import * as cxapi from '@aws-cdk/cx-api'; import { RegionInfo } from '@aws-cdk/region-info'; import { Construct } from 'constructs'; import { CfnLoadBalancer } from '../elasticloadbalancingv2.generated'; -import { Attributes, ifUndefined, renderAttributes } from './util'; +import { Attributes, ifUndefined, mapTagMapToCxschema, renderAttributes } from './util'; /** * Shared properties of both Application and Network Load Balancers @@ -64,10 +66,78 @@ export interface ILoadBalancerV2 extends IResource { readonly loadBalancerDnsName: string; } +/** + * Options for looking up load balancers + */ +export interface BaseLoadBalancerLookupOptions { + /** + * Find by load balancer's ARN + * @default - does not search by load balancer arn + */ + readonly loadBalancerArn?: string; + + /** + * Match load balancer tags. + * @default - does not match load balancers by tags + */ + readonly loadBalancerTags?: Record; +} + +/** + * Options for query context provider + * @internal + */ +export interface LoadBalancerQueryContextProviderOptions { + /** + * User's lookup options + */ + readonly userOptions: BaseLoadBalancerLookupOptions; + + /** + * Type of load balancer + */ + readonly loadBalancerType: cxschema.LoadBalancerType; +} + /** * Base class for both Application and Network Load Balancers */ export abstract class BaseLoadBalancer extends Resource { + /** + * Queries the load balancer context provider for load balancer info. + * @internal + */ + protected static _queryContextProvider(scope: Construct, options: LoadBalancerQueryContextProviderOptions) { + if (Token.isUnresolved(options.userOptions.loadBalancerArn) + || Object.values(options.userOptions.loadBalancerTags ?? {}).some(Token.isUnresolved)) { + throw new Error('All arguments to look up a load balancer must be concrete (no Tokens)'); + } + + let cxschemaTags: cxschema.Tag[] | undefined; + if (options.userOptions.loadBalancerTags) { + cxschemaTags = mapTagMapToCxschema(options.userOptions.loadBalancerTags); + } + + const props: cxapi.LoadBalancerContextResponse = ContextProvider.getValue(scope, { + provider: cxschema.ContextProvider.LOAD_BALANCER_PROVIDER, + props: { + loadBalancerArn: options.userOptions.loadBalancerArn, + loadBalancerTags: cxschemaTags, + loadBalancerType: options.loadBalancerType, + } as cxschema.LoadBalancerContextQuery, + dummyValue: { + ipAddressType: cxapi.LoadBalancerIpAddressType.DUAL_STACK, + loadBalancerArn: `arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/${options.loadBalancerType}/my-load-balancer/50dc6c495c0c9188`, + loadBalancerCanonicalHostedZoneId: 'Z3DZXE0EXAMPLE', + loadBalancerDnsName: 'my-load-balancer-1234567890.us-west-2.elb.amazonaws.com', + securityGroupIds: ['sg-1234'], + vpcId: 'vpc-12345', + } as cxapi.LoadBalancerContextResponse, + }).value; + + return props; + } + /** * The canonical hosted zone ID of this load balancer * diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts index 2776d533faefa..2ecc35c365694 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/shared/util.ts @@ -1,3 +1,4 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { ApplicationProtocol, Protocol } from './enums'; export type Attributes = {[key: string]: string | undefined}; @@ -79,4 +80,13 @@ export function validateNetworkProtocol(protocol: Protocol) { if (NLB_PROTOCOLS.indexOf(protocol) === -1) { throw new Error(`The protocol must be one of ${NLB_PROTOCOLS.join(', ')}. Found ${protocol}`); } -} \ No newline at end of file +} + +/** + * Helper to map a map of tags to cxschema tag format. + * @internal + */ +export function mapTagMapToCxschema(tagMap: Record): cxschema.Tag[] { + return Object.entries(tagMap) + .map(([key, value]) => ({ key, value })); +} diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json index 10fc9e52db4f2..d65911b6abd7f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/package.json @@ -85,7 +85,9 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "@aws-cdk/region-info": "0.0.0", "constructs": "^3.2.0" }, @@ -97,7 +99,9 @@ "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", + "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/core": "0.0.0", + "@aws-cdk/cx-api": "0.0.0", "constructs": "^3.2.0", "@aws-cdk/region-info": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts index 18f162b1f8210..b9de0961423ec 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/listener.test.ts @@ -1364,6 +1364,92 @@ describe('tests', () => { }); }).toThrow(/Specify at most one/); }); + + describe('lookup', () => { + test('Can look up an ApplicationListener', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + // WHEN + const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + expect(listener.listenerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/application/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2'); + expect(listener.connections.securityGroups[0].securityGroupId).toEqual('sg-12345'); + }); + + test('Can add rules to a looked-up ApplicationListener', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // WHEN + new elbv2.ApplicationListenerRule(stack, 'rule', { + listener, + conditions: [ + elbv2.ListenerCondition.hostHeaders(['example.com']), + ], + action: elbv2.ListenerAction.fixedResponse(200), + priority: 5, + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerRule', { + Priority: 5, + }); + }); + + test('Can add certificates to a looked-up ApplicationListener', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + const listener = elbv2.ApplicationListener.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // WHEN + listener.addCertificateArns('certs', [ + 'arn:something', + ]); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::ListenerCertificate', { + Certificates: [ + { CertificateArn: 'arn:something' }, + ], + }); + }); + }); }); class ResourceWithLBDependency extends cdk.CfnResource { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts index 41063924e863c..e2745a2fb02aa 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/alb/load-balancer.test.ts @@ -334,4 +334,57 @@ describe('tests', () => { Type: 'application', }); }); + + describe('lookup', () => { + test('Can look up an ApplicationLoadBalancer', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + // WHEN + const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::ApplicationLoadBalancer'); + expect(loadBalancer.loadBalancerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/app/my-load-balancer/50dc6c495c0c9188'); + expect(loadBalancer.loadBalancerCanonicalHostedZoneId).toEqual('Z3DZXE0EXAMPLE'); + expect(loadBalancer.loadBalancerDnsName).toEqual('my-load-balancer-1234567890.us-west-2.elb.amazonaws.com'); + expect(loadBalancer.ipAddressType).toEqual(elbv2.IpAddressType.DUAL_STACK); + expect(loadBalancer.connections.securityGroups[0].securityGroupId).toEqual('sg-1234'); + }); + + test('Can add listeners to a looked-up ApplicationLoadBalancer', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + const loadBalancer = elbv2.ApplicationLoadBalancer.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // WHEN + loadBalancer.addListener('listener', { + protocol: elbv2.ApplicationProtocol.HTTP, + }); + + // THEN + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + }); + }); }); diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts index 4d3573c24d96e..d40197d00a280 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/listener.test.ts @@ -388,6 +388,28 @@ describe('tests', () => { }); }).toThrow(/Specify at most one/); }); + + test('Can look up an NetworkListener', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + // WHEN + const listener = elbv2.NetworkListener.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + expect(listener.listenerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:listener/network/my-load-balancer/50dc6c495c0c9188/f2f7dc8efc522ab2'); + }); }); class ResourceWithLBDependency extends cdk.CfnResource { diff --git a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts index ee6272f7abae9..88a3999ec0a6f 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/test/nlb/load-balancer.test.ts @@ -401,4 +401,64 @@ describe('tests', () => { Type: 'network', }); }); + + describe('lookup', () => { + test('Can look up a NetworkLoadBalancer', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + // WHEN + const loadBalancer = elbv2.NetworkLoadBalancer.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::NetworkLoadBalancer'); + expect(loadBalancer.loadBalancerArn).toEqual('arn:aws:elasticloadbalancing:us-west-2:123456789012:loadbalancer/network/my-load-balancer/50dc6c495c0c9188'); + expect(loadBalancer.loadBalancerCanonicalHostedZoneId).toEqual('Z3DZXE0EXAMPLE'); + expect(loadBalancer.loadBalancerDnsName).toEqual('my-load-balancer-1234567890.us-west-2.elb.amazonaws.com'); + }); + + test('Can add listeners to a looked-up NetworkLoadBalancer', () => { + // GIVEN + const app = new cdk.App(); + const stack = new cdk.Stack(app, 'stack', { + env: { + account: '123456789012', + region: 'us-west-2', + }, + }); + + const loadBalancer = elbv2.NetworkLoadBalancer.fromLookup(stack, 'a', { + loadBalancerTags: { + some: 'tag', + }, + }); + + const targetGroup = new elbv2.NetworkTargetGroup(stack, 'tg', { + vpc: loadBalancer.vpc, + port: 3000, + }); + + // WHEN + loadBalancer.addListener('listener', { + protocol: elbv2.Protocol.TCP_UDP, + port: 3000, + defaultAction: elbv2.NetworkListenerAction.forward([targetGroup]), + }); + + // THEN + expect(stack).not.toHaveResource('AWS::ElasticLoadBalancingV2::NetworkLoadBalancer'); + expect(stack).toHaveResource('AWS::ElasticLoadBalancingV2::Listener'); + }); + }); }); + diff --git a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts index 406c3c6dd0314..4931bde8f4a1e 100644 --- a/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts +++ b/packages/@aws-cdk/aws-elasticsearch/lib/domain.ts @@ -1038,7 +1038,7 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default p99 over 5 minutes */ public metricSearchLatency(props?: MetricOptions): Metric { - return this.metric('SearchLatencyP99', { statistic: 'p99', ...props }); + return this.metric('SearchLatency', { statistic: 'p99', ...props }); } /** @@ -1047,7 +1047,7 @@ abstract class DomainBase extends cdk.Resource implements IDomain { * @default p99 over 5 minutes */ public metricIndexingLatency(props?: MetricOptions): Metric { - return this.metric('IndexingLatencyP99', { statistic: 'p99', ...props }); + return this.metric('IndexingLatency', { statistic: 'p99', ...props }); } private grant( diff --git a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts index fc5472affaf6f..e4b8abf50e29d 100644 --- a/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts +++ b/packages/@aws-cdk/aws-elasticsearch/test/domain.test.ts @@ -394,7 +394,7 @@ describe('metrics', () => { test('Can use metricSearchLatency on an Elasticsearch Domain', () => { testMetric( (domain) => domain.metricSearchLatency(), - 'SearchLatencyP99', + 'SearchLatency', 'p99', ); }); @@ -402,7 +402,7 @@ describe('metrics', () => { test('Can use metricIndexingLatency on an Elasticsearch Domain', () => { testMetric( (domain) => domain.metricIndexingLatency(), - 'IndexingLatencyP99', + 'IndexingLatency', 'p99', ); }); diff --git a/packages/@aws-cdk/aws-events-targets/lib/batch.ts b/packages/@aws-cdk/aws-events-targets/lib/batch.ts index 69f9a52fdbb35..19ade67f17d48 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/batch.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/batch.ts @@ -1,6 +1,7 @@ import * as batch from '@aws-cdk/aws-batch'; import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; +import { Names } from '@aws-cdk/core'; import { singletonEventRole } from './util'; /** @@ -59,7 +60,7 @@ export class BatchJob implements events.IRuleTarget { public bind(rule: events.IRule, _id?: string): events.RuleTargetConfig { const batchParameters: events.CfnRule.BatchParametersProperty = { jobDefinition: this.jobDefinition.jobDefinitionArn, - jobName: this.props.jobName ?? rule.node.uniqueId, + jobName: this.props.jobName ?? Names.nodeUniqueId(rule.node), arrayProperties: this.props.size ? { size: this.props.size } : undefined, retryStrategy: this.props.attempts ? { attempts: this.props.attempts } : undefined, }; diff --git a/packages/@aws-cdk/aws-events-targets/lib/util.ts b/packages/@aws-cdk/aws-events-targets/lib/util.ts index ddcd83adb5f1b..fe41154b6037c 100644 --- a/packages/@aws-cdk/aws-events-targets/lib/util.ts +++ b/packages/@aws-cdk/aws-events-targets/lib/util.ts @@ -1,7 +1,7 @@ import * as events from '@aws-cdk/aws-events'; import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Construct, IConstruct } from '@aws-cdk/core'; +import { Construct, ConstructNode, IConstruct, Names } from '@aws-cdk/core'; /** * Obtain the Role for the EventBridge event @@ -27,9 +27,18 @@ export function singletonEventRole(scope: IConstruct, policyStatements: iam.Poli * Allows a Lambda function to be called from a rule */ export function addLambdaPermission(rule: events.IRule, handler: lambda.IFunction): void { - const permissionId = `AllowEventRule${rule.node.uniqueId}`; - if (!handler.permissionsNode.tryFindChild(permissionId)) { + let scope: Construct | undefined; + let node: ConstructNode = handler.permissionsNode; + if (rule instanceof Construct) { + // Place the Permission resource in the same stack as Rule rather than the Function + // This is to reduce circular dependency when the lambda handler and the rule are across stacks. + scope = rule; + node = rule.node; + } + const permissionId = `AllowEventRule${Names.nodeUniqueId(rule.node)}`; + if (!node.tryFindChild(permissionId)) { handler.addPermission(permissionId, { + scope, action: 'lambda:InvokeFunction', principal: new iam.ServicePrincipal('events.amazonaws.com'), sourceArn: rule.ruleArn, diff --git a/packages/@aws-cdk/aws-events-targets/package.json b/packages/@aws-cdk/aws-events-targets/package.json index f8bc74d7a47f1..de9a8658335db 100644 --- a/packages/@aws-cdk/aws-events-targets/package.json +++ b/packages/@aws-cdk/aws-events-targets/package.json @@ -75,11 +75,11 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-codecommit": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json b/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json index 5d4b2eaedfcb2..031c41c4e954c 100644 --- a/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/aws-api/integ.aws-api.expected.json @@ -29,6 +29,25 @@ ] } }, + "ScheduleRuleAllowEventRuleawscdkawsapitargetintegScheduleRule51140722763E20C1": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "AWSb4cf1abd4e4f4bc699441af7ccd9ec371511E620", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "ScheduleRuleDA5BD877", + "Arn" + ] + } + } + }, "AWSb4cf1abd4e4f4bc699441af7ccd9ec37ServiceRole9FFE9C50": { "Type": "AWS::IAM::Role", "Properties": { @@ -146,44 +165,6 @@ "AWSb4cf1abd4e4f4bc699441af7ccd9ec37ServiceRole9FFE9C50" ] }, - "AWSb4cf1abd4e4f4bc699441af7ccd9ec37AllowEventRuleawscdkawsapitargetintegScheduleRule511407226CC02048": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "AWSb4cf1abd4e4f4bc699441af7ccd9ec371511E620", - "Arn" - ] - }, - "Principal": "events.amazonaws.com", - "SourceArn": { - "Fn::GetAtt": [ - "ScheduleRuleDA5BD877", - "Arn" - ] - } - } - }, - "AWSb4cf1abd4e4f4bc699441af7ccd9ec37AllowEventRuleawscdkawsapitargetintegPatternRule3D38858113E3D24D": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "AWSb4cf1abd4e4f4bc699441af7ccd9ec371511E620", - "Arn" - ] - }, - "Principal": "events.amazonaws.com", - "SourceArn": { - "Fn::GetAtt": [ - "PatternRule4AF6D328", - "Arn" - ] - } - } - }, "PatternRule4AF6D328": { "Type": "AWS::Events::Rule", "Properties": { @@ -216,6 +197,25 @@ } ] } + }, + "PatternRuleAllowEventRuleawscdkawsapitargetintegPatternRule3D388581AA4F776B": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "AWSb4cf1abd4e4f4bc699441af7ccd9ec371511E620", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "PatternRule4AF6D328", + "Arn" + ] + } + } } }, "Parameters": { @@ -232,4 +232,4 @@ "Description": "Artifact hash for asset \"4e52413f31cff0a335f5083fa6197a6cb61928644842d89026c42c2d2a98342e\"" } } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json index 472583a9f36ca..c9efaffc51134 100644 --- a/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/codebuild/integ.project-events.expected.json @@ -210,6 +210,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json index 320b77d6d9795..aad7c05f0bd5f 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/integ.events.expected.json @@ -50,26 +50,25 @@ "MyFuncServiceRole54065130" ] }, - "MyFuncAllowEventRulelambdaeventsTimer0E6AB6D8E3B334A3": { - "Type": "AWS::Lambda::Permission", + "TimerBF6F831F": { + "Type": "AWS::Events::Rule", "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "MyFunc8A243A2C", - "Arn" - ] - }, - "Principal": "events.amazonaws.com", - "SourceArn": { - "Fn::GetAtt": [ - "TimerBF6F831F", - "Arn" - ] - } + "ScheduleExpression": "rate(1 minute)", + "State": "ENABLED", + "Targets": [ + { + "Arn": { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "Id": "Target0" + } + ] } }, - "MyFuncAllowEventRulelambdaeventsTimer27F866A1E0669C645": { + "TimerAllowEventRulelambdaeventsTimer0E6AB6D890F582F4": { "Type": "AWS::Lambda::Permission", "Properties": { "Action": "lambda:InvokeFunction", @@ -82,16 +81,16 @@ "Principal": "events.amazonaws.com", "SourceArn": { "Fn::GetAtt": [ - "Timer2B6F162E9", + "TimerBF6F831F", "Arn" ] } } }, - "TimerBF6F831F": { + "Timer2B6F162E9": { "Type": "AWS::Events::Rule", "Properties": { - "ScheduleExpression": "rate(1 minute)", + "ScheduleExpression": "rate(2 minutes)", "State": "ENABLED", "Targets": [ { @@ -106,22 +105,23 @@ ] } }, - "Timer2B6F162E9": { - "Type": "AWS::Events::Rule", + "Timer2AllowEventRulelambdaeventsTimer27F866A1E50659689": { + "Type": "AWS::Lambda::Permission", "Properties": { - "ScheduleExpression": "rate(2 minutes)", - "State": "ENABLED", - "Targets": [ - { - "Arn": { - "Fn::GetAtt": [ - "MyFunc8A243A2C", - "Arn" - ] - }, - "Id": "Target0" - } - ] + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "MyFunc8A243A2C", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "Timer2B6F162E9", + "Arn" + ] + } } } } diff --git a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts index ff171b6ee5a80..28df0932c9ba4 100644 --- a/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts +++ b/packages/@aws-cdk/aws-events-targets/test/lambda/lambda.test.ts @@ -1,4 +1,4 @@ -import { countResources, expect, haveResource } from '@aws-cdk/assert'; +import '@aws-cdk/assert/jest'; import * as events from '@aws-cdk/aws-events'; import * as lambda from '@aws-cdk/aws-lambda'; import * as cdk from '@aws-cdk/core'; @@ -23,7 +23,7 @@ test('use lambda as an event rule target', () => { // THEN const lambdaId = 'MyLambdaCCE802FB'; - expect(stack).to(haveResource('AWS::Lambda::Permission', { + expect(stack).toHaveResource('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -33,9 +33,9 @@ test('use lambda as an event rule target', () => { }, Principal: 'events.amazonaws.com', SourceArn: { 'Fn::GetAtt': ['Rule4C995B7F', 'Arn'] }, - })); + }); - expect(stack).to(haveResource('AWS::Lambda::Permission', { + expect(stack).toHaveResource('AWS::Lambda::Permission', { Action: 'lambda:InvokeFunction', FunctionName: { 'Fn::GetAtt': [ @@ -45,17 +45,17 @@ test('use lambda as an event rule target', () => { }, Principal: 'events.amazonaws.com', SourceArn: { 'Fn::GetAtt': ['Rule270732244', 'Arn'] }, - })); + }); - expect(stack).to(countResources('AWS::Events::Rule', 2)); - expect(stack).to(haveResource('AWS::Events::Rule', { + expect(stack).toCountResources('AWS::Events::Rule', 2); + expect(stack).toHaveResource('AWS::Events::Rule', { Targets: [ { Arn: { 'Fn::GetAtt': [lambdaId, 'Arn'] }, Id: 'Target0', }, ], - })); + }); }); test('adding same lambda function as target mutiple times creates permission only once', () => { @@ -75,7 +75,7 @@ test('adding same lambda function as target mutiple times creates permission onl })); // THEN - expect(stack).to(countResources('AWS::Lambda::Permission', 1)); + expect(stack).toCountResources('AWS::Lambda::Permission', 1); }); test('adding same singleton lambda function as target mutiple times creates permission only once', () => { @@ -100,7 +100,30 @@ test('adding same singleton lambda function as target mutiple times creates perm })); // THEN - expect(stack).to(countResources('AWS::Lambda::Permission', 1)); + expect(stack).toCountResources('AWS::Lambda::Permission', 1); +}); + +test('lambda handler and cloudwatch event across stacks', () => { + // GIVEN + const app = new cdk.App(); + const lambdaStack = new cdk.Stack(app, 'LambdaStack'); + + const fn = new lambda.Function(lambdaStack, 'MyLambda', { + code: new lambda.InlineCode('foo'), + handler: 'bar', + runtime: lambda.Runtime.PYTHON_2_7, + }); + + const eventStack = new cdk.Stack(app, 'EventStack'); + new events.Rule(eventStack, 'Rule', { + schedule: events.Schedule.rate(cdk.Duration.minutes(1)), + targets: [new targets.LambdaFunction(fn)], + }); + + expect(() => app.synth()).not.toThrow(); + + // the Permission resource should be in the event stack + expect(eventStack).toCountResources('AWS::Lambda::Permission', 1); }); function newTestLambda(scope: constructs.Construct) { diff --git a/packages/@aws-cdk/aws-events/lib/event-bus.ts b/packages/@aws-cdk/aws-events/lib/event-bus.ts index 2dc1c35c9cded..6101b44f680f4 100644 --- a/packages/@aws-cdk/aws-events/lib/event-bus.ts +++ b/packages/@aws-cdk/aws-events/lib/event-bus.ts @@ -1,5 +1,5 @@ import * as iam from '@aws-cdk/aws-iam'; -import { IResource, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import { IResource, Lazy, Names, Resource, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnEventBus } from './events.generated'; @@ -219,7 +219,7 @@ export class EventBus extends Resource implements IEventBus { constructor(scope: Construct, id: string, props?: EventBusProps) { const { eventBusName, eventSourceName } = EventBus.eventBusProps( - Lazy.stringValue({ produce: () => this.node.uniqueId }), + Lazy.stringValue({ produce: () => Names.uniqueId(this) }), props, ); diff --git a/packages/@aws-cdk/aws-events/lib/rule.ts b/packages/@aws-cdk/aws-events/lib/rule.ts index ca0dcd24bf45e..98e629874e382 100644 --- a/packages/@aws-cdk/aws-events/lib/rule.ts +++ b/packages/@aws-cdk/aws-events/lib/rule.ts @@ -1,4 +1,4 @@ -import { App, Lazy, Resource, Stack, Token } from '@aws-cdk/core'; +import { App, Lazy, Names, Resource, Stack, Token } from '@aws-cdk/core'; import { Construct, Node } from 'constructs'; import { IEventBus } from './event-bus'; import { EventPattern } from './event-pattern'; @@ -276,7 +276,7 @@ export class Rule extends Resource implements IRule { } } - new CopyRule(targetStack, `${this.node.uniqueId}-${id}`, { + new CopyRule(targetStack, `${Names.uniqueId(this)}-${id}`, { targets: [target], eventPattern: this.eventPattern, schedule: this.scheduleExpression ? Schedule.expression(this.scheduleExpression) : undefined, diff --git a/packages/@aws-cdk/aws-events/test/test.rule.ts b/packages/@aws-cdk/aws-events/test/test.rule.ts index be9d4b86ff5e4..991d2e83d0249 100644 --- a/packages/@aws-cdk/aws-events/test/test.rule.ts +++ b/packages/@aws-cdk/aws-events/test/test.rule.ts @@ -362,7 +362,7 @@ export = { const t1: IRuleTarget = { bind: (eventRule: IRule) => { receivedRuleArn = eventRule.ruleArn; - receivedRuleId = eventRule.node.uniqueId; + receivedRuleId = cdk.Names.nodeUniqueId(eventRule.node); return { id: '', @@ -376,7 +376,7 @@ export = { rule.addTarget(t1); test.deepEqual(stack.resolve(receivedRuleArn), stack.resolve(rule.ruleArn)); - test.deepEqual(receivedRuleId, rule.node.uniqueId); + test.deepEqual(receivedRuleId, cdk.Names.uniqueId(rule)); test.done(); }, diff --git a/packages/@aws-cdk/aws-glue/package.json b/packages/@aws-cdk/aws-glue/package.json index ceea68cd2e99f..f8d190cad4a41 100644 --- a/packages/@aws-cdk/aws-glue/package.json +++ b/packages/@aws-cdk/aws-glue/package.json @@ -77,7 +77,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-iam/package.json b/packages/@aws-cdk/aws-iam/package.json index afa2349dbafca..5f8eb193f2539 100644 --- a/packages/@aws-cdk/aws-iam/package.json +++ b/packages/@aws-cdk/aws-iam/package.json @@ -76,7 +76,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0", "sinon": "^9.2.1" }, diff --git a/packages/@aws-cdk/aws-lambda-destinations/package.json b/packages/@aws-cdk/aws-lambda-destinations/package.json index 1aa2a9d9ae0d0..3f073267b5d75 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/package.json +++ b/packages/@aws-cdk/aws-lambda-destinations/package.json @@ -67,7 +67,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json b/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json index 851224d90d2f4..f8f6f78713d64 100644 --- a/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json +++ b/packages/@aws-cdk/aws-lambda-destinations/test/integ.lambda-chain.expected.json @@ -114,6 +114,25 @@ ] } }, + "FirstEventInvokeConfigFailureAllowEventRuleawscdklambdachainFirstEventInvokeConfigFailure7180F42FA8F1F1F0": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "ErrorD9F0B79D", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "FirstEventInvokeConfigFailureA1E005BC", + "Arn" + ] + } + } + }, "FirstEventInvokeConfigSuccess865FF6FF": { "Type": "AWS::Events::Rule", "Properties": { @@ -156,6 +175,25 @@ ] } }, + "FirstEventInvokeConfigSuccessAllowEventRuleawscdklambdachainFirstEventInvokeConfigSuccess2DCAE39FC2495AB7": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Second394350F9", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "FirstEventInvokeConfigSuccess865FF6FF", + "Arn" + ] + } + } + }, "FirstEventInvokeConfig7DE6209E": { "Type": "AWS::Lambda::EventInvokeConfig", "Properties": { @@ -284,25 +322,6 @@ "SecondServiceRole55940A31" ] }, - "SecondAllowEventRuleawscdklambdachainFirstEventInvokeConfigSuccess2DCAE39F08E88C92": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "Second394350F9", - "Arn" - ] - }, - "Principal": "events.amazonaws.com", - "SourceArn": { - "Fn::GetAtt": [ - "FirstEventInvokeConfigSuccess865FF6FF", - "Arn" - ] - } - } - }, "SecondEventInvokeConfigSuccess53614893": { "Type": "AWS::Events::Rule", "Properties": { @@ -345,6 +364,25 @@ ] } }, + "SecondEventInvokeConfigSuccessAllowEventRuleawscdklambdachainSecondEventInvokeConfigSuccess2078CDC9C7FB9F61": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Third1125870F", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "SecondEventInvokeConfigSuccess53614893", + "Arn" + ] + } + } + }, "SecondEventInvokeConfig3F9DE36C": { "Type": "AWS::Lambda::EventInvokeConfig", "Properties": { @@ -428,25 +466,6 @@ "ThirdServiceRole42701801" ] }, - "ThirdAllowEventRuleawscdklambdachainSecondEventInvokeConfigSuccess2078CDC9C6C3FA25": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "Third1125870F", - "Arn" - ] - }, - "Principal": "events.amazonaws.com", - "SourceArn": { - "Fn::GetAtt": [ - "SecondEventInvokeConfigSuccess53614893", - "Arn" - ] - } - } - }, "ErrorServiceRoleCE484966": { "Type": "AWS::IAM::Role", "Properties": { @@ -496,25 +515,6 @@ "DependsOn": [ "ErrorServiceRoleCE484966" ] - }, - "ErrorAllowEventRuleawscdklambdachainFirstEventInvokeConfigFailure7180F42F0285281B": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "ErrorD9F0B79D", - "Arn" - ] - }, - "Principal": "events.amazonaws.com", - "SourceArn": { - "Fn::GetAtt": [ - "FirstEventInvokeConfigFailureA1E005BC", - "Arn" - ] - } - } } }, "Outputs": { diff --git a/packages/@aws-cdk/aws-lambda-event-sources/README.md b/packages/@aws-cdk/aws-lambda-event-sources/README.md index 9f33fb689b54d..b1fec3c4c78a7 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/README.md +++ b/packages/@aws-cdk/aws-lambda-event-sources/README.md @@ -64,8 +64,8 @@ const queue = new sqs.Queue(this, 'MyQueue', { }); lambda.addEventSource(new SqsEventSource(queue, { - batchSize: 10 // default -}); + batchSize: 10, // default +})); ``` ### S3 diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/api.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/api.ts index 2f3f3bcdc15fb..d8430d96b0f87 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/api.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/api.ts @@ -1,6 +1,6 @@ import * as apigw from '@aws-cdk/aws-apigateway'; import * as lambda from '@aws-cdk/aws-lambda'; -import { Stack } from '@aws-cdk/core'; +import { Names, Stack } from '@aws-cdk/core'; export class ApiEventSource implements lambda.IEventSource { constructor(private readonly method: string, private readonly path: string, private readonly options?: apigw.MethodOptions) { @@ -10,7 +10,7 @@ export class ApiEventSource implements lambda.IEventSource { } public bind(target: lambda.IFunction): void { - const id = `${target.node.uniqueId}:ApiEventSourceA7A86A4F`; + const id = `${Names.nodeUniqueId(target.node)}:ApiEventSourceA7A86A4F`; const stack = Stack.of(target); let api = stack.node.tryFindChild(id) as apigw.RestApi; if (!api) { diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts index 43b2a99076d2f..f316ba69cf9ed 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/dynamodb.ts @@ -1,5 +1,6 @@ import * as dynamodb from '@aws-cdk/aws-dynamodb'; import * as lambda from '@aws-cdk/aws-lambda'; +import { Names } from '@aws-cdk/core'; import { StreamEventSource, StreamEventSourceProps } from './stream'; export interface DynamoEventSourceProps extends StreamEventSourceProps { @@ -24,7 +25,7 @@ export class DynamoEventSource extends StreamEventSource { throw new Error(`DynamoDB Streams must be enabled on the table ${this.table.node.path}`); } - const eventSourceMapping = target.addEventSourceMapping(`DynamoDBEventSource:${this.table.node.uniqueId}`, + const eventSourceMapping = target.addEventSourceMapping(`DynamoDBEventSource:${Names.nodeUniqueId(this.table.node)}`, this.enrichMappingOptions({ eventSourceArn: this.table.tableStreamArn }), ); this._eventSourceMappingId = eventSourceMapping.eventSourceMappingId; diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts index a72d2db989d32..f0847429c5a45 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/kinesis.ts @@ -23,7 +23,7 @@ export class KinesisEventSource extends StreamEventSource { } public bind(target: lambda.IFunction) { - const eventSourceMapping = target.addEventSourceMapping(`KinesisEventSource:${this.stream.node.uniqueId}`, + const eventSourceMapping = target.addEventSourceMapping(`KinesisEventSource:${cdk.Names.nodeUniqueId(this.stream.node)}`, this.enrichMappingOptions({ eventSourceArn: this.stream.streamArn }), ); this._eventSourceMappingId = eventSourceMapping.eventSourceMappingId; diff --git a/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts b/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts index 2c379e128541c..88cc1682f2be5 100644 --- a/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts +++ b/packages/@aws-cdk/aws-lambda-event-sources/lib/sqs.ts @@ -1,5 +1,6 @@ import * as lambda from '@aws-cdk/aws-lambda'; import * as sqs from '@aws-cdk/aws-sqs'; +import { Names } from '@aws-cdk/core'; export interface SqsEventSourceProps { /** @@ -34,7 +35,7 @@ export class SqsEventSource implements lambda.IEventSource { } public bind(target: lambda.IFunction) { - const eventSourceMapping = target.addEventSourceMapping(`SqsEventSource:${this.queue.node.uniqueId}`, { + const eventSourceMapping = target.addEventSourceMapping(`SqsEventSource:${Names.nodeUniqueId(this.queue.node)}`, { batchSize: this.props.batchSize, enabled: this.props.enabled, eventSourceArn: this.queue.queueArn, diff --git a/packages/@aws-cdk/aws-lambda-nodejs/jest.config.js b/packages/@aws-cdk/aws-lambda-nodejs/jest.config.js index fc310b5014407..11cf7330b303d 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/jest.config.js +++ b/packages/@aws-cdk/aws-lambda-nodejs/jest.config.js @@ -4,7 +4,7 @@ module.exports = { coverageThreshold: { global: { ...baseConfig.coverageThreshold.global, - branches: 60, + branches: 50, }, }, }; diff --git a/packages/@aws-cdk/aws-lambda-nodejs/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index ec0edd7833c78..a5ea9b0fad381 100644 --- a/packages/@aws-cdk/aws-lambda-nodejs/package.json +++ b/packages/@aws-cdk/aws-lambda-nodejs/package.json @@ -67,7 +67,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "delay": "4.4.0", - "parcel": "2.0.0-nightly.432", + "parcel": "2.0.0-nightly.442", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts index 17b17070e56e8..ad5db171f719a 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv-inline.ts @@ -1,3 +1,4 @@ +/// !cdk-integ pragma:ignore-assets import * as path from 'path'; import { Runtime } from '@aws-cdk/aws-lambda'; import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts index 68f2b2f18f4b4..e97ca65f8d035 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py27.ts @@ -1,3 +1,4 @@ +/// !cdk-integ pragma:ignore-assets import * as path from 'path'; import { Runtime } from '@aws-cdk/aws-lambda'; import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts index 619dd270ec206..75538599ca463 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.pipenv.py38.ts @@ -1,3 +1,4 @@ +/// !cdk-integ pragma:ignore-assets import * as path from 'path'; import { Runtime } from '@aws-cdk/aws-lambda'; import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.ts index 1259ba09bdfe1..352c4d8db45c9 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.project.ts @@ -1,3 +1,4 @@ +/// !cdk-integ pragma:ignore-assets import * as path from 'path'; import { Runtime } from '@aws-cdk/aws-lambda'; import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.ts index 22bcba46a270f..6788d56060967 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.py38.ts @@ -1,3 +1,4 @@ +/// !cdk-integ pragma:ignore-assets import * as path from 'path'; import { Runtime } from '@aws-cdk/aws-lambda'; import { App, CfnOutput, Stack, StackProps } from '@aws-cdk/core'; diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts index f9588e313e39e..b53754a003778 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.requirements.removed.ts @@ -1,3 +1,4 @@ +/// !cdk-integ pragma:ignore-assets import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; diff --git a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.ts b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.ts index f89fa07dc19c7..ca75399cb3b3f 100644 --- a/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.ts +++ b/packages/@aws-cdk/aws-lambda-python/test/integ.function.vpc.ts @@ -1,3 +1,4 @@ +/// !cdk-integ pragma:ignore-assets import * as path from 'path'; import { Vpc, SubnetType } from '@aws-cdk/aws-ec2'; import { Runtime } from '@aws-cdk/aws-lambda'; diff --git a/packages/@aws-cdk/aws-lambda/lib/function.ts b/packages/@aws-cdk/aws-lambda/lib/function.ts index dd16d89b54db8..d44a14a3aa4f1 100644 --- a/packages/@aws-cdk/aws-lambda/lib/function.ts +++ b/packages/@aws-cdk/aws-lambda/lib/function.ts @@ -4,7 +4,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; import * as logs from '@aws-cdk/aws-logs'; import * as sqs from '@aws-cdk/aws-sqs'; -import { Annotations, CfnResource, Duration, Fn, Lazy, Stack } from '@aws-cdk/core'; +import { Annotations, CfnResource, Duration, Fn, Lazy, Names, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Code, CodeConfig } from './code'; import { EventInvokeConfigOptions } from './event-invoke-config'; @@ -839,7 +839,7 @@ Environment variables can be marked for removal when used in Lambda@Edge by sett } else { const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'SecurityGroup', { vpc: props.vpc, - description: 'Automatic security group for Lambda Function ' + this.node.uniqueId, + description: 'Automatic security group for Lambda Function ' + Names.uniqueId(this), allowAllOutbound: props.allowAllOutbound, }); securityGroups = [securityGroup]; diff --git a/packages/@aws-cdk/aws-lambda/lib/layers.ts b/packages/@aws-cdk/aws-lambda/lib/layers.ts index 4a58d5ce501ef..92de56f57e7a8 100644 --- a/packages/@aws-cdk/aws-lambda/lib/layers.ts +++ b/packages/@aws-cdk/aws-lambda/lib/layers.ts @@ -106,7 +106,7 @@ export interface LayerVersionPermission { readonly accountId: string; /** - * The ID of the AWS Organization to hwich the grant is restricted. + * The ID of the AWS Organization to which the grant is restricted. * * Can only be specified if ``accountId`` is ``'*'`` */ diff --git a/packages/@aws-cdk/aws-lambda/package.json b/packages/@aws-cdk/aws-lambda/package.json index 8288e16cab85e..63319fe238f8f 100644 --- a/packages/@aws-cdk/aws-lambda/package.json +++ b/packages/@aws-cdk/aws-lambda/package.json @@ -78,11 +78,11 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/aws-lambda": "^8.10.64", - "@types/lodash": "^4.14.163", + "@types/lodash": "^4.14.165", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "lodash": "^4.17.20", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/aws-logs-destinations/package.json b/packages/@aws-cdk/aws-logs-destinations/package.json index e930eefe55fe9..088d42b51feeb 100644 --- a/packages/@aws-cdk/aws-logs-destinations/package.json +++ b/packages/@aws-cdk/aws-logs-destinations/package.json @@ -67,7 +67,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-logs/package.json b/packages/@aws-cdk/aws-logs/package.json index d529831126b85..29f42ad90bd1b 100644 --- a/packages/@aws-cdk/aws-logs/package.json +++ b/packages/@aws-cdk/aws-logs/package.json @@ -73,7 +73,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/aws-rds/README.md b/packages/@aws-cdk/aws-rds/README.md index 5ca811eabe8c7..4b5683d2dc37f 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -23,7 +23,7 @@ your instances will be launched privately or publicly: ```ts const cluster = new rds.DatabaseCluster(this, 'Database', { engine: rds.DatabaseClusterEngine.auroraMysql({ version: rds.AuroraMysqlEngineVersion.VER_2_08_1 }), - credentials: rds.Credentials.fromUsername('clusteradmin'), // Optional - will default to admin + credentials: rds.Credentials.fromGeneratedSecret('clusteradmin'), // Optional - will default to 'admin' username and generated password instanceProps: { // optional , defaults to t3.medium instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE2, ec2.InstanceSize.SMALL), @@ -70,7 +70,7 @@ const instance = new rds.DatabaseInstance(this, 'Instance', { engine: rds.DatabaseInstanceEngine.oracleSe2({ version: rds.OracleEngineVersion.VER_19_0_0_0_2020_04_R1 }), // optional, defaults to m5.large instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.SMALL), - credentials: rds.Credentials.fromUsername('syscdk'), // Optional - will default to admin + credentials: rds.Credentials.fromGeneratedSecret('syscdk'), // Optional - will default to 'admin' username and generated password vpc, vpcSubnets: { subnetType: ec2.SubnetType.PRIVATE @@ -146,13 +146,13 @@ const engine = rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngine new rds.DatabaseInstance(this, 'InstanceWithUsername', { engine, vpc, - credentials: rds.Credentials.fromUsername('postgres'), // Creates an admin user of postgres with a generated password + credentials: rds.Credentials.fromGeneratedSecret('postgres'), // Creates an admin user of postgres with a generated password }); new rds.DatabaseInstance(this, 'InstanceWithUsernameAndPassword', { engine, vpc, - credentials: rds.Credentials.fromUsername('postgres', { password: SecretValue.ssmSecure('/dbPassword', 1) }), // Use password from SSM + credentials: rds.Credentials.fromPassword('postgres', SecretValue.ssmSecure('/dbPassword', '1')), // Use password from SSM }); const mySecret = secretsmanager.Secret.fromSecretName(this, 'DBSecret', 'myDBLoginInfo'); @@ -229,7 +229,7 @@ See also [@aws-cdk/aws-secretsmanager](https://github.com/aws/aws-cdk/blob/maste ### IAM Authentication You can also authenticate to a database instance using AWS Identity and Access Management (IAM) database authentication; -See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.html for more information +See for more information and a list of supported versions and limitations. The following example shows enabling IAM authentication for a database instance and granting connection access to an IAM role. @@ -245,12 +245,12 @@ instance.grantConnect(role); // Grant the role connection access to the DB. ``` **Note**: In addition to the setup above, a database user will need to be created to support IAM auth. -See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/UsingWithRDS.IAMDBAuth.DBAccounts.html for setup instructions. +See for setup instructions. ### Kerberos Authentication You can also authenticate using Kerberos to a database instance using AWS Managed Microsoft AD for authentication; -See https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/kerberos-authentication.html for more information +See for more information and a list of supported versions and limitations. The following example shows enabling domain support for a database instance and creating an IAM role to access @@ -274,7 +274,7 @@ const instance = new rds.DatabaseInstance(stack, 'Instance', { **Note**: In addition to the setup above, you need to make sure that the database instance has network connectivity to the domain controllers. This includes enabling cross-VPC traffic if in a different VPC and setting up the appropriate security groups/network ACL to allow traffic between the database instance and domain controllers. -Once configured, see https://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/kerberos-authentication.html for details +Once configured, see for details on configuring users for each available database engine. ### Metrics @@ -412,7 +412,7 @@ in the cloud without managing any database instances. The following example initializes an Aurora Serverless PostgreSql cluster. Aurora Serverless clusters can specify scaling properties which will be used to -automatically scale the database cluster seamlessly based on the workload. +automatically scale the database cluster seamlessly based on the workload. ```ts import * as ec2 from '@aws-cdk/aws-ec2'; @@ -431,7 +431,9 @@ const cluster = new rds.ServerlessCluster(this, 'AnotherCluster', { } }); ``` + Aurora Serverless Clusters do not support the following features: + * Loading data from an Amazon S3 bucket * Saving data to an Amazon S3 bucket * Invoking an AWS Lambda function with an Aurora MySQL native function @@ -448,3 +450,38 @@ Aurora Serverless Clusters do not support the following features: Read more about the [limitations of Aurora Serverless](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless.html#aurora-serverless.limitations) Learn more about using Amazon Aurora Serverless by reading the [documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/aurora-serverless.html) + +#### Data API + +You can access your Aurora Serverless DB cluster using the built-in Data API. The Data API doesn't require a persistent connection to the DB cluster. Instead, it provides a secure HTTP endpoint and integration with AWS SDKs. + +The following example shows granting Data API access to a Lamba function. + +```ts +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as lambda from '@aws-cdk/aws-lambda'; +import * as rds from '@aws-cdk/aws-rds'; + +const vpc = new ec2.Vpc(this, 'MyVPC'); + +const cluster = new rds.ServerlessCluster(this, 'AnotherCluster', { + engine: rds.DatabaseClusterEngine.AURORA_MYSQL, + vpc, + enableDataApi: true, // Optional - will be automatically set if you call grantDataApiAccess() +}); + +const fn = new lambda.Function(this, 'MyFunction', { + runtime: lambda.Runtime.NODEJS_10_X, + handler: 'index.handler', + code: lambda.Code.fromAsset(path.join(__dirname, 'lambda-handler')), + environment: { + CLUSTER_ARN: cluster.clusterArn, + SECRET_ARN: cluster.secret.secretArn, + }, +}); +cluster.grantDataApiAccess(fn) +``` + +**Note**: To invoke the Data API, the resource will need to read the secret associated with the cluster. + +To learn more about using the Data API, see the [documentation](https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html). diff --git a/packages/@aws-cdk/aws-rds/lib/cluster.ts b/packages/@aws-cdk/aws-rds/lib/cluster.ts index c66eba30cd223..90b6ce18b2fcd 100644 --- a/packages/@aws-cdk/aws-rds/lib/cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/cluster.ts @@ -8,10 +8,9 @@ import { Annotations, Duration, RemovalPolicy, Resource, Token } from '@aws-cdk/ import { Construct } from 'constructs'; import { IClusterEngine } from './cluster-engine'; import { DatabaseClusterAttributes, IDatabaseCluster } from './cluster-ref'; -import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; import { IParameterGroup } from './parameter-group'; -import { applyRemovalPolicy, DEFAULT_PASSWORD_EXCLUDE_CHARS, defaultDeletionProtection, setupS3ImportExport } from './private/util'; +import { applyRemovalPolicy, DEFAULT_PASSWORD_EXCLUDE_CHARS, defaultDeletionProtection, renderCredentials, setupS3ImportExport } from './private/util'; import { BackupProps, Credentials, InstanceProps, PerformanceInsightRetention, RotationSingleUserOptions, RotationMultiUserOptions } from './props'; import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy'; import { CfnDBCluster, CfnDBClusterProps, CfnDBInstance } from './rds.generated'; @@ -489,14 +488,7 @@ export class DatabaseCluster extends DatabaseClusterNew { this.singleUserRotationApplication = props.engine.singleUserRotationApplication; this.multiUserRotationApplication = props.engine.multiUserRotationApplication; - let credentials = props.credentials ?? Credentials.fromUsername(props.engine.defaultUsername ?? 'admin'); - if (!credentials.secret && !credentials.password) { - credentials = Credentials.fromSecret(new DatabaseSecret(this, 'Secret', { - username: credentials.username, - encryptionKey: credentials.encryptionKey, - excludeCharacters: credentials.excludeCharacters, - })); - } + const credentials = renderCredentials(this, props.engine, props.credentials); const secret = credentials.secret; const cluster = new CfnDBCluster(this, 'Resource', { diff --git a/packages/@aws-cdk/aws-rds/lib/database-secret.ts b/packages/@aws-cdk/aws-rds/lib/database-secret.ts index ea19e2051e6d5..0df046424a420 100644 --- a/packages/@aws-cdk/aws-rds/lib/database-secret.ts +++ b/packages/@aws-cdk/aws-rds/lib/database-secret.ts @@ -1,6 +1,7 @@ +import * as crypto from 'crypto'; import * as kms from '@aws-cdk/aws-kms'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Aws } from '@aws-cdk/core'; +import { Aws, Names } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { DEFAULT_PASSWORD_EXCLUDE_CHARS } from './private/util'; @@ -33,6 +34,18 @@ export interface DatabaseSecretProps { * @default " %+~`#$&*()|[]{}:;<>?!'/@\"\\" */ readonly excludeCharacters?: string; + + /** + * Whether to replace this secret when the criteria for the password change. + * + * This is achieved by overriding the logical id of the AWS::SecretsManager::Secret + * with a hash of the options that influence the password generation. This + * way a new secret will be created when the password is regenerated and the + * cluster or instance consuming this secret will have its credentials updated. + * + * @default false + */ + readonly replaceOnPasswordCriteriaChanges?: boolean; } /** @@ -42,6 +55,8 @@ export interface DatabaseSecretProps { */ export class DatabaseSecret extends secretsmanager.Secret { constructor(scope: Construct, id: string, props: DatabaseSecretProps) { + const excludeCharacters = props.excludeCharacters ?? DEFAULT_PASSWORD_EXCLUDE_CHARS; + super(scope, id, { encryptionKey: props.encryptionKey, description: `Generated by the CDK for stack: ${Aws.STACK_NAME}`, @@ -52,8 +67,22 @@ export class DatabaseSecret extends secretsmanager.Secret { masterarn: props.masterSecret?.secretArn, }), generateStringKey: 'password', - excludeCharacters: props.excludeCharacters ?? DEFAULT_PASSWORD_EXCLUDE_CHARS, + excludeCharacters, }, }); + + if (props.replaceOnPasswordCriteriaChanges) { + const hash = crypto.createHash('md5'); + hash.update(JSON.stringify({ + // Use here the options that influence the password generation. + // If at some point we add other password customization options + // they sould be added here below (e.g. `passwordLength`). + excludeCharacters, + })); + const logicalId = `${Names.uniqueId(this)}${hash.digest('hex')}`; + + const secret = this.node.defaultChild as secretsmanager.CfnSecret; + secret.overrideLogicalId(logicalId.slice(-255)); // Take last 255 chars + } } } diff --git a/packages/@aws-cdk/aws-rds/lib/instance.ts b/packages/@aws-cdk/aws-rds/lib/instance.ts index fa3ecac082fab..392c75ef3564c 100644 --- a/packages/@aws-cdk/aws-rds/lib/instance.ts +++ b/packages/@aws-cdk/aws-rds/lib/instance.ts @@ -12,7 +12,7 @@ import { Endpoint } from './endpoint'; import { IInstanceEngine } from './instance-engine'; import { IOptionGroup } from './option-group'; import { IParameterGroup } from './parameter-group'; -import { applyRemovalPolicy, DEFAULT_PASSWORD_EXCLUDE_CHARS, defaultDeletionProtection, engineDescription, setupS3ImportExport } from './private/util'; +import { applyRemovalPolicy, DEFAULT_PASSWORD_EXCLUDE_CHARS, defaultDeletionProtection, engineDescription, renderCredentials, setupS3ImportExport } from './private/util'; import { Credentials, PerformanceInsightRetention, RotationMultiUserOptions, RotationSingleUserOptions, SnapshotCredentials } from './props'; import { DatabaseProxy, DatabaseProxyOptions, ProxyTarget } from './proxy'; import { CfnDBInstance, CfnDBInstanceProps } from './rds.generated'; @@ -947,14 +947,7 @@ export class DatabaseInstance extends DatabaseInstanceSource implements IDatabas constructor(scope: Construct, id: string, props: DatabaseInstanceProps) { super(scope, id, props); - let credentials = props.credentials ?? Credentials.fromUsername(props.engine.defaultUsername ?? 'admin'); - if (!credentials.secret && !credentials.password) { - credentials = Credentials.fromSecret(new DatabaseSecret(this, 'Secret', { - username: credentials.username, - encryptionKey: credentials.encryptionKey, - excludeCharacters: credentials.excludeCharacters, - })); - } + const credentials = renderCredentials(this, props.engine, props.credentials); const secret = credentials.secret; const instance = new CfnDBInstance(this, 'Resource', { @@ -1032,6 +1025,7 @@ export class DatabaseInstanceFromSnapshot extends DatabaseInstanceSource impleme username: credentials.username, encryptionKey: credentials.encryptionKey, excludeCharacters: credentials.excludeCharacters, + replaceOnPasswordCriteriaChanges: credentials.replaceOnPasswordCriteriaChanges, }); } diff --git a/packages/@aws-cdk/aws-rds/lib/perms.ts b/packages/@aws-cdk/aws-rds/lib/perms.ts new file mode 100644 index 0000000000000..247ec53d987eb --- /dev/null +++ b/packages/@aws-cdk/aws-rds/lib/perms.ts @@ -0,0 +1,8 @@ +// minimal set of permissions based on https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html#data-api.access +export const DATA_API_ACTIONS = [ + 'rds-data:BatchExecuteStatement', + 'rds-data:BeginTransaction', + 'rds-data:CommitTransaction', + 'rds-data:ExecuteStatement', + 'rds-data:RollbackTransaction', +]; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/lib/private/util.ts b/packages/@aws-cdk/aws-rds/lib/private/util.ts index 361e0228c62e4..8cba1e4a1ee1e 100644 --- a/packages/@aws-cdk/aws-rds/lib/private/util.ts +++ b/packages/@aws-cdk/aws-rds/lib/private/util.ts @@ -1,7 +1,9 @@ import * as iam from '@aws-cdk/aws-iam'; import * as s3 from '@aws-cdk/aws-s3'; import { Construct, CfnDeletionPolicy, CfnResource, RemovalPolicy } from '@aws-cdk/core'; +import { DatabaseSecret } from '../database-secret'; import { IEngine } from '../engine'; +import { Credentials } from '../props'; /** * The default set of characters we exclude from generated passwords for database users. @@ -90,3 +92,27 @@ export function defaultDeletionProtection(deletionProtection?: boolean, removalP ? deletionProtection : (removalPolicy === RemovalPolicy.RETAIN ? true : undefined); } + +/** + * Renders the credentials for an instance or cluster + */ +export function renderCredentials(scope: Construct, engine: IEngine, credentials?: Credentials): Credentials { + let renderedCredentials = credentials ?? Credentials.fromUsername(engine.defaultUsername ?? 'admin'); // For backwards compatibilty + + if (!renderedCredentials.secret && !renderedCredentials.password) { + renderedCredentials = Credentials.fromSecret( + new DatabaseSecret(scope, 'Secret', { + username: renderedCredentials.username, + encryptionKey: renderedCredentials.encryptionKey, + excludeCharacters: renderedCredentials.excludeCharacters, + // if username must be referenced as a string we can safely replace the + // secret when customization options are changed without risking a replacement + replaceOnPasswordCriteriaChanges: credentials?.usernameAsString, + }), + // pass username if it must be referenced as a string + credentials?.usernameAsString ? renderedCredentials.username : undefined, + ); + } + + return renderedCredentials; +} diff --git a/packages/@aws-cdk/aws-rds/lib/props.ts b/packages/@aws-cdk/aws-rds/lib/props.ts index a54d70a5bf2ac..81725c7ffbb54 100644 --- a/packages/@aws-cdk/aws-rds/lib/props.ts +++ b/packages/@aws-cdk/aws-rds/lib/props.ts @@ -116,18 +116,9 @@ export interface BackupProps { } /** - * Options for creating a Login from a username. + * Base options for creating Credentials. */ -export interface CredentialsFromUsernameOptions { - /** - * Password - * - * Do not put passwords in your CDK code directly. - * - * @default - a Secrets Manager generated password - */ - readonly password?: SecretValue; - +export interface CredentialsBaseOptions { /** * KMS encryption key to encrypt the generated secret. * @@ -144,14 +135,55 @@ export interface CredentialsFromUsernameOptions { readonly excludeCharacters?: string; } +/** + * Options for creating Credentials from a username. + */ +export interface CredentialsFromUsernameOptions extends CredentialsBaseOptions { + /** + * Password + * + * Do not put passwords in your CDK code directly. + * + * @default - a Secrets Manager generated password + */ + readonly password?: SecretValue; +} + /** * Username and password combination */ export abstract class Credentials { + /** + * Creates Credentials with a password generated and stored in Secrets Manager. + */ + public static fromGeneratedSecret(username: string, options: CredentialsBaseOptions = {}): Credentials { + return { + ...options, + username, + usernameAsString: true, + }; + } + + /** + * Creates Credentials from a password + * + * Do not put passwords in your CDK code directly. + */ + public static fromPassword(username: string, password: SecretValue): Credentials { + return { + username, + password, + usernameAsString: true, + }; + } /** * Creates Credentials for the given username, and optional password and key. - * If no password is provided, one will be generated and stored in SecretsManager. + * If no password is provided, one will be generated and stored in Secrets Manager. + * + * @deprecated use `fromGeneratedSecret()` or `fromPassword()` for new Clusters and Instances. + * Note that switching from `fromUsername()` to `fromGeneratedSecret()` or `fromPassword()` for already deployed + * Clusters or Instances will result in their replacement! */ public static fromUsername(username: string, options: CredentialsFromUsernameOptions = {}): Credentials { return { @@ -161,7 +193,7 @@ export abstract class Credentials { } /** - * Creates Credentials from an existing SecretsManager ``Secret`` (or ``DatabaseSecret``) + * Creates Credentials from an existing Secrets Manager ``Secret`` (or ``DatabaseSecret``) * * The Secret must be a JSON string with a ``username`` and ``password`` field: * ``` @@ -171,10 +203,16 @@ export abstract class Credentials { * "password": , * } * ``` + * + * @param secret The secret where the credentials are stored + * @param username The username defined in the secret. If specified the username + * will be referenced as a string and not a dynamic reference to the username + * field in the secret. This allows to replace the secret without replacing the + * instance or cluster. */ - public static fromSecret(secret: secretsmanager.ISecret): Credentials { + public static fromSecret(secret: secretsmanager.ISecret, username?: string): Credentials { return { - username: secret.secretValueFromJson('username').toString(), + username: username ?? secret.secretValueFromJson('username').toString(), password: secret.secretValueFromJson('password'), encryptionKey: secret.encryptionKey, secret, @@ -186,6 +224,14 @@ export abstract class Credentials { */ public abstract readonly username: string; + /** + * Whether the username should be referenced as a string and not as a dynamic + * reference to the username in the secret. + * + * @default false + */ + public abstract readonly usernameAsString?: boolean; + /** * Password * @@ -241,10 +287,29 @@ export interface SnapshotCredentialsFromGeneratedPasswordOptions { * Credentials to update the password for a ``DatabaseInstanceFromSnapshot``. */ export abstract class SnapshotCredentials { + /** + * Generate a new password for the snapshot, using the existing username and an optional encryption key. + * The new credentials are stored in Secrets Manager. + * + * Note - The username must match the existing master username of the snapshot. + */ + public static fromGeneratedSecret(username: string, options: SnapshotCredentialsFromGeneratedPasswordOptions = {}): SnapshotCredentials { + return { + ...options, + generatePassword: true, + replaceOnPasswordCriteriaChanges: true, + username, + }; + } + /** * Generate a new password for the snapshot, using the existing username and an optional encryption key. * * Note - The username must match the existing master username of the snapshot. + * + * @deprecated use `fromGeneratedSecret()` for new Clusters and Instances. + * Note that switching from `fromGeneratedPassword()` to `fromGeneratedSecret()` for already deployed + * Clusters or Instances will update their master password. */ public static fromGeneratedPassword(username: string, options: SnapshotCredentialsFromGeneratedPasswordOptions = {}): SnapshotCredentials { return { @@ -295,6 +360,13 @@ export abstract class SnapshotCredentials { */ public abstract readonly generatePassword: boolean; + /** + * Whether to replace the generated secret when the criteria for the password change. + * + * @default false + */ + public abstract readonly replaceOnPasswordCriteriaChanges?: boolean; + /** * The master user password. * diff --git a/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts b/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts index 43305ac2d6e26..899607166c919 100644 --- a/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts @@ -1,13 +1,14 @@ import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as secretsmanager from '@aws-cdk/aws-secretsmanager'; -import { Resource, Duration, Token, Annotations, RemovalPolicy, IResource, Stack } from '@aws-cdk/core'; +import { Resource, Duration, Token, Annotations, RemovalPolicy, IResource, Stack, Lazy } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { IClusterEngine } from './cluster-engine'; -import { DatabaseSecret } from './database-secret'; import { Endpoint } from './endpoint'; import { IParameterGroup } from './parameter-group'; -import { applyRemovalPolicy, defaultDeletionProtection, DEFAULT_PASSWORD_EXCLUDE_CHARS } from './private/util'; +import { DATA_API_ACTIONS } from './perms'; +import { applyRemovalPolicy, defaultDeletionProtection, DEFAULT_PASSWORD_EXCLUDE_CHARS, renderCredentials } from './private/util'; import { Credentials, RotationMultiUserOptions, RotationSingleUserOptions } from './props'; import { CfnDBCluster } from './rds.generated'; import { ISubnetGroup, SubnetGroup } from './subnet-group'; @@ -39,6 +40,13 @@ export interface IServerlessCluster extends IResource, ec2.IConnectable, secrets * @attribute ReadEndpointAddress */ readonly clusterReadEndpoint: Endpoint; + + /** + * Grant the given identity to access to the Data API. + * + * @param grantee The principal to grant access to + */ + grantDataApiAccess(grantee: iam.IGrantable): iam.Grant } /** * Properties to configure an Aurora Serverless Cluster @@ -89,14 +97,13 @@ export interface ServerlessClusterProps { readonly deletionProtection?: boolean; /** - * Whether to enable the HTTP endpoint for an Aurora Serverless database cluster. - * The HTTP endpoint must be explicitly enabled to enable the Data API. + * Whether to enable the Data API. * * @see https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/data-api.html * * @default false */ - readonly enableHttpEndpoint?: boolean; + readonly enableDataApi?: boolean; /** * The VPC that this Aurora Serverless cluster has been created in. @@ -194,6 +201,13 @@ export interface ServerlessClusterAttributes { * @default - no reader address */ readonly readerEndpointAddress?: string; + + /** + * The secret attached to the database cluster + * + * @default - no secret + */ + readonly secret?: secretsmanager.ISecret; } /** @@ -288,6 +302,13 @@ abstract class ServerlessClusterBase extends Resource implements IServerlessClus */ public abstract readonly connections: ec2.Connections; + /** + * The secret attached to this cluster + */ + public abstract readonly secret?: secretsmanager.ISecret + + protected abstract enableDataApi?: boolean; + /** * The ARN of the cluster */ @@ -300,6 +321,27 @@ abstract class ServerlessClusterBase extends Resource implements IServerlessClus }); } + /** + * Grant the given identity to access to the Data API, including read access to the secret attached to the cluster if present + * + * @param grantee The principal to grant access to + */ + public grantDataApiAccess(grantee: iam.IGrantable): iam.Grant { + if (this.enableDataApi === false) { + throw new Error('Cannot grant Data API access when the Data API is disabled'); + } + + this.enableDataApi = true; + const ret = iam.Grant.addToPrincipal({ + grantee, + actions: DATA_API_ACTIONS, + resourceArns: ['*'], + scope: this, + }); + this.secret?.grantRead(grantee); + return ret; + } + /** * Renders the secret attachment target specifications. */ @@ -334,11 +376,10 @@ export class ServerlessCluster extends ServerlessClusterBase { public readonly clusterReadEndpoint: Endpoint; public readonly connections: ec2.Connections; - /** - * The secret attached to this cluster - */ public readonly secret?: secretsmanager.ISecret; + protected enableDataApi?: boolean + private readonly subnetGroup: ISubnetGroup; private readonly vpc: ec2.IVpc; private readonly vpcSubnets?: ec2.SubnetSelection; @@ -355,6 +396,8 @@ export class ServerlessCluster extends ServerlessClusterBase { this.singleUserRotationApplication = props.engine.singleUserRotationApplication; this.multiUserRotationApplication = props.engine.multiUserRotationApplication; + this.enableDataApi = props.enableDataApi; + const { subnetIds } = this.vpc.selectSubnets(this.vpcSubnets); // Cannot test whether the subnets are in different AZs, but at least we can test the amount. @@ -376,14 +419,7 @@ export class ServerlessCluster extends ServerlessClusterBase { } } - let credentials = props.credentials ?? Credentials.fromUsername(props.engine.defaultUsername ?? 'admin'); - if (!credentials.secret && !credentials.password) { - credentials = Credentials.fromSecret(new DatabaseSecret(this, 'Secret', { - username: credentials.username, - encryptionKey: credentials.encryptionKey, - excludeCharacters: credentials.excludeCharacters, - })); - } + const credentials = renderCredentials(this, props.engine, props.credentials); const secret = credentials.secret; // bind the engine to the Cluster @@ -410,7 +446,7 @@ export class ServerlessCluster extends ServerlessClusterBase { engine: props.engine.engineType, engineVersion: props.engine.engineVersion?.fullVersion, engineMode: 'serverless', - enableHttpEndpoint: props.enableHttpEndpoint, + enableHttpEndpoint: Lazy.anyValue({ produce: () => this.enableDataApi }), kmsKeyId: props.storageEncryptionKey?.keyArn, masterUsername: credentials.username, masterUserPassword: credentials.password?.toString(), @@ -509,6 +545,10 @@ class ImportedServerlessCluster extends ServerlessClusterBase implements IServer public readonly clusterIdentifier: string; public readonly connections: ec2.Connections; + public readonly secret?: secretsmanager.ISecret; + + protected readonly enableDataApi = true + private readonly _clusterEndpoint?: Endpoint; private readonly _clusterReadEndpoint?: Endpoint; @@ -523,6 +563,8 @@ class ImportedServerlessCluster extends ServerlessClusterBase implements IServer defaultPort, }); + this.secret = attrs.secret; + this._clusterEndpoint = (attrs.clusterEndpointAddress && attrs.port) ? new Endpoint(attrs.clusterEndpointAddress, attrs.port) : undefined; this._clusterReadEndpoint = (attrs.readerEndpointAddress && attrs.port) ? new Endpoint(attrs.readerEndpointAddress, attrs.port) : undefined; } diff --git a/packages/@aws-cdk/aws-rds/package.json b/packages/@aws-cdk/aws-rds/package.json index fd86567318922..564ff3e89b77e 100644 --- a/packages/@aws-cdk/aws-rds/package.json +++ b/packages/@aws-cdk/aws-rds/package.json @@ -111,6 +111,7 @@ }, "awslint": { "exclude": [ + "attribute-tag:@aws-cdk/aws-rds.DatabaseSecret.secretFullArn", "attribute-tag:@aws-cdk/aws-rds.DatabaseSecret.secretName", "props-physical-name:@aws-cdk/aws-rds.ParameterGroupProps", "props-physical-name:@aws-cdk/aws-rds.DatabaseClusterProps", diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance-from-generated-password.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance-from-generated-password.expected.json new file mode 100644 index 0000000000000..a01587728dac5 --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.instance-from-generated-password.expected.json @@ -0,0 +1,625 @@ +{ + "Resources": { + "Vpc8378EB38": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc" + } + ] + } + }, + "VpcPublicSubnet1Subnet5C2D37C4": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTable6C95E38E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1RouteTableAssociation97140677": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + } + } + }, + "VpcPublicSubnet1DefaultRoute3DA9E72A": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet1RouteTable6C95E38E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet1EIPD7E02669": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet1NATGateway4D7517AA": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet1EIPD7E02669", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet1Subnet5C2D37C4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet1" + } + ] + } + }, + "VpcPublicSubnet2Subnet691E08A3": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTable94F7E489": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2RouteTableAssociationDD5762D8": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + } + } + }, + "VpcPublicSubnet2DefaultRoute97F91067": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet2RouteTable94F7E489" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet2EIP3C605A87": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet2NATGateway9182C01D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet2EIP3C605A87", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet2Subnet691E08A3" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet2" + } + ] + } + }, + "VpcPublicSubnet3SubnetBE12F0B6": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTable93458DBB": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3RouteTableAssociation1F1EDF02": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + } + } + }, + "VpcPublicSubnet3DefaultRoute4697774F": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPublicSubnet3RouteTable93458DBB" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VpcIGWD7BA715C" + } + }, + "DependsOn": [ + "VpcVPCGWBF912B6E" + ] + }, + "VpcPublicSubnet3EIP3A666A23": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPublicSubnet3NATGateway7640CD1D": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VpcPublicSubnet3EIP3A666A23", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VpcPublicSubnet3SubnetBE12F0B6" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PublicSubnet3" + } + ] + } + }, + "VpcPrivateSubnet1Subnet536B997A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableB2C5B500": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PrivateSubnet1" + } + ] + } + }, + "VpcPrivateSubnet1RouteTableAssociation70C59FA6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + } + } + }, + "VpcPrivateSubnet1DefaultRouteBE02A9ED": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet1RouteTableB2C5B500" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet1NATGateway4D7517AA" + } + } + }, + "VpcPrivateSubnet2Subnet3788AAA1": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableA678073B": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PrivateSubnet2" + } + ] + } + }, + "VpcPrivateSubnet2RouteTableAssociationA89CAD56": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + } + } + }, + "VpcPrivateSubnet2DefaultRoute060D2087": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet2RouteTableA678073B" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet2NATGateway9182C01D" + } + } + }, + "VpcPrivateSubnet3SubnetF258B56E": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableD98824C7": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc/PrivateSubnet3" + } + ] + } + }, + "VpcPrivateSubnet3RouteTableAssociation16BDDC43": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "SubnetId": { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + } + }, + "VpcPrivateSubnet3DefaultRoute94B74F0D": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VpcPrivateSubnet3RouteTableD98824C7" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VpcPublicSubnet3NATGateway7640CD1D" + } + } + }, + "VpcIGWD7BA715C": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "aws-cdk-rds-fixed-username/Vpc" + } + ] + } + }, + "VpcVPCGWBF912B6E": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "Vpc8378EB38" + }, + "InternetGatewayId": { + "Ref": "VpcIGWD7BA715C" + } + } + }, + "InstanceSubnetGroupF2CBA54F": { + "Type": "AWS::RDS::DBSubnetGroup", + "Properties": { + "DBSubnetGroupDescription": "Subnet group for Instance database", + "SubnetIds": [ + { + "Ref": "VpcPrivateSubnet1Subnet536B997A" + }, + { + "Ref": "VpcPrivateSubnet2Subnet3788AAA1" + }, + { + "Ref": "VpcPrivateSubnet3SubnetF258B56E" + } + ] + } + }, + "InstanceSecurityGroupB4E5FA83": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Security group for Instance database", + "SecurityGroupEgress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow all outbound traffic by default", + "IpProtocol": "-1" + } + ], + "VpcId": { + "Ref": "Vpc8378EB38" + } + } + }, + "awscdkrdsfixedusernameInstanceSecretADA7FA0A0ae21a5e1432db367b627106107972de": { + "Type": "AWS::SecretsManager::Secret", + "Properties": { + "Description": { + "Fn::Join": [ + "", + [ + "Generated by the CDK for stack: ", + { + "Ref": "AWS::StackName" + } + ] + ] + }, + "GenerateSecretString": { + "ExcludeCharacters": "!&*^#@()", + "GenerateStringKey": "password", + "PasswordLength": 30, + "SecretStringTemplate": "{\"username\":\"admin\"}" + } + } + }, + "InstanceSecretAttachment83BEE581": { + "Type": "AWS::SecretsManager::SecretTargetAttachment", + "Properties": { + "SecretId": { + "Ref": "awscdkrdsfixedusernameInstanceSecretADA7FA0A0ae21a5e1432db367b627106107972de" + }, + "TargetId": { + "Ref": "InstanceC1063A87" + }, + "TargetType": "AWS::RDS::DBInstance" + } + }, + "InstanceC1063A87": { + "Type": "AWS::RDS::DBInstance", + "Properties": { + "DBInstanceClass": "db.t3.small", + "AllocatedStorage": "100", + "BackupRetentionPeriod": 0, + "CopyTagsToSnapshot": true, + "DBName": "CDKDB", + "DBSubnetGroupName": { + "Ref": "InstanceSubnetGroupF2CBA54F" + }, + "DeleteAutomatedBackups": true, + "Engine": "mysql", + "EngineVersion": "8.0.21", + "MasterUsername": "admin", + "MasterUserPassword": { + "Fn::Join": [ + "", + [ + "{{resolve:secretsmanager:", + { + "Ref": "awscdkrdsfixedusernameInstanceSecretADA7FA0A0ae21a5e1432db367b627106107972de" + }, + ":SecretString:password::}}" + ] + ] + }, + "StorageEncrypted": true, + "StorageType": "gp2", + "VPCSecurityGroups": [ + { + "Fn::GetAtt": [ + "InstanceSecurityGroupB4E5FA83", + "GroupId" + ] + } + ] + }, + "UpdateReplacePolicy": "Snapshot" + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance-from-generated-password.ts b/packages/@aws-cdk/aws-rds/test/integ.instance-from-generated-password.ts new file mode 100644 index 0000000000000..04b9746aa409a --- /dev/null +++ b/packages/@aws-cdk/aws-rds/test/integ.instance-from-generated-password.ts @@ -0,0 +1,27 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as cdk from '@aws-cdk/core'; +import * as rds from '../lib'; + +const app = new cdk.App(); + +class DatabaseInstanceStack extends cdk.Stack { + constructor(scope: cdk.App, id: string, props?: cdk.StackProps) { + super(scope, id, props); + + const vpc = new ec2.Vpc(this, 'Vpc'); + + new rds.DatabaseInstance(this, 'Instance', { + engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_21 }), + instanceType: ec2.InstanceType.of(ec2.InstanceClass.BURSTABLE3, ec2.InstanceSize.SMALL), + credentials: rds.Credentials.fromGeneratedSecret('admin', { excludeCharacters: '!&*^#@()' }), + vpc, + databaseName: 'CDKDB', + storageEncrypted: true, + backupRetention: cdk.Duration.days(0), + deleteAutomatedBackups: true, + }); + } +} + +new DatabaseInstanceStack(app, 'aws-cdk-rds-fixed-username'); +app.synth(); diff --git a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json index 27c2eed74ca17..9952517f5e00e 100644 --- a/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json +++ b/packages/@aws-cdk/aws-rds/test/integ.instance.lit.expected.json @@ -908,6 +908,25 @@ ] } }, + "InstanceAvailabilityAllowEventRuleawscdkrdsinstanceInstanceAvailabilityCE39A6A7B066AA0D": { + "Type": "AWS::Lambda::Permission", + "Properties": { + "Action": "lambda:InvokeFunction", + "FunctionName": { + "Fn::GetAtt": [ + "Function76856677", + "Arn" + ] + }, + "Principal": "events.amazonaws.com", + "SourceArn": { + "Fn::GetAtt": [ + "InstanceAvailabilityAD5D452C", + "Arn" + ] + } + } + }, "LogRetentionaae0aa3c5b4d4f87b02d85b201efdd8aServiceRole9741ECFB": { "Type": "AWS::IAM::Role", "Properties": { @@ -970,7 +989,7 @@ "Runtime": "nodejs10.x", "Code": { "S3Bucket": { - "Ref": "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3Bucket48EF98C9" + "Ref": "AssetParameters884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147S3BucketAE1150B3" }, "S3Key": { "Fn::Join": [ @@ -983,7 +1002,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3VersionKeyF33C73AF" + "Ref": "AssetParameters884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147S3VersionKeyC76660C1" } ] } @@ -996,7 +1015,7 @@ "Fn::Split": [ "||", { - "Ref": "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3VersionKeyF33C73AF" + "Ref": "AssetParameters884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147S3VersionKeyC76660C1" } ] } @@ -1087,39 +1106,20 @@ "DependsOn": [ "FunctionServiceRole675BB04A" ] - }, - "FunctionAllowEventRuleawscdkrdsinstanceInstanceAvailabilityCE39A6A71E819C19": { - "Type": "AWS::Lambda::Permission", - "Properties": { - "Action": "lambda:InvokeFunction", - "FunctionName": { - "Fn::GetAtt": [ - "Function76856677", - "Arn" - ] - }, - "Principal": "events.amazonaws.com", - "SourceArn": { - "Fn::GetAtt": [ - "InstanceAvailabilityAD5D452C", - "Arn" - ] - } - } } }, "Parameters": { - "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3Bucket48EF98C9": { + "AssetParameters884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147S3BucketAE1150B3": { "Type": "String", - "Description": "S3 bucket for asset \"74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437b\"" + "Description": "S3 bucket for asset \"884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147\"" }, - "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bS3VersionKeyF33C73AF": { + "AssetParameters884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147S3VersionKeyC76660C1": { "Type": "String", - "Description": "S3 key for asset version \"74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437b\"" + "Description": "S3 key for asset version \"884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147\"" }, - "AssetParameters74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437bArtifactHash976CF1BD": { + "AssetParameters884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147ArtifactHash717FC602": { "Type": "String", - "Description": "Artifact hash for asset \"74a1cab76f5603c5e27101cb3809d8745c50f708b0f4b497ed0910eb533d437b\"" + "Description": "Artifact hash for asset \"884431e2bc651d2b61bd699a29dc9684b0f66911f06bd3ed0635f854bf18e147\"" } } } diff --git a/packages/@aws-cdk/aws-rds/test/test.cluster.ts b/packages/@aws-cdk/aws-rds/test/test.cluster.ts index 72c2e010b2092..db27106dbeeb8 100644 --- a/packages/@aws-cdk/aws-rds/test/test.cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.cluster.ts @@ -7,8 +7,8 @@ import * as s3 from '@aws-cdk/aws-s3'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { - AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, DatabaseCluster, DatabaseClusterEngine, - DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, SubnetGroup, + AuroraEngineVersion, AuroraMysqlEngineVersion, AuroraPostgresEngineVersion, CfnDBCluster, Credentials, DatabaseCluster, + DatabaseClusterEngine, DatabaseClusterFromSnapshot, ParameterGroup, PerformanceInsightRetention, SubnetGroup, } from '../lib'; export = { @@ -1730,6 +1730,40 @@ export = { test.done(); }, + + 'fromGeneratedSecret'(test: Test) { + // GIVEN + const stack = testStack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new DatabaseCluster(stack, 'Database', { + engine: DatabaseClusterEngine.aurora({ version: AuroraEngineVersion.VER_1_22_2 }), + credentials: Credentials.fromGeneratedSecret('admin'), + instanceProps: { + vpc, + }, + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBCluster', { + MasterUsername: 'admin', // username is a string + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'DatabaseSecretC9203AE33fdaad7efa858a3daf9490cf0a702aeb', // logical id is a hash + }, + ':SecretString:password::}}', + ], + ], + }, + })); + + test.done(); + }, }; function testStack() { diff --git a/packages/@aws-cdk/aws-rds/test/test.database-secret.ts b/packages/@aws-cdk/aws-rds/test/test.database-secret.ts index bc215c6da3487..6e4edb551d764 100644 --- a/packages/@aws-cdk/aws-rds/test/test.database-secret.ts +++ b/packages/@aws-cdk/aws-rds/test/test.database-secret.ts @@ -1,5 +1,5 @@ import { expect, haveResource } from '@aws-cdk/assert'; -import { Stack } from '@aws-cdk/core'; +import { CfnResource, Stack } from '@aws-cdk/core'; import { Test } from 'nodeunit'; import { DatabaseSecret } from '../lib'; import { DEFAULT_PASSWORD_EXCLUDE_CHARS } from '../lib/private/util'; @@ -10,7 +10,7 @@ export = { const stack = new Stack(); // WHEN - new DatabaseSecret(stack, 'Secret', { + const dbSecret = new DatabaseSecret(stack, 'Secret', { username: 'admin-username', }); @@ -35,6 +35,8 @@ export = { }, })); + test.equal(getSecretLogicalId(dbSecret, stack), 'SecretA720EF05'); + test.done(); }, @@ -75,4 +77,42 @@ export = { test.done(); }, + + 'replace on password critera change'(test: Test) { + // GIVEN + const stack = new Stack(); + + // WHEN + const dbSecret = new DatabaseSecret(stack, 'Secret', { + username: 'admin', + replaceOnPasswordCriteriaChanges: true, + }); + + // THEN + const dbSecretlogicalId = getSecretLogicalId(dbSecret, stack); + test.equal(dbSecretlogicalId, 'Secret3fdaad7efa858a3daf9490cf0a702aeb'); + + // same node path but other excluded characters + stack.node.tryRemoveChild('Secret'); + const otherSecret1 = new DatabaseSecret(stack, 'Secret', { + username: 'admin', + replaceOnPasswordCriteriaChanges: true, + excludeCharacters: '@!()[]', + }); + test.notEqual(dbSecretlogicalId, getSecretLogicalId(otherSecret1, stack)); + + // other node path but same excluded characters + const otherSecret2 = new DatabaseSecret(stack, 'Secret2', { + username: 'admin', + replaceOnPasswordCriteriaChanges: true, + }); + test.notEqual(dbSecretlogicalId, getSecretLogicalId(otherSecret2, stack)); + + test.done(); + }, }; + +function getSecretLogicalId(dbSecret: DatabaseSecret, stack: Stack): string { + const cfnSecret = dbSecret.node.defaultChild as CfnResource; + return stack.resolve(cfnSecret.logicalId); +} diff --git a/packages/@aws-cdk/aws-rds/test/test.instance.ts b/packages/@aws-cdk/aws-rds/test/test.instance.ts index 2af2afaf35d31..9cc8c02d9d5a9 100644 --- a/packages/@aws-cdk/aws-rds/test/test.instance.ts +++ b/packages/@aws-cdk/aws-rds/test/test.instance.ts @@ -319,6 +319,27 @@ export = { test.done(); }, + 'fromGeneratedSecret'(test: Test) { + new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { + snapshotIdentifier: 'my-snapshot', + engine: rds.DatabaseInstanceEngine.mysql({ version: rds.MysqlEngineVersion.VER_8_0_19 }), + vpc, + credentials: rds.SnapshotCredentials.fromGeneratedSecret('admin', { + excludeCharacters: '"@/\\', + }), + }); + + expect(stack).to(haveResourceLike('AWS::RDS::DBInstance', { + MasterUsername: ABSENT, + MasterUserPassword: { + // logical id of secret has a hash + 'Fn::Join': ['', ['{{resolve:secretsmanager:', { Ref: 'InstanceSecretB6DFA6BE8ee0a797cad8a68dbeb85f8698cdb5bb' }, ':SecretString:password::}}']], + }, + })); + + test.done(); + }, + 'throws if generating a new password without a username'(test: Test) { test.throws(() => new rds.DatabaseInstanceFromSnapshot(stack, 'Instance', { snapshotIdentifier: 'my-snapshot', @@ -1138,4 +1159,49 @@ export = { test.done(); }, }, + + 'fromGeneratedSecret'(test: Test) { + // WHEN + new rds.DatabaseInstance(stack, 'Database', { + engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }), + vpc, + credentials: rds.Credentials.fromGeneratedSecret('postgres'), + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + MasterUsername: 'postgres', // username is a string + MasterUserPassword: { + 'Fn::Join': [ + '', + [ + '{{resolve:secretsmanager:', + { + Ref: 'DatabaseSecretC9203AE33fdaad7efa858a3daf9490cf0a702aeb', // logical id is a hash + }, + ':SecretString:password::}}', + ], + ], + }, + })); + + test.done(); + }, + + 'fromPassword'(test: Test) { + // WHEN + new rds.DatabaseInstance(stack, 'Database', { + engine: rds.DatabaseInstanceEngine.postgres({ version: rds.PostgresEngineVersion.VER_12_3 }), + vpc, + credentials: rds.Credentials.fromPassword('postgres', cdk.SecretValue.ssmSecure('/dbPassword', '1')), + }); + + // THEN + expect(stack).to(haveResource('AWS::RDS::DBInstance', { + MasterUsername: 'postgres', // username is a string + MasterUserPassword: '{{resolve:ssm-secure:/dbPassword:1}}', // reference to SSM + })); + + test.done(); + }, }; diff --git a/packages/@aws-cdk/aws-rds/test/test.serverless-cluster.ts b/packages/@aws-cdk/aws-rds/test/test.serverless-cluster.ts index e3bc737ba9963..de3c8a0018beb 100644 --- a/packages/@aws-cdk/aws-rds/test/test.serverless-cluster.ts +++ b/packages/@aws-cdk/aws-rds/test/test.serverless-cluster.ts @@ -1,9 +1,10 @@ import { ABSENT, expect, haveResource, haveResourceLike, ResourcePart, SynthUtils } from '@aws-cdk/assert'; import * as ec2 from '@aws-cdk/aws-ec2'; +import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; import * as cdk from '@aws-cdk/core'; import { Test } from 'nodeunit'; -import { AuroraPostgresEngineVersion, ServerlessCluster, DatabaseClusterEngine, ParameterGroup, AuroraCapacityUnit } from '../lib'; +import { AuroraPostgresEngineVersion, ServerlessCluster, DatabaseClusterEngine, ParameterGroup, AuroraCapacityUnit, DatabaseSecret } from '../lib'; export = { 'can create a Serverless Cluster with Aurora Postgres database engine'(test: Test) { @@ -506,7 +507,7 @@ export = { test.done(); }, - 'can enable http endpoint'(test: Test) { + 'can enable Data API'(test: Test) { // GIVEN const stack = testStack(); const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { isDefault: true }); @@ -515,7 +516,7 @@ export = { new ServerlessCluster(stack, 'Database', { engine: DatabaseClusterEngine.AURORA_MYSQL, vpc, - enableHttpEndpoint: true, + enableDataApi: true, }); //THEN @@ -671,8 +672,155 @@ export = { }); test.done(); }, + + 'can grant Data API access'(test: Test) { + // GIVEN + const stack = testStack(); + const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { isDefault: true }); + const cluster = new ServerlessCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA_MYSQL, + vpc, + enableDataApi: true, + }); + const user = new iam.User(stack, 'User'); + + // WHEN + cluster.grantDataApiAccess(user); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'rds-data:BatchExecuteStatement', + 'rds-data:BeginTransaction', + 'rds-data:CommitTransaction', + 'rds-data:ExecuteStatement', + 'rds-data:RollbackTransaction', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { + Ref: 'DatabaseSecretAttachmentE5D1B020', + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'UserDefaultPolicy1F97781E', + Users: [ + { + Ref: 'User00B015A1', + }, + ], + })); + + test.done(); + }, + + 'can grant Data API access on imported cluster with given secret'(test: Test) { + // GIVEN + const stack = testStack(); + const secret = new DatabaseSecret(stack, 'Secret', { + username: 'admin', + }); + const cluster = ServerlessCluster.fromServerlessClusterAttributes(stack, 'Cluster', { + clusterIdentifier: 'ImportedDatabase', + secret, + }); + const user = new iam.User(stack, 'User'); + + + // WHEN + cluster.grantDataApiAccess(user); + + // THEN + expect(stack).to(haveResource('AWS::IAM::Policy', { + PolicyDocument: { + Statement: [ + { + Action: [ + 'rds-data:BatchExecuteStatement', + 'rds-data:BeginTransaction', + 'rds-data:CommitTransaction', + 'rds-data:ExecuteStatement', + 'rds-data:RollbackTransaction', + ], + Effect: 'Allow', + Resource: '*', + }, + { + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: { + Ref: 'SecretA720EF05', + }, + }, + ], + Version: '2012-10-17', + }, + PolicyName: 'UserDefaultPolicy1F97781E', + Users: [ + { + Ref: 'User00B015A1', + }, + ], + })); + + test.done(); + }, + + 'grant Data API access enables the Data API'(test: Test) { + // GIVEN + const stack = testStack(); + const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { isDefault: true }); + const cluster = new ServerlessCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA_MYSQL, + vpc, + }); + const user = new iam.User(stack, 'User'); + + // WHEN + cluster.grantDataApiAccess(user); + + //THEN + expect(stack).to(haveResource('AWS::RDS::DBCluster', { + EnableHttpEndpoint: true, + })); + + test.done(); + }, + + 'grant Data API access throws if the Data API is disabled'(test: Test) { + // GIVEN + const stack = testStack(); + const vpc = ec2.Vpc.fromLookup(stack, 'VPC', { isDefault: true }); + const cluster = new ServerlessCluster(stack, 'Database', { + engine: DatabaseClusterEngine.AURORA_MYSQL, + vpc, + enableDataApi: false, + }); + const user = new iam.User(stack, 'User'); + + // WHEN + test.throws(() => cluster.grantDataApiAccess(user), /Cannot grant Data API access when the Data API is disabled/); + + test.done(); + }, }; + function testStack() { const stack = new cdk.Stack(undefined, undefined, { env: { account: '12345', region: 'us-test-1' } }); stack.node.setContext('availability-zones:12345:us-test-1', ['us-test-1a', 'us-test-1b']); diff --git a/packages/@aws-cdk/aws-redshift/package.json b/packages/@aws-cdk/aws-redshift/package.json index d4bf4d3e9ad78..dbdc8994c5894 100644 --- a/packages/@aws-cdk/aws-redshift/package.json +++ b/packages/@aws-cdk/aws-redshift/package.json @@ -75,7 +75,7 @@ "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { @@ -102,6 +102,7 @@ }, "awslint": { "exclude": [ + "attribute-tag:@aws-cdk/aws-redshift.DatabaseSecret.secretFullArn", "attribute-tag:@aws-cdk/aws-redshift.DatabaseSecret.secretName", "docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterName", "docs-public-apis:@aws-cdk/aws-redshift.ParameterGroupParameters.parameterValue", diff --git a/packages/@aws-cdk/aws-route53-patterns/package.json b/packages/@aws-cdk/aws-route53-patterns/package.json index 93b1f67430a73..e5db2082593d0 100644 --- a/packages/@aws-cdk/aws-route53-patterns/package.json +++ b/packages/@aws-cdk/aws-route53-patterns/package.json @@ -68,7 +68,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-route53-targets/README.md b/packages/@aws-cdk/aws-route53-targets/README.md index 3bd36f68ba46f..06d7dd6568255 100644 --- a/packages/@aws-cdk/aws-route53-targets/README.md +++ b/packages/@aws-cdk/aws-route53-targets/README.md @@ -16,6 +16,13 @@ This library contains Route53 Alias Record targets for: // or - route53.RecordTarget.fromAlias(new alias.ApiGatewayDomain(domainName)), }); ``` +* API Gateway V2 custom domains + ```ts + new route53.ARecord(this, 'AliasRecord', { + zone, + target: route53.RecordTarget.fromAlias(new alias.ApiGatewayv2Domain(domainName)), + }); + ``` * CloudFront distributions ```ts new route53.ARecord(this, 'AliasRecord', { @@ -55,17 +62,17 @@ For example, if the Amazon-provided DNS for the load balancer is `ALB-xxxxxxx.us ``` * S3 Bucket Website: -**Important:** The Bucket name must strictly match the full DNS name. -See [the Developer Guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/getting-started.html) for more info. +**Important:** The Bucket name must strictly match the full DNS name. +See [the Developer Guide](https://docs.aws.amazon.com/Route53/latest/DeveloperGuide/getting-started.html) for more info. ```ts const [recordName, domainName] = ['www', 'example.com']; - + const bucketWebsite = new Bucket(this, 'BucketWebsite', { bucketName: [recordName, domainName].join('.'), // www.example.com publicReadAccess: true, websiteIndexDocument: 'index.html', }); - + const zone = HostedZone.fromLookup(this, 'Zone', {domainName}); // example.com new route53.ARecord(this, 'AliasRecord', { diff --git a/packages/@aws-cdk/aws-route53-targets/lib/api-gateway-domain-name.ts b/packages/@aws-cdk/aws-route53-targets/lib/api-gateway-domain-name.ts index 3668a4858ac59..5aa40d5a52f10 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/api-gateway-domain-name.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/api-gateway-domain-name.ts @@ -5,7 +5,7 @@ import * as route53 from '@aws-cdk/aws-route53'; * Defines an API Gateway domain name as the alias target. * * Use the `ApiGateway` class if you wish to map the alias to an REST API with a - * domain name defined throug the `RestApiProps.domainName` prop. + * domain name defined through the `RestApiProps.domainName` prop. */ export class ApiGatewayDomain implements route53.IAliasRecordTarget { constructor(private readonly domainName: apig.IDomainName) { } diff --git a/packages/@aws-cdk/aws-route53-targets/lib/api-gatewayv2-domain-name.ts b/packages/@aws-cdk/aws-route53-targets/lib/api-gatewayv2-domain-name.ts new file mode 100644 index 0000000000000..b78078fca525a --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/lib/api-gatewayv2-domain-name.ts @@ -0,0 +1,16 @@ +import * as apigv2 from '@aws-cdk/aws-apigatewayv2'; +import * as route53 from '@aws-cdk/aws-route53'; + +/** + * Defines an API Gateway V2 domain name as the alias target. + */ +export class ApiGatewayv2Domain implements route53.IAliasRecordTarget { + constructor(private readonly domainName: apigv2.IDomainName) { } + + public bind(_record: route53.IRecordSet): route53.AliasRecordTargetConfig { + return { + dnsName: this.domainName.regionalDomainName, + hostedZoneId: this.domainName.regionalHostedZoneId, + }; + } +} diff --git a/packages/@aws-cdk/aws-route53-targets/lib/index.ts b/packages/@aws-cdk/aws-route53-targets/lib/index.ts index 6df1bd67d6037..af574aa599519 100644 --- a/packages/@aws-cdk/aws-route53-targets/lib/index.ts +++ b/packages/@aws-cdk/aws-route53-targets/lib/index.ts @@ -1,4 +1,5 @@ export * from './api-gateway-domain-name'; +export * from './api-gatewayv2-domain-name'; export * from './bucket-website-target'; export * from './classic-load-balancer-target'; export * from './cloudfront-target'; diff --git a/packages/@aws-cdk/aws-route53-targets/package.json b/packages/@aws-cdk/aws-route53-targets/package.json index 159a9cfd85ff3..52e3e252fb960 100644 --- a/packages/@aws-cdk/aws-route53-targets/package.json +++ b/packages/@aws-cdk/aws-route53-targets/package.json @@ -69,11 +69,12 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-apigatewayv2": "0.0.0", "@aws-cdk/aws-cloudfront": "0.0.0", "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", @@ -89,6 +90,7 @@ "homepage": "https://github.com/aws/aws-cdk", "peerDependencies": { "@aws-cdk/aws-apigateway": "0.0.0", + "@aws-cdk/aws-apigatewayv2": "0.0.0", "@aws-cdk/aws-cloudfront": "0.0.0", "@aws-cdk/aws-cognito": "0.0.0", "@aws-cdk/aws-ec2": "0.0.0", diff --git a/packages/@aws-cdk/aws-route53-targets/test/apigatewayv2-target.test.ts b/packages/@aws-cdk/aws-route53-targets/test/apigatewayv2-target.test.ts new file mode 100644 index 0000000000000..66a504f630157 --- /dev/null +++ b/packages/@aws-cdk/aws-route53-targets/test/apigatewayv2-target.test.ts @@ -0,0 +1,49 @@ +import { expect as expectStack, haveResource } from '@aws-cdk/assert'; +import * as apigwv2 from '@aws-cdk/aws-apigatewayv2'; +import * as acm from '@aws-cdk/aws-certificatemanager'; +import * as route53 from '@aws-cdk/aws-route53'; +import { Stack } from '@aws-cdk/core'; +import * as targets from '../lib'; + +test('targets.ApiGatewayv2Domain can be used to directly reference a domain', () => { + // GIVEN + const stack = new Stack(); + const domainName = 'example.com'; + const cert = new acm.Certificate(stack, 'cert', { domainName }); + const dn = new apigwv2.DomainName(stack, 'DN', { + domainName, + certificate: cert, + }); + const zone = new route53.HostedZone(stack, 'zone', { + zoneName: 'example.com', + }); + + // WHEN + new route53.ARecord(stack, 'A', { + zone, + target: route53.RecordTarget.fromAlias(new targets.ApiGatewayv2Domain(dn)), + }); + + // THEN + expectStack(stack).to(haveResource('AWS::Route53::RecordSet', { + Name: 'example.com.', + Type: 'A', + AliasTarget: { + DNSName: { + 'Fn::GetAtt': [ + 'DNFDC76583', + 'RegionalDomainName', + ], + }, + HostedZoneId: { + 'Fn::GetAtt': [ + 'DNFDC76583', + 'RegionalHostedZoneId', + ], + }, + }, + HostedZoneId: { + Ref: 'zoneEB40FF1E', + }, + })); +}); diff --git a/packages/@aws-cdk/aws-route53/package.json b/packages/@aws-cdk/aws-route53/package.json index ea055e83cb8d0..be9f0e15a67f1 100644 --- a/packages/@aws-cdk/aws-route53/package.json +++ b/packages/@aws-cdk/aws-route53/package.json @@ -73,7 +73,7 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-s3-deployment/package.json b/packages/@aws-cdk/aws-s3-deployment/package.json index 26305003000cf..fa62ca2e0f789 100644 --- a/packages/@aws-cdk/aws-s3-deployment/package.json +++ b/packages/@aws-cdk/aws-s3-deployment/package.json @@ -87,7 +87,7 @@ "@types/jest": "^26.0.15", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts b/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts index a0bd0acaeaee6..36ad917ec4ec4 100644 --- a/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts +++ b/packages/@aws-cdk/aws-s3-notifications/lib/lambda.ts @@ -1,7 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as s3 from '@aws-cdk/aws-s3'; -import { CfnResource, Construct, Stack } from '@aws-cdk/core'; +import { CfnResource, Construct, Names, Stack } from '@aws-cdk/core'; /** * Use a Lambda function as a bucket notification destination @@ -11,10 +11,10 @@ export class LambdaDestination implements s3.IBucketNotificationDestination { } public bind(_scope: Construct, bucket: s3.IBucket): s3.BucketNotificationDestinationConfig { - const permissionId = `AllowBucketNotificationsTo${this.fn.permissionsNode.uniqueId}`; + const permissionId = `AllowBucketNotificationsTo${Names.nodeUniqueId(this.fn.permissionsNode)}`; if (!Construct.isConstruct(bucket)) { - throw new Error(`LambdaDestination for function ${this.fn.permissionsNode.uniqueId} can only be configured on a + throw new Error(`LambdaDestination for function ${Names.nodeUniqueId(this.fn.permissionsNode)} can only be configured on a bucket construct (Bucket ${bucket.bucketName})`); } diff --git a/packages/@aws-cdk/aws-s3-notifications/package.json b/packages/@aws-cdk/aws-s3-notifications/package.json index 0c4238fb948c5..e78e96ff53d62 100644 --- a/packages/@aws-cdk/aws-s3-notifications/package.json +++ b/packages/@aws-cdk/aws-s3-notifications/package.json @@ -66,7 +66,7 @@ "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-sam/package.json b/packages/@aws-cdk/aws-sam/package.json index 32f18719db68f..7a788ee71a606 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -77,9 +77,9 @@ "@types/jest": "^26.0.15", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.4.3" + "ts-jest": "^26.4.4" }, "dependencies": { "@aws-cdk/core": "0.0.0", diff --git a/packages/@aws-cdk/aws-secretsmanager/README.md b/packages/@aws-cdk/aws-secretsmanager/README.md index 286e83ea37b6b..383a4ff2870c1 100644 --- a/packages/@aws-cdk/aws-secretsmanager/README.md +++ b/packages/@aws-cdk/aws-secretsmanager/README.md @@ -160,20 +160,21 @@ credentials generation and rotation is integrated. ### Importing Secrets Existing secrets can be imported by ARN, name, and other attributes (including the KMS key used to encrypt the secret). -Secrets imported by name can used the short-form of the name (without the SecretsManager-provided suffx); +Secrets imported by name should use the short-form of the name (without the SecretsManager-provided suffx); the secret name must exist in the same account and region as the stack. Importing by name makes it easier to reference secrets created in different regions, each with their own suffix and ARN. ```ts import * as kms from '@aws-cdk/aws-kms'; -const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9'; +const secretCompleteArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9'; +const secretPartialArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret'; // No Secrets Manager suffix const encryptionKey = kms.Key.fromKeyArn(stack, 'MyEncKey', 'arn:aws:kms:eu-west-1:111111111111:key/21c4b39b-fde2-4273-9ac0-d9bb5c0d0030'); -const mySecretFromArn = secretsmanager.Secret.fromSecretArn(stack, 'SecretFromArn', secretArn); -const mySecretFromName = secretsmanager.Secret.fromSecretName(stack, 'SecretFromName', 'MySecret') // Note: the -f3gDy9 suffix is optional +const mySecretFromCompleteArn = secretsmanager.Secret.fromSecretCompleteArn(stack, 'SecretFromCompleteArn', secretCompleteArn); +const mySecretFromPartialArn = secretsmanager.Secret.fromSecretPartialArn(stack, 'SecretFromPartialArn', secretPartialArn); +const mySecretFromName = secretsmanager.Secret.fromSecretNameV2(stack, 'SecretFromName', 'MySecret') const mySecretFromAttrs = secretsmanager.Secret.fromSecretAttributes(stack, 'SecretFromAttributes', { - secretArn, + secretCompleteArn, encryptionKey, - secretName: 'MySecret', // Optional, will be calculated from the ARN }); ``` diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts index 388933895af51..0d0ed6e75c348 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret-rotation.ts @@ -1,7 +1,7 @@ import * as ec2 from '@aws-cdk/aws-ec2'; import * as lambda from '@aws-cdk/aws-lambda'; import * as serverless from '@aws-cdk/aws-sam'; -import { Duration, Stack, Token } from '@aws-cdk/core'; +import { Duration, Names, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { ISecret } from './secret'; @@ -224,7 +224,7 @@ export class SecretRotation extends CoreConstruct { } // Max length of 64 chars, get the last 64 chars - const uniqueId = this.node.uniqueId; + const uniqueId = Names.uniqueId(this); const rotationFunctionName = uniqueId.substring(Math.max(uniqueId.length - 64, 0), uniqueId.length); const securityGroup = props.securityGroup || new ec2.SecurityGroup(this, 'SecurityGroup', { diff --git a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts index d60a534375483..33bc8a709efcf 100644 --- a/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts +++ b/packages/@aws-cdk/aws-secretsmanager/lib/secret.ts @@ -17,11 +17,18 @@ export interface ISecret extends IResource { readonly encryptionKey?: kms.IKey; /** - * The ARN of the secret in AWS Secrets Manager. + * The ARN of the secret in AWS Secrets Manager. Will return the full ARN if available, otherwise a partial arn. + * For secrets imported by the deprecated `fromSecretName`, it will return the `secretName`. * @attribute */ readonly secretArn: string; + /** + * The full ARN of the secret in AWS Secrets Manager, which is the ARN including the Secrets Manager-supplied 6-character suffix. + * This is equal to `secretArn` in most cases, but is undefined when a full ARN is not available (e.g., secrets imported by name). + */ + readonly secretFullArn?: string; + /** * The name of the secret */ @@ -127,6 +134,7 @@ export interface SecretProps { /** * Attributes required to import an existing secret into the Stack. + * One ARN format (`secretArn`, `secretCompleteArn`, `secretPartialArn`) must be provided. */ export interface SecretAttributes { /** @@ -136,8 +144,22 @@ export interface SecretAttributes { /** * The ARN of the secret in SecretsManager. + * Cannot be used with `secretCompleteArn` or `secretPartialArn`. + * @deprecated use `secretCompleteArn` or `secretPartialArn` instead. */ - readonly secretArn: string; + readonly secretArn?: string; + + /** + * The complete ARN of the secret in SecretsManager. This is the ARN including the Secrets Manager 6-character suffix. + * Cannot be used with `secretArn` or `secretPartialArn`. + */ + readonly secretCompleteArn?: string; + + /** + * The partial ARN of the secret in SecretsManager. This is the ARN without the Secrets Manager 6-character suffix. + * Cannot be used with `secretArn` or `secretCompleteArn`. + */ + readonly secretPartialArn?: string; } /** @@ -152,6 +174,8 @@ abstract class SecretBase extends Resource implements ISecret { private policy?: ResourcePolicy; + public get secretFullArn(): string | undefined { return this.secretArn; } + public grantRead(grantee: iam.IGrantable, versionStages?: string[]): iam.Grant { // @see https://docs.aws.amazon.com/secretsmanager/latest/userguide/auth-and-access_identity-based-policies.html @@ -270,13 +294,26 @@ abstract class SecretBase extends Resource implements ISecret { */ export class Secret extends SecretBase { + /** @deprecated use `fromSecretCompleteArn` or `fromSecretPartialArn` */ public static fromSecretArn(scope: Construct, id: string, secretArn: string): ISecret { - return Secret.fromSecretAttributes(scope, id, { secretArn }); + const attrs = arnIsComplete(secretArn) ? { secretCompleteArn: secretArn } : { secretPartialArn: secretArn }; + return Secret.fromSecretAttributes(scope, id, attrs); + } + + /** Imports a secret by complete ARN. The complete ARN is the ARN with the Secrets Manager-supplied suffix. */ + public static fromSecretCompleteArn(scope: Construct, id: string, secretCompleteArn: string): ISecret { + return Secret.fromSecretAttributes(scope, id, { secretCompleteArn }); + } + + /** Imports a secret by partial ARN. The partial ARN is the ARN without the Secrets Manager-supplied suffix. */ + public static fromSecretPartialArn(scope: Construct, id: string, secretPartialArn: string): ISecret { + return Secret.fromSecretAttributes(scope, id, { secretPartialArn }); } /** * Imports a secret by secret name; the ARN of the Secret will be set to the secret name. * A secret with this name must exist in the same account & region. + * @deprecated use `fromSecretNameV2` */ public static fromSecretName(scope: Construct, id: string, secretName: string): ISecret { return new class extends SecretBase { @@ -284,6 +321,7 @@ export class Secret extends SecretBase { public readonly secretArn = secretName; public readonly secretName = secretName; protected readonly autoCreatePolicy = false; + public get secretFullArn() { return undefined; } // Overrides the secretArn for grant* methods, where the secretArn must be in ARN format. // Also adds a wildcard to the resource name to support the SecretsManager-provided suffix. protected get arnForPolicies() { @@ -297,6 +335,35 @@ export class Secret extends SecretBase { }(scope, id); } + /** + * Imports a secret by secret name. + * A secret with this name must exist in the same account & region. + * Replaces the deprecated `fromSecretName`. + */ + public static fromSecretNameV2(scope: Construct, id: string, secretName: string): ISecret { + return new class extends SecretBase { + public readonly encryptionKey = undefined; + public readonly secretName = secretName; + public readonly secretArn = this.partialArn; + protected readonly autoCreatePolicy = false; + public get secretFullArn() { return undefined; } + // Overrides the secretArn for grant* methods, where the secretArn must be in ARN format. + // Also adds a wildcard to the resource name to support the SecretsManager-provided suffix. + protected get arnForPolicies(): string { + return this.partialArn + '-??????'; + } + // Creates a "partial" ARN from the secret name. The "full" ARN would include the SecretsManager-provided suffix. + private get partialArn(): string { + return Stack.of(this).formatArn({ + service: 'secretsmanager', + resource: 'secret', + resourceName: secretName, + sep: ':', + }); + } + }(scope, id); + } + /** * Import an existing secret into the Stack. * @@ -305,14 +372,33 @@ export class Secret extends SecretBase { * @param attrs the attributes of the imported secret. */ public static fromSecretAttributes(scope: Construct, id: string, attrs: SecretAttributes): ISecret { - class Import extends SecretBase { - public readonly encryptionKey = attrs.encryptionKey; - public readonly secretArn = attrs.secretArn; - public readonly secretName = parseSecretName(scope, attrs.secretArn); - protected readonly autoCreatePolicy = false; + let secretArn: string; + let secretArnIsPartial: boolean; + + if (attrs.secretArn) { + if (attrs.secretCompleteArn || attrs.secretPartialArn) { + throw new Error('cannot use `secretArn` with `secretCompleteArn` or `secretPartialArn`'); + } + secretArn = attrs.secretArn; + secretArnIsPartial = false; + } else { + if ((attrs.secretCompleteArn && attrs.secretPartialArn) || + (!attrs.secretCompleteArn && !attrs.secretPartialArn)) { + throw new Error('must use only one of `secretCompleteArn` or `secretPartialArn`'); + } + if (attrs.secretCompleteArn && !arnIsComplete(attrs.secretCompleteArn)) { + throw new Error('`secretCompleteArn` does not appear to be complete; missing 6-character suffix'); + } + [secretArn, secretArnIsPartial] = attrs.secretCompleteArn ? [attrs.secretCompleteArn, false] : [attrs.secretPartialArn!, true]; } - return new Import(scope, id); + return new class extends SecretBase { + public readonly encryptionKey = attrs.encryptionKey; + public readonly secretArn = secretArn; + public readonly secretName = parseSecretName(scope, secretArn); + protected readonly autoCreatePolicy = false; + public get secretFullArn() { return secretArnIsPartial ? undefined : secretArn; } + }(scope, id); } public readonly encryptionKey?: kms.IKey; @@ -351,7 +437,7 @@ export class Secret extends SecretBase { }); this.encryptionKey = props.encryptionKey; - this.secretName = this.physicalName; + this.secretName = parseSecretName(this, this.secretArn); // @see https://docs.aws.amazon.com/kms/latest/developerguide/services-secrets-manager.html#asm-authz const principal = @@ -612,9 +698,16 @@ function parseSecretName(construct: IConstruct, secretArn: string) { return resourceName; } - // Secret resource names are in the format `${secretName}-${SecretsManager suffix}` - // If there is no hyphen, assume no suffix was provided, and return the whole name. - return resourceName.substr(0, resourceName.lastIndexOf('-')) || resourceName; + // Secret resource names are in the format `${secretName}-${6-character SecretsManager suffix}` + // If there is no hyphen (or 6-character suffix) assume no suffix was provided, and return the whole name. + const lastHyphenIndex = resourceName.lastIndexOf('-'); + const hasSecretsSuffix = lastHyphenIndex !== -1 && resourceName.substr(lastHyphenIndex + 1).length === 6; + return hasSecretsSuffix ? resourceName.substr(0, lastHyphenIndex) : resourceName; } throw new Error('invalid ARN format; no secret name provided'); } + +/** Performs a best guess if an ARN is complete, based on if it ends with a 6-character suffix. */ +function arnIsComplete(secretArn: string): boolean { + return Token.isUnresolved(secretArn) || /-[a-z0-9]{6}$/i.test(secretArn); +} diff --git a/packages/@aws-cdk/aws-secretsmanager/package.json b/packages/@aws-cdk/aws-secretsmanager/package.json index da37d7206d2a5..9cf9e71ee87e7 100644 --- a/packages/@aws-cdk/aws-secretsmanager/package.json +++ b/packages/@aws-cdk/aws-secretsmanager/package.json @@ -103,6 +103,12 @@ "awslint": { "exclude": [ "attribute-tag:@aws-cdk/aws-secretsmanager.Secret.secretName", + "attribute-tag:@aws-cdk/aws-secretsmanager.Secret.secretFullArn", + "from-signature:@aws-cdk/aws-secretsmanager.Secret.fromSecretNameV2", + "from-signature:@aws-cdk/aws-secretsmanager.Secret.fromSecretNameV2.params[2]", + "props-default-doc:@aws-cdk/aws-secretsmanager.SecretAttributes.secretArn", + "props-default-doc:@aws-cdk/aws-secretsmanager.SecretAttributes.secretCompleteArn", + "props-default-doc:@aws-cdk/aws-secretsmanager.SecretAttributes.secretPartialArn", "from-signature:@aws-cdk/aws-secretsmanager.SecretTargetAttachment.fromSecretTargetAttachmentSecretArn", "from-attributes:fromSecretTargetAttachmentAttributes", "props-physical-name:@aws-cdk/aws-secretsmanager.RotationScheduleProps", diff --git a/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts b/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts index fab017f44e609..0e56a72bc3c54 100644 --- a/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts +++ b/packages/@aws-cdk/aws-secretsmanager/test/secret.test.ts @@ -425,6 +425,23 @@ test('secretValue', () => { }); }); +describe('secretName', () => { + test.each([undefined, 'mySecret'])('when secretName is %s', (secretName) => { + const secret = new secretsmanager.Secret(stack, 'Secret', { + secretName, + }); + new cdk.CfnOutput(stack, 'MySecretName', { + value: secret.secretName, + }); + + // Creates secret name by parsing ARN. + expect(stack).toHaveOutput({ + outputName: 'MySecretName', + outputValue: { 'Fn::Select': [6, { 'Fn::Split': [':', { Ref: 'SecretA720EF05' }] }] }, + }); + }); +}); + test('import by secretArn', () => { // GIVEN const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9'; @@ -434,6 +451,7 @@ test('import by secretArn', () => { // THEN expect(secret.secretArn).toBe(secretArn); + expect(secret.secretFullArn).toBe(secretArn); expect(secret.secretName).toBe('MySecret'); expect(secret.encryptionKey).toBeUndefined(); expect(stack.resolve(secret.secretValue)).toEqual(`{{resolve:secretsmanager:${secretArn}:SecretString:::}}`); @@ -460,6 +478,18 @@ test('import by secretArn supports secret ARNs without suffixes', () => { expect(secret.secretName).toBe('MySecret'); }); +test('import by secretArn does not strip suffixes unless the suffix length is six', () => { + // GIVEN + const arnWith5CharacterSuffix = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:github-token'; + const arnWith6CharacterSuffix = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:github-token-f3gDy9'; + const arnWithMultiple6CharacterSuffix = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:github-token-f3gDy9-acb123'; + + // THEN + expect(secretsmanager.Secret.fromSecretArn(stack, 'Secret5', arnWith5CharacterSuffix).secretName).toEqual('github-token'); + expect(secretsmanager.Secret.fromSecretArn(stack, 'Secret6', arnWith6CharacterSuffix).secretName).toEqual('github-token'); + expect(secretsmanager.Secret.fromSecretArn(stack, 'Secret6Twice', arnWithMultiple6CharacterSuffix).secretName).toEqual('github-token-f3gDy9'); +}); + test('import by secretArn supports tokens for ARNs', () => { // GIVEN const app = new cdk.App(); @@ -479,22 +509,103 @@ test('import by secretArn supports tokens for ARNs', () => { }); }); -test('import by attributes', () => { +test('import by secretArn guesses at complete or partial ARN', () => { + // GIVEN + const secretArnWithSuffix = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9'; + const secretArnWithoutSuffix = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret'; + + // WHEN + const secretWithCompleteArn = secretsmanager.Secret.fromSecretArn(stack, 'SecretWith', secretArnWithSuffix); + const secretWithoutCompleteArn = secretsmanager.Secret.fromSecretArn(stack, 'SecretWithout', secretArnWithoutSuffix); + + // THEN + expect(secretWithCompleteArn.secretFullArn).toEqual(secretArnWithSuffix); + expect(secretWithoutCompleteArn.secretFullArn).toBeUndefined(); +}); + +test('fromSecretCompleteArn', () => { // GIVEN - const encryptionKey = new kms.Key(stack, 'KMS'); const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9'; // WHEN - const secret = secretsmanager.Secret.fromSecretAttributes(stack, 'Secret', { - secretArn, encryptionKey, - }); + const secret = secretsmanager.Secret.fromSecretCompleteArn(stack, 'Secret', secretArn); + + // THEN + expect(secret.secretArn).toBe(secretArn); + expect(secret.secretFullArn).toBe(secretArn); + expect(secret.secretName).toBe('MySecret'); + expect(secret.encryptionKey).toBeUndefined(); + expect(stack.resolve(secret.secretValue)).toEqual(`{{resolve:secretsmanager:${secretArn}:SecretString:::}}`); + expect(stack.resolve(secret.secretValueFromJson('password'))).toEqual(`{{resolve:secretsmanager:${secretArn}:SecretString:password::}}`); +}); + +test('fromSecretPartialArn', () => { + // GIVEN + const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret'; + + // WHEN + const secret = secretsmanager.Secret.fromSecretPartialArn(stack, 'Secret', secretArn); // THEN expect(secret.secretArn).toBe(secretArn); + expect(secret.secretFullArn).toBeUndefined(); expect(secret.secretName).toBe('MySecret'); - expect(secret.encryptionKey).toBe(encryptionKey); - expect(stack.resolve(secret.secretValue)).toBe(`{{resolve:secretsmanager:${secretArn}:SecretString:::}}`); - expect(stack.resolve(secret.secretValueFromJson('password'))).toBe(`{{resolve:secretsmanager:${secretArn}:SecretString:password::}}`); + expect(secret.encryptionKey).toBeUndefined(); + expect(stack.resolve(secret.secretValue)).toEqual(`{{resolve:secretsmanager:${secretArn}:SecretString:::}}`); + expect(stack.resolve(secret.secretValueFromJson('password'))).toEqual(`{{resolve:secretsmanager:${secretArn}:SecretString:password::}}`); +}); + +describe('fromSecretAttributes', () => { + test('import by attributes', () => { + // GIVEN + const encryptionKey = new kms.Key(stack, 'KMS'); + const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9'; + + // WHEN + const secret = secretsmanager.Secret.fromSecretAttributes(stack, 'Secret', { + secretArn, encryptionKey, + }); + + // THEN + expect(secret.secretArn).toBe(secretArn); + expect(secret.secretFullArn).toBe(secretArn); + expect(secret.secretName).toBe('MySecret'); + expect(secret.encryptionKey).toBe(encryptionKey); + expect(stack.resolve(secret.secretValue)).toBe(`{{resolve:secretsmanager:${secretArn}:SecretString:::}}`); + expect(stack.resolve(secret.secretValueFromJson('password'))).toBe(`{{resolve:secretsmanager:${secretArn}:SecretString:password::}}`); + }); + + test('throws if secretArn and either secretCompleteArn or secretPartialArn are provided', () => { + const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9'; + + const error = /cannot use `secretArn` with `secretCompleteArn` or `secretPartialArn`/; + expect(() => secretsmanager.Secret.fromSecretAttributes(stack, 'Secret', { + secretArn, + secretCompleteArn: secretArn, + })).toThrow(error); + expect(() => secretsmanager.Secret.fromSecretAttributes(stack, 'Secret', { + secretArn, + secretPartialArn: secretArn, + })).toThrow(error); + }); + + test('throws if no ARN is provided', () => { + expect(() => secretsmanager.Secret.fromSecretAttributes(stack, 'Secret', {})).toThrow(/must use only one of `secretCompleteArn` or `secretPartialArn`/); + }); + + test('throws if both complete and partial ARNs are provided', () => { + const secretArn = 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret-f3gDy9'; + expect(() => secretsmanager.Secret.fromSecretAttributes(stack, 'Secret', { + secretPartialArn: secretArn, + secretCompleteArn: secretArn, + })).toThrow(/must use only one of `secretCompleteArn` or `secretPartialArn`/); + }); + + test('throws if secretCompleteArn is not complete', () => { + expect(() => secretsmanager.Secret.fromSecretAttributes(stack, 'Secret', { + secretCompleteArn: 'arn:aws:secretsmanager:eu-west-1:111111111111:secret:MySecret', + })).toThrow(/does not appear to be complete/); + }); }); test('import by secret name', () => { @@ -507,6 +618,7 @@ test('import by secret name', () => { // THEN expect(secret.secretArn).toBe(secretName); expect(secret.secretName).toBe(secretName); + expect(secret.secretFullArn).toBeUndefined(); expect(stack.resolve(secret.secretValue)).toBe(`{{resolve:secretsmanager:${secretName}:SecretString:::}}`); expect(stack.resolve(secret.secretValueFromJson('password'))).toBe(`{{resolve:secretsmanager:${secretName}:SecretString:password::}}`); }); @@ -555,6 +667,74 @@ test('import by secret name with grants', () => { }); }); +test('import by secret name v2', () => { + // GIVEN + const secretName = 'MySecret'; + + // WHEN + const secret = secretsmanager.Secret.fromSecretNameV2(stack, 'Secret', secretName); + + // THEN + expect(secret.secretArn).toBe(`arn:${stack.partition}:secretsmanager:${stack.region}:${stack.account}:secret:MySecret`); + expect(secret.secretName).toBe(secretName); + expect(secret.secretFullArn).toBeUndefined(); + expect(stack.resolve(secret.secretValue)).toEqual({ + 'Fn::Join': ['', [ + '{{resolve:secretsmanager:arn:', + { Ref: 'AWS::Partition' }, + ':secretsmanager:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':secret:MySecret:SecretString:::}}', + ]], + }); +}); + +test('import by secret name v2 with grants', () => { + // GIVEN + const role = new iam.Role(stack, 'Role', { assumedBy: new iam.AccountRootPrincipal() }); + const secret = secretsmanager.Secret.fromSecretNameV2(stack, 'Secret', 'MySecret'); + + // WHEN + secret.grantRead(role); + secret.grantWrite(role); + + // THEN + const expectedSecretReference = { + 'Fn::Join': ['', [ + 'arn:', + { Ref: 'AWS::Partition' }, + ':secretsmanager:', + { Ref: 'AWS::Region' }, + ':', + { Ref: 'AWS::AccountId' }, + ':secret:MySecret-??????', + ]], + }; + expect(stack).toHaveResource('AWS::IAM::Policy', { + PolicyDocument: { + Version: '2012-10-17', + Statement: [{ + Action: [ + 'secretsmanager:GetSecretValue', + 'secretsmanager:DescribeSecret', + ], + Effect: 'Allow', + Resource: expectedSecretReference, + }, + { + Action: [ + 'secretsmanager:PutSecretValue', + 'secretsmanager:UpdateSecret', + ], + Effect: 'Allow', + Resource: expectedSecretReference, + }], + }, + }); +}); + test('can attach a secret with attach()', () => { // GIVEN const secret = new secretsmanager.Secret(stack, 'Secret'); diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/alias-target-instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/alias-target-instance.ts index a8302b5e1dbc9..271b386963cab 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/alias-target-instance.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/alias-target-instance.ts @@ -1,3 +1,4 @@ +import { Names } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { BaseInstanceProps, InstanceBase } from './instance'; import { NamespaceType } from './namespace'; @@ -65,7 +66,7 @@ export class AliasTargetInstance extends InstanceBase { AWS_ALIAS_DNS_NAME: props.dnsName, ...props.customAttributes, }, - instanceId: props.instanceId || this.node.uniqueId, + instanceId: props.instanceId || Names.uniqueId(this), serviceId: props.service.serviceId, }); diff --git a/packages/@aws-cdk/aws-servicediscovery/lib/instance.ts b/packages/@aws-cdk/aws-servicediscovery/lib/instance.ts index 00e7d6a126934..76cbf171f8ae9 100644 --- a/packages/@aws-cdk/aws-servicediscovery/lib/instance.ts +++ b/packages/@aws-cdk/aws-servicediscovery/lib/instance.ts @@ -1,4 +1,4 @@ -import { IResource, Resource } from '@aws-cdk/core'; +import { IResource, Names, Resource } from '@aws-cdk/core'; import { IService } from './service'; export interface IInstance extends IResource { @@ -50,7 +50,7 @@ export abstract class InstanceBase extends Resource implements IInstance { */ protected uniqueInstanceId() { // Max length of 64 chars, get the last 64 chars - const id = this.node.uniqueId; + const id = Names.uniqueId(this); return id.substring(Math.max(id.length - 64, 0), id.length); } } diff --git a/packages/@aws-cdk/aws-ses-actions/package.json b/packages/@aws-cdk/aws-ses-actions/package.json index f06d590aed053..ea8d76c7cd66a 100644 --- a/packages/@aws-cdk/aws-ses-actions/package.json +++ b/packages/@aws-cdk/aws-ses-actions/package.json @@ -68,7 +68,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts index 943813184ed1f..3ecab463d2c2c 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/lambda.ts @@ -1,7 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as lambda from '@aws-cdk/aws-lambda'; import * as sns from '@aws-cdk/aws-sns'; -import { Construct, Stack } from '@aws-cdk/core'; +import { Construct, Names, Stack } from '@aws-cdk/core'; import { SubscriptionProps } from './subscription'; /** @@ -27,7 +27,7 @@ export class LambdaSubscription implements sns.ITopicSubscription { throw new Error('The supplied lambda Function object must be an instance of Construct'); } - this.fn.addPermission(`AllowInvoke:${topic.node.uniqueId}`, { + this.fn.addPermission(`AllowInvoke:${Names.nodeUniqueId(topic.node)}`, { sourceArn: topic.topicArn, principal: new iam.ServicePrincipal('sns.amazonaws.com'), }); diff --git a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts index 39c8362d60c4f..bac6a13859d9c 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts +++ b/packages/@aws-cdk/aws-sns-subscriptions/lib/sqs.ts @@ -1,7 +1,7 @@ import * as iam from '@aws-cdk/aws-iam'; import * as sns from '@aws-cdk/aws-sns'; import * as sqs from '@aws-cdk/aws-sqs'; -import { Construct, Stack } from '@aws-cdk/core'; +import { Construct, Names, Stack } from '@aws-cdk/core'; import { SubscriptionProps } from './subscription'; /** @@ -48,7 +48,7 @@ export class SqsSubscription implements sns.ITopicSubscription { return { subscriberScope: this.queue, - subscriberId: topic.node.uniqueId, + subscriberId: Names.nodeUniqueId(topic.node), endpoint: this.queue.queueArn, protocol: sns.SubscriptionProtocol.SQS, rawMessageDelivery: this.props.rawMessageDelivery, diff --git a/packages/@aws-cdk/aws-sns-subscriptions/package.json b/packages/@aws-cdk/aws-sns-subscriptions/package.json index 6b912736592ca..59c95c7d06c43 100644 --- a/packages/@aws-cdk/aws-sns-subscriptions/package.json +++ b/packages/@aws-cdk/aws-sns-subscriptions/package.json @@ -67,7 +67,7 @@ "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-sqs/package.json b/packages/@aws-cdk/aws-sqs/package.json index da13a61049b03..538d5d7a6f3c6 100644 --- a/packages/@aws-cdk/aws-sqs/package.json +++ b/packages/@aws-cdk/aws-sqs/package.json @@ -74,7 +74,7 @@ "@aws-cdk/assert": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@types/nodeunit": "^0.0.31", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/get-query-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/get-query-execution.ts index 2f7ea7f53d556..555a42f107cd8 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/get-query-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/get-query-execution.ts @@ -26,7 +26,6 @@ export class AthenaGetQueryExecution extends sfn.TaskStateBase { private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ sfn.IntegrationPattern.REQUEST_RESPONSE, - sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, ]; protected readonly taskMetrics?: sfn.TaskMetricsConfig; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/get-query-results.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/get-query-results.ts index 781ed8da2d557..4adc8cd4ce379 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/get-query-results.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/get-query-results.ts @@ -40,7 +40,6 @@ export class AthenaGetQueryResults extends sfn.TaskStateBase { private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ sfn.IntegrationPattern.REQUEST_RESPONSE, - sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, ]; protected readonly taskMetrics?: sfn.TaskMetricsConfig; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/start-query-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/start-query-execution.ts index 17328e7e97648..50dc1b03a24bc 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/start-query-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/start-query-execution.ts @@ -1,5 +1,6 @@ import * as iam from '@aws-cdk/aws-iam'; import * as kms from '@aws-cdk/aws-kms'; +import * as s3 from '@aws-cdk/aws-s3'; import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; @@ -54,7 +55,7 @@ export class AthenaStartQueryExecution extends sfn.TaskStateBase { private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ sfn.IntegrationPattern.REQUEST_RESPONSE, - sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, + sfn.IntegrationPattern.RUN_JOB, ]; protected readonly taskMetrics?: sfn.TaskMetricsConfig; @@ -87,28 +88,34 @@ export class AthenaStartQueryExecution extends sfn.TaskStateBase { }), ], - actions: ['athena:getDataCatalog', 'athena:startQueryExecution'], + actions: ['athena:getDataCatalog', 'athena:startQueryExecution', 'athena:getQueryExecution'], }), ]; policyStatements.push( new iam.PolicyStatement({ - actions: ['s3:AbortMultipartUpload', - 's3:CreateBucket', - 's3:GetBucketLocation', - 's3:GetObject', + actions: ['s3:CreateBucket', 's3:ListBucket', + 's3:GetBucketLocation', + 's3:GetObject'], + resources: ['*'], // Need * permissions to create new output location https://docs.aws.amazon.com/athena/latest/ug/security-iam-athena.html + }), + ); + + policyStatements.push( + new iam.PolicyStatement({ + actions: ['s3:AbortMultipartUpload', 's3:ListBucketMultipartUploads', 's3:ListMultipartUploadParts', 's3:PutObject'], - resources: [this.props.resultConfiguration?.outputLocation ?? '*'], // Need S3 location where data is stored https://docs.aws.amazon.com/athena/latest/ug/security-iam-athena.html + resources: [this.props.resultConfiguration?.outputLocation?.bucketName ? `arn:aws:s3:::${this.props.resultConfiguration?.outputLocation?.bucketName}/${this.props.resultConfiguration?.outputLocation?.objectKey}/*` : '*'], // Need S3 location where data is stored or Athena throws an Unable to verify/create output bucket https://docs.aws.amazon.com/athena/latest/ug/security-iam-athena.html }), ); policyStatements.push( new iam.PolicyStatement({ actions: ['lakeformation:GetDataAccess'], - resources: [this.props.resultConfiguration?.outputLocation ?? '*'], // Workflow role permissions https://docs.aws.amazon.com/lake-formation/latest/dg/permissions-reference.html + resources: ['*'], // State machines scoped to output location fail and * permissions are required as per documentation https://docs.aws.amazon.com/lake-formation/latest/dg/permissions-reference.html }), ); @@ -167,25 +174,46 @@ export class AthenaStartQueryExecution extends sfn.TaskStateBase { * @internal */ protected _renderTask(): any { - return { - Resource: integrationResourceArn('athena', 'startQueryExecution', this.integrationPattern), - Parameters: sfn.FieldUtils.renderObject({ - QueryString: this.props.queryString, - ClientRequestToken: this.props.clientRequestToken, - QueryExecutionContext: { - Catalog: this.props.queryExecutionContext?.catalogName, - Database: this.props.queryExecutionContext?.databaseName, - }, - ResultConfiguration: { - EncryptionConfiguration: { - EncryptionOption: this.props.resultConfiguration?.encryptionConfiguration?.encryptionOption, - KmsKey: this.props.resultConfiguration?.encryptionConfiguration?.encryptionKey, + if (this.props.resultConfiguration?.outputLocation) { + return { + Resource: integrationResourceArn('athena', 'startQueryExecution', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + QueryString: this.props.queryString, + ClientRequestToken: this.props.clientRequestToken, + QueryExecutionContext: { + Catalog: this.props.queryExecutionContext?.catalogName, + Database: this.props.queryExecutionContext?.databaseName, }, - OutputLocation: this.props.resultConfiguration?.outputLocation, - }, - WorkGroup: this.props.workGroup, - }), - }; + ResultConfiguration: { + EncryptionConfiguration: { + EncryptionOption: this.props.resultConfiguration?.encryptionConfiguration?.encryptionOption, + KmsKey: this.props.resultConfiguration?.encryptionConfiguration?.encryptionKey, + }, + OutputLocation: `s3://${this.props.resultConfiguration?.outputLocation?.bucketName}/${this.props.resultConfiguration?.outputLocation?.objectKey}/`, + }, + WorkGroup: this.props.workGroup, + }), + }; + } else { + return { + Resource: integrationResourceArn('athena', 'startQueryExecution', this.integrationPattern), + Parameters: sfn.FieldUtils.renderObject({ + QueryString: this.props.queryString, + ClientRequestToken: this.props.clientRequestToken, + QueryExecutionContext: { + Catalog: this.props.queryExecutionContext?.catalogName, + Database: this.props.queryExecutionContext?.databaseName, + }, + ResultConfiguration: { + EncryptionConfiguration: { + EncryptionOption: this.props.resultConfiguration?.encryptionConfiguration?.encryptionOption, + KmsKey: this.props.resultConfiguration?.encryptionConfiguration?.encryptionKey, + }, + }, + WorkGroup: this.props.workGroup, + }), + }; + } } } @@ -203,7 +231,7 @@ export interface ResultConfiguration { * @default - Query Result Location set in Athena settings for this workgroup * @example s3://query-results-bucket/folder/ */ - readonly outputLocation?: string; + readonly outputLocation?: s3.Location; /** * Encryption option used if enabled in S3 @@ -286,4 +314,4 @@ export interface QueryExecutionContext { * @default - No database */ readonly databaseName?: string; -} \ No newline at end of file +} diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/stop-query-execution.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/stop-query-execution.ts index 27c5c606949fc..7a51559a79631 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/stop-query-execution.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/lib/athena/stop-query-execution.ts @@ -24,7 +24,6 @@ export class AthenaStopQueryExecution extends sfn.TaskStateBase { private static readonly SUPPORTED_INTEGRATION_PATTERNS: sfn.IntegrationPattern[] = [ sfn.IntegrationPattern.REQUEST_RESPONSE, - sfn.IntegrationPattern.WAIT_FOR_TASK_TOKEN, ]; protected readonly taskMetrics?: sfn.TaskMetricsConfig; diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json index a4324e74a0106..9d7dca5eee6fd 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/package.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/package.json @@ -69,7 +69,7 @@ "@aws-cdk/aws-sns-subscriptions": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.get-query-execution.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.get-query-execution.expected.json index de3041464cf88..07442708a7080 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.get-query-execution.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.get-query-execution.expected.json @@ -36,7 +36,8 @@ { "Action": [ "athena:getDataCatalog", - "athena:startQueryExecution" + "athena:startQueryExecution", + "athena:getQueryExecution" ], "Effect": "Allow", "Resource": [ @@ -84,11 +85,17 @@ }, { "Action": [ - "s3:AbortMultipartUpload", "s3:CreateBucket", - "s3:GetBucketLocation", - "s3:GetObject", "s3:ListBucket", + "s3:GetBucketLocation", + "s3:GetObject" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:PutObject" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.get-query-results.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.get-query-results.expected.json index f593dc785feab..b11ec6cee2c3a 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.get-query-results.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.get-query-results.expected.json @@ -36,7 +36,8 @@ { "Action": [ "athena:getDataCatalog", - "athena:startQueryExecution" + "athena:startQueryExecution", + "athena:getQueryExecution" ], "Effect": "Allow", "Resource": [ @@ -84,11 +85,17 @@ }, { "Action": [ - "s3:AbortMultipartUpload", "s3:CreateBucket", - "s3:GetBucketLocation", - "s3:GetObject", "s3:ListBucket", + "s3:GetBucketLocation", + "s3:GetObject" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:PutObject" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.start-query-execution.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.start-query-execution.expected.json index 4cff9888545d8..200fc56302b66 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.start-query-execution.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.start-query-execution.expected.json @@ -36,7 +36,8 @@ { "Action": [ "athena:getDataCatalog", - "athena:startQueryExecution" + "athena:startQueryExecution", + "athena:getQueryExecution" ], "Effect": "Allow", "Resource": [ @@ -84,11 +85,17 @@ }, { "Action": [ - "s3:AbortMultipartUpload", "s3:CreateBucket", - "s3:GetBucketLocation", - "s3:GetObject", "s3:ListBucket", + "s3:GetBucketLocation", + "s3:GetObject" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:PutObject" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.stop-query-execution.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.stop-query-execution.expected.json index 28aaf5d4ab243..a25e8d93b5bba 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.stop-query-execution.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/integ.stop-query-execution.expected.json @@ -36,7 +36,8 @@ { "Action": [ "athena:getDataCatalog", - "athena:startQueryExecution" + "athena:startQueryExecution", + "athena:getQueryExecution" ], "Effect": "Allow", "Resource": [ @@ -84,11 +85,17 @@ }, { "Action": [ - "s3:AbortMultipartUpload", "s3:CreateBucket", - "s3:GetBucketLocation", - "s3:GetObject", "s3:ListBucket", + "s3:GetBucketLocation", + "s3:GetObject" + ], + "Effect": "Allow", + "Resource": "*" + }, + { + "Action": [ + "s3:AbortMultipartUpload", "s3:ListBucketMultipartUploads", "s3:ListMultipartUploadParts", "s3:PutObject" diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/start-query-execution.test.ts b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/start-query-execution.test.ts index 3ab4c0e3202ff..c96e4356c7a11 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/start-query-execution.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/athena/start-query-execution.test.ts @@ -1,3 +1,4 @@ +import * as sfn from '@aws-cdk/aws-stepfunctions'; import * as cdk from '@aws-cdk/core'; import { AthenaStartQueryExecution, EncryptionOption } from '../../lib/athena/start-query-execution'; @@ -17,7 +18,10 @@ describe('Start Query Execution', () => { }, resultConfiguration: { encryptionConfiguration: { encryptionOption: EncryptionOption.S3_MANAGED }, - outputLocation: 'https://s3.Region.amazonaws.com/bucket-name/key-name', + outputLocation: { + bucketName: 'query-results-bucket', + objectKey: 'folder', + }, }, workGroup: 'primary', }); @@ -47,10 +51,54 @@ describe('Start Query Execution', () => { }, ResultConfiguration: { EncryptionConfiguration: { EncryptionOption: EncryptionOption.S3_MANAGED }, - OutputLocation: 'https://s3.Region.amazonaws.com/bucket-name/key-name', + OutputLocation: 's3://query-results-bucket/folder/', }, WorkGroup: 'primary', }, }); }); -}); \ No newline at end of file + + test('sync integrationPattern', () => { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const task = new AthenaStartQueryExecution(stack, 'Query', { + integrationPattern: sfn.IntegrationPattern.RUN_JOB, + queryString: 'CREATE DATABASE database', + queryExecutionContext: { + databaseName: 'mydatabase', + }, + resultConfiguration: { + encryptionConfiguration: { encryptionOption: EncryptionOption.S3_MANAGED }, + }, + }); + + // THEN + expect(stack.resolve(task.toStateJson())).toEqual({ + Type: 'Task', + Resource: { + 'Fn::Join': [ + '', + [ + 'arn:', + { + Ref: 'AWS::Partition', + }, + ':states:::athena:startQueryExecution.sync', + ], + ], + }, + End: true, + Parameters: { + QueryString: 'CREATE DATABASE database', + QueryExecutionContext: { + Database: 'mydatabase', + }, + ResultConfiguration: { + EncryptionConfiguration: { EncryptionOption: EncryptionOption.S3_MANAGED }, + }, + }, + }); + }); +}); diff --git a/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json index 326a551cf89bd..ad853ea6241c3 100644 --- a/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json +++ b/packages/@aws-cdk/aws-stepfunctions-tasks/test/codebuild/integ.start-build.expected.json @@ -140,6 +140,7 @@ } ], "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts index e40673730b1cf..601479fbed0e4 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/activity.ts @@ -1,6 +1,6 @@ import * as cloudwatch from '@aws-cdk/aws-cloudwatch'; import * as iam from '@aws-cdk/aws-iam'; -import { IResource, Lazy, Resource, Stack } from '@aws-cdk/core'; +import { IResource, Lazy, Names, Resource, Stack } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnActivity } from './stepfunctions.generated'; @@ -186,7 +186,7 @@ export class Activity extends Resource implements IActivity { } private generateName(): string { - const name = this.node.uniqueId; + const name = Names.uniqueId(this); if (name.length > 80) { return name.substring(0, 40) + name.substring(name.length - 40); } diff --git a/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts b/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts index c72d1ced22d4f..b172ed94de411 100644 --- a/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts +++ b/packages/@aws-cdk/aws-stepfunctions/lib/json-path.ts @@ -81,8 +81,13 @@ interface FieldHandlers { handleBoolean(key: string, x: boolean): {[key: string]: boolean}; } -export function recurseObject(obj: object | undefined, handlers: FieldHandlers): object | undefined { +export function recurseObject(obj: object | undefined, handlers: FieldHandlers, visited: object[] = []): object | undefined { if (obj === undefined) { return undefined; } + if (visited.includes(obj)) { + return {}; + } else { + visited.push(obj); + } const ret: any = {}; for (const [key, value] of Object.entries(obj)) { @@ -91,13 +96,13 @@ export function recurseObject(obj: object | undefined, handlers: FieldHandlers): } else if (typeof value === 'number') { Object.assign(ret, handlers.handleNumber(key, value)); } else if (Array.isArray(value)) { - Object.assign(ret, recurseArray(key, value, handlers)); + Object.assign(ret, recurseArray(key, value, handlers, visited)); } else if (typeof value === 'boolean') { Object.assign(ret, handlers.handleBoolean(key, value)); } else if (value === null || value === undefined) { // Nothing } else if (typeof value === 'object') { - ret[key] = recurseObject(value, handlers); + ret[key] = recurseObject(value, handlers, visited); } } @@ -107,7 +112,7 @@ export function recurseObject(obj: object | undefined, handlers: FieldHandlers): /** * Render an array that may or may not contain a string list token */ -function recurseArray(key: string, arr: any[], handlers: FieldHandlers): {[key: string]: any[] | string} { +function recurseArray(key: string, arr: any[], handlers: FieldHandlers, visited: object[] = []): {[key: string]: any[] | string} { if (isStringArray(arr)) { const path = jsonPathStringList(arr); if (path !== undefined) { @@ -126,7 +131,7 @@ function recurseArray(key: string, arr: any[], handlers: FieldHandlers): {[key: throw new Error('Cannot use JsonPath fields in an array, they must be used in objects'); } if (typeof value === 'object' && value !== null) { - return recurseObject(value, handlers); + return recurseObject(value, handlers, visited); } return value; }), diff --git a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts index 1235dc9e5d526..a919912bf7c30 100644 --- a/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts +++ b/packages/@aws-cdk/aws-stepfunctions/test/fields.test.ts @@ -130,4 +130,26 @@ describe('Fields', () => { field: `contains ${JsonPath.stringAt('$.hello')}`, })).toThrowError(/Field references must be the entire string/); }); + test('infinitely recursive object graphs do not break referenced path finding', () => { + const deepObject = { + field: JsonPath.stringAt('$.stringField'), + deepField: JsonPath.numberAt('$.numField'), + recursiveField: undefined as any, + }; + const paths = { + bool: false, + literal: 'literal', + field: JsonPath.stringAt('$.stringField'), + listField: JsonPath.listAt('$.listField'), + recursiveField: undefined as any, + deep: [ + 'literal', + deepObject, + ], + }; + paths.recursiveField = paths; + deepObject.recursiveField = paths; + expect(FieldUtils.findReferencedPaths(paths)) + .toStrictEqual(['$.listField', '$.numField', '$.stringField']); + }); }); diff --git a/packages/@aws-cdk/aws-synthetics/lib/canary.ts b/packages/@aws-cdk/aws-synthetics/lib/canary.ts index 7d46e3f6a896f..f2bbb1407d76a 100644 --- a/packages/@aws-cdk/aws-synthetics/lib/canary.ts +++ b/packages/@aws-cdk/aws-synthetics/lib/canary.ts @@ -388,7 +388,7 @@ export class Canary extends cdk.Resource { * Creates a unique name for the canary. The generated name is the physical ID of the canary. */ private generateUniqueName(): string { - const name = this.node.uniqueId.toLowerCase().replace(' ', '-'); + const name = cdk.Names.uniqueId(this).toLowerCase().replace(' ', '-'); if (name.length <= 21) { return name; } else { diff --git a/packages/@aws-cdk/cdk-assets-schema/package.json b/packages/@aws-cdk/cdk-assets-schema/package.json index ddf0543214beb..c8b0b1988e21a 100644 --- a/packages/@aws-cdk/cdk-assets-schema/package.json +++ b/packages/@aws-cdk/cdk-assets-schema/package.json @@ -53,7 +53,7 @@ "devDependencies": { "@types/jest": "^26.0.15", "cdk-build-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "repository": { diff --git a/packages/@aws-cdk/cfnspec/spec-source/660_Route53_HealthCheck_patch.json b/packages/@aws-cdk/cfnspec/spec-source/660_Route53_HealthCheck_patch.json new file mode 100644 index 0000000000000..b03232b4e72a5 --- /dev/null +++ b/packages/@aws-cdk/cfnspec/spec-source/660_Route53_HealthCheck_patch.json @@ -0,0 +1,157 @@ +{ + "PropertyTypes": { + "patch": { + "description": "Patch Route 53 HealthCheck casing regression - mirrors cfn-lint (https://github.com/aws-cloudformation/cfn-python-lint/blob/master/src/cfnlint/data/ExtendedSpecs/all/01_spec_patch.json) ", + "operations": [ + { + "op": "add", + "path": "/AWS::Route53::HealthCheck.AlarmIdentifier", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-alarmidentifier.html", + "Properties": { + "Name": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-alarmidentifier.html#cfn-route53-healthcheck-alarmidentifier-name", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + }, + "Region": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-alarmidentifier.html#cfn-route53-healthcheck-alarmidentifier-region", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Mutable" + } + } + } + }, + { + "op": "add", + "path": "/AWS::Route53::HealthCheck.HealthCheckConfig", + "value": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html", + "Properties": { + "AlarmIdentifier": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-alarmidentifier", + "Required": false, + "Type": "AlarmIdentifier", + "UpdateType": "Mutable" + }, + "ChildHealthChecks": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-childhealthchecks", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "EnableSNI": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-enablesni", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "FailureThreshold": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-failurethreshold", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "FullyQualifiedDomainName": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-fullyqualifieddomainname", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "HealthThreshold": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-healththreshold", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "IPAddress": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-ipaddress", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "InsufficientDataHealthStatus": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-insufficientdatahealthstatus", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Inverted": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-inverted", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Mutable" + }, + "MeasureLatency": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-measurelatency", + "PrimitiveType": "Boolean", + "Required": false, + "UpdateType": "Immutable" + }, + "Port": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-port", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Mutable" + }, + "Regions": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-regions", + "DuplicatesAllowed": false, + "PrimitiveItemType": "String", + "Required": false, + "Type": "List", + "UpdateType": "Mutable" + }, + "RequestInterval": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-requestinterval", + "PrimitiveType": "Integer", + "Required": false, + "UpdateType": "Immutable" + }, + "ResourcePath": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-resourcepath", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "SearchString": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-searchstring", + "PrimitiveType": "String", + "Required": false, + "UpdateType": "Mutable" + }, + "Type": { + "Documentation": "http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-properties-route53-healthcheck-healthcheckconfig.html#cfn-route53-healthcheck-healthcheckconfig-type", + "PrimitiveType": "String", + "Required": true, + "UpdateType": "Immutable" + } + } + } + } + ] + } + }, + "ResourceTypes": { + "AWS::Route53::HealthCheck": { + "patch": { + "description": "Patch Route 53 HealthCheck casing regression - mirrors cfn-lint (https://github.com/aws-cloudformation/cfn-python-lint/blob/master/src/cfnlint/data/ExtendedSpecs/all/01_spec_patch.json) ", + "operations": [ + { + "op": "add", + "path": "/Properties/HealthCheckConfig/Type", + "value": "HealthCheckConfig" + }, + { + "op": "remove", + "path": "/Properties/HealthCheckConfig/PrimitiveType" + } + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts index f2d11b76eac39..56c609a08dd39 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts +++ b/packages/@aws-cdk/cloud-assembly-schema/lib/cloud-assembly/context-queries.ts @@ -1,3 +1,4 @@ +import { Tag } from './metadata-schema'; /** * Identifier for the context provider @@ -33,6 +34,20 @@ export enum ContextProvider { */ ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER = 'endpoint-service-availability-zones', + /** + * Load balancer provider + */ + LOAD_BALANCER_PROVIDER = 'load-balancer', + + /** + * Load balancer listener provider + */ + LOAD_BALANCER_LISTENER_PROVIDER = 'load-balancer-listener', + + /** + * Security group provider + */ + SECURITY_GROUP_PROVIDER = 'security-group', } /** @@ -196,9 +211,152 @@ export interface EndpointServiceAvailabilityZonesContextQuery { readonly serviceName: string; } +/** + * Type of load balancer + */ +export enum LoadBalancerType { + /** + * Network load balancer + */ + NETWORK = 'network', + + /** + * Application load balancer + */ + APPLICATION = 'application', +} + +/** + * Filters for selecting load balancers + */ +export interface LoadBalancerFilter { + /** + * Filter load balancers by their type + */ + readonly loadBalancerType: LoadBalancerType; + + /** + * Find by load balancer's ARN + * @default - does not search by load balancer arn + */ + readonly loadBalancerArn?: string; + + /** + * Match load balancer tags + * @default - does not match load balancers by tags + */ + readonly loadBalancerTags?: Tag[]; +} + +/** + * Query input for looking up a load balancer + */ +export interface LoadBalancerContextQuery extends LoadBalancerFilter { + /** + * Query account + */ + readonly account: string; + + /** + * Query region + */ + readonly region: string; +} + +/** + * The protocol for connections from clients to the load balancer + */ +export enum LoadBalancerListenerProtocol { + /** + * HTTP protocol + */ + HTTP = 'HTTP', + + /** + * HTTPS protocol + */ + HTTPS = 'HTTPS', + + /** + * TCP protocol + */ + TCP = 'TCP', + + /** + * TLS protocol + */ + TLS = 'TLS', + + /** + * UDP protocol + * */ + UDP = 'UDP', + + /** + * TCP and UDP protocol + * */ + TCP_UDP = 'TCP_UDP', +} + +/** + * Query input for looking up a load balancer listener + */ +export interface LoadBalancerListenerContextQuery extends LoadBalancerFilter { + /** + * Query account + */ + readonly account: string; + + /** + * Query region + */ + readonly region: string; + + /** + * Find by listener's arn + * @default - does not find by listener arn + */ + readonly listenerArn?: string; + + /** + * Filter by listener protocol + * @default - does not filter by listener protocol + */ + readonly listenerProtocol?: LoadBalancerListenerProtocol; + + /** + * Filter listeners by listener port + * @default - does not filter by a listener port + */ + readonly listenerPort?: number; +} + +/** + * Query input for looking up a security group + */ +export interface SecurityGroupContextQuery { + /** + * Query account + */ + readonly account: string; + + /** + * Query region + */ + readonly region: string; + + /** + * Security group id + */ + readonly securityGroupId: string; +} + export type ContextQueryProperties = AmiContextQuery | AvailabilityZonesContextQuery | HostedZoneContextQuery | SSMParameterContextQuery | VpcContextQuery -| EndpointServiceAvailabilityZonesContextQuery; +| EndpointServiceAvailabilityZonesContextQuery +| LoadBalancerContextQuery +| LoadBalancerListenerContextQuery +| SecurityGroupContextQuery; diff --git a/packages/@aws-cdk/cloud-assembly-schema/package.json b/packages/@aws-cdk/cloud-assembly-schema/package.json index 41bc7dc316754..c9258888b3c6d 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/package.json +++ b/packages/@aws-cdk/cloud-assembly-schema/package.json @@ -55,7 +55,7 @@ "@types/jest": "^26.0.15", "@types/mock-fs": "^4.13.0", "cdk-build-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "mock-fs": "^4.13.0", "pkglint": "0.0.0", "typescript-json-schema": "^0.43.0" diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json index a154b78a0b508..99fbaedb6c416 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.schema.json @@ -247,11 +247,11 @@ "type": "object", "properties": { "key": { - "description": "Tag key.", + "description": "Tag key.\n\n(In the actual file on disk this will be cased as \"Key\", and the structure is\npatched to match this structure upon loading:\nhttps://github.com/aws/aws-cdk/blob/4aadaa779b48f35838cccd4e25107b2338f05547/packages/%40aws-cdk/cloud-assembly-schema/lib/manifest.ts#L137)", "type": "string" }, "value": { - "description": "Tag value.", + "description": "Tag value.\n\n(In the actual file on disk this will be cased as \"Value\", and the structure is\npatched to match this structure upon loading:\nhttps://github.com/aws/aws-cdk/blob/4aadaa779b48f35838cccd4e25107b2338f05547/packages/%40aws-cdk/cloud-assembly-schema/lib/manifest.ts#L137)", "type": "string" } }, @@ -391,6 +391,15 @@ }, { "$ref": "#/definitions/EndpointServiceAvailabilityZonesContextQuery" + }, + { + "$ref": "#/definitions/LoadBalancerContextQuery" + }, + { + "$ref": "#/definitions/LoadBalancerListenerContextQuery" + }, + { + "$ref": "#/definitions/SecurityGroupContextQuery" } ] } @@ -408,6 +417,9 @@ "availability-zones", "endpoint-service-availability-zones", "hosted-zone", + "load-balancer", + "load-balancer-listener", + "security-group", "ssm", "vpc-provider" ], @@ -580,6 +592,125 @@ "serviceName" ] }, + "LoadBalancerContextQuery": { + "description": "Query input for looking up a load balancer", + "type": "object", + "properties": { + "account": { + "description": "Query account", + "type": "string" + }, + "region": { + "description": "Query region", + "type": "string" + }, + "loadBalancerType": { + "$ref": "#/definitions/LoadBalancerType", + "description": "Filter load balancers by their type" + }, + "loadBalancerArn": { + "description": "Find by load balancer's ARN (Default - does not search by load balancer arn)", + "type": "string" + }, + "loadBalancerTags": { + "description": "Match load balancer tags (Default - does not match load balancers by tags)", + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + } + } + }, + "required": [ + "account", + "loadBalancerType", + "region" + ] + }, + "LoadBalancerType": { + "description": "Type of load balancer", + "enum": [ + "application", + "network" + ], + "type": "string" + }, + "LoadBalancerListenerContextQuery": { + "description": "Query input for looking up a load balancer listener", + "type": "object", + "properties": { + "account": { + "description": "Query account", + "type": "string" + }, + "region": { + "description": "Query region", + "type": "string" + }, + "listenerArn": { + "description": "Find by listener's arn (Default - does not find by listener arn)", + "type": "string" + }, + "listenerProtocol": { + "description": "Filter by listener protocol (Default - does not filter by listener protocol)", + "enum": [ + "HTTP", + "HTTPS", + "TCP", + "TCP_UDP", + "TLS", + "UDP" + ], + "type": "string" + }, + "listenerPort": { + "description": "Filter listeners by listener port (Default - does not filter by a listener port)", + "type": "number" + }, + "loadBalancerType": { + "$ref": "#/definitions/LoadBalancerType", + "description": "Filter load balancers by their type" + }, + "loadBalancerArn": { + "description": "Find by load balancer's ARN (Default - does not search by load balancer arn)", + "type": "string" + }, + "loadBalancerTags": { + "description": "Match load balancer tags (Default - does not match load balancers by tags)", + "type": "array", + "items": { + "$ref": "#/definitions/Tag" + } + } + }, + "required": [ + "account", + "loadBalancerType", + "region" + ] + }, + "SecurityGroupContextQuery": { + "description": "Query input for looking up a security group", + "type": "object", + "properties": { + "account": { + "description": "Query account", + "type": "string" + }, + "region": { + "description": "Query region", + "type": "string" + }, + "securityGroupId": { + "description": "Security group id", + "type": "string" + } + }, + "required": [ + "account", + "region", + "securityGroupId" + ] + }, "RuntimeInfo": { "description": "Information about the application's runtime components.", "type": "object", diff --git a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json index 2211f30276a5e..bdc5a9f306dec 100644 --- a/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json +++ b/packages/@aws-cdk/cloud-assembly-schema/schema/cloud-assembly.version.json @@ -1 +1 @@ -{"version":"6.0.0"} +{"version":"7.0.0"} \ No newline at end of file diff --git a/packages/@aws-cdk/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index c2e716a807586..2fb02810b2182 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -23,7 +23,7 @@ "dependencies": { "@aws-cdk/cfnspec": "0.0.0", "colors": "^1.4.0", - "diff": "^4.0.2", + "diff": "^5.0.0", "fast-deep-equal": "^3.1.3", "string-width": "^4.2.0", "table": "^6.0.3" @@ -33,10 +33,10 @@ "@types/string-width": "^4.0.1", "@types/table": "^5.0.0", "cdk-build-tools": "0.0.0", - "fast-check": "^2.6.0", - "jest": "^26.6.1", + "fast-check": "^2.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.4.3" + "ts-jest": "^26.4.4" }, "repository": { "url": "https://github.com/aws/aws-cdk.git", diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 92fb2d7adf0a6..5504214ff2f62 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -330,9 +330,9 @@ "@types/jest": "^26.0.15", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0", - "ts-jest": "^26.4.3" + "ts-jest": "^26.4.4" }, "keywords": [ "aws", diff --git a/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts index bf729dcedd38b..0e9dcadc5ce60 100644 --- a/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/nested-stacks.test.ts @@ -444,6 +444,9 @@ describe('CDK Include for nested stacks', () => { let child: inc.IncludedNestedStack; let grandChild: inc.IncludedNestedStack; + let hash1: string; + let hash2: string; + let parentBucketParam: string; let parentKeyParam: string; let grandChildBucketParam: string; @@ -471,13 +474,16 @@ describe('CDK Include for nested stacks', () => { child = parentTemplate.getNestedStack('ChildStack'); grandChild = child.includedTemplate.getNestedStack('GrandChildStack'); - parentBucketParam = 'AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0C'; - parentKeyParam = 'AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2'; - grandChildBucketParam = 'referencetoAssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3BucketEAA24F0CRef'; - grandChildKeyParam = 'referencetoAssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50S3VersionKey1194CAB2Ref'; + hash1 = '5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50'; + hash2 = '7775730164edb5faae717ac1d2e90d9c0d0fdbeafe48763e5c1b7fb5e39e00a5'; + + parentBucketParam = `AssetParameters${hash1}S3BucketEAA24F0C`; + parentKeyParam = `AssetParameters${hash1}S3VersionKey1194CAB2`; + grandChildBucketParam = `referencetoAssetParameters${hash1}S3BucketEAA24F0CRef`; + grandChildKeyParam = `referencetoAssetParameters${hash1}S3VersionKey1194CAB2Ref`; - childBucketParam = 'AssetParameters891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5S3Bucket23278F13'; - childKeyParam = 'AssetParameters891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5S3VersionKey7316205A'; + childBucketParam = `AssetParameters${hash2}S3BucketDEB194C6`; + childKeyParam = `AssetParameters${hash2}S3VersionKey8B342ED1`; }); test('correctly creates parameters in the parent stack, and passes them to the child stack', () => { @@ -485,27 +491,27 @@ describe('CDK Include for nested stacks', () => { "Parameters": { [parentBucketParam]: { "Type": "String", - "Description": "S3 bucket for asset \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"", + "Description": `S3 bucket for asset \"${hash1}\"`, }, [parentKeyParam]: { "Type": "String", - "Description": "S3 key for asset version \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"", + "Description": `S3 key for asset version \"${hash1}\"`, }, - "AssetParameters5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50ArtifactHash9C417847": { + [`AssetParameters${hash1}ArtifactHash9C417847`]: { "Type": "String", - "Description": "Artifact hash for asset \"5dc7d4a99cfe2979687dc74f2db9fd75f253b5505a1912b5ceecf70c9aefba50\"", + "Description": `Artifact hash for asset \"${hash1}\"`, }, [childBucketParam]: { "Type": "String", - "Description": "S3 bucket for asset \"891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5\"", + "Description": `S3 bucket for asset \"${hash2}\"`, }, [childKeyParam]: { "Type": "String", - "Description": "S3 key for asset version \"891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5\"", + "Description": `S3 key for asset version \"${hash2}\"`, }, - "AssetParameters891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5ArtifactHashA1DE5198": { + [`AssetParameters${hash2}ArtifactHashAA82D4CC`]: { "Type": "String", - "Description": "Artifact hash for asset \"891fd3ec75dc881b0fe40dc9fd1b433672637585c015265a5f0dab6bf79818d5\"", + "Description": `Artifact hash for asset \"${hash2}\"`, }, }, "Resources": { diff --git a/packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-for-boolean-property.json b/packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-for-boolean-property.json new file mode 100644 index 0000000000000..cdc5181de12e9 --- /dev/null +++ b/packages/@aws-cdk/cloudformation-include/test/test-templates/find-in-map-for-boolean-property.json @@ -0,0 +1,25 @@ +{ + "Mappings": { + "SomeMapping": { + "region": { + "key1": true + } + } + }, + "Resources": { + "Bucket": { + "Type": "AWS::S3::Bucket", + "Properties": { + "PublicAccessBlockConfiguration": { + "BlockPublicAcls": { + "Fn::FindInMap": [ + "SomeMapping", + { "Ref": "AWS::Region" }, + "key1" + ] + } + } + } + } + } +} diff --git a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts index fce331ca452eb..3c2d4b81f3fe4 100644 --- a/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts +++ b/packages/@aws-cdk/cloudformation-include/test/valid-templates.test.ts @@ -777,6 +777,14 @@ describe('CDK Include', () => { }); }); + test('can ingest a template that uses Fn::FindInMap for the value of a boolean property', () => { + includeTestTemplate(stack, 'find-in-map-for-boolean-property.json'); + + expect(stack).toMatchTemplate( + loadTestFileToJsObject('find-in-map-for-boolean-property.json'), + ); + }); + test('can ingest a template that contains Rules, and allows retrieving those Rules', () => { const cfnTemplate = includeTestTemplate(stack, 'only-parameters-and-rule.json'); const rule = cfnTemplate.getRule('TestVpcRule'); diff --git a/packages/@aws-cdk/core/lib/cfn-fn.ts b/packages/@aws-cdk/core/lib/cfn-fn.ts index 8add880b01674..27cadbc99cc51 100644 --- a/packages/@aws-cdk/core/lib/cfn-fn.ts +++ b/packages/@aws-cdk/core/lib/cfn-fn.ts @@ -22,11 +22,6 @@ export class Fn { return new FnRef(logicalName).toString(); } - /** @internal */ - public static _ref(logicalId: string): IResolvable { - return new FnRef(logicalId); - } - /** * The ``Fn::GetAtt`` intrinsic function returns the value of an attribute * from a resource in the template. @@ -178,7 +173,17 @@ export class Fn { * @returns a token represented as a string */ public static findInMap(mapName: string, topLevelKey: string, secondLevelKey: string): string { - return new FnFindInMap(mapName, topLevelKey, secondLevelKey).toString(); + return Fn._findInMap(mapName, topLevelKey, secondLevelKey).toString(); + } + + /** + * An additional function used in CfnParser, + * as Fn::FindInMap does not always return a string. + * + * @internal + */ + public static _findInMap(mapName: string, topLevelKey: string, secondLevelKey: string): IResolvable { + return new FnFindInMap(mapName, topLevelKey, secondLevelKey); } /** diff --git a/packages/@aws-cdk/core/lib/cfn-parse.ts b/packages/@aws-cdk/core/lib/cfn-parse.ts index 3013c7f757296..663bdd6437a98 100644 --- a/packages/@aws-cdk/core/lib/cfn-parse.ts +++ b/packages/@aws-cdk/core/lib/cfn-parse.ts @@ -505,7 +505,7 @@ export class CfnParser { if (!mapping) { throw new Error(`Mapping used in FindInMap expression with name '${value[0]}' was not found in the template`); } - return Fn.findInMap(mapping.logicalId, value[1], value[2]); + return Fn._findInMap(mapping.logicalId, value[1], value[2]); } case 'Fn::Select': { const value = this.parseValue(object[key]); diff --git a/packages/@aws-cdk/core/lib/index.ts b/packages/@aws-cdk/core/lib/index.ts index d63f847fe2687..4aa0cda188201 100644 --- a/packages/@aws-cdk/core/lib/index.ts +++ b/packages/@aws-cdk/core/lib/index.ts @@ -66,3 +66,4 @@ export * from './feature-flags'; // WARNING: Should not be exported, but currently is because of a bug. See the // class description for more information. export * from './private/intrinsic'; +export * from './names'; \ No newline at end of file diff --git a/packages/@aws-cdk/core/lib/names.ts b/packages/@aws-cdk/core/lib/names.ts new file mode 100644 index 0000000000000..03998fcebe902 --- /dev/null +++ b/packages/@aws-cdk/core/lib/names.ts @@ -0,0 +1,40 @@ +import { Construct, Node } from 'constructs'; +import { ConstructNode } from './construct-compat'; +import { makeUniqueId } from './private/uniqueid'; + +/** + * Functions for devising unique names for constructs. For example, those can be + * used to allocate unique physical names for resources. + */ +export class Names { + /** + * Returns a CloudFormation-compatible unique identifier for a construct based + * on its path. The identifier includes a human readable porition rendered + * from the path components and a hash suffix. + * + * @param construct The construct + * @returns a unique id based on the construct path + */ + public static uniqueId(construct: Construct): string { + const node = Node.of(construct); + const components = node.scopes.slice(1).map(c => Node.of(c).id); + return components.length > 0 ? makeUniqueId(components) : ''; + } + + /** + * Returns a CloudFormation-compatible unique identifier for a construct based + * on its path. The identifier includes a human readable porition rendered + * from the path components and a hash suffix. + * + * TODO (v2): replace with API to use `constructs.Node`. + * + * @param node The construct node + * @returns a unique id based on the construct path + */ + public static nodeUniqueId(node: ConstructNode): string { + const components = node.scopes.slice(1).map(c => c.node.id); + return components.length > 0 ? makeUniqueId(components) : ''; + } + + private constructor() {} +} diff --git a/packages/@aws-cdk/core/lib/nested-stack.ts b/packages/@aws-cdk/core/lib/nested-stack.ts index 395d3bd63c8c5..cca4b827b57cf 100644 --- a/packages/@aws-cdk/core/lib/nested-stack.ts +++ b/packages/@aws-cdk/core/lib/nested-stack.ts @@ -7,6 +7,7 @@ import { CfnResource } from './cfn-resource'; import { CfnStack } from './cloudformation.generated'; import { Duration } from './duration'; import { Lazy } from './lazy'; +import { Names } from './names'; import { IResolveContext } from './resolvable'; import { Stack } from './stack'; import { NestedStackSynthesizer } from './stack-synthesizers'; @@ -114,7 +115,7 @@ export class NestedStack extends Stack { Object.defineProperty(this, NESTED_STACK_SYMBOL, { value: true }); // this is the file name of the synthesized template file within the cloud assembly - this.templateFile = `${this.node.uniqueId}.nested.template.json`; + this.templateFile = `${Names.uniqueId(this)}.nested.template.json`; this.parameters = props.parameters || {}; diff --git a/packages/@aws-cdk/core/lib/private/physical-name-generator.ts b/packages/@aws-cdk/core/lib/private/physical-name-generator.ts index 7c9fae2ab15da..1671e0c5bf2ef 100644 --- a/packages/@aws-cdk/core/lib/private/physical-name-generator.ts +++ b/packages/@aws-cdk/core/lib/private/physical-name-generator.ts @@ -1,5 +1,6 @@ import * as crypto from 'crypto'; import { Node } from 'constructs'; +import { Names } from '../names'; import { IResolvable, IResolveContext } from '../resolvable'; import { IResource } from '../resource'; import { Stack } from '../stack'; @@ -9,7 +10,7 @@ import { TokenMap } from './token-map'; export function generatePhysicalName(resource: IResource): string { const stack = Stack.of(resource); const stackPart = new PrefixNamePart(stack.stackName, 25); - const idPart = new SuffixNamePart(Node.of(resource).uniqueId, 24); + const idPart = new SuffixNamePart(Names.nodeUniqueId(resource.node), 24); const region: string = stack.region; if (Token.isUnresolved(region) || !region) { diff --git a/packages/@aws-cdk/core/lib/private/prepare-app.ts b/packages/@aws-cdk/core/lib/private/prepare-app.ts index ad900acf803c0..e90a40ff211f8 100644 --- a/packages/@aws-cdk/core/lib/private/prepare-app.ts +++ b/packages/@aws-cdk/core/lib/private/prepare-app.ts @@ -28,19 +28,32 @@ export function prepareApp(root: IConstruct) { } } + resolveReferences(root); + // depth-first (children first) queue of nested stacks. We will pop a stack // from the head of this queue to prepare its template asset. + // + // Depth-first since the a nested stack's template hash will be reflected in + // its parent's template, which then changes the parent's hash, etc. const queue = findAllNestedStacks(root); - while (true) { - resolveReferences(root); - - const nested = queue.shift(); - if (!nested) { - break; + if (queue.length > 0) { + while (queue.length > 0) { + const nested = queue.shift()!; + defineNestedStackAsset(nested); } - defineNestedStackAsset(nested); + // ▷[ Given the legacy synthesizer and a 3-or-deeper nesting of nested stacks ] + // + // Adding nested stack assets may haved added CfnParameters to the top-level + // stack which are referenced in a deeper-level stack. The values of these + // parameters need to be carried through to the right location via Nested + // Stack parameters, which `resolveReferences()` will do. + // + // Yes, this may add `Parameter` elements to a template whose hash has + // already been calculated, but the invariant that if the functional part + // of the template changes its hash will change is still upheld. + resolveReferences(root); } } diff --git a/packages/@aws-cdk/core/lib/private/refs.ts b/packages/@aws-cdk/core/lib/private/refs.ts index 0fdc5e1bed40f..05bb7930bc34f 100644 --- a/packages/@aws-cdk/core/lib/private/refs.ts +++ b/packages/@aws-cdk/core/lib/private/refs.ts @@ -8,6 +8,7 @@ import { CfnOutput } from '../cfn-output'; import { CfnParameter } from '../cfn-parameter'; import { Construct, IConstruct } from '../construct-compat'; import { FeatureFlags } from '../feature-flags'; +import { Names } from '../names'; import { Reference } from '../reference'; import { IResolvable } from '../resolvable'; import { Stack } from '../stack'; @@ -226,7 +227,7 @@ function generateExportName(stackExports: Construct, id: string) { */ function createNestedStackParameter(nested: Stack, reference: CfnReference, value: IResolvable) { // we call "this.resolve" to ensure that tokens do not creep in (for example, if the reference display name includes tokens) - const paramId = nested.resolve(`reference-to-${reference.target.node.uniqueId}.${reference.displayName}`); + const paramId = nested.resolve(`reference-to-${ Names.nodeUniqueId(reference.target.node)}.${reference.displayName}`); let param = nested.node.tryFindChild(paramId) as CfnParameter; if (!param) { param = new CfnParameter(nested, paramId, { type: 'String' }); @@ -247,7 +248,7 @@ function createNestedStackParameter(nested: Stack, reference: CfnReference, valu * intrinsic that can be used to reference this output in the parent stack. */ function createNestedStackOutput(producer: Stack, reference: Reference): CfnReference { - const outputId = `${reference.target.node.uniqueId}${reference.displayName}`; + const outputId = `${Names.nodeUniqueId(reference.target.node)}${reference.displayName}`; let output = producer.node.tryFindChild(outputId) as CfnOutput; if (!output) { output = new CfnOutput(producer, outputId, { value: Token.asString(reference) }); diff --git a/packages/@aws-cdk/core/lib/private/runtime-info.ts b/packages/@aws-cdk/core/lib/private/runtime-info.ts index 0cc74d3c9f8f3..34a4129abe491 100644 --- a/packages/@aws-cdk/core/lib/private/runtime-info.ts +++ b/packages/@aws-cdk/core/lib/private/runtime-info.ts @@ -3,9 +3,9 @@ import * as cxschema from '@aws-cdk/cloud-assembly-schema'; import { major as nodeMajorVersion } from './node-version'; // list of NPM scopes included in version reporting e.g. @aws-cdk and @aws-solutions-konstruk -const WHITELIST_SCOPES = ['@aws-cdk', '@aws-solutions-konstruk', '@aws-solutions-constructs', '@amzn']; +const WHITELIST_SCOPES = ['@aws-cdk', '@aws-cdk-containers', '@aws-solutions-konstruk', '@aws-solutions-constructs', '@amzn']; // list of NPM packages included in version reporting -const WHITELIST_PACKAGES = ['aws-rfdk']; +const WHITELIST_PACKAGES = ['aws-rfdk', 'aws-cdk-lib']; /** * Returns a list of loaded modules and their versions. diff --git a/packages/@aws-cdk/core/lib/stack.ts b/packages/@aws-cdk/core/lib/stack.ts index 88f407ef01839..3c254ea803e88 100644 --- a/packages/@aws-cdk/core/lib/stack.ts +++ b/packages/@aws-cdk/core/lib/stack.ts @@ -718,9 +718,9 @@ export class Stack extends CoreConstruct implements ITaggable { throw new Error(`'${target.node.path}' depends on '${this.node.path}' (${cycle.join(', ')}). Adding this dependency (${reason}) would create a cyclic reference.`); } - let dep = this._stackDependencies[target.node.uniqueId]; + let dep = this._stackDependencies[Names.uniqueId(target)]; if (!dep) { - dep = this._stackDependencies[target.node.uniqueId] = { + dep = this._stackDependencies[Names.uniqueId(target)] = { stack: target, reasons: [], }; @@ -1125,6 +1125,7 @@ import { Stage } from './stage'; import { ITaggable, TagManager } from './tag-manager'; import { Token } from './token'; import { FileSystem } from './fs'; +import { Names } from './names'; interface StackDependency { stack: Stack; diff --git a/packages/@aws-cdk/core/lib/stage.ts b/packages/@aws-cdk/core/lib/stage.ts index 072a4b9cc34c3..8dfa18834604c 100644 --- a/packages/@aws-cdk/core/lib/stage.ts +++ b/packages/@aws-cdk/core/lib/stage.ts @@ -7,6 +7,8 @@ import { synthesize } from './private/synthesis'; // eslint-disable-next-line import { Construct as CoreConstruct } from './construct-compat'; +const STAGE_SYMBOL = Symbol.for('@aws-cdk/core.Stage'); + /** * Initialization props for a stage. */ @@ -85,7 +87,7 @@ export class Stage extends CoreConstruct { * @experimental */ public static isStage(x: any ): x is Stage { - return x !== null && x instanceof Stage; + return x !== null && typeof(x) === 'object' && STAGE_SYMBOL in x; } /** @@ -137,6 +139,8 @@ export class Stage extends CoreConstruct { throw new Error(`invalid stage name "${id}". Stage name must start with a letter and contain only alphanumeric characters, hypens ('-'), underscores ('_') and periods ('.')`); } + Object.defineProperty(this, STAGE_SYMBOL, { value: true }); + this.parentStage = Stage.of(this); this.region = props.env?.region ?? this.parentStage?.region; diff --git a/packages/@aws-cdk/core/package.json b/packages/@aws-cdk/core/package.json index d181a3f8ce75a..af60767b0c135 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -168,13 +168,13 @@ }, "license": "Apache-2.0", "devDependencies": { - "@types/lodash": "^4.14.163", + "@types/lodash": "^4.14.165", "@types/minimatch": "^3.0.3", "@types/node": "^10.17.44", "@types/sinon": "^9.0.8", "cdk-build-tools": "0.0.0", "cfn2ts": "0.0.0", - "fast-check": "^2.6.0", + "fast-check": "^2.6.1", "lodash": "^4.17.20", "nodeunit-shim": "0.0.0", "pkglint": "0.0.0", diff --git a/packages/@aws-cdk/core/test/app.test.ts b/packages/@aws-cdk/core/test/app.test.ts index 27b567b0f71be..69486987f0085 100644 --- a/packages/@aws-cdk/core/test/app.test.ts +++ b/packages/@aws-cdk/core/test/app.test.ts @@ -261,22 +261,23 @@ nodeunitShim({ }, 'runtime library versions'(test: Test) { - MetadataResource.clearModulesCache(); - - const response = withApp({ analyticsReporting: true }, app => { - const stack = new Stack(app, 'stack1'); - new CfnResource(stack, 'MyResource', { type: 'Resource::Type' }); - }); + v1(() => { + MetadataResource.clearModulesCache(); - const stackTemplate = response.getStackByName('stack1').template; - const libs = parseModules(stackTemplate.Resources?.CDKMetadata?.Properties?.Modules); + const response = withApp({ analyticsReporting: true }, app => { + const stack = new Stack(app, 'stack1'); + new CfnResource(stack, 'MyResource', { type: 'Resource::Type' }); + }); - // eslint-disable-next-line @typescript-eslint/no-require-imports - const version = require('../package.json').version; - test.deepEqual(libs['@aws-cdk/core'], version); - test.deepEqual(libs['@aws-cdk/cx-api'], version); - test.deepEqual(libs['jsii-runtime'], `node.js/${process.version}`); + const stackTemplate = response.getStackByName('stack1').template; + const libs = parseModules(stackTemplate.Resources?.CDKMetadata?.Properties?.Modules); + // eslint-disable-next-line @typescript-eslint/no-require-imports + const version = require('../package.json').version; + test.deepEqual(libs['@aws-cdk/core'], version); + test.deepEqual(libs['@aws-cdk/cx-api'], version); + test.deepEqual(libs['jsii-runtime'], `node.js/${process.version}`); + }); test.done(); }, @@ -320,25 +321,26 @@ nodeunitShim({ }, 'version reporting includes only @aws-cdk, aws-cdk and jsii libraries'(test: Test) { - MetadataResource.clearModulesCache(); + v1(() => { + MetadataResource.clearModulesCache(); - const response = withApp({ analyticsReporting: true }, app => { - const stack = new Stack(app, 'stack1'); - new CfnResource(stack, 'MyResource', { type: 'Resource::Type' }); - }); - - const stackTemplate = response.getStackByName('stack1').template; - const libs = parseModules(stackTemplate.Resources?.CDKMetadata?.Properties?.Modules); - const libNames = Object.keys(libs).sort(); - - test.deepEqual(libNames, [ - '@aws-cdk/cloud-assembly-schema', - '@aws-cdk/core', - '@aws-cdk/cx-api', - '@aws-cdk/region-info', - 'jsii-runtime', - ]); + const response = withApp({ analyticsReporting: true }, app => { + const stack = new Stack(app, 'stack1'); + new CfnResource(stack, 'MyResource', { type: 'Resource::Type' }); + }); + const stackTemplate = response.getStackByName('stack1').template; + const libs = parseModules(stackTemplate.Resources?.CDKMetadata?.Properties?.Modules); + const libNames = Object.keys(libs).sort(); + + test.deepEqual(libNames, [ + '@aws-cdk/cloud-assembly-schema', + '@aws-cdk/core', + '@aws-cdk/cx-api', + '@aws-cdk/region-info', + 'jsii-runtime', + ]); + }); test.done(); }, @@ -445,3 +447,15 @@ function withCliVersion(block: () => A): A { delete process.env[cxapi.CLI_VERSION_ENV]; } } + +function v1(block: () => void) { + onVersion(1, block); +} + +function onVersion(version: number, block: () => void) { + // eslint-disable-next-line @typescript-eslint/no-require-imports + const mv: number = require('../../../../release.json').majorVersion; + if (version === mv) { + block(); + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/core/test/stage.test.ts b/packages/@aws-cdk/core/test/stage.test.ts index c878c1485d6ce..8a4b27a4d412a 100644 --- a/packages/@aws-cdk/core/test/stage.test.ts +++ b/packages/@aws-cdk/core/test/stage.test.ts @@ -280,6 +280,35 @@ nodeunitShim({ test.throws(() => new Stage(app, 'mystage', { outdir: '/tmp/foo/bar' }), /"outdir" cannot be specified for nested stages/); test.done(); }, + + 'Stage.isStage indicates that a construct is a stage'(test: Test) { + // WHEN + const app = new App(); + const stack = new Stack(); + const stage = new Stage(app, 'Stage'); + + // THEN + test.ok(Stage.isStage(stage)); + test.ok(Stage.isStage(app)); + test.ok(!Stage.isStage(stack)); + test.done(); + }, + + 'Stage.isStage indicates that a construct is a stage based on symbol'(test: Test) { + // WHEN + const app = new App(); + const stage = new Stage(app, 'Stage'); + + const externalStage = {}; + const STAGE_SYMBOL = Symbol.for('@aws-cdk/core.Stage'); + Object.defineProperty(externalStage, STAGE_SYMBOL, { value: true }); + + // THEN + test.ok(Stage.isStage(stage)); + test.ok(Stage.isStage(app)); + test.ok(Stage.isStage(externalStage)); + test.done(); + }, }); class TouchingAspect implements IAspect { diff --git a/packages/@aws-cdk/custom-resources/package.json b/packages/@aws-cdk/custom-resources/package.json index 8472edf850213..6b9ebd0219673 100644 --- a/packages/@aws-cdk/custom-resources/package.json +++ b/packages/@aws-cdk/custom-resources/package.json @@ -79,7 +79,7 @@ "@types/aws-lambda": "^8.10.64", "@types/fs-extra": "^8.1.1", "@types/sinon": "^9.0.8", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", diff --git a/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts b/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts new file mode 100644 index 0000000000000..bea7c432de57f --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/context/load-balancer.ts @@ -0,0 +1,69 @@ +/** + * Load balancer ip address type. + */ +export enum LoadBalancerIpAddressType { + /** + * IPV4 ip address + */ + IPV4 = 'ipv4', + + /** + * Dual stack address + */ + DUAL_STACK = 'dualstack', +} + +/** + * Properties of a discovered load balancer + */ +export interface LoadBalancerContextResponse { + /** + * The ARN of the load balancer. + */ + readonly loadBalancerArn: string; + + /** + * The hosted zone ID of the load balancer's name. + */ + readonly loadBalancerCanonicalHostedZoneId: string; + + /** + * Load balancer's DNS name + */ + readonly loadBalancerDnsName: string; + + /** + * Type of IP address + */ + readonly ipAddressType: LoadBalancerIpAddressType; + + /** + * Load balancer's security groups + */ + readonly securityGroupIds: string[]; + + /** + * Load balancer's VPC + */ + readonly vpcId: string; +} + +/** + * Properties of a discovered load balancer listener. + */ +export interface LoadBalancerListenerContextResponse { + /** + * The ARN of the listener. + */ + readonly listenerArn: string; + + /** + * The port the listener is listening on. + */ + readonly listenerPort: number; + + /** + * The security groups of the load balancer. + */ + readonly securityGroupIds: string[]; +} diff --git a/packages/@aws-cdk/cx-api/lib/context/security-group.ts b/packages/@aws-cdk/cx-api/lib/context/security-group.ts new file mode 100644 index 0000000000000..f1ea8c2a7ca37 --- /dev/null +++ b/packages/@aws-cdk/cx-api/lib/context/security-group.ts @@ -0,0 +1,17 @@ + +/** + * Properties of a discovered SecurityGroup. + */ +export interface SecurityGroupContextResponse { + /** + * The security group's id. + */ + readonly securityGroupId: string; + + /** + * Whether the security group allows all outbound traffic. This will be true + * when the security group has all-protocol egress permissions to access both + * `0.0.0.0/0` and `::/0`. + */ + readonly allowAllOutbound: boolean; +} diff --git a/packages/@aws-cdk/cx-api/lib/index.ts b/packages/@aws-cdk/cx-api/lib/index.ts index a6ac4977a6d17..8bab2151735c0 100644 --- a/packages/@aws-cdk/cx-api/lib/index.ts +++ b/packages/@aws-cdk/cx-api/lib/index.ts @@ -1,8 +1,10 @@ export * from './cxapi'; export * from './context/vpc'; export * from './context/ami'; +export * from './context/load-balancer'; export * from './context/availability-zones'; export * from './context/endpoint-service-availability-zones'; +export * from './context/security-group'; export * from './cloud-artifact'; export * from './artifacts/asset-manifest-artifact'; export * from './artifacts/cloudformation-artifact'; diff --git a/packages/@aws-cdk/cx-api/package.json b/packages/@aws-cdk/cx-api/package.json index a1fa098f13738..ea8aa01a47345 100644 --- a/packages/@aws-cdk/cx-api/package.json +++ b/packages/@aws-cdk/cx-api/package.json @@ -62,7 +62,7 @@ "@types/mock-fs": "^4.13.0", "@types/semver": "^7.3.4", "cdk-build-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "mock-fs": "^4.13.0", "pkglint": "0.0.0" }, diff --git a/packages/@aws-cdk/example-construct-library/package.json b/packages/@aws-cdk/example-construct-library/package.json index 98b5f8f67b509..5874fd037829d 100644 --- a/packages/@aws-cdk/example-construct-library/package.json +++ b/packages/@aws-cdk/example-construct-library/package.json @@ -68,7 +68,7 @@ "@aws-cdk/assert": "0.0.0", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "dependencies": { diff --git a/packages/@aws-cdk/example-construct-library/test/example-resource.test.ts b/packages/@aws-cdk/example-construct-library/test/example-resource.test.ts index 4d73c8be33973..e9f56d52769a6 100644 --- a/packages/@aws-cdk/example-construct-library/test/example-resource.test.ts +++ b/packages/@aws-cdk/example-construct-library/test/example-resource.test.ts @@ -1,7 +1,7 @@ /* * We write unit tests using the Jest framework * (some modules might still use NodeUnit, - * but it's considered legacy, and we want to migrate to Jest). + * but it's considered Names, and we want to migrate to Jest). */ // import the various CDK assertion helpers diff --git a/packages/@aws-cdk/pipelines/README.md b/packages/@aws-cdk/pipelines/README.md index b575fcd00732d..238fa51dbc710 100644 --- a/packages/@aws-cdk/pipelines/README.md +++ b/packages/@aws-cdk/pipelines/README.md @@ -362,7 +362,8 @@ stage.addActions(new ShellScriptAction({ vpc, // Optionally specify SecurityGroups securityGroups, - // ... more configuration ... + // Optionally specify a BuildEnvironment + environment, })); ``` diff --git a/packages/@aws-cdk/pipelines/lib/pipeline.ts b/packages/@aws-cdk/pipelines/lib/pipeline.ts index cf9445681d200..6e39f92b7582d 100644 --- a/packages/@aws-cdk/pipelines/lib/pipeline.ts +++ b/packages/@aws-cdk/pipelines/lib/pipeline.ts @@ -2,7 +2,7 @@ import * as path from 'path'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; -import { Annotations, App, CfnOutput, PhysicalName, Stack, Stage, Aspects } from '@aws-cdk/core'; +import { Annotations, App, CfnOutput, PhysicalName, Stack, Stage } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { AssetType, DeployCdkStackAction, PublishAssetsAction, UpdatePipelineAction } from './actions'; import { appOf, assemblyBuilderOf } from './private/construct-internals'; @@ -212,8 +212,6 @@ export class CdkPipeline extends CoreConstruct { vpc: props.vpc, subnetSelection: props.subnetSelection, }); - - Aspects.of(this).add({ visit: () => this._assets.removeAssetsStageIfEmpty() }); } /** @@ -325,10 +323,10 @@ export class CdkPipeline extends CoreConstruct { if (depAction === undefined) { Annotations.of(this).addWarning(`Stack '${stackAction.stackName}' depends on stack ` + - `'${depId}', but that dependency is not deployed through the pipeline!`); + `'${depId}', but that dependency is not deployed through the pipeline!`); } else if (!(depAction.executeRunOrder < stackAction.prepareRunOrder)) { yield `Stack '${stackAction.stackName}' depends on stack ` + - `'${depAction.stackName}', but is deployed before it in the pipeline!`; + `'${depAction.stackName}', but is deployed before it in the pipeline!`; } } } @@ -366,23 +364,29 @@ interface AssetPublishingProps { * Add appropriate publishing actions to the asset publishing stage */ class AssetPublishing extends CoreConstruct { + // CodePipelines has a hard limit of 50 actions per stage. See https://github.com/aws/aws-cdk/issues/9353 + private readonly MAX_PUBLISHERS_PER_STAGE = 50; + private readonly publishers: Record = {}; private readonly assetRoles: Record = {}; private readonly myCxAsmRoot: string; - private readonly stage: codepipeline.IStage; + private readonly lastStageBeforePublishing?: codepipeline.IStage; + private readonly stages: codepipeline.IStage[] = []; private readonly pipeline: codepipeline.Pipeline; - private _fileAssetCtr = 1; - private _dockerAssetCtr = 1; + + private _fileAssetCtr = 0; + private _dockerAssetCtr = 0; constructor(scope: Construct, id: string, private readonly props: AssetPublishingProps) { super(scope, id); this.myCxAsmRoot = path.resolve(assemblyBuilderOf(appOf(this)).outdir); - // We MUST add the Stage immediately here, otherwise it will be in the wrong place - // in the pipeline! - this.stage = this.props.pipeline.addStage({ stageName: 'Assets' }); this.pipeline = this.props.pipeline; + // Hacks to get access to the innards of Pipeline + const stages: codepipeline.IStage[] = (this.props.pipeline as any)._stages; + // Any asset publishing stages will be added directly after the last stage that currently exists. + this.lastStageBeforePublishing = stages.slice(-1)[0]; } /** @@ -410,13 +414,23 @@ class AssetPublishing extends CoreConstruct { let action = this.publishers[command.assetId]; if (!action) { + // Dynamically create new stages as needed, with `MAX_PUBLISHERS_PER_STAGE` assets per stage. + const stageIndex = Math.floor((this._fileAssetCtr + this._dockerAssetCtr) / this.MAX_PUBLISHERS_PER_STAGE); + if (stageIndex >= this.stages.length) { + const previousStage = this.stages.slice(-1)[0] ?? this.lastStageBeforePublishing; + this.stages.push(this.pipeline.addStage({ + stageName: `Assets${stageIndex > 0 ? stageIndex + 1 : ''}`, + placement: { justAfter: previousStage }, + })); + } + // The asset ID would be a logical candidate for the construct path and project names, but if the asset // changes it leads to recreation of a number of Role/Policy/Project resources which is slower than // necessary. Number sequentially instead. // // FIXME: The ultimate best solution is probably to generate a single Project per asset type // and reuse that for all assets. - const id = command.assetType === AssetType.FILE ? `FileAsset${this._fileAssetCtr++}` : `DockerAsset${this._dockerAssetCtr++}`; + const id = command.assetType === AssetType.FILE ? `FileAsset${++this._fileAssetCtr}` : `DockerAsset${++this._dockerAssetCtr}`; // NOTE: It's important that asset changes don't force a pipeline self-mutation. // This can cause an infinite loop of updates (see https://github.com/aws/aws-cdk/issues/9080). @@ -430,28 +444,12 @@ class AssetPublishing extends CoreConstruct { vpc: this.props.vpc, subnetSelection: this.props.subnetSelection, }); - this.stage.addAction(action); + this.stages[stageIndex].addAction(action); } action.addPublishCommand(relativePath, command.assetSelector); } - /** - * Remove the Assets stage if it turns out we didn't add any Assets to publish - */ - public removeAssetsStageIfEmpty() { - if (Object.keys(this.publishers).length === 0) { - // Hacks to get access to innards of Pipeline - // Modify 'stages' array in-place to remove Assets stage if empty - const stages: codepipeline.IStage[] = (this.props.pipeline as any)._stages; - - const ix = stages.indexOf(this.stage); - if (ix > -1) { - stages.splice(ix, 1); - } - } - } - /** * This role is used by both the CodePipeline build action and related CodeBuild project. Consolidating these two * roles into one, and re-using across all assets, saves significant size of the final synthesized output. diff --git a/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts b/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts index 4fb610688e9a4..1b439fe309c7e 100644 --- a/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts +++ b/packages/@aws-cdk/pipelines/lib/validation/shell-script-action.ts @@ -54,6 +54,13 @@ export interface ShellScriptActionProps { */ readonly additionalArtifacts?: codepipeline.Artifact[]; + /** + * The CodeBuild environment where scripts are executed. + * + * @default LinuxBuildImage.STANDARD_4_0 + */ + readonly environment?: codebuild.BuildEnvironment + /** * RunOrder for this action * @@ -177,7 +184,7 @@ export class ShellScriptAction implements codepipeline.IAction, iam.IGrantable { } this._project = new codebuild.PipelineProject(scope, 'Project', { - environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_4_0 }, + environment: this.props.environment || { buildImage: codebuild.LinuxBuildImage.STANDARD_4_0 }, vpc: this.props.vpc, securityGroups: this.props.securityGroups, subnetSelection: this.props.subnetSelection, diff --git a/packages/@aws-cdk/pipelines/package.json b/packages/@aws-cdk/pipelines/package.json index db990d5b0c366..845da0c6be1f5 100644 --- a/packages/@aws-cdk/pipelines/package.json +++ b/packages/@aws-cdk/pipelines/package.json @@ -30,11 +30,9 @@ }, "devDependencies": { "@aws-cdk/assert": "0.0.0", - "@types/nodeunit": "^0.0.31", "cdk-build-tools": "0.0.0", "cdk-integ-tools": "0.0.0", "cfn2ts": "0.0.0", - "nodeunit": "^0.11.3", "pkglint": "0.0.0", "@aws-cdk/aws-s3": "0.0.0", "@aws-cdk/aws-ecr": "0.0.0", diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json index 80159b7a0e368..365e0fa9d06ee 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline-with-assets.expected.json @@ -962,6 +962,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:4.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, @@ -1267,6 +1268,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:4.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, @@ -1473,6 +1475,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:4.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, @@ -1647,6 +1650,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:4.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, @@ -1677,6 +1681,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:4.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json b/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json index 2531bf13bc642..9e0541be1e17d 100644 --- a/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json +++ b/packages/@aws-cdk/pipelines/test/integ.pipeline.expected.json @@ -861,6 +861,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:4.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, @@ -1166,6 +1167,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:4.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, @@ -1372,6 +1374,7 @@ "Environment": { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:4.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER" }, diff --git a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts index 2b4facf654fc6..c10906e7ad7bb 100644 --- a/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts +++ b/packages/@aws-cdk/pipelines/test/pipeline-assets.test.ts @@ -1,12 +1,13 @@ import * as path from 'path'; import { arrayWith, deepObjectLike, encodedJson, notMatching, objectLike, stringLike } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; +import * as cp from '@aws-cdk/aws-codepipeline'; import * as ecr_assets from '@aws-cdk/aws-ecr-assets'; import * as s3_assets from '@aws-cdk/aws-s3-assets'; import { Stack, Stage, StageProps } from '@aws-cdk/core'; import { Construct } from 'constructs'; import * as cdkp from '../lib'; -import { BucketStack, PIPELINE_ENV, TestApp, TestGitHubNpmPipeline } from './testutil'; +import { BucketStack, PIPELINE_ENV, TestApp, TestGitHubAction, TestGitHubNpmPipeline } from './testutil'; const FILE_ASSET_SOURCE_HASH = '8289faf53c7da377bb2b90615999171adef5e1d8f6b88810e5fef75e6ca09ba5'; @@ -36,6 +37,110 @@ test('no assets stage if the application has no assets', () => { }); }); +describe('asset stage placement', () => { + test('assets stage comes before any user-defined stages', () => { + // WHEN + pipeline.addApplicationStage(new FileAssetApp(app, 'App')); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'Source' }), + objectLike({ Name: 'Build' }), + objectLike({ Name: 'UpdatePipeline' }), + objectLike({ Name: 'Assets' }), + objectLike({ Name: 'App' }), + ], + }); + }); + + test('assets stage inserted after existing pipeline actions', () => { + // WHEN + const sourceArtifact = new cp.Artifact(); + const cloudAssemblyArtifact = new cp.Artifact(); + const existingCodePipeline = new cp.Pipeline(pipelineStack, 'CodePipeline', { + stages: [ + { + stageName: 'CustomSource', + actions: [new TestGitHubAction(sourceArtifact)], + }, + { + stageName: 'CustomBuild', + actions: [cdkp.SimpleSynthAction.standardNpmSynth({ sourceArtifact, cloudAssemblyArtifact })], + }, + ], + }); + pipeline = new cdkp.CdkPipeline(pipelineStack, 'CdkEmptyPipeline', { + cloudAssemblyArtifact: cloudAssemblyArtifact, + selfMutating: false, + codePipeline: existingCodePipeline, + // No source/build actions + }); + pipeline.addApplicationStage(new FileAssetApp(app, 'App')); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'CustomSource' }), + objectLike({ Name: 'CustomBuild' }), + objectLike({ Name: 'Assets' }), + objectLike({ Name: 'App' }), + ], + }); + }); + + test('up to 50 assets fit in a single stage', () => { + // WHEN + pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 50 })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'Source' }), + objectLike({ Name: 'Build' }), + objectLike({ Name: 'UpdatePipeline' }), + objectLike({ Name: 'Assets' }), + objectLike({ Name: 'App' }), + ], + }); + }); + + test('51 assets triggers a second stage', () => { + // WHEN + pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 51 })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'Source' }), + objectLike({ Name: 'Build' }), + objectLike({ Name: 'UpdatePipeline' }), + objectLike({ Name: 'Assets' }), + objectLike({ Name: 'Assets2' }), + objectLike({ Name: 'App' }), + ], + }); + }); + + test('101 assets triggers a third stage', () => { + // WHEN + pipeline.addApplicationStage(new MegaAssetsApp(app, 'App', { numAssets: 101 })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: [ + objectLike({ Name: 'Source' }), + objectLike({ Name: 'Build' }), + objectLike({ Name: 'UpdatePipeline' }), + objectLike({ Name: 'Assets' }), + objectLike({ Name: 'Assets2' }), + objectLike({ Name: 'Assets3' }), + objectLike({ Name: 'App' }), + ], + }); + }); +}); + test('command line properly locates assets in subassembly', () => { // WHEN pipeline.addApplicationStage(new FileAssetApp(app, 'FileAssetApp')); @@ -313,6 +418,32 @@ class DockerAssetApp extends Stage { } } +interface MegaAssetsAppProps extends StageProps { + readonly numAssets: number; +} + +// Creates a mix of file and image assets, up to a specified count +class MegaAssetsApp extends Stage { + constructor(scope: Construct, id: string, props: MegaAssetsAppProps) { + super(scope, id, props); + const stack = new Stack(this, 'Stack'); + + let assetCount = 0; + for (; assetCount < props.numAssets / 2; assetCount++) { + new s3_assets.Asset(stack, `Asset${assetCount}`, { + path: path.join(__dirname, 'test-file-asset.txt'), + assetHash: `FileAsset${assetCount}`, + }); + } + for (; assetCount < props.numAssets; assetCount++) { + new ecr_assets.DockerImageAsset(stack, `Asset${assetCount}`, { + directory: path.join(__dirname, 'test-docker-asset'), + extraHash: `FileAsset${assetCount}`, + }); + } + } +} + function expectedAssetRolePolicy(assumeRolePattern: string, attachedRole: string) { return { PolicyDocument: { diff --git a/packages/@aws-cdk/pipelines/test/validation.test.ts b/packages/@aws-cdk/pipelines/test/validation.test.ts index 9986aad4d2758..4f1cffbef61ec 100644 --- a/packages/@aws-cdk/pipelines/test/validation.test.ts +++ b/packages/@aws-cdk/pipelines/test/validation.test.ts @@ -1,5 +1,6 @@ import { anything, arrayWith, deepObjectLike, encodedJson } from '@aws-cdk/assert'; import '@aws-cdk/assert/jest'; +import * as codebuild from '@aws-cdk/aws-codebuild'; import * as codepipeline from '@aws-cdk/aws-codepipeline'; import * as ec2 from '@aws-cdk/aws-ec2'; import * as iam from '@aws-cdk/aws-iam'; @@ -330,6 +331,64 @@ test('run ShellScriptAction with Security Group', () => { }); }); +test('run ShellScriptAction with specified codebuild image', () => { + // WHEN + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'imageAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + environment: { buildImage: codebuild.LinuxBuildImage.STANDARD_2_0 }, + })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodePipeline::Pipeline', { + Stages: arrayWith({ + Name: 'Test', + Actions: [ + deepObjectLike({ + Name: 'imageAction', + }), + ], + }), + }); + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:2.0', + }, + }); +}); + +test('run ShellScriptAction with specified BuildEnvironment', () => { + // WHEN + pipeline.addStage('Test').addActions(new cdkp.ShellScriptAction({ + actionName: 'imageAction', + additionalArtifacts: [integTestArtifact], + commands: ['true'], + environment: { + buildImage: codebuild.LinuxBuildImage.STANDARD_2_0, + computeType: codebuild.ComputeType.LARGE, + environmentVariables: { FOO: { value: 'BAR', type: codebuild.BuildEnvironmentVariableType.PLAINTEXT } }, + privileged: true, + }, + })); + + // THEN + expect(pipelineStack).toHaveResourceLike('AWS::CodeBuild::Project', { + Environment: { + Image: 'aws/codebuild/standard:2.0', + PrivilegedMode: true, + ComputeType: 'BUILD_GENERAL1_LARGE', + EnvironmentVariables: [ + { + Type: 'PLAINTEXT', + Value: 'BAR', + Name: 'FOO', + }, + ], + }, + }); +}); + class AppWithStackOutput extends Stage { public readonly output: CfnOutput; diff --git a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts index 6680629073cf2..528cd8463f05e 100644 --- a/packages/@aws-cdk/region-info/build-tools/fact-tables.ts +++ b/packages/@aws-cdk/region-info/build-tools/fact-tables.ts @@ -146,7 +146,7 @@ export const APPMESH_ECR_ACCOUNTS: { [region: string]: string } = { 'ca-central-1': '840364872350', 'eu-central-1': '840364872350', 'eu-north-1': '840364872350', - 'eu-south-1': '840364872350', + 'eu-south-1': '422531588944', 'eu-west-1': '840364872350', 'eu-west-2': '840364872350', 'eu-west-3': '840364872350', diff --git a/packages/@aws-cdk/yaml-cfn/package.json b/packages/@aws-cdk/yaml-cfn/package.json index f7a89fbf1aa84..b3b63cbf69e38 100644 --- a/packages/@aws-cdk/yaml-cfn/package.json +++ b/packages/@aws-cdk/yaml-cfn/package.json @@ -71,7 +71,7 @@ "@types/jest": "^26.0.15", "@types/yaml": "^1.9.7", "cdk-build-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "bundledDependencies": [ diff --git a/packages/@monocdk-experiment/assert/package.json b/packages/@monocdk-experiment/assert/package.json index 00310c9815af3..d366f5c00f20b 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -38,17 +38,17 @@ "@types/node": "^10.17.44", "cdk-build-tools": "0.0.0", "constructs": "^3.2.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "monocdk": "0.0.0", "pkglint": "0.0.0", - "ts-jest": "^26.4.3" + "ts-jest": "^26.4.4" }, "dependencies": { "@aws-cdk/cloudformation-diff": "0.0.0" }, "peerDependencies": { "constructs": "^3.0.4", - "jest": "^26.6.1", + "jest": "^26.6.3", "monocdk": "^0.0.0" }, "repository": { diff --git a/packages/aws-cdk/CONTRIBUTING.md b/packages/aws-cdk/CONTRIBUTING.md index cbf6933dff920..f0839777aecc0 100644 --- a/packages/aws-cdk/CONTRIBUTING.md +++ b/packages/aws-cdk/CONTRIBUTING.md @@ -116,7 +116,7 @@ Note that these tests can only be executed using the `run-against-dist` wrapper. ##### Implementation -The implemention of the regression suites is not trivial to reason about and follow. Even though the code includes inline comments, we break down the exact details to better serve us in maintaining it and regaining context. +The implementation of the regression suites is not trivial to reason about and follow. Even though the code includes inline comments, we break down the exact details to better serve us in maintaining it and regaining context. Before diving into it, we establish a few key concepts: @@ -126,7 +126,7 @@ Before diving into it, we establish a few key concepts: - `FRAMEWORK_VERSION` - This is the version of the framework we are testing. It varries between the two variation of the regression suites. Its value can either be that of `CANDIDATE_VERSION` (for testing against the latest framework code), or `PREVIOUS_VERSION` (for testing against the previously published version of the framework code). -Following are the steps invovled in running these tests: +Following are the steps involved in running these tests: 1. Run [`./bump-candidate.sh`](../../bump-candidate.sh) to differentiate between the local version and the published version. For example, if the version in `lerna.json` is `1.67.0`, this script will result in a version `1.67.0-rc.0`. This is needed so that we can launch a verdaccio instance serving local tarballs without worrying about conflicts with the public npm uplink. This will help us avoid version quirks that might happen during the *post-release-pre-merge-back* time window. @@ -143,7 +143,7 @@ Following are the steps invovled in running these tests: - [Install the CLI](./test/integ/run-against-dist#L30) using the `CANDIDATE_VERSION` version `CANDIDATE_VERSION` env variable. - Execute the given script. -6. Both cli regression test scripts run the same [`run_regression_against_framework_version`](./test/integ/test-cli-regression.bash#L22) function. This function accepts which framework version should the regression run against, it can be either `CANDIDATE_VERSION` or `PREVIOUS_VERSION`. Note that the argument is not the actual value of the versio, but instead is just an [indirection indentifier](./test/integ/test-cli-regression.bash#L81). The function will: +6. Both cli regression test scripts run the same [`run_regression_against_framework_version`](./test/integ/test-cli-regression.bash#L22) function. This function accepts which framework version should the regression run against, it can be either `CANDIDATE_VERSION` or `PREVIOUS_VERSION`. Note that the argument is not the actual value of the version, but instead is just an [indirection indentifier](./test/integ/test-cli-regression.bash#L81). The function will: - Calculate the actual value of the previous version based on the candidate version. (fetches from github) - Download the previous version tarball from npm and extract the integration tests. diff --git a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts index 12e2cfd375268..d92321542cdd0 100644 --- a/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts +++ b/packages/aws-cdk/lib/api/aws-auth/awscli-compatible.ts @@ -40,7 +40,10 @@ export class AwsCliCompatible { if (options.profile) { await forceSdkToReadConfigIfPresent(); const theProfile = options.profile; - return new AWS.CredentialProviderChain([() => profileCredentials(theProfile)]); + return new AWS.CredentialProviderChain([ + () => profileCredentials(theProfile), + () => new AWS.ProcessCredentials({ profile: theProfile }), + ]); } const implicitProfile = process.env.AWS_PROFILE || process.env.AWS_DEFAULT_PROFILE || 'default'; @@ -55,6 +58,7 @@ export class AwsCliCompatible { // environment variable. await forceSdkToReadConfigIfPresent(); sources.push(() => profileCredentials(implicitProfile)); + sources.push(() => new AWS.ProcessCredentials({ profile: implicitProfile })); } if (options.containerCreds ?? hasEcsCredentials()) { diff --git a/packages/aws-cdk/lib/api/aws-auth/sdk.ts b/packages/aws-cdk/lib/api/aws-auth/sdk.ts index 2a20b9560b0fd..5825d0f627d39 100644 --- a/packages/aws-cdk/lib/api/aws-auth/sdk.ts +++ b/packages/aws-cdk/lib/api/aws-auth/sdk.ts @@ -29,6 +29,7 @@ export interface ISDK { s3(): AWS.S3; route53(): AWS.Route53; ecr(): AWS.ECR; + elbv2(): AWS.ELBv2; } /** @@ -92,6 +93,10 @@ export class SDK implements ISDK { return wrapServiceErrorHandling(new AWS.ECR(this.config)); } + public elbv2(): AWS.ELBv2 { + return wrapServiceErrorHandling(new AWS.ELBv2(this.config)); + } + public async currentAccount(): Promise { return cached(this, CURRENT_ACCOUNT_KEY, () => SDK.accountCache.fetch(this.credentials.accessKeyId, async () => { // if we don't have one, resolve from STS and store in cache. diff --git a/packages/aws-cdk/lib/api/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 86d1dc4828e1d..b13582ce31623 100644 --- a/packages/aws-cdk/lib/api/deploy-stack.ts +++ b/packages/aws-cdk/lib/api/deploy-stack.ts @@ -10,7 +10,7 @@ import { publishAssets } from '../util/asset-publishing'; import { contentHash } from '../util/content-hash'; import { ISDK, SdkProvider } from './aws-auth'; import { ToolkitInfo } from './toolkit-info'; -import { changeSetHasNoChanges, CloudFormationStack, StackParameters, TemplateParameters, waitForChangeSet, waitForStackDeploy, waitForStackDelete } from './util/cloudformation'; +import { changeSetHasNoChanges, CloudFormationStack, TemplateParameters, waitForChangeSet, waitForStackDeploy, waitForStackDelete } from './util/cloudformation'; import { StackActivityMonitor, StackActivityProgress } from './util/cloudformation/stack-activity-monitor'; // We need to map regions to domain suffixes, and the SDK already has a function to do this. @@ -210,10 +210,10 @@ export async function deployStack(options: DeployStackOptions): Promise { + parameterChanges: boolean): Promise { const deployName = deployStackOptions.deployName || deployStackOptions.stack.stackName; debug(`${deployName}: checking if we can skip deploy`); @@ -435,7 +435,7 @@ async function canSkipDeploy( } // Parameters have changed - if (params.changed) { + if (parameterChanges) { debug(`${deployName}: parameters have changed`); return false; } diff --git a/packages/aws-cdk/lib/api/util/cloudformation.ts b/packages/aws-cdk/lib/api/util/cloudformation.ts index 63f4558d32bcd..19db988fdc15e 100644 --- a/packages/aws-cdk/lib/api/util/cloudformation.ts +++ b/packages/aws-cdk/lib/api/util/cloudformation.ts @@ -331,6 +331,9 @@ export async function stabilizeStack(cfn: CloudFormation, stackName: string) { }); } +/** + * The set of (formal) parameters that have been declared in a template + */ export class TemplateParameters { public static fromTemplate(template: Template) { return new TemplateParameters(template.Parameters || {}); @@ -345,8 +348,8 @@ export class TemplateParameters { * Will throw if parameters without a Default value or a Previous value are not * supplied. */ - public toStackParameters(updates: Record): StackParameters { - return new StackParameters(this.params, updates); + public supplyAll(updates: Record): ParameterValues { + return new ParameterValues(this.params, updates); } /** @@ -357,44 +360,50 @@ export class TemplateParameters { * throw if parameters without a Default value or a Previous value are not * supplied. */ - public diff(updates: Record, previousValues: Record): StackParameters { - return new StackParameters(this.params, updates, previousValues); + public updateExisting(updates: Record, previousValues: Record): ParameterValues { + return new ParameterValues(this.params, updates, previousValues); } } -export class StackParameters { - /** - * The CloudFormation parameters to pass to the CreateStack or UpdateStack API - */ +/** + * The set of parameters we're going to pass to a Stack + */ +export class ParameterValues { + public readonly values: Record = {}; public readonly apiParameters: CloudFormation.Parameter[] = []; - private _changes = false; - constructor( - private readonly params: Record, + private readonly formalParams: Record, updates: Record, previousValues: Record = {}) { const missingRequired = new Array(); - for (const [key, param] of Object.entries(this.params)) { - // If any of the parameters are SSM parameters, they will always lead to a change - if (param.Type.startsWith('AWS::SSM::Parameter::')) { - this._changes = true; - } - - if (key in updates && updates[key] !== undefined) { + for (const [key, formalParam] of Object.entries(this.formalParams)) { + // Check updates first, then use the previous value (if available), then use + // the default (if available). + // + // If we don't find a parameter value using any of these methods, then that's an error. + const updatedValue = updates[key]; + if (updatedValue !== undefined) { + this.values[key] = updatedValue; this.apiParameters.push({ ParameterKey: key, ParameterValue: updates[key] }); + continue; + } - // If the updated value is different than the current value, this will lead to a change - if (!(key in previousValues) || updates[key] !== previousValues[key]) { - this._changes = true; - } - } else if (key in previousValues) { + if (key in previousValues) { + this.values[key] = previousValues[key]; this.apiParameters.push({ ParameterKey: key, UsePreviousValue: true }); - } else if (param.Default === undefined) { - missingRequired.push(key); + continue; } + + if (formalParam.Default !== undefined) { + this.values[key] = formalParam.Default; + continue; + } + + // Oh no + missingRequired.push(key); } if (missingRequired.length > 0) { @@ -404,9 +413,10 @@ export class StackParameters { // Just append all supplied overrides that aren't really expected (this // will fail CFN but maybe people made typos that they want to be notified // of) - const unknownParam = ([key, _]: [string, any]) => this.params[key] === undefined; + const unknownParam = ([key, _]: [string, any]) => this.formalParams[key] === undefined; const hasValue = ([_, value]: [string, any]) => !!value; for (const [key, value] of Object.entries(updates).filter(unknownParam).filter(hasValue)) { + this.values[key] = value!; this.apiParameters.push({ ParameterKey: key, ParameterValue: value }); } } @@ -414,7 +424,24 @@ export class StackParameters { /** * Whether this set of parameter updates will change the actual stack values */ - public get changed() { - return this._changes; + public hasChanges(currentValues: Record): boolean { + // If any of the parameters are SSM parameters, deploying must always happen + // because we can't predict what the values will be. + if (Object.values(this.formalParams).some(p => p.Type.startsWith('AWS::SSM::Parameter::'))) { + return true; + } + + // Otherwise we're dirty if: + // - any of the existing values are removed, or changed + if (Object.entries(currentValues).some(([key, value]) => !(key in this.values) || value !== this.values[key])) { + return true; + } + + // - any of the values we're setting are new + if (Object.keys(this.values).some(key => !(key in currentValues))) { + return true; + } + + return false; } -} \ No newline at end of file +} diff --git a/packages/aws-cdk/lib/context-providers/index.ts b/packages/aws-cdk/lib/context-providers/index.ts index a4a67351d4daf..e60ad6066d280 100644 --- a/packages/aws-cdk/lib/context-providers/index.ts +++ b/packages/aws-cdk/lib/context-providers/index.ts @@ -7,7 +7,9 @@ import { AmiContextProviderPlugin } from './ami'; import { AZContextProviderPlugin } from './availability-zones'; import { EndpointServiceAZContextProviderPlugin } from './endpoint-service-availability-zones'; import { HostedZoneContextProviderPlugin } from './hosted-zones'; +import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin } from './load-balancers'; import { ContextProviderPlugin } from './provider'; +import { SecurityGroupContextProviderPlugin } from './security-groups'; import { SSMContextProviderPlugin } from './ssm-parameters'; import { VpcNetworkContextProviderPlugin } from './vpcs'; @@ -61,4 +63,7 @@ const availableContextProviders: ProviderMap = { [cxschema.ContextProvider.VPC_PROVIDER]: VpcNetworkContextProviderPlugin, [cxschema.ContextProvider.AMI_PROVIDER]: AmiContextProviderPlugin, [cxschema.ContextProvider.ENDPOINT_SERVICE_AVAILABILITY_ZONE_PROVIDER]: EndpointServiceAZContextProviderPlugin, + [cxschema.ContextProvider.SECURITY_GROUP_PROVIDER]: SecurityGroupContextProviderPlugin, + [cxschema.ContextProvider.LOAD_BALANCER_PROVIDER]: LoadBalancerContextProviderPlugin, + [cxschema.ContextProvider.LOAD_BALANCER_LISTENER_PROVIDER]: LoadBalancerListenerContextProviderPlugin, }; diff --git a/packages/aws-cdk/lib/context-providers/load-balancers.ts b/packages/aws-cdk/lib/context-providers/load-balancers.ts new file mode 100644 index 0000000000000..26f00c7746fe3 --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/load-balancers.ts @@ -0,0 +1,302 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as AWS from 'aws-sdk'; +import { Mode, SdkProvider } from '../api'; +import { ContextProviderPlugin } from './provider'; + +/** + * Provides load balancer context information. + */ +export class LoadBalancerContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SdkProvider) { + } + + async getValue(query: cxschema.LoadBalancerContextQuery): Promise { + const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(query.account, query.region), Mode.ForReading)).elbv2(); + + if (!query.loadBalancerArn && !query.loadBalancerTags) { + throw new Error('The load balancer lookup query must specify either `loadBalancerArn` or `loadBalancerTags`'); + } + + const loadBalancers = await findLoadBalancers(elbv2, query); + + if (loadBalancers.length === 0) { + throw new Error(`No load balancers found matching ${JSON.stringify(query)}`); + } + + if (loadBalancers.length > 1) { + throw new Error(`Multiple load balancers found matching ${JSON.stringify(query)} - please provide more specific criteria`); + } + + const loadBalancer = loadBalancers[0]; + + const ipAddressType = loadBalancer.IpAddressType === 'ipv4' + ? cxapi.LoadBalancerIpAddressType.IPV4 + : cxapi.LoadBalancerIpAddressType.DUAL_STACK; + + return { + loadBalancerArn: loadBalancer.LoadBalancerArn!, + loadBalancerCanonicalHostedZoneId: loadBalancer.CanonicalHostedZoneId!, + loadBalancerDnsName: loadBalancer.DNSName!, + vpcId: loadBalancer.VpcId!, + securityGroupIds: loadBalancer.SecurityGroups ?? [], + ipAddressType: ipAddressType, + }; + } +} + +// Decreases line length +type LoadBalancerListenerQuery = cxschema.LoadBalancerListenerContextQuery; +type LoadBalancerListenerResponse = cxapi.LoadBalancerListenerContextResponse; + +/** + * Provides load balancer listener context information + */ +export class LoadBalancerListenerContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SdkProvider) { + } + + async getValue(query: LoadBalancerListenerQuery): Promise { + const elbv2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(query.account, query.region), Mode.ForReading)).elbv2(); + + if (!query.listenerArn && !query.loadBalancerArn && !query.loadBalancerTags) { + throw new Error('The load balancer listener query must specify at least one of: `listenerArn`, `loadBalancerArn` or `loadBalancerTags`'); + } + + return query.listenerArn ? this.getListenerByArn(elbv2, query) : this.getListenerByFilteringLoadBalancers(elbv2, query); + } + + /** + * Look up a listener by querying listeners for query's listener arn and then + * resolve its load balancer for the security group information. + */ + private async getListenerByArn(elbv2: AWS.ELBv2, query: LoadBalancerListenerQuery) { + const listenerArn = query.listenerArn!; + const listenerResults = await elbv2.describeListeners({ ListenerArns: [listenerArn] }).promise(); + const listeners = (listenerResults.Listeners ?? []); + + if (listeners.length === 0) { + throw new Error(`No load balancer listeners found matching arn ${listenerArn}`); + } + + const listener = listeners[0]; + + const loadBalancers = await findLoadBalancers(elbv2, { + ...query, + loadBalancerArn: listener.LoadBalancerArn!, + }); + + if (loadBalancers.length === 0) { + throw new Error(`No associated load balancer found for listener arn ${listenerArn}`); + } + + const loadBalancer = loadBalancers[0]; + + return { + listenerArn: listener.ListenerArn!, + listenerPort: listener.Port!, + securityGroupIds: loadBalancer.SecurityGroups ?? [], + }; + } + + /** + * Look up a listener by starting from load balancers, filtering out + * unmatching load balancers, and then by querying the listeners of each load + * balancer and filtering out unmatching listeners. + */ + private async getListenerByFilteringLoadBalancers(elbv2: AWS.ELBv2, args: LoadBalancerListenerQuery) { + // Find matching load balancers + const loadBalancers = await findLoadBalancers(elbv2, args); + + if (loadBalancers.length === 0) { + throw new Error(`No associated load balancers found for load balancer listener query ${JSON.stringify(args)}`); + } + + return this.findMatchingListener(elbv2, loadBalancers, args); + } + + /** + * Finds the matching listener from the list of load balancers. This will + * error unless there is exactly one match so that the user is prompted to + * provide more specific criteria rather than us providing a nondeterministic + * result. + */ + private async findMatchingListener(elbv2: AWS.ELBv2, loadBalancers: AWS.ELBv2.LoadBalancers, query: LoadBalancerListenerQuery) { + const loadBalancersByArn = indexLoadBalancersByArn(loadBalancers); + const loadBalancerArns = Object.keys(loadBalancersByArn); + + const matches = Array(); + + for await (const listener of describeListenersByLoadBalancerArn(elbv2, loadBalancerArns)) { + const loadBalancer = loadBalancersByArn[listener.LoadBalancerArn!]; + if (listenerMatchesQueryFilter(listener, query) && loadBalancer) { + matches.push({ + listenerArn: listener.ListenerArn!, + listenerPort: listener.Port!, + securityGroupIds: loadBalancer.SecurityGroups ?? [], + }); + } + } + + if (matches.length === 0) { + throw new Error(`No load balancer listeners found matching ${JSON.stringify(query)}`); + } + + if (matches.length > 1) { + throw new Error(`Multiple load balancer listeners found matching ${JSON.stringify(query)} - please provide more specific criteria`); + } + + return matches[0]; + } +} + +/** + * Find load balancers by the given filter args. + */ +async function findLoadBalancers(elbv2: AWS.ELBv2, args: cxschema.LoadBalancerFilter) { + // List load balancers + let loadBalancers = await describeLoadBalancers(elbv2, { + LoadBalancerArns: args.loadBalancerArn ? [args.loadBalancerArn] : undefined, + }); + + // Filter by load balancer type + loadBalancers = loadBalancers.filter(lb => lb.Type === args.loadBalancerType); + + // Filter by load balancer tags + if (args.loadBalancerTags) { + loadBalancers = await filterLoadBalancersByTags(elbv2, loadBalancers, args.loadBalancerTags); + } + + return loadBalancers; +} + +/** + * Helper to paginate over describeLoadBalancers + * @internal + */ +export async function describeLoadBalancers(elbv2: AWS.ELBv2, request: AWS.ELBv2.DescribeLoadBalancersInput) { + const loadBalancers = Array(); + let page: AWS.ELBv2.DescribeLoadBalancersOutput | undefined; + do { + page = await elbv2.describeLoadBalancers({ + ...request, + Marker: page?.NextMarker, + }).promise(); + + loadBalancers.push(...Array.from(page.LoadBalancers ?? [])); + } while (page.NextMarker); + + return loadBalancers; +} + +/** + * Describes the tags of each load balancer and returns the load balancers that + * match the given tags. + */ +async function filterLoadBalancersByTags(elbv2: AWS.ELBv2, loadBalancers: AWS.ELBv2.LoadBalancers, loadBalancerTags: cxschema.Tag[]) { + const loadBalancersByArn = indexLoadBalancersByArn(loadBalancers); + const loadBalancerArns = Object.keys(loadBalancersByArn); + const matchingLoadBalancers = Array(); + + // Consume the items of async generator. + for await (const tags of describeTags(elbv2, loadBalancerArns)) { + if (tagsMatch(tags, loadBalancerTags) && loadBalancersByArn[tags.ResourceArn!]) { + matchingLoadBalancers.push(loadBalancersByArn[tags.ResourceArn!]); + } + } + + return matchingLoadBalancers; +} + +/** + * Generator function that yields `TagDescriptions`. The API doesn't support + * pagination, so this generator breaks the resource list into chunks and issues + * the appropriate requests, yielding each tag description as it receives it. + * @internal + */ +export async function* describeTags(elbv2: AWS.ELBv2, resourceArns: string[]) { + // Max of 20 resource arns per request. + const chunkSize = 20; + for (let i = 0; i < resourceArns.length; i += chunkSize) { + const chunk = resourceArns.slice(i, Math.min(i + chunkSize, resourceArns.length)); + const chunkTags = await elbv2.describeTags({ + ResourceArns: chunk, + }).promise(); + + for (const tag of chunkTags.TagDescriptions ?? []) { + yield tag; + } + } +} + +/** + * Determines if the given TagDescription matches the required tags. + * @internal + */ +export function tagsMatch(tagDescription: AWS.ELBv2.TagDescription, requiredTags: cxschema.Tag[]) { + const tagsByName: Record = {}; + for (const tag of tagDescription.Tags ?? []) { + tagsByName[tag.Key!] = tag.Value; + } + + for (const tag of requiredTags) { + if (tagsByName[tag.key] !== tag.value) { + return false; + } + } + + return true; +} + +/** + * Async generator that produces listener descriptions by traversing the + * pagination. Because describeListeners only lets you search by one load + * balancer arn at a time, we request them individually and yield the listeners + * as they come in. + * @internal + */ +export async function* describeListenersByLoadBalancerArn(elbv2: AWS.ELBv2, loadBalancerArns: string[]) { + for (const loadBalancerArn of loadBalancerArns) { + let page: AWS.ELBv2.DescribeListenersOutput | undefined; + do { + page = await elbv2.describeListeners({ + LoadBalancerArn: loadBalancerArn, + Marker: page?.NextMarker, + }).promise(); + + for (const listener of page.Listeners ?? []) { + yield listener; + } + } while (page.NextMarker); + } +} + +/** + * Determines if a listener matches the query filters. + */ +function listenerMatchesQueryFilter(listener: AWS.ELBv2.Listener, args: cxschema.LoadBalancerListenerContextQuery): boolean { + if (args.listenerPort && listener.Port !== args.listenerPort) { + // No match. + return false; + } + + if (args.listenerProtocol && listener.Protocol !== args.listenerProtocol) { + // No match. + return false; + } + + return true; +} + +/** + * Returns a record of load balancers indexed by their arns + */ +function indexLoadBalancersByArn(loadBalancers: AWS.ELBv2.LoadBalancer[]): Record { + const loadBalancersByArn: Record = {}; + + for (const loadBalancer of loadBalancers) { + loadBalancersByArn[loadBalancer.LoadBalancerArn!] = loadBalancer; + } + + return loadBalancersByArn; +} diff --git a/packages/aws-cdk/lib/context-providers/security-groups.ts b/packages/aws-cdk/lib/context-providers/security-groups.ts new file mode 100644 index 0000000000000..e8f464128b68d --- /dev/null +++ b/packages/aws-cdk/lib/context-providers/security-groups.ts @@ -0,0 +1,55 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as cxapi from '@aws-cdk/cx-api'; +import * as AWS from 'aws-sdk'; +import { Mode, SdkProvider } from '../api'; +import { ContextProviderPlugin } from './provider'; + +export class SecurityGroupContextProviderPlugin implements ContextProviderPlugin { + constructor(private readonly aws: SdkProvider) { + } + + async getValue(args: cxschema.SecurityGroupContextQuery): Promise { + const account: string = args.account!; + const region: string = args.region!; + + const ec2 = (await this.aws.forEnvironment(cxapi.EnvironmentUtils.make(account, region), Mode.ForReading)).ec2(); + + const response = await ec2.describeSecurityGroups({ + GroupIds: [args.securityGroupId], + }).promise(); + + const securityGroups = response.SecurityGroups ?? []; + if (securityGroups.length === 0) { + throw new Error(`No security groups found matching ${JSON.stringify(args)}`); + } + + const [securityGroup] = securityGroups; + + return { + securityGroupId: securityGroup.GroupId!, + allowAllOutbound: hasAllTrafficEgress(securityGroup), + }; + } +} + +/** + * @internal + */ +export function hasAllTrafficEgress(securityGroup: AWS.EC2.SecurityGroup) { + let hasAllTrafficCidrV4 = false; + let hasAllTrafficCidrV6 = false; + + for (const ipPermission of securityGroup.IpPermissionsEgress ?? []) { + const isAllProtocols = ipPermission.IpProtocol === '-1'; + + if (isAllProtocols && ipPermission.IpRanges?.some(m => m.CidrIp === '0.0.0.0/0')) { + hasAllTrafficCidrV4 = true; + } + + if (isAllProtocols && ipPermission.Ipv6Ranges?.some(m => m.CidrIpv6 === '::/0')) { + hasAllTrafficCidrV6 = true; + } + } + + return hasAllTrafficCidrV4 && hasAllTrafficCidrV6; +} diff --git a/packages/aws-cdk/lib/init-templates/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py b/packages/aws-cdk/lib/init-templates/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py index adf164cd876c7..9d86ad16906e6 100644 --- a/packages/aws-cdk/lib/init-templates/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py +++ b/packages/aws-cdk/lib/init-templates/app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py @@ -3,7 +3,7 @@ class %name.PascalCased%Stack(core.Stack): - def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: - super().__init__(scope, id, **kwargs) + def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) # The code that defines your stack goes here diff --git a/packages/aws-cdk/lib/init-templates/app/python/README.template.md b/packages/aws-cdk/lib/init-templates/app/python/README.template.md index 0f3fce3e943cd..ecb028bfa951e 100644 --- a/packages/aws-cdk/lib/init-templates/app/python/README.template.md +++ b/packages/aws-cdk/lib/init-templates/app/python/README.template.md @@ -6,7 +6,7 @@ This is a blank project for Python development with CDK. The `cdk.json` file tells the CDK Toolkit how to execute your app. This project is set up like a standard Python project. The initialization -process also creates a virtualenv within this project, stored under the .env +process also creates a virtualenv within this project, stored under the `.venv` directory. To create the virtualenv it assumes that there is a `python3` (or `python` for Windows) executable in your path with access to the `venv` package. If for any reason the automatic creation of the virtualenv fails, diff --git a/packages/aws-cdk/lib/init-templates/sample-app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py b/packages/aws-cdk/lib/init-templates/sample-app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py index 910a4b838c933..ace9c193df291 100644 --- a/packages/aws-cdk/lib/init-templates/sample-app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py +++ b/packages/aws-cdk/lib/init-templates/sample-app/python/%name.PythonModule%/%name.PythonModule%_stack.template.py @@ -9,8 +9,8 @@ class %name.PascalCased%Stack(core.Stack): - def __init__(self, scope: core.Construct, id: str, **kwargs) -> None: - super().__init__(scope, id, **kwargs) + def __init__(self, scope: core.Construct, construct_id: str, **kwargs) -> None: + super().__init__(scope, construct_id, **kwargs) queue = sqs.Queue( self, "%name.PascalCased%Queue", diff --git a/packages/aws-cdk/package.json b/packages/aws-cdk/package.json index cc67af22ad1de..553d64b28bf7a 100644 --- a/packages/aws-cdk/package.json +++ b/packages/aws-cdk/package.json @@ -55,13 +55,13 @@ "@types/yargs": "^15.0.9", "aws-sdk-mock": "^5.1.0", "cdk-build-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "mockery": "^2.1.0", "pkglint": "0.0.0", "sinon": "^9.2.1", - "ts-jest": "^26.4.3", + "ts-jest": "^26.4.4", "ts-mock-imports": "^1.3.0", - "@octokit/rest": "^18.0.6", + "@octokit/rest": "^18.0.9", "make-runnable": "^1.3.8" }, "dependencies": { @@ -71,7 +71,7 @@ "@aws-cdk/region-info": "0.0.0", "@aws-cdk/yaml-cfn": "0.0.0", "archiver": "^5.0.2", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "camelcase": "^6.2.0", "cdk-assets": "0.0.0", "colors": "^1.4.0", diff --git a/packages/aws-cdk/test/api/sdk-provider.test.ts b/packages/aws-cdk/test/api/sdk-provider.test.ts index 44b618954c94e..ad604f895e3a1 100644 --- a/packages/aws-cdk/test/api/sdk-provider.test.ts +++ b/packages/aws-cdk/test/api/sdk-provider.test.ts @@ -3,6 +3,7 @@ import * as cxapi from '@aws-cdk/cx-api'; import * as AWS from 'aws-sdk'; import * as SDKMock from 'aws-sdk-mock'; import type { ConfigurationOptions } from 'aws-sdk/lib/config-base'; +import * as promptly from 'promptly'; import * as uuid from 'uuid'; import { PluginHost } from '../../lib'; import { ISDK, Mode, SdkProvider } from '../../lib/api/aws-auth'; @@ -195,12 +196,16 @@ describe('with default config files', () => { // WHEN const provider = await SdkProvider.withAwsCliCompatibleDefaults({ ...defaultCredOptions, profile: 'mfa-role' }); + const promptlyMockCalls = (promptly.prompt as jest.Mock).mock.calls.length; + // THEN try { await provider.withAssumedRole('arn:aws:iam::account:role/role', undefined, undefined); + fail('Should error as no credentials could be loaded'); } catch (e) { - // Mock response was set to fail with message test to make sure we don't call STS - expect(e.message).toEqual('Error fetching MFA token: test'); + // Mock response was set to fail to make sure we don't call STS + // Make sure the MFA mock was called during this test + expect((promptly.prompt as jest.Mock).mock.calls.length).toBe(promptlyMockCalls + 1); } }); diff --git a/packages/aws-cdk/test/context-providers/load-balancers.test.ts b/packages/aws-cdk/test/context-providers/load-balancers.test.ts new file mode 100644 index 0000000000000..03ab1ac4cf989 --- /dev/null +++ b/packages/aws-cdk/test/context-providers/load-balancers.test.ts @@ -0,0 +1,881 @@ +import * as cxschema from '@aws-cdk/cloud-assembly-schema'; +import * as aws from 'aws-sdk'; +import * as AWS from 'aws-sdk-mock'; +import { LoadBalancerListenerContextProviderPlugin, LoadBalancerContextProviderPlugin, tagsMatch, describeListenersByLoadBalancerArn, describeTags, describeLoadBalancers } from '../../lib/context-providers/load-balancers'; +import { MockSdkProvider } from '../util/mock-sdk'; + +AWS.setSDK(require.resolve('aws-sdk')); + +const mockSDK = new MockSdkProvider(); + +type AwsCallback = (err: Error | null, val: T) => void; + +afterEach(done => { + AWS.restore(); + done(); +}); + +describe('utilities', () => { + test('describeTags yields tags by chunk', async () => { + const resourceTags: Record = {}; + for (const resourceArn of [...Array(100)].map((_, i) => `arn:load-balancer-${i}`)) { + resourceTags[resourceArn] = { + ResourceArn: resourceArn, + Tags: [ + { Key: 'name', Value: resourceArn }, + ], + }; + }; + + AWS.mock('ELBv2', 'describeTags', (_params: aws.ELBv2.DescribeTagsInput, cb: AwsCallback) => { + expect(_params.ResourceArns.length).toBeLessThanOrEqual(20); + + cb(null, { + TagDescriptions: _params.ResourceArns.map(resourceArn => ({ + ResourceArn: resourceArn, + Tags: [ + { Key: 'name', Value: resourceArn }, + ], + })), + }); + }); + + const elbv2 = await (await mockSDK.forEnvironment()).elbv2(); + + const resourceTagsOut: Record = {}; + for await (const tagDescription of describeTags(elbv2, Object.keys(resourceTags))) { + resourceTagsOut[tagDescription.ResourceArn!] = tagDescription; + } + + expect(resourceTagsOut).toEqual(resourceTags); + }); + + test('describeListenersByLoadBalancerArn traverses pages', async () => { + // arn:listener-0, arn:listener-1, ..., arn:listener-99 + const listenerArns = [...Array(100)].map((_, i) => `arn:listener-${i}`); + expect(listenerArns[0]).toEqual('arn:listener-0'); + + AWS.mock('ELBv2', 'describeListeners', (_params: aws.ELBv2.DescribeListenersInput, cb: AwsCallback) => { + const start = parseInt(_params.Marker ?? '0'); + const end = start + 10; + const slice = listenerArns.slice(start, end); + + cb(null, { + Listeners: slice.map(arn => ({ + ListenerArn: arn, + })), + NextMarker: end < listenerArns.length ? end.toString() : undefined, + }); + }); + + const elbv2 = await (await mockSDK.forEnvironment()).elbv2(); + + const listenerArnsFromPages = Array(); + for await (const listener of describeListenersByLoadBalancerArn(elbv2, ['arn:load-balancer'])) { + listenerArnsFromPages.push(listener.ListenerArn!); + } + + expect(listenerArnsFromPages).toEqual(listenerArns); + }); + + test('describeLoadBalancers traverses pages', async () => { + const loadBalancerArns = [...Array(100)].map((_, i) => `arn:load-balancer-${i}`); + expect(loadBalancerArns[0]).toEqual('arn:load-balancer-0'); + + AWS.mock('ELBv2', 'describeLoadBalancers', (_params: aws.ELBv2.DescribeLoadBalancersInput, cb: AwsCallback) => { + const start = parseInt(_params.Marker ?? '0'); + const end = start + 10; + const slice = loadBalancerArns.slice(start, end); + + cb(null, { + LoadBalancers: slice.map(loadBalancerArn => ({ + LoadBalancerArn: loadBalancerArn, + })), + NextMarker: end < loadBalancerArns.length ? end.toString() : undefined, + }); + }); + + const elbv2 = await (await mockSDK.forEnvironment()).elbv2(); + const loadBalancerArnsFromPages = (await describeLoadBalancers(elbv2, {})).map(l => l.LoadBalancerArn!); + + expect(loadBalancerArnsFromPages).toEqual(loadBalancerArns); + }); + + describe('tagsMatch', () => { + test('all tags match', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [{ Key: 'some', Value: 'tag' }], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(true); + }); + + test('extra tags match', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [ + { Key: 'some', Value: 'tag' }, + { Key: 'other', Value: 'tag2' }, + ], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(true); + }); + + test('no tags matches no tags', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [], + }; + + expect(tagsMatch(tagDescription, [])).toEqual(true); + }); + + test('one tag matches of several', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [{ Key: 'some', Value: 'tag' }], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + { key: 'other', value: 'value' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(false); + }); + + test('undefined tag does not error', () => { + const tagDescription = { + ResourceArn: 'arn:whatever', + Tags: [{ Key: 'some' }], + }; + + const requiredTags = [ + { key: 'some', value: 'tag' }, + { key: 'other', value: 'value' }, + ]; + + expect(tagsMatch(tagDescription, requiredTags)).toEqual(false); + }); + }); +}); + +describe('load balancer context provider plugin', () => { + test('errors when no matches are found', async () => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerArn: 'arn:load-balancer1', + }), + ).rejects.toThrow(/No load balancers found/i); + }); + + test('errors when multiple load balancers match', async () => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns1.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + Type: 'application', + }, + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer2', + DNSName: 'dns2.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + Type: 'application', + }, + ], + describeTagsExpected: { ResourceArns: ['arn:load-balancer1', 'arn:load-balancer2'] }, + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer1', + Tags: [ + { Key: 'some', Value: 'tag' }, + ], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [ + { Key: 'some', Value: 'tag' }, + ], + }, + ], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], + }), + ).rejects.toThrow(/Multiple load balancers found/i); + }); + + test('looks up by arn', async () => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeLoadBalancersExpected: { LoadBalancerArns: ['arn:load-balancer1'] }, + loadBalancers: [ + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + Type: 'application', + }, + ], + }); + + // WHEN + const result = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerArn: 'arn:load-balancer1', + }); + + // THEN + expect(result.ipAddressType).toEqual('ipv4'); + expect(result.loadBalancerArn).toEqual('arn:load-balancer1'); + expect(result.loadBalancerCanonicalHostedZoneId).toEqual('Z1234'); + expect(result.loadBalancerDnsName).toEqual('dns.example.com'); + expect(result.securityGroupIds).toEqual(['sg-1234']); + expect(result.vpcId).toEqual('vpc-1234'); + }); + + test('looks up by tags', async() => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns1.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + Type: 'application', + }, + { + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer2', + DNSName: 'dns2.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + Type: 'application', + }, + ], + describeTagsExpected: { ResourceArns: ['arn:load-balancer1', 'arn:load-balancer2'] }, + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer1', + Tags: [ + { Key: 'some', Value: 'tag' }, + ], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [ + { Key: 'some', Value: 'tag' }, + { Key: 'second', Value: 'tag2' }, + ], + }, + ], + }); + + // WHEN + const result = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + { key: 'second', value: 'tag2' }, + ], + }); + + expect(result.loadBalancerArn).toEqual('arn:load-balancer2'); + }); + + test('filters by type', async () => { + // GIVEN + const provider = new LoadBalancerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + IpAddressType: 'ipv4', + Type: 'network', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns1.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + { + IpAddressType: 'ipv4', + Type: 'application', + LoadBalancerArn: 'arn:load-balancer2', + DNSName: 'dns2.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + }, + ], + + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer1', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + ], + }); + + // WHEN + const loadBalancer = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerTags: [{ key: 'some', value: 'tag' }], + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + }); + + expect(loadBalancer.loadBalancerArn).toEqual('arn:load-balancer2'); + }); +}); + +describe('load balancer listener context provider plugin', () => { + test('errors when no associated load balancers match', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerTags: [{ key: 'some', value: 'tag' }], + }), + ).rejects.toThrow(/No associated load balancers found/i); + }); + + test('errors when no listeners match', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + LoadBalancerArn: 'arn:load-balancer', + Type: 'application', + }, + ], + listeners: [ + { + LoadBalancerArn: 'arn:load-balancer', + ListenerArn: 'arn:listener', + Port: 80, + Protocol: 'HTTP', + }, + ], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerArn: 'arn:load-balancer', + listenerPort: 443, + listenerProtocol: cxschema.LoadBalancerListenerProtocol.HTTPS, + }), + ).rejects.toThrow(/No load balancer listeners found/i); + }); + + test('errors when multiple listeners match', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + loadBalancers: [ + { + LoadBalancerArn: 'arn:load-balancer', + Type: 'application', + }, + { + LoadBalancerArn: 'arn:load-balancer2', + Type: 'application', + }, + ], + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + ], + listeners: [ + { + LoadBalancerArn: 'arn:load-balancer', + ListenerArn: 'arn:listener', + Port: 80, + Protocol: 'HTTP', + }, + { + LoadBalancerArn: 'arn:load-balancer2', + ListenerArn: 'arn:listener2', + Port: 80, + Protocol: 'HTTP', + }, + ], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerTags: [{ key: 'some', value: 'tag' }], + listenerPort: 80, + listenerProtocol: cxschema.LoadBalancerListenerProtocol.HTTP, + }), + ).rejects.toThrow(/Multiple load balancer listeners/i); + }); + + test('looks up by listener arn', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeListenersExpected: { ListenerArns: ['arn:listener-arn'] }, + listeners: [ + { + ListenerArn: 'arn:listener-arn', + LoadBalancerArn: 'arn:load-balancer-arn', + Port: 999, + }, + ], + describeLoadBalancersExpected: { LoadBalancerArns: ['arn:load-balancer-arn'] }, + loadBalancers: [ + { + LoadBalancerArn: 'arn:load-balancer-arn', + SecurityGroups: ['sg-1234', 'sg-2345'], + Type: 'application', + }, + ], + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + listenerArn: 'arn:listener-arn', + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn'); + expect(listener.listenerPort).toEqual(999); + expect(listener.securityGroupIds).toEqual(['sg-1234', 'sg-2345']); + }); + + test('looks up by associated load balancer arn', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeLoadBalancersExpected: { LoadBalancerArns: ['arn:load-balancer-arn1'] }, + loadBalancers: [ + { + LoadBalancerArn: 'arn:load-balancer-arn1', + SecurityGroups: ['sg-1234'], + Type: 'application', + }, + ], + + describeListenersExpected: { LoadBalancerArn: 'arn:load-balancer-arn1' }, + listeners: [ + { + // This one + ListenerArn: 'arn:listener-arn1', + LoadBalancerArn: 'arn:load-balancer-arn1', + Port: 80, + }, + ], + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerArn: 'arn:load-balancer-arn1', + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn1'); + expect(listener.listenerPort).toEqual(80); + expect(listener.securityGroupIds).toEqual(['sg-1234']); + }); + + test('looks up by associated load balancer tags', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeLoadBalancersExpected: { LoadBalancerArns: undefined }, + loadBalancers: [ + { + // This one should have the wrong tags + LoadBalancerArn: 'arn:load-balancer-arn1', + SecurityGroups: ['sg-1234', 'sg-2345'], + Type: 'application', + }, + { + // Expecting this one + LoadBalancerArn: 'arn:load-balancer-arn2', + SecurityGroups: ['sg-3456', 'sg-4567'], + Type: 'application', + }, + ], + + describeTagsExpected: { ResourceArns: ['arn:load-balancer-arn1', 'arn:load-balancer-arn2'] }, + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer-arn1', + Tags: [], + }, + { + // Expecting this one + ResourceArn: 'arn:load-balancer-arn2', + Tags: [ + { Key: 'some', Value: 'tag' }, + ], + }, + ], + + describeListenersExpected: { LoadBalancerArn: 'arn:load-balancer-arn2' }, + listeners: [ + { + // This one + ListenerArn: 'arn:listener-arn1', + LoadBalancerArn: 'arn:load-balancer-arn2', + Port: 80, + }, + { + ListenerArn: 'arn:listener-arn2', + LoadBalancerArn: 'arn:load-balancer-arn2', + Port: 999, + }, + ], + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerTags: [ + { key: 'some', value: 'tag' }, + ], + listenerPort: 999, + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn2'); + expect(listener.listenerPort).toEqual(999); + expect(listener.securityGroupIds).toEqual(['sg-3456', 'sg-4567']); + }); + + test('looks up by listener port and proto', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + AWS.mock('ELBv2', 'describeLoadBalancers', (_params: aws.ELBv2.DescribeLoadBalancersInput, cb: AwsCallback) => { + expect(_params).toEqual({}); + cb(null, { + LoadBalancers: [ + { + // Shouldn't have any matching listeners + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer1', + DNSName: 'dns1.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-1234'], + VpcId: 'vpc-1234', + Type: 'application', + }, + { + // Should have a matching listener + IpAddressType: 'ipv4', + LoadBalancerArn: 'arn:load-balancer2', + DNSName: 'dns2.example.com', + CanonicalHostedZoneId: 'Z1234', + SecurityGroups: ['sg-2345'], + VpcId: 'vpc-1234', + Type: 'application', + }, + ], + }); + }); + + AWS.mock('ELBv2', 'describeTags', (_params: aws.ELBv2.DescribeTagsInput, cb: AwsCallback) => { + cb(null, { + TagDescriptions: [ + { + ResourceArn: 'arn:load-balancer1', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + { + ResourceArn: 'arn:load-balancer2', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + ], + }); + }); + + AWS.mock('ELBv2', 'describeListeners', (params: aws.ELBv2.DescribeListenersInput, cb: AwsCallback) => { + if (params.LoadBalancerArn === 'arn:load-balancer1') { + cb(null, { + Listeners: [ + { + // Wrong port, wrong protocol => no match + ListenerArn: 'arn:listener-arn1', + LoadBalancerArn: 'arn:load-balancer1', + Protocol: 'HTTP', + Port: 80, + }, + { + // Wrong protocol, right port => no match + ListenerArn: 'arn:listener-arn3', + LoadBalancerArn: 'arn:load-balancer1', + Protocol: 'HTTPS', + Port: 443, + }, + { + // Wrong port, right protocol => no match + ListenerArn: 'arn:listener-arn4', + LoadBalancerArn: 'arn:load-balancer1', + Protocol: 'TCP', + Port: 999, + }, + ], + }); + } else if (params.LoadBalancerArn === 'arn:load-balancer2') { + cb(null, { + Listeners: [ + { + // Wrong port, wrong protocol => no match + ListenerArn: 'arn:listener-arn5', + LoadBalancerArn: 'arn:load-balancer2', + Protocol: 'HTTP', + Port: 80, + }, + { + // Right port, right protocol => match + ListenerArn: 'arn:listener-arn6', + LoadBalancerArn: 'arn:load-balancer2', + Port: 443, + Protocol: 'TCP', + }, + ], + }); + } else { + cb(new Error(`Unexpected request: ${JSON.stringify(params)}'`), {}); + } + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.APPLICATION, + loadBalancerTags: [{ key: 'some', value: 'tag' }], + listenerProtocol: cxschema.LoadBalancerListenerProtocol.TCP, + listenerPort: 443, + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn6'); + expect(listener.listenerPort).toEqual(443); + expect(listener.securityGroupIds).toEqual(['sg-2345']); + }); + + test('filters by associated load balancer type', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeLoadBalancersExpected: { LoadBalancerArns: undefined }, + loadBalancers: [ + { + // This one has wrong type => no match + LoadBalancerArn: 'arn:load-balancer-arn1', + SecurityGroups: [], + Type: 'application', + }, + { + // Right type => match + LoadBalancerArn: 'arn:load-balancer-arn2', + SecurityGroups: [], + Type: 'network', + }, + ], + + tagDescriptions: [ + { + ResourceArn: 'arn:load-balancer-arn1', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + { + ResourceArn: 'arn:load-balancer-arn2', + Tags: [{ Key: 'some', Value: 'tag' }], + }, + ], + + describeListenersExpected: { LoadBalancerArn: 'arn:load-balancer-arn2' }, + listeners: [ + { + ListenerArn: 'arn:listener-arn2', + LoadBalancerArn: 'arn:load-balancer-arn2', + Port: 443, + }, + ], + }); + + // WHEN + const listener = await provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.NETWORK, + loadBalancerTags: [{ key: 'some', value: 'tag' }], + listenerPort: 443, + }); + + // THEN + expect(listener.listenerArn).toEqual('arn:listener-arn2'); + expect(listener.listenerPort).toEqual(443); + }); + + test('errors when associated load balancer is wrong type', async () => { + // GIVEN + const provider = new LoadBalancerListenerContextProviderPlugin(mockSDK); + + mockALBLookup({ + describeListenersExpected: { ListenerArns: ['arn:listener-arn1'] }, + listeners: [ + { + ListenerArn: 'arn:listener-arn1', + LoadBalancerArn: 'arn:load-balancer-arn1', + Port: 443, + }, + ], + + describeLoadBalancersExpected: { LoadBalancerArns: ['arn:load-balancer-arn1'] }, + loadBalancers: [ + { + // This one has wrong type => no match + LoadBalancerArn: 'arn:load-balancer-arn1', + SecurityGroups: [], + Type: 'application', + }, + ], + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + loadBalancerType: cxschema.LoadBalancerType.NETWORK, + listenerArn: 'arn:listener-arn1', + }), + ).rejects.toThrow(/no associated load balancer found/i); + }); +}); + +interface ALBLookupOptions { + describeLoadBalancersExpected?: any; + loadBalancers?: aws.ELBv2.LoadBalancers; + describeTagsExpected?: any; + tagDescriptions?: aws.ELBv2.TagDescriptions; + describeListenersExpected?: any; + listeners?: aws.ELBv2.Listeners; +} + +function mockALBLookup(options: ALBLookupOptions) { + AWS.mock('ELBv2', 'describeLoadBalancers', (_params: aws.ELBv2.DescribeLoadBalancersInput, cb: AwsCallback) => { + if (options.describeLoadBalancersExpected !== undefined) { + expect(_params).toEqual(options.describeLoadBalancersExpected); + } + cb(null, { LoadBalancers: options.loadBalancers }); + }); + + AWS.mock('ELBv2', 'describeTags', (_params: aws.ELBv2.DescribeTagsInput, cb: AwsCallback) => { + if (options.describeTagsExpected !== undefined) { + expect(_params).toEqual(options.describeTagsExpected); + } + cb(null, { TagDescriptions: options.tagDescriptions }); + }); + + AWS.mock('ELBv2', 'describeListeners', (_params: aws.ELBv2.DescribeListenersInput, cb: AwsCallback) => { + if (options.describeListenersExpected !== undefined) { + expect(_params).toEqual(options.describeListenersExpected); + } + cb(null, { Listeners: options.listeners }); + }); +} diff --git a/packages/aws-cdk/test/context-providers/security-groups.test.ts b/packages/aws-cdk/test/context-providers/security-groups.test.ts new file mode 100644 index 0000000000000..7f24e684819b6 --- /dev/null +++ b/packages/aws-cdk/test/context-providers/security-groups.test.ts @@ -0,0 +1,178 @@ +import * as aws from 'aws-sdk'; +import * as AWS from 'aws-sdk-mock'; +import { hasAllTrafficEgress, SecurityGroupContextProviderPlugin } from '../../lib/context-providers/security-groups'; +import { MockSdkProvider } from '../util/mock-sdk'; + +AWS.setSDK(require.resolve('aws-sdk')); + +const mockSDK = new MockSdkProvider(); + +type AwsCallback = (err: Error | null, val: T) => void; + +afterEach(done => { + AWS.restore(); + done(); +}); + +describe('security group context provider plugin', () => { + test('errors when no matches are found', async () => { + // GIVEN + const provider = new SecurityGroupContextProviderPlugin(mockSDK); + + AWS.mock('EC2', 'describeSecurityGroups', (_params: aws.EC2.DescribeSecurityGroupsRequest, cb: AwsCallback) => { + cb(null, { SecurityGroups: [] }); + }); + + // WHEN + await expect( + provider.getValue({ + account: '1234', + region: 'us-east-1', + securityGroupId: 'sg-1234', + }), + ).rejects.toThrow(/No security groups found/i); + }); + + test('looks up by security group id', async () => { + // GIVEN + const provider = new SecurityGroupContextProviderPlugin(mockSDK); + + AWS.mock('EC2', 'describeSecurityGroups', (_params: aws.EC2.DescribeSecurityGroupsRequest, cb: AwsCallback) => { + expect(_params).toEqual({ GroupIds: ['sg-1234'] }); + cb(null, { + SecurityGroups: [ + { + GroupId: 'sg-1234', + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '0.0.0.0/0' }, + ], + }, + { + IpProtocol: '-1', + Ipv6Ranges: [ + { CidrIpv6: '::/0' }, + ], + }, + ], + }, + ], + }); + }); + + // WHEN + const res = await provider.getValue({ + account: '1234', + region: 'us-east-1', + securityGroupId: 'sg-1234', + }); + + // THEN + expect(res.securityGroupId).toEqual('sg-1234'); + expect(res.allowAllOutbound).toEqual(true); + }); + + test('detects non all-outbound egress', async () => { + // GIVEN + const provider = new SecurityGroupContextProviderPlugin(mockSDK); + + AWS.mock('EC2', 'describeSecurityGroups', (_params: aws.EC2.DescribeSecurityGroupsRequest, cb: AwsCallback) => { + expect(_params).toEqual({ GroupIds: ['sg-1234'] }); + cb(null, { + SecurityGroups: [ + { + GroupId: 'sg-1234', + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '10.0.0.0/16' }, + ], + }, + ], + }, + ], + }); + }); + + // WHEN + const res = await provider.getValue({ + account: '1234', + region: 'us-east-1', + securityGroupId: 'sg-1234', + }); + + // THEN + expect(res.securityGroupId).toEqual('sg-1234'); + expect(res.allowAllOutbound).toEqual(false); + }); + + test('identifies allTrafficEgress from SecurityGroup permissions', () => { + expect( + hasAllTrafficEgress({ + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '0.0.0.0/0' }, + ], + }, + { + IpProtocol: '-1', + Ipv6Ranges: [ + { CidrIpv6: '::/0' }, + ], + }, + ], + }), + ).toBe(true); + }); + + test('identifies allTrafficEgress from SecurityGroup permissions when combined', () => { + expect( + hasAllTrafficEgress({ + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '0.0.0.0/0' }, + ], + Ipv6Ranges: [ + { CidrIpv6: '::/0' }, + ], + }, + ], + }), + ).toBe(true); + }); + + test('identifies lacking allTrafficEgress from SecurityGroup permissions', () => { + expect( + hasAllTrafficEgress({ + IpPermissionsEgress: [ + { + IpProtocol: '-1', + IpRanges: [ + { CidrIp: '10.0.0.0/16' }, + ], + }, + ], + }), + ).toBe(false); + + expect( + hasAllTrafficEgress({ + IpPermissions: [ + { + IpProtocol: 'TCP', + IpRanges: [ + { CidrIp: '0.0.0.0/0' }, + ], + }, + ], + }), + ).toBe(false); + }); +}); diff --git a/packages/aws-cdk/test/util/cloudformation.test.ts b/packages/aws-cdk/test/util/cloudformation.test.ts index ca7b2afc570ca..c8c034ba23f67 100644 --- a/packages/aws-cdk/test/util/cloudformation.test.ts +++ b/packages/aws-cdk/test/util/cloudformation.test.ts @@ -55,10 +55,10 @@ test('no default, yes prev, no override => use previous', () => { }); }); -test('default, no prev, no override => empty param set', () => { +test('default, no prev, no override => empty param set (and obviously changes to be applied)', () => { expect(makeParams(true, false, false)).toEqual({ apiParameters: [], - changed: false, + changed: true, }); }); @@ -78,12 +78,13 @@ test('if a parameter is retrieved from SSM, the parameters always count as chang }, }, }); + const oldValues = { Foo: '/Some/Key' }; // If we don't pass a new value - expect(params.diff({}, { Foo: '/Some/Key' }).changed).toEqual(true); + expect(params.updateExisting({}, oldValues).hasChanges(oldValues)).toEqual(true); // If we do pass a new value but it's the same as the old one - expect(params.diff({ Foo: '/Some/Key' }, { Foo: '/Some/Key' }).changed).toEqual(true); + expect(params.updateExisting({ Foo: '/Some/Key' }, oldValues).hasChanges(oldValues)).toEqual(true); }); test('empty string is a valid update value', () => { @@ -93,7 +94,7 @@ test('empty string is a valid update value', () => { }, }); - expect(params.diff({ Foo: '' }, { Foo: 'ThisIsOld' }).apiParameters).toEqual([ + expect(params.updateExisting({ Foo: '' }, { Foo: 'ThisIsOld' }).apiParameters).toEqual([ { ParameterKey: 'Foo', ParameterValue: '' }, ]); }); @@ -108,11 +109,27 @@ test('unknown parameter in overrides, pass it anyway', () => { }, }); - expect(params.diff({ Bar: 'Bar' }, {}).apiParameters).toEqual([ + expect(params.updateExisting({ Bar: 'Bar' }, {}).apiParameters).toEqual([ { ParameterKey: 'Bar', ParameterValue: 'Bar' }, ]); }); +test('if an unsupplied parameter reverts to its default, it can still be dirty', () => { + // GIVEN + const templateParams = TemplateParameters.fromTemplate({ + Parameters: { + Foo: { Type: 'String', Default: 'Foo' }, + }, + }); + + // WHEN + const stackParams = templateParams.supplyAll({}); + + // THEN + expect(stackParams.hasChanges({ Foo: 'NonStandard' })).toEqual(true); + expect(stackParams.hasChanges({ Foo: 'Foo' })).toEqual(false); +}); + function makeParams(defaultValue: boolean, hasPrevValue: boolean, override: boolean) { const params = TemplateParameters.fromTemplate({ Parameters: { @@ -123,7 +140,7 @@ function makeParams(defaultValue: boolean, hasPrevValue: boolean, override: bool }, }); const prevParams: Record = hasPrevValue ? { [PARAM]: 'Foo' } : {}; - const stackParams = params.diff({ [PARAM]: override ? OVERRIDE : undefined }, prevParams); + const stackParams = params.updateExisting({ [PARAM]: override ? OVERRIDE : undefined }, prevParams); - return { apiParameters: stackParams.apiParameters, changed: stackParams.changed }; -} \ No newline at end of file + return { apiParameters: stackParams.apiParameters, changed: stackParams.hasChanges(prevParams) }; +} diff --git a/packages/aws-cdk/test/util/mock-sdk.ts b/packages/aws-cdk/test/util/mock-sdk.ts index 2ce2bbd8cf346..a060cef470a86 100644 --- a/packages/aws-cdk/test/util/mock-sdk.ts +++ b/packages/aws-cdk/test/util/mock-sdk.ts @@ -77,6 +77,13 @@ export class MockSdkProvider extends SdkProvider { public stubSTS(stubs: SyncHandlerSubsetOf) { (this.sdk as any).sts = jest.fn().mockReturnValue(partialAwsService(stubs)); } + + /** + * Replace the ELBv2 client with the given object + */ + public stubELBv2(stubs: SyncHandlerSubsetOf) { + (this.sdk as any).elbv2 = jest.fn().mockReturnValue(partialAwsService(stubs)); + } } export class MockSdk implements ISDK { @@ -87,6 +94,7 @@ export class MockSdk implements ISDK { public readonly s3 = jest.fn(); public readonly route53 = jest.fn(); public readonly ecr = jest.fn(); + public readonly elbv2 = jest.fn(); public currentAccount(): Promise { return Promise.resolve({ accountId: '123456789012', partition: 'aws' }); diff --git a/packages/awslint/package.json b/packages/awslint/package.json index e47c9135317d9..abe3c5a357ca1 100644 --- a/packages/awslint/package.json +++ b/packages/awslint/package.json @@ -16,11 +16,11 @@ "awslint": "bin/awslint" }, "dependencies": { - "@jsii/spec": "^1.14.0", + "@jsii/spec": "^1.14.1", "camelcase": "^6.2.0", "colors": "^1.4.0", "fs-extra": "^9.0.1", - "jsii-reflect": "^1.14.0", + "jsii-reflect": "^1.14.1", "yargs": "^16.1.0" }, "devDependencies": { diff --git a/packages/cdk-assets/package.json b/packages/cdk-assets/package.json index b570b0bf71728..3c85b9a1d40ec 100644 --- a/packages/cdk-assets/package.json +++ b/packages/cdk-assets/package.json @@ -38,7 +38,7 @@ "@types/node": "^10.17.44", "@types/yargs": "^15.0.9", "cdk-build-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "jszip": "^3.5.0", "mock-fs": "^4.13.0", "pkglint": "0.0.0" @@ -47,7 +47,7 @@ "@aws-cdk/cloud-assembly-schema": "0.0.0", "@aws-cdk/cx-api": "0.0.0", "archiver": "^5.0.2", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.787.0", "glob": "^7.1.6", "yargs": "^16.1.0" }, diff --git a/packages/cdk-dasm/package.json b/packages/cdk-dasm/package.json index 22917b59c1fcc..276016f4c2794 100644 --- a/packages/cdk-dasm/package.json +++ b/packages/cdk-dasm/package.json @@ -26,13 +26,13 @@ }, "license": "Apache-2.0", "dependencies": { - "codemaker": "^1.14.0", + "codemaker": "^1.14.1", "yaml": "1.10.0" }, "devDependencies": { "@types/jest": "^26.0.15", "@types/yaml": "1.9.7", - "jest": "^26.6.1" + "jest": "^26.6.3" }, "keywords": [ "aws", diff --git a/packages/decdk/package.json b/packages/decdk/package.json index ab608bcbd2c31..0871274f0b016 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -190,7 +190,7 @@ "@aws-cdk/yaml-cfn": "0.0.0", "constructs": "^3.2.0", "fs-extra": "^9.0.1", - "jsii-reflect": "^1.14.0", + "jsii-reflect": "^1.14.1", "jsonschema": "^1.4.0", "yaml": "1.10.0", "yargs": "^16.1.0" @@ -200,8 +200,8 @@ "@types/jest": "^26.0.15", "@types/yaml": "1.9.7", "@types/yargs": "^15.0.9", - "jest": "^26.6.1", - "jsii": "^1.14.0" + "jest": "^26.6.3", + "jsii": "^1.14.1" }, "keywords": [ "aws", diff --git a/packages/decdk/test/__snapshots__/synth.test.js.snap b/packages/decdk/test/__snapshots__/synth.test.js.snap index 2147d4a000dcf..80ee75c27a559 100644 --- a/packages/decdk/test/__snapshots__/synth.test.js.snap +++ b/packages/decdk/test/__snapshots__/synth.test.js.snap @@ -1638,6 +1638,7 @@ Object { "Environment": Object { "ComputeType": "BUILD_GENERAL1_SMALL", "Image": "aws/codebuild/standard:1.0", + "ImagePullCredentialsType": "CODEBUILD", "PrivilegedMode": false, "Type": "LINUX_CONTAINER", }, diff --git a/release.json b/release.json new file mode 100644 index 0000000000000..318d5d9a12072 --- /dev/null +++ b/release.json @@ -0,0 +1,4 @@ +{ + "majorVersion": 1, + "releaseType": "stable" +} diff --git a/scripts/align-version.js b/scripts/align-version.js index 2b372e373dcee..950e7bf251d85 100755 --- a/scripts/align-version.js +++ b/scripts/align-version.js @@ -4,8 +4,9 @@ // const fs = require('fs'); -const marker = require('./get-version-marker'); -const repoVersion = require('./get-version'); +const ver = require('./resolve-version'); +const marker = ver.marker; +const repoVersion = ver.version; for (const file of process.argv.splice(2)) { const pkg = JSON.parse(fs.readFileSync(file).toString()); diff --git a/scripts/align-version.sh b/scripts/align-version.sh index 6ab4f97be95af..e48e2dd421990 100755 --- a/scripts/align-version.sh +++ b/scripts/align-version.sh @@ -13,7 +13,7 @@ files="./package.json $(npx lerna ls -p -a | xargs -n1 -I@ echo @/package.json)" ${scriptdir}/align-version.js ${files} # validation -marker=$(node -p "require('./scripts/get-version-marker').replace(/\./g, '\\\.')") +marker=$(node -p "require('./scripts/resolve-version').marker.replace(/\./g, '\\\.')") # Get a list of all package.json files. None of them shouldn contain 0.0.0 anymore. # Exclude a couple of specific ones that we don't care about. diff --git a/scripts/bump-candidate.sh b/scripts/bump-candidate.sh index a88122308e6a1..abdbbe8a3f3a3 100755 --- a/scripts/bump-candidate.sh +++ b/scripts/bump-candidate.sh @@ -6,8 +6,8 @@ # a verdaccio instance that serves local tarballs, and if those tarballs have the same version as # already published modules, it messes things up. # -# It does so by using a pre-release rc tag, making it so that locally packed versions will always be -# suffixed with '-rc', distinguishing it from publisehd modules. +# It does so by using a pre-release "rc" tag, making it so that locally packed versions will always be +# suffixed with '-rc', distinguishing it from published modules. # # If you need to run integration tests locally against the distribution tarballs, you should run this # script locally as well before building and packing the repository. @@ -16,8 +16,6 @@ # # -------------------------------------------------------------------------------------------------- set -euo pipefail -version=${1:-minor} - -echo "Starting candidate ${version} version bump" - -npx standard-version --release-as ${version} --prerelease=rc --skip.commit --skip.changelog +scriptdir=$(cd $(dirname $0) && pwd) +rootdir=${scriptdir}/.. +BUMP_CANDIDATE=true ${rootdir}/bump.sh ${1:-minor} diff --git a/scripts/bump-cfnspec.sh b/scripts/bump-cfnspec.sh index 38b0033650170..dd9d182ef3ee8 100755 --- a/scripts/bump-cfnspec.sh +++ b/scripts/bump-cfnspec.sh @@ -10,11 +10,17 @@ pwd=$(pwd) ${pwd}/install.sh -# cfn2ts is invoked by cfnspec when a new module is created. -# However, cfnspec module is a dependency of the cfn2ts module. -# 'Building up' cfn2ts will build both cfnspec and cfn2ts -cd tools/cfn2ts -${pwd}/scripts/buildup +# Running the `@aws-cdk/cfnspec` update script requires both `cfn2ts` and +# `ubergen` to be readily available. The dependency can however not be modeled +# cleanly without introducing dependency cycles... This is due to how these +# dependencies are in fact involved in the building of new construct libraries +# created upon their introduction in the CFN Specification (they incur the +# dependency, not `@aws-cdk/cfnspec` itself). +yarn lerna run build --stream \ + --scope=@aws-cdk/cfnspec \ + --scope=cfn2ts \ + --scope=ubergen \ + --include-dependencies # Run the cfnspec update cd ${pwd}/packages/@aws-cdk/cfnspec @@ -24,4 +30,4 @@ version=$(cat cfn.version) # Come back to root, add all files to git and commit cd ${pwd} git add . -git commit -a -m "feat: cloudformation spec v${version}" || true # don't fail if there are no updates +git commit -a -m "feat: cloudformation spec v${version}" || true # don't fail if there are no updates \ No newline at end of file diff --git a/scripts/bump.js b/scripts/bump.js new file mode 100755 index 0000000000000..a1d678c57ce18 --- /dev/null +++ b/scripts/bump.js @@ -0,0 +1,49 @@ +#!/usr/bin/env node +const standardVersion = require('standard-version'); +const fs = require('fs'); +const path = require('path'); +const ver = require('./resolve-version'); +const repoRoot = path.join(__dirname, '..'); + +const releaseAs = process.argv[2] || 'minor'; +const forTesting = process.env.BUMP_CANDIDATE || false; + +async function main() { + if (releaseAs !== 'minor' && releaseAs !== 'patch') { + throw new error(`invalid bump type "${releaseAs}". only "minor" (the default) and "patch" are allowed. major version bumps require *slightly* more intention`); + } + + console.error(`Starting ${releaseAs} version bump`); + console.error('Current version information:', JSON.stringify(ver, undefined, 2)); + + const changelogPath = path.join(repoRoot, ver.changelogFile); + const opts = { + releaseAs: releaseAs, + skip: { tag: true }, + packageFiles: [ { filename: ver.versionFile, type: 'json' } ], + bumpFiles: [ { filename: ver.versionFile, type: 'json' } ], + infile: ver.changelogFile, + prerelease: ver.prerelease, + scripts: { + postchangelog: `${path.join(__dirname, 'changelog-experimental-fix.sh')} ${changelogPath}` + } + }; + + if (forTesting) { + opts.skip.commit = true; + opts.skip.changelog = true; + + // if we are on a "stable" branch, add a pre-release tag ("rc") to the + // version number as a safety in case this version will accidentally be + // published. + opts.prerelease = ver.prerelease || 'rc' + console.error(`BUMP_CANDIDATE is set, so bumping version for testing (with the "${opts.prerelease}" prerelease tag)`); + } + + return standardVersion(opts); +} + +main().catch(err => { + console.error(err.stack); + process.exit(1); +}); diff --git a/scripts/changelog-experimental-fix.sh b/scripts/changelog-experimental-fix.sh new file mode 100755 index 0000000000000..15284ac0c69bc --- /dev/null +++ b/scripts/changelog-experimental-fix.sh @@ -0,0 +1,11 @@ +#!/bin/bash +set -euo pipefail +changelog="${1:-}" + +if [ -z "${changelog}" ]; then + echo "Usage: $0 CHANGELOG.md" + exit 1 +fi + +sed -i.tmp -e 's/BREAKING CHANGES$/BREAKING CHANGES TO EXPERIMENTAL FEATURES/' ${changelog} +rm ${changelog}.tmp diff --git a/scripts/get-version-marker.js b/scripts/get-version-marker.js deleted file mode 100644 index e5f8c49806a67..0000000000000 --- a/scripts/get-version-marker.js +++ /dev/null @@ -1,13 +0,0 @@ -/** - * Returns the version marker used to indicate this is a local dependency. - * - * Usage: - * - * const version = require('./get-version-marker'); - * - * Or from the command line: - * - * node -p require('./get-version-marker') - * - */ -module.exports = '0.0.0'; diff --git a/scripts/get-version.js b/scripts/get-version.js deleted file mode 100644 index 9e6972582c427..0000000000000 --- a/scripts/get-version.js +++ /dev/null @@ -1,18 +0,0 @@ -/** - * Returns the current repo version. - * - * Usage: - * - * const version = require('./get-version'); - * - * Or from the command line: - * - * node -p require('./get-version') - * - */ -const versionFile = require('../.versionrc.json').packageFiles[0].filename; -if (!versionFile) { - throw new Error(`unable to determine version filename from .versionrc.json at the root of the repo`); -} - -module.exports = require(`../${versionFile}`).version; diff --git a/scripts/resolve-version-lib.js b/scripts/resolve-version-lib.js new file mode 100755 index 0000000000000..1be5dc06d6876 --- /dev/null +++ b/scripts/resolve-version-lib.js @@ -0,0 +1,75 @@ +#!/usr/bin/env node +const path = require('path'); +const fs = require('fs'); + +//============================================================= +// UNIT TESTS: tools/script-tests/test/resolve-version.test.js +//============================================================= + +function resolveVersion(rootdir) { + const ALLOWED_RELEASE_TYPES = [ 'alpha', 'rc', 'stable' ]; + const MIN_MAJOR = 1, MAX_MAJOR = 2; // extra safety: update to allow new major versions + + // + // parse release.json + // + const releaseFile = path.join(rootdir, 'release.json'); + const releaseConfig = require(releaseFile); + const majorVersion = releaseConfig.majorVersion; + const releaseType = releaseConfig.releaseType; + if (!majorVersion) { throw new Error(`"majorVersion"" must be defined in ${releaseFile}`); } + if (!releaseType) { throw new Error(`"releaseType" must be defined in ${releaseFile}`); } + if (typeof(majorVersion) !== 'number') { throw new Error(`majorVersion=${majorVersion} must be a number`); } + if (majorVersion < MIN_MAJOR || majorVersion > MAX_MAJOR) { throw new Error(`majorVersion=${majorVersion} is an unsupported major version (should be between ${MIN_MAJOR} and ${MAX_MAJOR})`); } + if (!ALLOWED_RELEASE_TYPES.includes(releaseType)) { throw new Error(`releaseType=${releaseType} is not allowed. Allowed values: ${ALLOWED_RELEASE_TYPES.join(',')}`); } + + // + // resolve and check that we have a version file + // + + const versionFile = `version.v${majorVersion}.json`; + const versionFilePath = path.join(rootdir, versionFile); + if (!fs.existsSync(versionFilePath)) { + throw new Error(`unable to find version file ${versionFile} for major version ${majorVersion}`); + } + + // + // validate that current version matches the requirements + // + + const currentVersion = require(versionFilePath).version; + console.error(`current version: ${currentVersion}`); + if (!currentVersion.startsWith(`${majorVersion}.`)) { + throw new Error(`current version "${currentVersion}" does not use the expected major version ${majorVersion}`); + } + // if this is a pre-release, make sure current version includes the + // pre-release tag (e.g. "1.0.0-alpha.0"). we allow stable branches to bump to + // a pre-release for testing purposes when BUMP_CANDIDATE=true (see bump.js) + if (releaseType !== 'stable') { + if (!currentVersion.includes(`-${releaseType}.`)) { + throw new Error(`could not find pre-release tag "${releaseType}" in current version "${currentVersion}" defined in ${versionFile}`); + } + } + + // + // determine changelog file name + // + + const changelogFile = majorVersion === 1 + ? 'CHANGELOG.md' + : `CHANGELOG.v${majorVersion}.md`; + + // + // export all of it + // + + return { + version: currentVersion, + versionFile: versionFile, + changelogFile: changelogFile, + prerelease: releaseType !== 'stable' ? releaseType : undefined, + marker: '0.0.0', + }; +} + +module.exports = resolveVersion; \ No newline at end of file diff --git a/scripts/resolve-version.js b/scripts/resolve-version.js new file mode 100755 index 0000000000000..60ee3a018a3fe --- /dev/null +++ b/scripts/resolve-version.js @@ -0,0 +1,5 @@ +#!/usr/bin/env node +const path = require('path'); +const ROOTDIR = path.resolve(__dirname, '..'); +const resolveVersion = require('./resolve-version-lib'); +module.exports = resolveVersion(ROOTDIR); diff --git a/scripts/script-tests/.gitignore b/scripts/script-tests/.gitignore new file mode 100644 index 0000000000000..dfd5365951031 --- /dev/null +++ b/scripts/script-tests/.gitignore @@ -0,0 +1,8 @@ + +.LAST_BUILD +*.snk +junit.xml +.nyc_output +coverage +nyc.config.js +!.eslintrc.js \ No newline at end of file diff --git a/scripts/script-tests/README.md b/scripts/script-tests/README.md new file mode 100644 index 0000000000000..a819fff580b1e --- /dev/null +++ b/scripts/script-tests/README.md @@ -0,0 +1,3 @@ +# script tests + +This directory includes tests for scripts under `./scripts`. \ No newline at end of file diff --git a/scripts/script-tests/package.json b/scripts/script-tests/package.json new file mode 100644 index 0000000000000..2c6d0ff48e94a --- /dev/null +++ b/scripts/script-tests/package.json @@ -0,0 +1,15 @@ +{ + "name": "script-tests", + "private": true, + "version": "0.0.0", + "description": "various tests for development and build scripts", + "scripts": { + "build": "echo ok", + "test": "jest", + "build+test": "npm run build && npm test", + "build+test+package": "npm run build+test" + }, + "devDependencies": { + "jest": "^26.6.2" + } +} diff --git a/scripts/script-tests/resolve-version.test.js b/scripts/script-tests/resolve-version.test.js new file mode 100644 index 0000000000000..f1b6f596a82d2 --- /dev/null +++ b/scripts/script-tests/resolve-version.test.js @@ -0,0 +1,153 @@ +const fs = require('fs'); +const os = require('os'); +const path = require('path'); +const resolveVersion = require('../resolve-version-lib'); + +beforeAll(() => spyOn(console, 'error')); + +happy({ + name: 'stable release', + inputs: { + 'release.json': { majorVersion: 2, releaseType: 'stable' }, + 'version.v2.json': { version: '2.1.0' }, + }, + expected: { + changelogFile: 'CHANGELOG.v2.md', + marker: '0.0.0', + prerelease: undefined, + version: '2.1.0', + versionFile: 'version.v2.json' + } +}); + +happy({ + name: 'alpha releases', + inputs: { + 'release.json': { majorVersion: 2, releaseType: 'alpha' }, + 'version.v2.json': { version: '2.1.0-alpha.0' }, + }, + expected: { + changelogFile: 'CHANGELOG.v2.md', + marker: '0.0.0', + prerelease: 'alpha', + version: '2.1.0-alpha.0', + versionFile: 'version.v2.json' + } +}); + +happy({ + name: 'rc releases', + inputs: { + 'release.json': { majorVersion: 2, releaseType: 'rc' }, + 'version.v2.json': { version: '2.1.0-rc.0' }, + }, + expected: { + changelogFile: 'CHANGELOG.v2.md', + marker: '0.0.0', + prerelease: 'rc', + version: '2.1.0-rc.0', + versionFile: 'version.v2.json' + } +}); + +happy({ + name: 'v1 changelog is still called CHANGELOG.md for backwards compatibility', + inputs: { + 'release.json': { majorVersion: 1, releaseType: 'stable' }, + 'version.v1.json': { version: '1.72.0' } + }, + expected: { + changelogFile: 'CHANGELOG.md', + marker: '0.0.0', + prerelease: undefined, + version: '1.72.0', + versionFile: 'version.v1.json' + } +}); + +happy({ + name: 'to support BUMP_CANDIDATE stable branches can be bumped towards a pre-release', + inputs: { + 'release.json': { majorVersion: 2, releaseType: 'stable' }, + 'version.v2.json': { version: '2.0.0-rc.0' } + }, + expected: { + changelogFile: 'CHANGELOG.v2.md', + marker: '0.0.0', + prerelease: undefined, + version: '2.0.0-rc.0', + versionFile: 'version.v2.json' + } +}); + +failure({ + name: 'invalid release type', + inputs: { 'release.json': { majorVersion: 2, releaseType: 'build' } }, + expected: 'releaseType=build is not allowed. Allowed values: alpha,rc,stable' +}); + +failure({ + name: 'invalid major version (less then min)', + inputs: { 'release.json': { majorVersion: -1, releaseType: 'rc' } }, + expected: 'majorVersion=-1 is an unsupported major version (should be between 1 and 2)' +}); + +failure({ + name: 'invalid major version (over max)', + inputs: { 'release.json': { majorVersion: 3, releaseType: 'rc' } }, + expected: 'majorVersion=3 is an unsupported major version (should be between 1 and 2)' +}); + +failure({ + name: 'invalid major version (non-number)', + inputs: { 'release.json': { majorVersion: '2', releaseType: 'rc' } }, + expected: 'majorVersion=2 must be a number' +}); + +failure({ + name: 'no version file', + inputs: { 'release.json': { majorVersion: 2, releaseType: 'alpha' } }, + expected: 'unable to find version file version.v2.json for major version 2' +}); + +failure({ + name: 'actual version not the right major', + inputs: { + 'release.json': { majorVersion: 1, releaseType: 'stable' }, + 'version.v1.json': { version: '2.0.0' } + }, + expected: 'current version "2.0.0" does not use the expected major version 1' +}); + +failure({ + name: 'actual version not the right pre-release', + inputs: { + 'release.json': { majorVersion: 2, releaseType: 'alpha' }, + 'version.v2.json': { version: '2.0.0-rc.0' } + }, + expected: 'could not find pre-release tag "alpha" in current version "2.0.0-rc.0" defined in version.v2.json' +}); + +function happy({ name, inputs, expected } = opts) { + test(name, () => { + const tmpdir = stage(inputs); + const actual = resolveVersion(tmpdir); + expect(actual).toStrictEqual(expected); + }); +} + +function failure({ name, inputs, expected } = opts) { + test(name, () => { + const tmpdir = stage(inputs); + expect(() => resolveVersion(tmpdir)).toThrow(expected); + }); +} + +function stage(inputs) { + const tmpdir = fs.mkdtempSync(path.join(os.tmpdir(), 'resolve-version-')); + for (const [ name, contents ] of Object.entries(inputs)) { + const data = typeof(contents) === 'string' ? contents : JSON.stringify(contents); + fs.writeFileSync(path.join(tmpdir, name), data); + } + return tmpdir; +} diff --git a/tools/cdk-build-tools/package.json b/tools/cdk-build-tools/package.json index 53e2c7198fc50..3aa024ae4127e 100644 --- a/tools/cdk-build-tools/package.json +++ b/tools/cdk-build-tools/package.json @@ -39,8 +39,8 @@ "pkglint": "0.0.0" }, "dependencies": { - "@typescript-eslint/eslint-plugin": "^4.6.0", - "@typescript-eslint/parser": "^4.6.0", + "@typescript-eslint/eslint-plugin": "^4.6.1", + "@typescript-eslint/parser": "^4.6.1", "eslint-plugin-cdk": "0.0.0", "awslint": "0.0.0", "colors": "^1.4.0", @@ -49,12 +49,12 @@ "eslint-import-resolver-typescript": "^2.3.0", "eslint-plugin-import": "^2.22.1", "fs-extra": "^9.0.1", - "jest": "^26.6.1", - "jsii": "^1.14.0", - "jsii-pacmak": "^1.14.0", + "jest": "^26.6.3", + "jsii": "^1.14.1", + "jsii-pacmak": "^1.14.1", "nodeunit": "^0.11.3", "nyc": "^15.1.0", - "ts-jest": "^26.4.3", + "ts-jest": "^26.4.4", "typescript": "~3.9.7", "yargs": "^16.1.0", "yarn-cling": "0.0.0" diff --git a/tools/cdk-integ-tools/bin/cdk-integ-assert.ts b/tools/cdk-integ-tools/bin/cdk-integ-assert.ts index 758a8288fb0d6..2fd262a933792 100644 --- a/tools/cdk-integ-tools/bin/cdk-integ-assert.ts +++ b/tools/cdk-integ-tools/bin/cdk-integ-assert.ts @@ -16,7 +16,7 @@ async function main() { process.stdout.write(`Verifying ${test.name} against ${test.expectedFileName} ... `); if (!test.hasExpected()) { - throw new Error(`No such file: ${test.expectedFileName}. Run 'npm run integ'.`); + throw new Error(`No such file: ${test.expectedFileName}. Run 'yarn integ'.`); } let expected = await test.readExpected(); @@ -40,7 +40,7 @@ async function main() { if (failures.length > 0) { // eslint-disable-next-line max-len - throw new Error(`Some stacks have changed. To verify that they still deploy successfully, run: 'npm run integ ${failures.join(' ')}'`); + throw new Error(`Some stacks have changed. To verify that they still deploy successfully, run: 'yarn integ ${failures.join(' ')}'`); } } diff --git a/tools/cfn2ts/package.json b/tools/cfn2ts/package.json index d1aedf2f03824..76ff69dc0f04a 100644 --- a/tools/cfn2ts/package.json +++ b/tools/cfn2ts/package.json @@ -30,7 +30,7 @@ "license": "Apache-2.0", "dependencies": { "@aws-cdk/cfnspec": "0.0.0", - "codemaker": "^1.14.0", + "codemaker": "^1.14.1", "fast-json-patch": "^3.0.0-1", "fs-extra": "^9.0.1", "yargs": "^16.1.0" @@ -40,7 +40,7 @@ "@types/jest": "^26.0.15", "@types/yargs": "^15.0.9", "cdk-build-tools": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0" }, "keywords": [ diff --git a/tools/eslint-plugin-cdk/package.json b/tools/eslint-plugin-cdk/package.json index 840b5b578a2ce..24c0c14813522 100644 --- a/tools/eslint-plugin-cdk/package.json +++ b/tools/eslint-plugin-cdk/package.json @@ -17,11 +17,11 @@ "@types/jest": "^26.0.15", "@types/node": "^10.17.44", "eslint-plugin-rulesdir": "^0.1.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "typescript": "~3.9.7" }, "dependencies": { - "@typescript-eslint/parser": "^4.6.0", + "@typescript-eslint/parser": "^4.6.1", "eslint": "^7.12.1", "fs-extra": "^9.0.1" }, diff --git a/tools/nodeunit-shim/package.json b/tools/nodeunit-shim/package.json index 4d1981eaed8d1..9019561be7df6 100644 --- a/tools/nodeunit-shim/package.json +++ b/tools/nodeunit-shim/package.json @@ -17,7 +17,7 @@ "typescript": "~3.9.7" }, "dependencies": { - "jest": "^26.6.1" + "jest": "^26.6.3" }, "keywords": [], "author": "", diff --git a/tools/pkglint/lib/rules.ts b/tools/pkglint/lib/rules.ts index 9a9e56980c214..9c35dc39ca0f0 100644 --- a/tools/pkglint/lib/rules.ts +++ b/tools/pkglint/lib/rules.ts @@ -147,11 +147,22 @@ export class ThirdPartyAttributions extends ValidationRule { } const bundled = pkg.getBundledDependencies(); const lines = fs.readFileSync(path.join(pkg.packageRoot, 'NOTICE'), { encoding: 'utf8' }).split('\n'); + + const re = /^\*\* (\S+)/; + const attributions = lines.filter(l => re.test(l)).map(l => l.match(re)![1]); + for (const dep of bundled) { - const re = new RegExp(`^\\*\\* ${dep}`); - if (!lines.find(l => re.test(l))) { + if (!attributions.includes(dep)) { + pkg.report({ + message: `Missing attribution for bundled dependency '${dep}' in NOTICE file.`, + ruleName: this.name, + }); + } + } + for (const attr of attributions) { + if (!bundled.includes(attr)) { pkg.report({ - message: `Missing attribution for bundled dependency '${dep}' in NOTICE file`, + message: `Unnecessary attribution found for dependency '${attr}' in NOTICE file.`, ruleName: this.name, }); } @@ -1488,9 +1499,14 @@ function hasIntegTests(pkg: PackageJson) { * Return whether this package should use CDK build tools */ function shouldUseCDKBuildTools(pkg: PackageJson) { - // The packages that DON'T use CDKBuildTools are the package itself - // and the packages used by it. - return pkg.packageName !== 'cdk-build-tools' && pkg.packageName !== 'merkle-build' && pkg.packageName !== 'awslint'; + const exclude = [ + 'cdk-build-tools', + 'merkle-build', + 'awslint', + 'script-tests', + ]; + + return !exclude.includes(pkg.packageName); } function repoRoot(dir: string) { diff --git a/tools/pkglint/package.json b/tools/pkglint/package.json index cbddea940b58f..3f6a213c666ab 100644 --- a/tools/pkglint/package.json +++ b/tools/pkglint/package.json @@ -39,7 +39,7 @@ "@types/semver": "^7.3.4", "@types/yargs": "^15.0.9", "eslint-plugin-cdk": "0.0.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "typescript": "~3.9.7" }, "dependencies": { diff --git a/tools/pkglint/test/rules.test.ts b/tools/pkglint/test/rules.test.ts index db07e312daa0b..70e8942697271 100644 --- a/tools/pkglint/test/rules.test.ts +++ b/tools/pkglint/test/rules.test.ts @@ -234,7 +234,38 @@ describe('ThirdPartyAttributions', () => { expect(pkgJson.reports.length).toEqual(2); for (const report of pkgJson.reports) { expect(report.ruleName).toEqual('license/3p-attributions'); + expect(report.message).toContain('Missing attribution'); } + expect(pkgJson.reports[0].message).toContain('dep1'); + expect(pkgJson.reports[1].message).toContain('dep2'); + }); + + test('errors when there are excessive attributions', async() => { + fakeModule = new FakeModule({ + packagejson: { + bundledDependencies: ['dep1'], + }, + notice: [ + '** dep1 - https://link-somewhere', + '** dep2 - https://link-elsewhere', + '** dep3-rev - https://link-elsewhere', + ], + }); + const dirPath = await fakeModule.tmpdir(); + + const rule = new rules.ThirdPartyAttributions(); + + const pkgJson = new PackageJson(path.join(dirPath, 'package.json')); + rule.validate(pkgJson); + + expect(pkgJson.hasReports).toBe(true); + expect(pkgJson.reports.length).toEqual(2); + for (const report of pkgJson.reports) { + expect(report.ruleName).toEqual('license/3p-attributions'); + expect(report.message).toContain('Unnecessary attribution'); + } + expect(pkgJson.reports[0].message).toContain('dep2'); + expect(pkgJson.reports[1].message).toContain('dep3-rev'); }); test('passes when attribution is present', async() => { @@ -290,4 +321,4 @@ describe('ThirdPartyAttributions', () => { expect(pkgJson.hasReports).toBe(false); }); -}); \ No newline at end of file +}); diff --git a/tools/yarn-cling/package.json b/tools/yarn-cling/package.json index 9ab91dbd1b5bd..448e2cc950c2e 100644 --- a/tools/yarn-cling/package.json +++ b/tools/yarn-cling/package.json @@ -41,7 +41,7 @@ "@types/jest": "^26.0.15", "@types/node": "^10.17.44", "@types/yarnpkg__lockfile": "^1.1.4", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0", "typescript": "~3.9.7" }, diff --git a/version.v1.json b/version.v1.json new file mode 100644 index 0000000000000..b7d663f683e0e --- /dev/null +++ b/version.v1.json @@ -0,0 +1,3 @@ +{ + "version": "1.72.0" +} diff --git a/yarn.lock b/yarn.lock index fbbfb283d1dba..bc0ef657e520a 100644 --- a/yarn.lock +++ b/yarn.lock @@ -497,6 +497,13 @@ dependencies: "@babel/helper-plugin-utils" "^7.10.4" +"@babel/plugin-syntax-top-level-await@^7.8.3": + version "7.12.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.12.1.tgz#dd6c0b357ac1bb142d98537450a319625d13d2a0" + integrity sha512-i7ooMZFS+a/Om0crxZodrTzNEPJHZrlMVGMTEpFAj6rYY/bKCddB0Dk/YxfPuYXOopuhKk/e1jV6h+WUU9XN3A== + dependencies: + "@babel/helper-plugin-utils" "^7.10.4" + "@babel/plugin-syntax-typescript@^7.10.4": version "7.10.4" resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.10.4.tgz#2f55e770d3501e83af217d782cb7517d7bb34d25" @@ -1084,98 +1091,93 @@ resolved "https://registry.yarnpkg.com/@istanbuljs/schema/-/schema-0.1.2.tgz#26520bf09abe4a5644cd5414e37125a8954241dd" integrity sha512-tsAQNx32a8CoFhjhijUIhI4kccIAgmGhy8LZMZgGfmXcpMbPRUqn5LWmgRttILi6yeGmBJd2xsPkFMs0PzgPCw== -"@jest/console@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.1.tgz#6a19eaac4aa8687b4db9130495817c65aec3d34e" - integrity sha512-cjqcXepwC5M+VeIhwT6Xpi/tT4AiNzlIx8SMJ9IihduHnsSrnWNvTBfKIpmqOOCNOPqtbBx6w2JqfoLOJguo8g== +"@jest/console@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/console/-/console-26.6.2.tgz#4e04bc464014358b03ab4937805ee36a0aeb98f2" + integrity sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" - jest-message-util "^26.6.1" - jest-util "^26.6.1" + jest-message-util "^26.6.2" + jest-util "^26.6.2" slash "^3.0.0" -"@jest/core@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.1.tgz#77426822f667a2cda82bf917cee11cc8ba71f9ac" - integrity sha512-p4F0pgK3rKnoS9olXXXOkbus1Bsu6fd8pcvLMPsUy4CVXZ8WSeiwQ1lK5hwkCIqJ+amZOYPd778sbPha/S8Srw== +"@jest/core@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/core/-/core-26.6.3.tgz#7639fcb3833d748a4656ada54bde193051e45fad" + integrity sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw== dependencies: - "@jest/console" "^26.6.1" - "@jest/reporters" "^26.6.1" - "@jest/test-result" "^26.6.1" - "@jest/transform" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/console" "^26.6.2" + "@jest/reporters" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" - jest-changed-files "^26.6.1" - jest-config "^26.6.1" - jest-haste-map "^26.6.1" - jest-message-util "^26.6.1" + jest-changed-files "^26.6.2" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" jest-regex-util "^26.0.0" - jest-resolve "^26.6.1" - jest-resolve-dependencies "^26.6.1" - jest-runner "^26.6.1" - jest-runtime "^26.6.1" - jest-snapshot "^26.6.1" - jest-util "^26.6.1" - jest-validate "^26.6.1" - jest-watcher "^26.6.1" + jest-resolve "^26.6.2" + jest-resolve-dependencies "^26.6.3" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" + jest-watcher "^26.6.2" micromatch "^4.0.2" p-each-series "^2.1.0" rimraf "^3.0.0" slash "^3.0.0" strip-ansi "^6.0.0" -"@jest/create-cache-key-function@^26.5.0": - version "26.5.0" - resolved "https://registry.yarnpkg.com/@jest/create-cache-key-function/-/create-cache-key-function-26.5.0.tgz#1d07947adc51ea17766d9f0ccf5a8d6ea94c47dc" - integrity sha512-DJ+pEBUIqarrbv1W/C39f9YH0rJ4wsXZ/VC6JafJPlHW2HOucKceeaqTOQj9MEDQZjySxMLkOq5mfXZXNZcmWw== - -"@jest/environment@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.1.tgz#38a56f1cc66f96bf53befcc5ebeaf1c2dce90e9a" - integrity sha512-GNvHwkOFJtNgSwdzH9flUPzF9AYAZhUg124CBoQcwcZCM9s5TLz8Y3fMtiaWt4ffbigoetjGk5PU2Dd8nLrSEw== +"@jest/environment@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/environment/-/environment-26.6.2.tgz#ba364cc72e221e79cc8f0a99555bf5d7577cf92c" + integrity sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA== dependencies: - "@jest/fake-timers" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" - jest-mock "^26.6.1" + jest-mock "^26.6.2" -"@jest/fake-timers@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.1.tgz#5aafba1822075b7142e702b906094bea15f51acf" - integrity sha512-T/SkMLgOquenw/nIisBRD6XAYpFir0kNuclYLkse5BpzeDUukyBr+K31xgAo9M0hgjU9ORlekAYPSzc0DKfmKg== +"@jest/fake-timers@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/fake-timers/-/fake-timers-26.6.2.tgz#459c329bcf70cee4af4d7e3f3e67848123535aad" + integrity sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" "@sinonjs/fake-timers" "^6.0.1" "@types/node" "*" - jest-message-util "^26.6.1" - jest-mock "^26.6.1" - jest-util "^26.6.1" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" + jest-util "^26.6.2" -"@jest/globals@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.1.tgz#b232c7611d8a2de62b4bf9eb9a007138322916f4" - integrity sha512-acxXsSguuLV/CeMYmBseefw6apO7NuXqpE+v5r3yD9ye2PY7h1nS20vY7Obk2w6S7eJO4OIAJeDnoGcLC/McEQ== +"@jest/globals@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/globals/-/globals-26.6.2.tgz#5b613b78a1aa2655ae908eba638cc96a20df720a" + integrity sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA== dependencies: - "@jest/environment" "^26.6.1" - "@jest/types" "^26.6.1" - expect "^26.6.1" + "@jest/environment" "^26.6.2" + "@jest/types" "^26.6.2" + expect "^26.6.2" -"@jest/reporters@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.1.tgz#582ede05278cf5eeffe58bc519f4a35f54fbcb0d" - integrity sha512-J6OlXVFY3q1SXWJhjme5i7qT/BAZSikdOK2t8Ht5OS32BDo6KfG5CzIzzIFnAVd82/WWbc9Hb7SJ/jwSvVH9YA== +"@jest/reporters@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/reporters/-/reporters-26.6.2.tgz#1f518b99637a5f18307bd3ecf9275f6882a667f6" + integrity sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw== dependencies: "@bcoe/v8-coverage" "^0.2.3" - "@jest/console" "^26.6.1" - "@jest/test-result" "^26.6.1" - "@jest/transform" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/console" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" chalk "^4.0.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" @@ -1186,63 +1188,63 @@ istanbul-lib-report "^3.0.0" istanbul-lib-source-maps "^4.0.0" istanbul-reports "^3.0.2" - jest-haste-map "^26.6.1" - jest-resolve "^26.6.1" - jest-util "^26.6.1" - jest-worker "^26.6.1" + jest-haste-map "^26.6.2" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" slash "^3.0.0" source-map "^0.6.0" string-length "^4.0.1" terminal-link "^2.0.0" - v8-to-istanbul "^6.0.1" + v8-to-istanbul "^7.0.0" optionalDependencies: node-notifier "^8.0.0" -"@jest/source-map@^26.5.0": - version "26.5.0" - resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.5.0.tgz#98792457c85bdd902365cd2847b58fff05d96367" - integrity sha512-jWAw9ZwYHJMe9eZq/WrsHlwF8E3hM9gynlcDpOyCb9bR8wEd9ZNBZCi7/jZyzHxC7t3thZ10gO2IDhu0bPKS5g== +"@jest/source-map@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/source-map/-/source-map-26.6.2.tgz#29af5e1e2e324cafccc936f218309f54ab69d535" + integrity sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA== dependencies: callsites "^3.0.0" graceful-fs "^4.2.4" source-map "^0.6.0" -"@jest/test-result@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.1.tgz#d75698d8a06aa663e8936663778c831512330cc1" - integrity sha512-wqAgIerIN2gSdT2A8WeA5+AFh9XQBqYGf8etK143yng3qYd0mF0ie2W5PVmgnjw4VDU6ammI9NdXrKgNhreawg== +"@jest/test-result@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/test-result/-/test-result-26.6.2.tgz#55da58b62df134576cc95476efa5f7949e3f5f18" + integrity sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ== dependencies: - "@jest/console" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/console" "^26.6.2" + "@jest/types" "^26.6.2" "@types/istanbul-lib-coverage" "^2.0.0" collect-v8-coverage "^1.0.0" -"@jest/test-sequencer@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.1.tgz#34216ac2c194b0eeebde30d25424d1134703fd2e" - integrity sha512-0csqA/XApZiNeTIPYh6koIDCACSoR6hi29T61tKJMtCZdEC+tF3PoNt7MS0oK/zKC6daBgCbqXxia5ztr/NyCQ== +"@jest/test-sequencer@^26.6.3": + version "26.6.3" + resolved "https://registry.yarnpkg.com/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz#98e8a45100863886d074205e8ffdc5a7eb582b17" + integrity sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw== dependencies: - "@jest/test-result" "^26.6.1" + "@jest/test-result" "^26.6.2" graceful-fs "^4.2.4" - jest-haste-map "^26.6.1" - jest-runner "^26.6.1" - jest-runtime "^26.6.1" + jest-haste-map "^26.6.2" + jest-runner "^26.6.3" + jest-runtime "^26.6.3" -"@jest/transform@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.1.tgz#f70786f96e0f765947b4fb4f54ffcfb7bd783711" - integrity sha512-oNFAqVtqRxZRx6vXL3I4bPKUK0BIlEeaalkwxyQGGI8oXDQBtYQBpiMe5F7qPs4QdvvFYB42gPGIMMcxXaBBxQ== +"@jest/transform@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/transform/-/transform-26.6.2.tgz#5ac57c5fa1ad17b2aae83e73e45813894dcf2e4b" + integrity sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA== dependencies: "@babel/core" "^7.1.0" - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" babel-plugin-istanbul "^6.0.0" chalk "^4.0.0" convert-source-map "^1.4.0" fast-json-stable-stringify "^2.0.0" graceful-fs "^4.2.4" - jest-haste-map "^26.6.1" + jest-haste-map "^26.6.2" jest-regex-util "^26.0.0" - jest-util "^26.6.1" + jest-util "^26.6.2" micromatch "^4.0.2" pirates "^4.0.1" slash "^3.0.0" @@ -1260,10 +1262,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@jest/types@^26.6.1": - version "26.6.1" - resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.1.tgz#2638890e8031c0bc8b4681e0357ed986e2f866c5" - integrity sha512-ywHavIKNpAVrStiRY5wiyehvcktpijpItvGiK72RAn5ctqmzvPk8OvKnvHeBqa1XdQr959CTWAJMqxI8BTibyg== +"@jest/types@^26.6.2": + version "26.6.2" + resolved "https://registry.yarnpkg.com/@jest/types/-/types-26.6.2.tgz#bef5a532030e1d88a2f5a6d933f84e97226ed48e" + integrity sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ== dependencies: "@types/istanbul-lib-coverage" "^2.0.0" "@types/istanbul-reports" "^3.0.0" @@ -1271,10 +1273,10 @@ "@types/yargs" "^15.0.0" chalk "^4.0.0" -"@jsii/spec@^1.14.0": - version "1.14.0" - resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.14.0.tgz#79ef7626616e3cd6eaf503f8f4c0c9640c220a5b" - integrity sha512-hgJG0d1W+VgXZD8TeXt4wlFwdkT9izUN5fY+yzKkh+zZUNebEayXDP6LXOFD4iJZ83nUGjEVayzaZt4rAhwt5A== +"@jsii/spec@^1.14.1": + version "1.14.1" + resolved "https://registry.yarnpkg.com/@jsii/spec/-/spec-1.14.1.tgz#9544e94e590dafd37d46f91ae3da925f39ca73de" + integrity sha512-h+HXPYD+k8zbkQRXzR9zWxXoSyBTBQL2N+t+iTgMuHpWvnrd6ZUegpWh/M1voMpmT5JHS7MftwIRjnp7yP92KQ== dependencies: jsonschema "^1.4.0" @@ -2066,10 +2068,10 @@ "@octokit/types" "^2.0.1" deprecation "^2.3.1" -"@octokit/plugin-rest-endpoint-methods@4.2.0": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.2.0.tgz#c5a0691b3aba5d8b4ef5dffd6af3649608f167ba" - integrity sha512-1/qn1q1C1hGz6W/iEDm9DoyNoG/xdFDt78E3eZ5hHeUfJTLJgyAMdj9chL/cNBHjcjd+FH5aO1x0VCqR2RE0mw== +"@octokit/plugin-rest-endpoint-methods@4.2.1": + version "4.2.1" + resolved "https://registry.yarnpkg.com/@octokit/plugin-rest-endpoint-methods/-/plugin-rest-endpoint-methods-4.2.1.tgz#8224833a45c3394836dc6e86f1e6c49269a2c350" + integrity sha512-QyFr4Bv807Pt1DXZOC5a7L5aFdrwz71UHTYoHVajYV5hsqffWm8FUl9+O7nxRu5PDMtB/IKrhFqTmdBTK5cx+A== dependencies: "@octokit/types" "^5.5.0" deprecation "^2.3.1" @@ -2128,15 +2130,15 @@ once "^1.4.0" universal-user-agent "^4.0.0" -"@octokit/rest@^18.0.6": - version "18.0.6" - resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.6.tgz#76c274f1a68f40741a131768ef483f041e7b98b6" - integrity sha512-ES4lZBKPJMX/yUoQjAZiyFjei9pJ4lTTfb9k7OtYoUzKPDLl/M8jiHqt6qeSauyU4eZGLw0sgP1WiQl9FYeM5w== +"@octokit/rest@^18.0.9": + version "18.0.9" + resolved "https://registry.yarnpkg.com/@octokit/rest/-/rest-18.0.9.tgz#964d707d914eb34b1787895fdcacff96de47844d" + integrity sha512-CC5+cIx974Ygx9lQNfUn7/oXDQ9kqGiKUC6j1A9bAVZZ7aoTF8K6yxu0pQhQrLBwSl92J6Z3iVDhGhGFgISCZg== dependencies: "@octokit/core" "^3.0.0" "@octokit/plugin-paginate-rest" "^2.2.0" "@octokit/plugin-request-log" "^1.0.0" - "@octokit/plugin-rest-endpoint-methods" "4.2.0" + "@octokit/plugin-rest-endpoint-methods" "4.2.1" "@octokit/types@^2.0.0", "@octokit/types@^2.0.1": version "2.16.2" @@ -2152,128 +2154,128 @@ dependencies: "@types/node" ">= 8" -"@parcel/babel-ast-utils@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/babel-ast-utils/-/babel-ast-utils-2.0.0-nightly.2056.tgz#c05216883ee9a53477f3890e978ff39bb4687d19" - integrity sha512-eH5rskkMGdoVHCw5t7tuJdfDChl3mXFwl6MFA83SfGEsfOAdrLreQDGN+Z5jV6chZ+cq6gtGje2ckOSs6zzSfg== +"@parcel/babel-ast-utils@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/babel-ast-utils/-/babel-ast-utils-2.0.0-nightly.2066.tgz#bd4d0214b84acc4146831f66bfa4b4e8e81a5b17" + integrity sha512-Nl8ddCuIWRDMpr3tGPSfSAX0rdKyndlCIEd5LM8rQ8IAcR03NuX6IG2h4jGcgCIAfGaqS/9M1Xfpr1rFQHXYoQ== dependencies: "@babel/generator" "^7.0.0" "@babel/parser" "^7.0.0" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.444+e2136965" -"@parcel/babel-preset-env@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/babel-preset-env/-/babel-preset-env-2.0.0-nightly.434.tgz#400a4cbf7c36bf5a25a446b79ff833e50292949d" - integrity sha512-C9dsU6WHw4+k3MxHyf55HTlIJ6jBeOsVJAvTZfGPC2wmMNmbWBjpzzwN08FXUbH46I1myl8EJeUcePyi+J6aMw== +"@parcel/babel-preset-env@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/babel-preset-env/-/babel-preset-env-2.0.0-nightly.444.tgz#6ae1995f17958d159a5df615ab80b61edce9a880" + integrity sha512-JYZczFbDrTOo0H8Iu6t8UAF58L9CTFO+Ojhg/kjw9+0ookmLlKMA2Ar8u/Wozh2A1lWe1xCyZQFC0xYZTuQIBA== dependencies: "@babel/preset-env" "^7.4.0" semver "^5.4.1" -"@parcel/babylon-walk@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/babylon-walk/-/babylon-walk-2.0.0-nightly.2056.tgz#e7ed28ca5eac5f49c6ddc5813305b75a22b2497d" - integrity sha512-zoEnlXO6xNgsg96ItuSVqqwtaqrcDBmMNP9NTHdwX0iHWO2fWE3v16CJAXoI8E8py9nnbtvbCczfKpEW9c2YQg== +"@parcel/babylon-walk@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/babylon-walk/-/babylon-walk-2.0.0-nightly.2066.tgz#375072084e5da071fa9493ce77e16443dd0dfa04" + integrity sha512-lDHJ5wUPe4vCGh1wiW8CH7j0Tqo7o6yz2UG9UQhzhDviS7jHfpkCg4f3ss1YpJS8jlYQ/TY+PypoWGZcOQa+8g== dependencies: "@babel/types" "^7.0.0" lodash.clone "^4.5.0" -"@parcel/bundler-default@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.0.0-nightly.434.tgz#94e2d8d04509926cce04263bb2b1b7a73869a88f" - integrity sha512-cwSGZJreoJXInzRM3tzvEDMZh9tv2Hrze1N5QscTRuRQFOTY31S/l8OROXI6yfqjHfbZo33Uvglif3hiJsikjg== +"@parcel/bundler-default@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.0.0-nightly.444.tgz#77ce4e22ca41442f564e5045a9ed9d25fef693d8" + integrity sha512-VaB0rkF3B7deqz7xUj+jXvD+emSWo4CeBWcEEjhYHdpj/GeqFSAW/o9CxcgB1pDWiIWU3kI4es+qHJ4zf6icEw== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" -"@parcel/cache@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.0.0-nightly.434.tgz#34171b2bfe8047e8bd9890f4a43c28ec5085547f" - integrity sha512-Dr5P/gOKDvmYZEPrYuWmJHyJ3F2RBSlNGsI1UYFWH8ViCj/yuKKzEiRqBmwjoI7Nx5IRAURrWM7ki77t4VF1Lg== +"@parcel/cache@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.0.0-nightly.444.tgz#d1994e86be596b96cb8017e39fb7e23459ce279c" + integrity sha512-uV4o+Rh3XerFjnFy7uQHwIad6BBgRvyH/sapzNBXpxzyAcE9VL4+edWLjUVSSlaEDGsn/goPpg3+idjWNlGJQw== dependencies: - "@parcel/logger" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/logger" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" -"@parcel/codeframe@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.0.0-nightly.434.tgz#a8d9794dec02f26550dbdcadff54872c3e560c7c" - integrity sha512-WClnfIwSbfw0l+ANBeqiPpd2oObFefX8wgb5JlTPrMgiymDgCy+g3k5YiCuNPZ6WY0GD5whFEV4PkddUHC1P5A== +"@parcel/codeframe@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.0.0-nightly.444.tgz#cfc546cebe4c8794b4c3bae20ee2bec30a5e759e" + integrity sha512-BCulOXUU2iuGdJqxv1yenXggD+BLhA9mSBAfx7Wvlye1D9tAamuqXN4V4oROk4sFY6UK6wFSMR7zMlS4QsuAVg== dependencies: chalk "^2.4.2" emphasize "^2.1.0" slice-ansi "^4.0.0" string-width "^4.2.0" -"@parcel/config-default@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.0.0-nightly.434.tgz#6b830e65252caa1c5bb7aedb248ff61fa90c326a" - integrity sha512-aGYSfV+MM8qC+mrNGSh0+ZXBa0IGCyyiM9kReD/L7KgwTWgO59RXqg7B37j5uV5e8p5l9nc+CC/fjX1zRDu8Ig== - dependencies: - "@parcel/bundler-default" "2.0.0-nightly.434+146cffb6" - "@parcel/namer-default" "2.0.0-nightly.434+146cffb6" - "@parcel/optimizer-cssnano" "2.0.0-nightly.434+146cffb6" - "@parcel/optimizer-data-url" "2.0.0-nightly.434+146cffb6" - "@parcel/optimizer-htmlnano" "2.0.0-nightly.434+146cffb6" - "@parcel/optimizer-terser" "2.0.0-nightly.434+146cffb6" - "@parcel/packager-css" "2.0.0-nightly.434+146cffb6" - "@parcel/packager-html" "2.0.0-nightly.434+146cffb6" - "@parcel/packager-js" "2.0.0-nightly.434+146cffb6" - "@parcel/packager-raw" "2.0.0-nightly.434+146cffb6" - "@parcel/packager-raw-url" "2.0.0-nightly.2056+146cffb6" - "@parcel/packager-ts" "2.0.0-nightly.434+146cffb6" - "@parcel/reporter-bundle-analyzer" "2.0.0-nightly.2056+146cffb6" - "@parcel/reporter-bundle-buddy" "2.0.0-nightly.2056+146cffb6" - "@parcel/reporter-cli" "2.0.0-nightly.434+146cffb6" - "@parcel/reporter-dev-server" "2.0.0-nightly.434+146cffb6" - "@parcel/resolver-default" "2.0.0-nightly.434+146cffb6" - "@parcel/runtime-browser-hmr" "2.0.0-nightly.434+146cffb6" - "@parcel/runtime-js" "2.0.0-nightly.434+146cffb6" - "@parcel/runtime-react-refresh" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-babel" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-coffeescript" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-css" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-glsl" "2.0.0-nightly.2056+146cffb6" - "@parcel/transformer-graphql" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-html" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-image" "2.0.0-nightly.2056+146cffb6" - "@parcel/transformer-inline-string" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-js" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-json" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-jsonld" "2.0.0-nightly.2056+146cffb6" - "@parcel/transformer-less" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-mdx" "2.0.0-nightly.2056+146cffb6" - "@parcel/transformer-postcss" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-posthtml" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-pug" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-raw" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-react-refresh-babel" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-react-refresh-wrap" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-sass" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-stylus" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-sugarss" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-toml" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-typescript-types" "2.0.0-nightly.434+146cffb6" - "@parcel/transformer-vue" "2.0.0-nightly.2056+146cffb6" - "@parcel/transformer-yaml" "2.0.0-nightly.434+146cffb6" - -"@parcel/core@2.0.0-nightly.432+146cffb6": - version "2.0.0-nightly.432" - resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.0.0-nightly.432.tgz#fd230c0349532530f18b0b865c3803557a327dec" - integrity sha512-pM7PKWmyzf6l+zXI1mUbpDx1KfSLcEMl6jyOuZL7gHSBKFiKCT+DzW8hkmR+Hr2g/sCzlGFey0pN49yYPOGKqQ== - dependencies: - "@parcel/cache" "2.0.0-nightly.434+146cffb6" - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/events" "2.0.0-nightly.434+146cffb6" - "@parcel/fs" "2.0.0-nightly.434+146cffb6" - "@parcel/logger" "2.0.0-nightly.434+146cffb6" - "@parcel/package-manager" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" +"@parcel/config-default@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.0.0-nightly.444.tgz#757c0a17291486b845ffbe11eb24e0d43bdec372" + integrity sha512-N7Jxznaz6CCb9EUL3jrxhTySWjYFkCLXqRZCQInyqeawXQHP5esemBUEhe0uybzFjt+vUUKxVTXE2qZQb+YEsA== + dependencies: + "@parcel/bundler-default" "2.0.0-nightly.444+e2136965" + "@parcel/namer-default" "2.0.0-nightly.444+e2136965" + "@parcel/optimizer-cssnano" "2.0.0-nightly.444+e2136965" + "@parcel/optimizer-data-url" "2.0.0-nightly.444+e2136965" + "@parcel/optimizer-htmlnano" "2.0.0-nightly.444+e2136965" + "@parcel/optimizer-terser" "2.0.0-nightly.444+e2136965" + "@parcel/packager-css" "2.0.0-nightly.444+e2136965" + "@parcel/packager-html" "2.0.0-nightly.444+e2136965" + "@parcel/packager-js" "2.0.0-nightly.444+e2136965" + "@parcel/packager-raw" "2.0.0-nightly.444+e2136965" + "@parcel/packager-raw-url" "2.0.0-nightly.2066+e2136965" + "@parcel/packager-ts" "2.0.0-nightly.444+e2136965" + "@parcel/reporter-bundle-analyzer" "2.0.0-nightly.2066+e2136965" + "@parcel/reporter-bundle-buddy" "2.0.0-nightly.2066+e2136965" + "@parcel/reporter-cli" "2.0.0-nightly.444+e2136965" + "@parcel/reporter-dev-server" "2.0.0-nightly.444+e2136965" + "@parcel/resolver-default" "2.0.0-nightly.444+e2136965" + "@parcel/runtime-browser-hmr" "2.0.0-nightly.444+e2136965" + "@parcel/runtime-js" "2.0.0-nightly.444+e2136965" + "@parcel/runtime-react-refresh" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-babel" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-coffeescript" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-css" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-glsl" "2.0.0-nightly.2066+e2136965" + "@parcel/transformer-graphql" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-html" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-image" "2.0.0-nightly.2066+e2136965" + "@parcel/transformer-inline-string" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-js" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-json" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-jsonld" "2.0.0-nightly.2066+e2136965" + "@parcel/transformer-less" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-mdx" "2.0.0-nightly.2066+e2136965" + "@parcel/transformer-postcss" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-posthtml" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-pug" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-raw" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-react-refresh-babel" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-react-refresh-wrap" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-sass" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-stylus" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-sugarss" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-toml" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-typescript-types" "2.0.0-nightly.444+e2136965" + "@parcel/transformer-vue" "2.0.0-nightly.2066+e2136965" + "@parcel/transformer-yaml" "2.0.0-nightly.444+e2136965" + +"@parcel/core@2.0.0-nightly.442+e2136965": + version "2.0.0-nightly.442" + resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.0.0-nightly.442.tgz#058195bb3c4f333874c923488a4fcdafac28c925" + integrity sha512-sEWAqRYCyBG+Cf8bvFs7rbMQovZivaUwUcnbqwaBhoUqmoHuxUJnBH2z0ht5TqFrzGiyZmNWXa3F4s875QzbMA== + dependencies: + "@parcel/cache" "2.0.0-nightly.444+e2136965" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/events" "2.0.0-nightly.444+e2136965" + "@parcel/fs" "2.0.0-nightly.444+e2136965" + "@parcel/logger" "2.0.0-nightly.444+e2136965" + "@parcel/package-manager" "2.0.0-nightly.444+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/types" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" - "@parcel/workers" "2.0.0-nightly.434+146cffb6" + "@parcel/types" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" + "@parcel/workers" "2.0.0-nightly.444+e2136965" abortcontroller-polyfill "^1.1.9" base-x "^3.0.8" browserslist "^4.6.6" @@ -2287,72 +2289,72 @@ querystring "^0.2.0" semver "^5.4.1" -"@parcel/diagnostic@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.0.0-nightly.434.tgz#40a08090cd88d498fae13d7d8c53ce0518a6d824" - integrity sha512-4a54sXQUs9LjreNSH5piZgHbXQDhOv7hb6s1PSD0BEf3h7uxIJttu2R13WcY2OxfTEdGwabbB5HzuSObJpsvaw== +"@parcel/diagnostic@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.0.0-nightly.444.tgz#ed1db5d1b4b30ee9490c03b07c38dca4790be426" + integrity sha512-nGlyju2MlkcufTZkFMZiOtk7/YKF7+L8ScWpM6i2W6nMTfDxh9OTlO68FBgXTYFEqr9eVeh4vzXZP6Dr4UQ3aQ== dependencies: json-source-map "^0.6.1" nullthrows "^1.1.1" -"@parcel/events@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.0.0-nightly.434.tgz#944e69e93292a3cbc84d4991b20bcd13207cb682" - integrity sha512-zI8rtX8x8Z5BrqHH1MhNJVWIfmJA/ihrLEWC4xjqm0R6+hnx51wvpFG1Nt059cSq7M9TTrrWB63xT8YWnPR91w== +"@parcel/events@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.0.0-nightly.444.tgz#6f8cfa720590ffa52c763ab2a17137425de47993" + integrity sha512-r6EDHAWippZd2Zj24O4XQYvm3we9wExlmj+nVzAz4l4j8GQdzhS+qMyUsGh9UFOT0y0/SxyKhbxFpH27KqirgQ== -"@parcel/fs-write-stream-atomic@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/fs-write-stream-atomic/-/fs-write-stream-atomic-2.0.0-nightly.2056.tgz#a25e56fae139d8a3f69d7cbcf039029b9b8c81be" - integrity sha512-pVVxzPdUvF+ZK/FPld+TYzME+5O6kmcaY4QTyhBoSpiikYcKrvg87pAH4L/Y9JGMokYpS8QizYGwSRf3gqN8AA== +"@parcel/fs-write-stream-atomic@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/fs-write-stream-atomic/-/fs-write-stream-atomic-2.0.0-nightly.2066.tgz#1b47bd5b64a4f83071323ed5587c827af6571986" + integrity sha512-mM/MXdDQmxD+K3o8Bjmt9tnEzDPyKUYy55OhFCNfAAn2WVjKp+lXX/Qd+naIX2/D4yzsFa0FsXC17n1wpAh4Dw== dependencies: graceful-fs "^4.1.2" iferr "^1.0.2" imurmurhash "^0.1.4" readable-stream "1 || 2" -"@parcel/fs@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.0.0-nightly.434.tgz#966ea521886ef556fb85368e4e0f789ac0287f10" - integrity sha512-S/ChW/OOLk93TPH3axORIXJYb4o1p/OOg/juRD+fluu/a46S0rHXAvfX4U/SIw07xEO5nIZbGFiijdtcmgVP7Q== +"@parcel/fs@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.0.0-nightly.444.tgz#3ca648e57120df1f279f6fe706454c14140664a2" + integrity sha512-csiTigyfyoJlb/nbt3Ga1Ib8CkfRboNMAwdrnHp0pc7omqx733B9Xs8iImNfqR1KLdL5yD8oIqqpzB8Kr24GfQ== dependencies: - "@parcel/fs-write-stream-atomic" "2.0.0-nightly.2056+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/fs-write-stream-atomic" "2.0.0-nightly.2066+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" "@parcel/watcher" "2.0.0-alpha.8" - "@parcel/workers" "2.0.0-nightly.434+146cffb6" + "@parcel/workers" "2.0.0-nightly.444+e2136965" graceful-fs "^4.2.4" mkdirp "^0.5.1" ncp "^2.0.0" nullthrows "^1.1.1" - rimraf "^2.6.2" + rimraf "^3.0.2" -"@parcel/logger@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.0.0-nightly.434.tgz#06a84f8c6ae8ab16c00eaf62df253dcb4ed9d19c" - integrity sha512-JQqvlYVT6AHnWowpAB0iRRdcQKqfCkgtE2D6IAgC0DyRX5fkn5IfkcUgapdOPf1eEmE24g0y4Iao6Scyz1nEkw== +"@parcel/logger@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.0.0-nightly.444.tgz#217a523f8f342fb49eb591c90e048b01d879b739" + integrity sha512-oSIVRt/w/JiKeD0nnrW/XecAHHtxjPeQvY6wOqp6h1gTrnE9Id6WBOB8up42JkcNsRGicC+FXa7ag252BBjxcQ== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/events" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/events" "2.0.0-nightly.444+e2136965" -"@parcel/markdown-ansi@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.0.0-nightly.434.tgz#3f731c66b65178e9b945afa30847c81da91734e7" - integrity sha512-Ab+4CDD/a15vrogld1Q6xD8c5aFLIrwhXriPjSfcLqs9xopNKXWuQ9C/wJki41/tdTohqILYqXfIR/ZOYNZdeA== +"@parcel/markdown-ansi@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.0.0-nightly.444.tgz#2f2fbca0fe5eeb79d5eb205a99e1c6d346c72181" + integrity sha512-0HiYTFpdIc4/v/ZkTo7CqhfiQSXBvekX/1mb2hk2LG7khQcmqXFeSXH/GohlHnY9NZBZNmH+LsBVQ0VWR9V/ag== dependencies: chalk "^2.4.2" -"@parcel/namer-default@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.0.0-nightly.434.tgz#adba98ed883ba9b7111c9dc756b41e062725daa9" - integrity sha512-TqDlaWHH/eg4158qlXm+MC0OxrFTN5x83L9PWRiCRHRi5Wk0Ax2GKYu0q4RtOOGoSIS0X145CRpzmQVLP2HkAw== +"@parcel/namer-default@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.0.0-nightly.444.tgz#8fa2c0c98a8b706feb3d89af8ff89850b2a004b3" + integrity sha512-qJvXfXnPKM7eZedxV+Um+1UxN1QugFYkH6eNJx1HvbxV/Vwj0OBJ8AM4BobqPkoA1IxxsPW/d8uthCK+XVqsxA== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" -"@parcel/node-libs-browser@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/node-libs-browser/-/node-libs-browser-2.0.0-nightly.2056.tgz#f9fa82ba72e04b97c30f3d5c41da0e600859e449" - integrity sha512-7GCMoMPEqh5lWRAdibuvT6UfPZbIhWutI/3mHLpMJQjggpkZAe9nx/pXqEwe+MKQH6aj7bXUsSuf/i57O9qONQ== +"@parcel/node-libs-browser@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/node-libs-browser/-/node-libs-browser-2.0.0-nightly.2066.tgz#78dd59bf9e4e136c0f115c27bb18e3d0145a120e" + integrity sha512-nzDPDjFqIqoQ68DEnVTP4GAFm28gililBSWTLWyWOXfLiN03WO5Fa2uCEQRBbod+Tlb8eonwcrglza8lxGlZOg== dependencies: assert "^2.0.0" browserify-zlib "^0.2.0" @@ -2377,71 +2379,71 @@ util "^0.12.3" vm-browserify "^1.1.2" -"@parcel/node-resolver-core@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-2.0.0-nightly.2056.tgz#9f2604b7d082ef644308fff0035e590d965125d5" - integrity sha512-RKl0offRQPO345exJ6kfK4k+cFMQqHKdj5JCGpKtxkpe/T5pL2/wryhXNkpYfqboFd6oH5i+cHxWRisOxQzcnQ== +"@parcel/node-resolver-core@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-2.0.0-nightly.2066.tgz#2b4b318c250d39c2580a3e43d840eaa8c4b625b6" + integrity sha512-qOujpQyPwVStiU+bHYkl9JTgl14eSQzmrIsXJ/RNrSHfadqgkgUcNp5DFPppqDmnLXIp7YwXl6mxjtgfFgb4Zg== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/node-libs-browser" "2.0.0-nightly.2056+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/node-libs-browser" "2.0.0-nightly.2066+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" micromatch "^3.0.4" nullthrows "^1.1.1" querystring "^0.2.0" -"@parcel/optimizer-cssnano@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-cssnano/-/optimizer-cssnano-2.0.0-nightly.434.tgz#3c5822c1c2f4a91150edf9658dd90beb8b393437" - integrity sha512-CDkGSTpu8vNZt7s15jmSusXFu97/e43eaFPdLWWirTjCeOWetzVUxhnE34spE/0lLWsetUNogQ4Jx9w09wy+bg== +"@parcel/optimizer-cssnano@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-cssnano/-/optimizer-cssnano-2.0.0-nightly.444.tgz#3ec149c63adb9a34b91596c2366f425bd54791f9" + integrity sha512-mKlXZlZfm1oe4cmUVh8CciqMQaO1wNZfkm0zQn5hLeAfI2Sd6krWNOesccE9+o6+7PlWVA1VP5g9cJbLOeyCqw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" cssnano "^4.1.10" postcss "^8.0.5" -"@parcel/optimizer-data-url@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-data-url/-/optimizer-data-url-2.0.0-nightly.434.tgz#d6c68ec93414bf9e794e09374b66722f94cef39d" - integrity sha512-A5bOEW8VdVO5PU53tWyKQkIEjplO9SIia0bWAnsEilZahzYO2jG29WdNDta7VNc8XM4/Ovy0Z/sxUxT9lZNMFA== +"@parcel/optimizer-data-url@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-data-url/-/optimizer-data-url-2.0.0-nightly.444.tgz#381d1d5f0f91eb6822a05381326b6d769bda1616" + integrity sha512-s2f2fWIiWKl8CxgPL68JHt90qgxJ3UTYa7bhG2CqFnuBqxLkGAorqYHPq+ElRHy9pjKZuvGL5sOfFPYhmcUuLg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" isbinaryfile "^4.0.2" mime "^2.4.4" -"@parcel/optimizer-htmlnano@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.0.0-nightly.434.tgz#eca364a376dc164df970f73e2ae7889a0585b930" - integrity sha512-JcwRgceO1QVzekre4M/nE2SYmsrjkP7k7tC8aZY26phry/CcdaNXpSyfoMkJ2xag/1U8ZP8DGotln+iJrO8AHw== +"@parcel/optimizer-htmlnano@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.0.0-nightly.444.tgz#5008efbd4cd98b74c3edd197245827e32bde3458" + integrity sha512-uLN3Kj7n6Ppbic1EP4I8T8ZKktVz44TbcEV1B98HD0CEviThMOKIw+XG1UnI2rpbP//d13Qy/pHEl2YoXlr9uA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" htmlnano "^0.2.2" nullthrows "^1.1.1" posthtml "^0.11.3" -"@parcel/optimizer-terser@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/optimizer-terser/-/optimizer-terser-2.0.0-nightly.434.tgz#f277e6a6fdc56d8e7d00d2694b29aa614c605615" - integrity sha512-IFg+6pOwNmMKcuZd8AVG0xdZsg0ndERUFimD+XROq5hadv/QRUY+Xs/sFpJs8LG33h0PDcTFqvtoxKA/VYTkhg== +"@parcel/optimizer-terser@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-terser/-/optimizer-terser-2.0.0-nightly.444.tgz#4eacd7ea22fe11c2fc65a66103b23cb4b9e08e02" + integrity sha512-DvauanAQ8ywyZYPtluq0FnbB2VNv5rzRsh8q3i3MKE0ElSN9aTn5MSKBMMzTIdVk5M8jzNldQW3toaXe83LhcA== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" terser "^5.2.0" -"@parcel/package-manager@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.0.0-nightly.434.tgz#c10023364890c1407c8bc61304c6c473fbd26187" - integrity sha512-He9F62ZWZOnb7l4Oe6xV+afLsZv/5yR8On0lQY4Cuy7LO0Aemud4SS192gS0i8Ns+l5CHDap/M6tpYEKmQ1Urg== +"@parcel/package-manager@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.0.0-nightly.444.tgz#98414d390b72aa96eb5d1783f709b4730139e395" + integrity sha512-FS+neGRcUGnOxRMnRPsrg9yu5YotgrGZepm/cTVnitLJh/de4dXto+GIJWlI/yjNdYPRWgObn+dnaJfTNg/3cQ== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/fs" "2.0.0-nightly.434+146cffb6" - "@parcel/logger" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" - "@parcel/workers" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/fs" "2.0.0-nightly.444+e2136965" + "@parcel/logger" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" + "@parcel/workers" "2.0.0-nightly.444+e2136965" command-exists "^1.2.6" cross-spawn "^6.0.4" nullthrows "^1.1.1" @@ -2449,91 +2451,91 @@ semver "^5.4.1" split2 "^3.1.1" -"@parcel/packager-css@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.0.0-nightly.434.tgz#c4c8d96bdef390f23e3caffa5b441ab58c944ec0" - integrity sha512-vcAelclqSXoG5mXJeD3LG1l/Pi/VX1ULp8QmSQ/4dh3bwa7YvEC/1XHQHqUwFxMcOtVQHu7GLQBjpPb/vBxHJg== +"@parcel/packager-css@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.0.0-nightly.444.tgz#43b5f848b0ccee2c2a11016d0f7734a29104f53b" + integrity sha512-jQRxz00jHtnbh8vunIjYzGYYxvyC0ToSh3gh2uiGogbHWtTzdWhL6x3cBOQq7n4B4BlBKx6KFqARnE6lcDHMFw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.444+e2136965" -"@parcel/packager-html@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.0.0-nightly.434.tgz#fba28b8bb027d07ddc36c63d9f575c5812be7eb1" - integrity sha512-G5lHBn7xfRDr5cNp558CFk46Ivnk4N7G+az58mSTS6byB3k1ZQEldnxYUMtUBpPxZXp8FRMw0FkntoHo/H1p/w== +"@parcel/packager-html@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.0.0-nightly.444.tgz#786245074813277a6d41d6acf727572470335978" + integrity sha512-azFEsn9PzWXpollTLN0qpvy8wjgl6OuOP5IF1JJm9sa3k0mWQNNiNSKKkPhQVYIxPXhcXIUFGY8QZDsdAzVIzg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/types" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/types" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" posthtml "^0.11.3" -"@parcel/packager-js@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.0.0-nightly.434.tgz#7ae693501d7497171562ab9b2599170e4e5ca3d1" - integrity sha512-FuKEGO9XlaPDBeGDMMeMPFtrHE5t9kE1iM2Xx/x4wu+4zBopSZTi3XRaqbQ2QCldpss53x/5OclgDUww6Qd1zA== +"@parcel/packager-js@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.0.0-nightly.444.tgz#a456bf058cffaafbb73ae7fd76b0734d6bc93619" + integrity sha512-xKNIXEQpkuLKWqjaj0Y582P8cqe+J2F1zWxUKbGHeaBvGFjnLqWtHA6SpP44/JyasnmdHfCOcz5BGyBY/6gG2w== dependencies: "@babel/traverse" "^7.2.3" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/scope-hoisting" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/scope-hoisting" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" -"@parcel/packager-raw-url@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/packager-raw-url/-/packager-raw-url-2.0.0-nightly.2056.tgz#b5c8603f9c690a86635272f86ae16f9466b94fa6" - integrity sha512-zbzebs44fDhJjkyx+RYUwy6Lnil+XkEJVy7UbFy0E+HVD9yiyZRA+8p+4H4+0HgCtdeJBRWx8vBjyfh4ehMN6Q== +"@parcel/packager-raw-url@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/packager-raw-url/-/packager-raw-url-2.0.0-nightly.2066.tgz#914a8cd87a6a39a5409cbacd79b0bee4ca230071" + integrity sha512-THMH4Oa7x679lew3praIt0xYOCykMWuH5/DRt8AdOVXTGDYc5JfARHvXZc4SI0ZQCqksBhU22B436hePqH/qSA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" -"@parcel/packager-raw@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.0.0-nightly.434.tgz#de4357d51ab4edfa252f4dda2f3973d3583d6783" - integrity sha512-8xy6j64me3C7LIs7ldoOjhnb/BSeElXFd0na3hKijUXLAm8lOfxmPjyhW6CNO0cE0SzhDQ0pc5OTnW3uBlWzvA== +"@parcel/packager-raw@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.0.0-nightly.444.tgz#4b904509fd4b7567056032e216bc8f9868327de6" + integrity sha512-/CI4PpeelABTja4oYnTtANWYYfr/z7u+9YNz84w3zJOynQJ20zAVatYOH0N+4KapDfoSz27MfQujNwhrax3l1w== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/packager-ts@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/packager-ts/-/packager-ts-2.0.0-nightly.434.tgz#6d383753051ea32b264df6abfb45eefebbf1138c" - integrity sha512-180o4rKA+1L1z/k2YQyErXnhVXNyVSqwLDb/6gBkQD6t1I6nEIXv/y4U7OW1mBRFKOBHcay/oQF3qUZdo3gpCA== +"@parcel/packager-ts@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/packager-ts/-/packager-ts-2.0.0-nightly.444.tgz#21957c442510b4e2b9f51a1a167874a9d6283506" + integrity sha512-Mr0tiSV5+0f0bTDouFQBmA/gR2x8fbqFOOtLGHmRjn+1X5+20ClrcQPnwPCiuvwd4PhFn7BahrI+YJsGtxw9vg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/plugin@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.0.0-nightly.434.tgz#e7134b0d4719b080f9d015c8390f5f9285d605ff" - integrity sha512-F6XERkc4Y7NeJiys6f5gMeu33suciEaQmLvnyt4ShYeX1ZWKjoze0kt1A1AQf3U3h7wZE4JpzAqjFm7ojdsJPg== +"@parcel/plugin@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.0.0-nightly.444.tgz#ac6a0bdb861d7b76fad28060ca74cf4735d1b23c" + integrity sha512-7EIliO7OHlzljKeb9n353xahcdGh4F6WIuu1lrM82bB4tsmnvFDeLetmEXppKo1brr7I3J15joF15TtZi7wBaQ== dependencies: - "@parcel/types" "2.0.0-nightly.434+146cffb6" + "@parcel/types" "2.0.0-nightly.444+e2136965" -"@parcel/reporter-bundle-analyzer@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/reporter-bundle-analyzer/-/reporter-bundle-analyzer-2.0.0-nightly.2056.tgz#de8265f5e142d71e21fcb65fb935251887f9fcfe" - integrity sha512-3HdVmrE0zb6NyVgttG+8T0cnLXSfqpgYrZUIMm7IS13iZVsL5yY3aPf5XLoFzo+9ZHlzY+ViPo5f5kqR2ilPqw== +"@parcel/reporter-bundle-analyzer@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/reporter-bundle-analyzer/-/reporter-bundle-analyzer-2.0.0-nightly.2066.tgz#1dc919854376ef9124cee2de2064346b1fab8c9c" + integrity sha512-OUWFYSWLkOoqta2hQuVb4wctStJ96gsolNGzOjANTdirOIYcN8IH9EcDPic0KNZTdRQYS4K5Yr7TkNU2x/JlsQ== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" -"@parcel/reporter-bundle-buddy@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/reporter-bundle-buddy/-/reporter-bundle-buddy-2.0.0-nightly.2056.tgz#39c2bac29bfd2034140e391daa365ddf8dd3f285" - integrity sha512-rUhKqzPVHRNEILF6wtoHXqvV+XduQqLBOtyBcCYN0C6jEwh51teDu+1GoP5OhdPRAIY2kkQoMadkO/Q6LKk8qQ== +"@parcel/reporter-bundle-buddy@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/reporter-bundle-buddy/-/reporter-bundle-buddy-2.0.0-nightly.2066.tgz#c4e7a1031895458e769166856d4bf0625b3eb1d9" + integrity sha512-Vh+Hfq1ynme6AZPKXPa/LCacTX3GzgpxM8xIIuXCbvLa6yK2Mx6P8CUz8uW13Sjkw+6HPmmVKVFv7FMCwPAB0A== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/reporter-cli@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.0.0-nightly.434.tgz#5fe891d0ec692bf80200a9bd5a8dd250ebfda246" - integrity sha512-7p4cV9nXqWC0M3P4NrJZ9IecIVEE0HGBb3k34bLHlrvL4bRoaHpoe2+T1QvyrNYesUf+CUBlRXohe4h3QEw4yQ== +"@parcel/reporter-cli@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.0.0-nightly.444.tgz#d87857853edf7395be3aa905ba35a5e0155e990e" + integrity sha512-4eHfegZGPVbEa4wWjbfSI02n+QjUEiMo2slxvEL8kLTjs01n7T76Z4QTOlqofOuymkF3vyI3PZtw+jSDfSJK/g== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/types" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/types" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" chalk "^3.0.0" filesize "^3.6.0" nullthrows "^1.1.1" @@ -2542,13 +2544,13 @@ strip-ansi "^6.0.0" term-size "^2.1.1" -"@parcel/reporter-dev-server@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.0.0-nightly.434.tgz#6637e29703b225cfedb43f5145936df5257430b6" - integrity sha512-ZloWZBo1DqMKaQBqs5hPMblySCDd76l4oY8vpmPYvdtO1skWY6VDL7aoVp4qkPPbXmECnvxdys9e2uIqtH/8rw== +"@parcel/reporter-dev-server@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.0.0-nightly.444.tgz#0c45bc2e7c2a89297c18833ba0d171a7a645abdc" + integrity sha512-kjnAkdHkOoaUTS3+aVC42w+tGf7U6f5gWjeloKl1TkuChbV0dzNzyCGJHLve4tBtBoyAscb+hhOR96WFbBTs+w== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" connect "^3.7.0" ejs "^2.6.1" http-proxy-middleware "^1.0.0" @@ -2556,54 +2558,54 @@ serve-handler "^6.0.0" ws "^6.2.0" -"@parcel/resolver-default@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.0.0-nightly.434.tgz#5e9ca29004097580bf3c40fe7f18571ec40550fb" - integrity sha512-Zy2wSAwRgZcW8Wug/yFFhI/FNOh9ElMcl96nJhY1YFK6wjwfRDj84vx3fSWJmWofHUOwe9eLmTgaL0Rn+83pEQ== +"@parcel/resolver-default@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.0.0-nightly.444.tgz#4ee9126d5989cbfe72c6daa0e70c2ddd50615e89" + integrity sha512-fIIw8FV35XoaTYsBZ4Y5ObtxujhUMYkmGbRSIapCr7kUqXCoPsxuUbEM5WA9dYvZ9i+6nh7wfzIT1IMn0Quv/g== dependencies: - "@parcel/node-resolver-core" "2.0.0-nightly.2056+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/node-resolver-core" "2.0.0-nightly.2066+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/runtime-browser-hmr@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.0.0-nightly.434.tgz#ec49a1df9def8c206dc401b28806ad12656785e4" - integrity sha512-xCDn17bRGHvWd8ygPmUo3yJX9lRngX5WeMwbF5DwvdxTjsg26y+MI8tF2wlHjdOl472ESgv1ZXnBIblDFO3wJw== +"@parcel/runtime-browser-hmr@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.0.0-nightly.444.tgz#f56f3ad6beb9db061bb033c43ad873cf688d736d" + integrity sha512-S5cEICwMvMm+zfyvrOKTmj0jwc1zGE2t9eKTCqJ87c2WGJZwPHqawc43cWj6jkaEtxIsNf5m3CwNhKrEosWaBg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" -"@parcel/runtime-js@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.0.0-nightly.434.tgz#cadb98b1968950d606cdf96a97b178af121adc68" - integrity sha512-z2B+Sb3ObKoDnqrQU5jrL+Fg5Woy/csW3QtOX4lSmyoZp1NpaTQaB5KpBPe4WrusWMBEXPLea3b8MWAmlrx1rQ== +"@parcel/runtime-js@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.0.0-nightly.444.tgz#c466f4237460518826ea96117438fae7cee3a407" + integrity sha512-k6hbNdxBptsg3AYztDNoeLlQ2FV/RbIM+qCADYslynIYFRJv8BiXQwOwusioUTscUVvFqfOHFxq67eOhLPYjpw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" -"@parcel/runtime-react-refresh@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.0.0-nightly.434.tgz#28df595c871aeca0ad0fe571328a1803de4f3119" - integrity sha512-KjNDA+z45DIPGybXIiyV2aVxCDYjZAq/nEZc07mVzuQ1jNm0rCR0ndgHQT71rAGUdkAqdOnCSdnN0sVuv2nNXQ== +"@parcel/runtime-react-refresh@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.0.0-nightly.444.tgz#99fbbab6da0859a08661e8eeaad06e9e4870f081" + integrity sha512-HS+aGPvVg9LlkPiBiBvhMV4b8tanEiy++V5E7weOjqMMfVxHZsJDg6Q1MKyedsgC4rvk2v5jmFsbgqk0hbjBag== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - react-refresh "^0.6.0" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + react-refresh "^0.9.0" -"@parcel/scope-hoisting@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/scope-hoisting/-/scope-hoisting-2.0.0-nightly.434.tgz#145ebc92ca90f956b1c6b50cc38aade4d5ffb7b7" - integrity sha512-YUxBMSGWPPxmjTjD/1YA5twcBDWKYmo1p1CNUZKp6Toa0R0tiyTrzy3EQEZKzFR0AVMsLUd4wcjBLodnl5/SKQ== +"@parcel/scope-hoisting@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/scope-hoisting/-/scope-hoisting-2.0.0-nightly.444.tgz#1c46e31469259f489ab4c1e8d89a27231d4c4ec2" + integrity sha512-lHBWPFnWrqusp1XPvTqJaa4oSFALrjvsj9Ae31qJOStsbOxrpJCzJPl6+sJfmYSjH2IizwlQjWiXq4PKrq1tRQ== dependencies: "@babel/generator" "^7.3.3" "@babel/parser" "^7.0.0" "@babel/template" "^7.4.0" "@babel/traverse" "^7.2.3" "@babel/types" "^7.3.3" - "@parcel/babel-ast-utils" "2.0.0-nightly.2056+146cffb6" - "@parcel/babylon-walk" "2.0.0-nightly.2056+146cffb6" - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" + "@parcel/babel-ast-utils" "2.0.0-nightly.2066+e2136965" + "@parcel/babylon-walk" "2.0.0-nightly.2066+e2136965" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" "@parcel/source-map@2.0.0-alpha.4.16": @@ -2614,10 +2616,10 @@ node-addon-api "^3.0.0" node-gyp-build "^4.2.2" -"@parcel/transformer-babel@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.0.0-nightly.434.tgz#9f6a998743d222444d0571865e8850cf577371c8" - integrity sha512-stlpy8s4cmRXwf1qS7tBNwGV1wcyNjirRwFN78/DTldEH6Is2cIyD7oNWjEULggwfLQCYi95Vw0dNFqPsxXdxw== +"@parcel/transformer-babel@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.0.0-nightly.444.tgz#c88446ceda04674c7552cbd4e96bfb5bf8c11dbe" + integrity sha512-/ADoA06rrvZN66/Xod8Fsy0xT/N6azn1XT9CoM0G2saXPyN9xpJINbTIT2tfci8zq8vdLoJGfpnP2MOYQyo38A== dependencies: "@babel/core" "^7.0.0" "@babel/generator" "^7.0.0" @@ -2627,85 +2629,85 @@ "@babel/preset-env" "^7.0.0" "@babel/preset-react" "^7.0.0" "@babel/traverse" "^7.0.0" - "@parcel/babel-ast-utils" "2.0.0-nightly.2056+146cffb6" - "@parcel/babel-preset-env" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/babel-ast-utils" "2.0.0-nightly.2066+e2136965" + "@parcel/babel-preset-env" "2.0.0-nightly.444+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" browserslist "^4.6.6" core-js "^3.2.1" nullthrows "^1.1.1" semver "^5.7.0" -"@parcel/transformer-coffeescript@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-coffeescript/-/transformer-coffeescript-2.0.0-nightly.434.tgz#e294131fc679f50a9ad99e547ae8f6f48930d457" - integrity sha512-uDJfJx/Q3PSZ9K5dQPi4xBM3z+1R5QCaAe9FxMbAVkoD2e2/RifalyMbvVrricz8jbGopRDFDacjW7XM3GeKgA== +"@parcel/transformer-coffeescript@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-coffeescript/-/transformer-coffeescript-2.0.0-nightly.444.tgz#abf0cb23a431084efae9752579db883fcc24033a" + integrity sha512-3KBX8NP6I3O4wnxC3mkqvgcOinKQIf4vmjlQryMysUKZt7rK8451J3PHQqVwlzK3ALHvLsWTC7I0JmXFR4hwUg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.444+e2136965" coffeescript "^2.0.3" nullthrows "^1.1.1" semver "^5.4.1" -"@parcel/transformer-css@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.0.0-nightly.434.tgz#f9610bca5f8ca1b754672159cd8d009b9d4cc5af" - integrity sha512-Te0QQ1tzs18LEODXX9rpAYpnN+3+MOmGnLSRdR3K1cHc4kdaN76jUKSiCC47UnJMm29I2DQ8aNfdnW/G6U9/tQ== +"@parcel/transformer-css@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.0.0-nightly.444.tgz#d9c3c83b5b5b87faa721254031b91d8f5da3a091" + integrity sha512-dxXsV4kAdRGRcdzALOEXG1rsV1Y270F17y2k6HdKRcsashOkAiEr3AxKkj3i8y3ztfnQleQcyr+TXX8ErX/EPg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.444+e2136965" postcss "^8.0.5" postcss-value-parser "^4.1.0" semver "^5.4.1" -"@parcel/transformer-glsl@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/transformer-glsl/-/transformer-glsl-2.0.0-nightly.2056.tgz#7615de1278d9d227b942ead9342ca3989c913260" - integrity sha512-FoO/POuYmahss2NewubSXNm1WLLgAREQMAOwkpIuHCY/zxu2cGADAZFX9OS7BlZugL/CJQ4jvPN561QC6SX0yw== +"@parcel/transformer-glsl@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/transformer-glsl/-/transformer-glsl-2.0.0-nightly.2066.tgz#e9a010dfa6da01decabd8c182a9c7e604b83a1a6" + integrity sha512-lLuoWjTS3Z+DHW4HSNnkFh6uG62gcxMjWRERlsozIzW9h3M86tYn/o8otd1veZfKQAj33cCmcSJ6VMSCsXlB2w== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-graphql@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-graphql/-/transformer-graphql-2.0.0-nightly.434.tgz#83a75180fc0f1abe1df31c961290d6732128a55f" - integrity sha512-YBYp2FRfSBrQKAkh1dkonHE6InWsES0EZkSeKm3fJEITfLeka6AlHeUeWdrVNcQOeiyJOhMcR+ebhWCAlk/o1w== +"@parcel/transformer-graphql@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-graphql/-/transformer-graphql-2.0.0-nightly.444.tgz#f307aef9fe2f8286ee34487d419d2398257edb6f" + integrity sha512-rT7AANVXFdhv+URURKvNGc7EyH4lsaQp5528BxN6lQ+LfmG9sjN7ZVcUp1YU0enm3ul4fiv1AUCFJhfCFgXNBA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-html@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.0.0-nightly.434.tgz#d340120036eaa0e13624949c3d721d00083be2ca" - integrity sha512-UJiTIL6lC8PtHbS77yceECunySZczto0yUE0fxwoL7pzibEbs8BC1HNTm4PgngglG8GD2zLeb/9/N3tquA5mEw== +"@parcel/transformer-html@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.0.0-nightly.444.tgz#525c8dd1d18347f3434f642f5a5867e8f43fa5ad" + integrity sha512-rZJOvDpNPdDq6dNroY86ofeDJQDV0N2z81dk1itR44lyWS/W6Xu4Nx1x7zp6bK/CMzoF6rk6mEATo1D0neEzlw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" posthtml "^0.11.3" posthtml-parser "^0.4.1" posthtml-render "^1.1.5" semver "^5.4.1" -"@parcel/transformer-image@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.0.0-nightly.2056.tgz#3871a636039daf8b7c4b07ce642f9922ef0611b3" - integrity sha512-e3FguqR0HbyxeLpAxaKre2mNVhdoRDLCVEg05nNpaztROKzbqV6ASEj7S87FWgvm1FwJfAv70XMLRNRQOzFJZQ== +"@parcel/transformer-image@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.0.0-nightly.2066.tgz#2919e3d84003c50aac70492bc1d24d3dc61578d7" + integrity sha512-NzEvG5JidDx71YlvmqM6jiQsUOEtfCvKdbGVXxiLj6GVrjtILwMhu1A3BI5f5ltasmCOa6q2ZlNnsxSuBOtiOg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-inline-string@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-inline-string/-/transformer-inline-string-2.0.0-nightly.434.tgz#69d29c6e53f8e49a8dade7f63f44777c9d0fb0f4" - integrity sha512-wD92e82sbAqKDis0iGR/QjgEgCQDSt8DrBqLoEdeQO3i63uNt3k5OBoBBz37C3D3GI0gPxBmIxBAloj7XGnVlw== +"@parcel/transformer-inline-string@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-inline-string/-/transformer-inline-string-2.0.0-nightly.444.tgz#b80d88074a5d10e44ac4287cbf7803283c6cdd6b" + integrity sha512-/OCAydewYjYaUKJwjBLOoRjqEvTZnqqZX3M75f9WhoaILoVqBZzX1HwSxkm5JG7UPc9252rCfLLY0H3MjreQGA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-js@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.0.0-nightly.434.tgz#5d600e49719ba9c65f34f9b9eb87352dafadb751" - integrity sha512-2qFWzhQmJUrSk+5WS1M5osSIe8umN503foLeS/tWYopAM+SJzRJewqRpDRUgP4L3JtAvWRRLy7eOcb+VkiY38w== +"@parcel/transformer-js@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.0.0-nightly.444.tgz#b3c9e90a1c461ba382d69c646cc2311f1217e9ca" + integrity sha512-/IpSYzhdBMwPuytsjCBYq5YrwatHeVhn2Hk+oa0YYnLuAwRqrVlDYuFGto1afmOsHNYlbt56ME8p2WNQu0BF3Q== dependencies: "@babel/core" "^7.0.0" "@babel/generator" "^7.0.0" @@ -2714,193 +2716,193 @@ "@babel/template" "^7.4.0" "@babel/traverse" "^7.0.0" "@babel/types" "^7.0.0" - "@parcel/babel-ast-utils" "2.0.0-nightly.2056+146cffb6" - "@parcel/babylon-walk" "2.0.0-nightly.2056+146cffb6" - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/scope-hoisting" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/babel-ast-utils" "2.0.0-nightly.2066+e2136965" + "@parcel/babylon-walk" "2.0.0-nightly.2066+e2136965" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/scope-hoisting" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" micromatch "^4.0.2" nullthrows "^1.1.1" semver "^5.4.1" -"@parcel/transformer-json@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.0.0-nightly.434.tgz#b432b6f43c07f9e869ebe3f836f35ba0d38ef826" - integrity sha512-CeaFtEEyTNC1c8JObEBIo2EI6PBxGhjV6XUGuXOAbNXrBsFdyRq8+M5kp0z1CZXQxuKuB8qpd+5YEE7te9m+KQ== +"@parcel/transformer-json@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.0.0-nightly.444.tgz#d2775cb08cb3e40e04ee173d86dae4dd33ab566a" + integrity sha512-DsdsFngJpq4j8TgGFGaP2DiQQHN1E8mQDDGvgJFcNjxAESYaVvWOXXANLdNA5pfnSnZP94BcdTb67ZU5a870kA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" json5 "^2.1.0" -"@parcel/transformer-jsonld@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/transformer-jsonld/-/transformer-jsonld-2.0.0-nightly.2056.tgz#103a7d2d1f8bd9fac1fa448dbda21ba84bbc1d12" - integrity sha512-DpiJtFfKXHtOxdP6Gdq7vAzHxssFb9Hq9bNqqxtDCtiKgHGNtpl9Zr34n16efG0T/uYqqiU3cfOWFmnm+AjZUg== +"@parcel/transformer-jsonld@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/transformer-jsonld/-/transformer-jsonld-2.0.0-nightly.2066.tgz#30321d699974c74fd595bf91f4b3675611982683" + integrity sha512-tt/avA5iW5HgxTkqubzhDtCpgrsR8VMddVDtuaB1S3fWUJkC25gDcyLabPWQ4HjQKQeRMtQ3HdRSJGFEY33OWQ== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/types" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/types" "2.0.0-nightly.444+e2136965" json5 "^2.1.2" -"@parcel/transformer-less@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-less/-/transformer-less-2.0.0-nightly.434.tgz#3320a4c295996fdafaa8e9bd468e22e1534b14f1" - integrity sha512-56D81BeWFMi5hsR8qoP1dzqmX2pU0rII1EIOjzeRn06yu0ScOdQkFlNvkN6b/u+KKzM/JSosbEJyKMZToTfpeA== +"@parcel/transformer-less@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-less/-/transformer-less-2.0.0-nightly.444.tgz#f356264dd5c4f0904a7b5d74d645bc51ae0c3857" + integrity sha512-rlAqR/Kxu6qBfkAhgDSkQClWYl+Oy0bIUJ8f8RGbj+EltCAUeUpVzM1V4/Cf1pskx2dA/ddbzU6HNPnTLnw4QQ== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" -"@parcel/transformer-mdx@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/transformer-mdx/-/transformer-mdx-2.0.0-nightly.2056.tgz#321492eeb92410c3102710e6810ba62b672ecf7e" - integrity sha512-ypivteof3yhLH5aqEUFF5vF+g9a8VomLd7WFEhir0YPAUJmgyhWouT6pwaNsqJz94X1LDfVAD91LM76LXn4mjg== +"@parcel/transformer-mdx@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/transformer-mdx/-/transformer-mdx-2.0.0-nightly.2066.tgz#6e198221e557e45cc69aa52967d6e08b77297fc6" + integrity sha512-vUlLXliFLswYEyluM3UnFlWJBP1UWTP0PsFAji9JnSgwJjJdd8VN2ionUgfxt127GCa2207nEwoCKFJndxRXaw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-postcss@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.0.0-nightly.434.tgz#740915497ba6d64eec75f906591ece20beceb118" - integrity sha512-1qGookpv/S0gT3f7RzgwHmvIGxExMoeKqR3kKVx6dD2mp/OnFd6q/apJqLdDSn8zmBUJh19W8Vxo0TIhTsCTeA== +"@parcel/transformer-postcss@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.0.0-nightly.444.tgz#1edaef51580df346e615008bd00b3cf603011ae1" + integrity sha512-lABqSvFMgwXc9GQk4ab4eACSZBoWWNQ29TM0ggyCMvWbmVCdRAyRWxcuihot1IeJseEW21/p3VzPwKvOViXE/A== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" css-modules-loader-core "^1.1.0" nullthrows "^1.1.1" postcss-value-parser "^4.1.0" semver "^5.4.1" -"@parcel/transformer-posthtml@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.0.0-nightly.434.tgz#d0e9c37beae66ca520c1c84724b164cb973f58b6" - integrity sha512-+/QBFXKsSil5CAU2+IW1mCEvxrUib370BYxj61z2OvLl+vIERHp9NM1/wiVQgn4vlqL/u7ikycBzGdgtqHsCww== +"@parcel/transformer-posthtml@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.0.0-nightly.444.tgz#23b8a5476d88612200c0e21fc8e6491b8474c85c" + integrity sha512-dKfoy86FoDaPic9I7J3h6zGbUy0qrAmIDIQJDoIySrhYlgr1qgJTEgKFg5s2E4IIzmw4leCQzVNYCDmEBHq3Qg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" posthtml "^0.11.3" posthtml-parser "^0.4.1" posthtml-render "^1.1.5" semver "^5.4.1" -"@parcel/transformer-pug@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-pug/-/transformer-pug-2.0.0-nightly.434.tgz#b425f2f9961fa4bfa7c43e6fc02ef51709e55774" - integrity sha512-nhvItcQCOZaK1Bd70/SfRpPbTCYdxdjcaGJN5xvCmI1datH36mbeQCyVNmf05BRYCD6UTJ+MVrrESUsYjnhQhg== +"@parcel/transformer-pug@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-pug/-/transformer-pug-2.0.0-nightly.444.tgz#194e02ec75d79360096db41fde133f3f6f542c6b" + integrity sha512-Z9W/PmpRG+MPUEi83tr2hUZ7sS42QHuWxo+7N0ODQ+oCiSnZjWWhMgOWnuXkqkTK49J8ELIkzMkX8qm21Yk4qg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-raw@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.0.0-nightly.434.tgz#0fbcc96fc722fa35b61ac1765b681de0ea009fca" - integrity sha512-F9KIct14mo89ehEPRo9oa+U+LHxVn9aXvjUbDnIGArj0O+mI6NcSKfhnGDfH0tTdh/i1jMtkT1tilW6PK3Yejw== +"@parcel/transformer-raw@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.0.0-nightly.444.tgz#d4a83b228310aa8ecf6e1d9bd85fbc38f9c57eb8" + integrity sha512-aKfHRTttYOG+084n8O/H6lG4bfg54Lfxl20/hg6NY2bANrfFKUz/2pvRxnv5zr24Wt4hvDKSuyfVnos6uw4i6g== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-react-refresh-babel@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-babel/-/transformer-react-refresh-babel-2.0.0-nightly.434.tgz#44aa1adf7a4516d88379012cca4f2c95ddfc2ed2" - integrity sha512-PCDMQsCTapoiM3W4FE8C6x/g4DA8nW8cs6UGAmKZZGQ0QqjqxnfvQoJ73uGSpgkRKmyuuxPU483fwA8xMDGqcQ== +"@parcel/transformer-react-refresh-babel@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-babel/-/transformer-react-refresh-babel-2.0.0-nightly.444.tgz#e34a1e5fb5023bfcfc5ced3727153c9a43732f7a" + integrity sha512-G9coZvaKEUwZLvzxQ66FOw9/KLNeKdSN2v7LeFG0V1SZ+pXOV7FWnEmTKF6cbBt4BCdin6QxncUNKKSL9vI+mw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - react-refresh "^0.6.0" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + react-refresh "^0.9.0" -"@parcel/transformer-react-refresh-wrap@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.0.0-nightly.434.tgz#95f1feb950a4c079f05286b355e5ead66abb6438" - integrity sha512-hWEfYqpv0PH8a78i2UoFCir7icPufm5xSiL9R7YegLTh2WMjiYWl890ao2dfPoCYCdRp1rhYEjXEwVPvEerYDQ== +"@parcel/transformer-react-refresh-wrap@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.0.0-nightly.444.tgz#8ea823786dab7f595c6dbadf1661f44131d766f6" + integrity sha512-iLZpR/CiMyNwGGIV+sJvfk+OkhFT372UBxDb2PdApp+2hVMAW97pwGb+X+Uz3dehvXoJtXDhWqT1OIXq09Yjkg== dependencies: "@babel/generator" "^7.0.0" "@babel/parser" "^7.0.0" "@babel/template" "^7.4.0" "@babel/types" "^7.0.0" - "@parcel/babel-ast-utils" "2.0.0-nightly.2056+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" - react-refresh "^0.6.0" + "@parcel/babel-ast-utils" "2.0.0-nightly.2066+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" + react-refresh "^0.9.0" semver "^5.4.1" -"@parcel/transformer-sass@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-sass/-/transformer-sass-2.0.0-nightly.434.tgz#62a11f9bb78651034a59e7fab4b8dd2d253eccdf" - integrity sha512-6T+HuwYXHt+06ms2fdWAMvlG/a3k30vZ8njiRn542BhNaeCaQ8hSS3BzQG1TPhvqLCAosIZi9YqETBAsriVhug== +"@parcel/transformer-sass@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-sass/-/transformer-sass-2.0.0-nightly.444.tgz#99a17fcf1107cc213a215ead7862f9795fa7cf34" + integrity sha512-0L8Ld2UdoCgslw5YEm+5TSqBcLgjdvW3paJv/y6d9d2Rqq6ol5x1aIeQ5lEELI6f4RgPaAcik2UIQzxhvW54og== dependencies: - "@parcel/fs" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/fs" "2.0.0-nightly.444+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-stylus@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-stylus/-/transformer-stylus-2.0.0-nightly.434.tgz#a1584bb84b652b22e4afefece38ca195d799dec3" - integrity sha512-06brKPO894GOs/8k5UxXayIhg2xYG6l/4WKArnCehN2If0ZLpgLlRMPxOEFdW4RROmFwnX3ye7EvLwTEKFROAg== +"@parcel/transformer-stylus@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-stylus/-/transformer-stylus-2.0.0-nightly.444.tgz#e7ea63c0fff39064dfb4788b5c6db6f76be5a054" + integrity sha512-dUfoeeWYivV1yPUWB6fGFzhyags1jPe0i1CKrsSgRkQ7vOEUZi7zWlJ1bZyUMBlZvNDDL8TQ0eEjvAb5Jx3DzQ== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-sugarss@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-sugarss/-/transformer-sugarss-2.0.0-nightly.434.tgz#6ca9fc6b7997b06082f09df6a627967265d3ab4d" - integrity sha512-poGDU4z5LwSmqnI05SILlamj+x2qGqd05WKNaxKyOH9TaGhAN4qZ8yjCfnCocEf+Kiw5dWfIUubVqHgs1tVBzg== +"@parcel/transformer-sugarss@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-sugarss/-/transformer-sugarss-2.0.0-nightly.444.tgz#f9502ababfd319278b39a7a27d11229534f45d10" + integrity sha512-MI0+tx8RbyBy0fYgCRDm0wSox7Uz3UkP/4WuUszOWuPpBM1y00bq+fbiEm/aaiX2UGMsncXPaNjbTHkeCJD0Yg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" postcss "^8.0.5" -"@parcel/transformer-toml@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-toml/-/transformer-toml-2.0.0-nightly.434.tgz#7cf42bd6fc483dca0db9de313ec98ffc3fb50268" - integrity sha512-XviVqL6ldpzZhwJJgsWi54BMVxA8uPgQPNaMUW+8vv0wYpSsqN4ulJxh2n8jTPheLQiqmENpRO+CFWKd44piMw== +"@parcel/transformer-toml@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-toml/-/transformer-toml-2.0.0-nightly.444.tgz#679faef0dcc7b67b0435e152ac9a75e09b006226" + integrity sha512-ad0iwx3K47r/EiatDGI0p/P8h/COKUcgwmbJFNVuVi9Aj6uueoNTQS/Sqto1Y+a/ZQZrWMUxnNei/JcYTG09gA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/transformer-typescript-types@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-typescript-types/-/transformer-typescript-types-2.0.0-nightly.434.tgz#350d086de67943c87327de68b05396c43c830743" - integrity sha512-hMtIFXFWkbtW5Vo+bMu/zlbtw9gN+op+RoSLbvcGxI4HtGNZf/KPeGWoPVKqCepoAuWBn8xgWTf65ojmXvDyfA== +"@parcel/transformer-typescript-types@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-typescript-types/-/transformer-typescript-types-2.0.0-nightly.444.tgz#f05a2280ce0cf99afceee6303bc8eb969c7ce244" + integrity sha512-kxUbCuemWLKnu3xqd/hEnLvbjf9AhLnc+RN7aA6EXAmXJ+K0Q/jWrXw+3B2enLtNEjrZ8Ig+6MrThpWIIpgKJw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/ts-utils" "2.0.0-nightly.434+146cffb6" + "@parcel/ts-utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" -"@parcel/transformer-vue@2.0.0-nightly.2056+146cffb6": - version "2.0.0-nightly.2056" - resolved "https://registry.yarnpkg.com/@parcel/transformer-vue/-/transformer-vue-2.0.0-nightly.2056.tgz#ffdcaea0b9b891dc42c1e03469e2805c6fdec491" - integrity sha512-EJhqBDLQ7qbq5Tq6JEr4YnRPI4ngK06L5b1jEVBaBv/OCk5O7kdyoItPzzgkAyE5Bwucj8p7ZeGScVD2vNZQpA== +"@parcel/transformer-vue@2.0.0-nightly.2066+e2136965": + version "2.0.0-nightly.2066" + resolved "https://registry.yarnpkg.com/@parcel/transformer-vue/-/transformer-vue-2.0.0-nightly.2066.tgz#8ab6d500ed5042f915392ce2e87fdbcdee34a2f9" + integrity sha512-nYxUCCodPkYgJy11/07YkHACJsepssEAkpvczdKBhWx5VLmvUksqdbYV90o+l9EcEXnV7hWjJHz/IYfFPROQDg== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.444+e2136965" nullthrows "^1.1.1" semver "^5.4.1" -"@parcel/transformer-yaml@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/transformer-yaml/-/transformer-yaml-2.0.0-nightly.434.tgz#e379a0f854f13a5bf2427a01a4f7606696d1c010" - integrity sha512-QdYl7vH7KRYr94kf500IQjybkjYfQldUN9HviZZmJ+ABH5C8mlPVC3LU/XuJQ0W+BOBVbP3aoqsRPAmKb2D1Zg== +"@parcel/transformer-yaml@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/transformer-yaml/-/transformer-yaml-2.0.0-nightly.444.tgz#40dec778624aa31090afe719070254f5170076b5" + integrity sha512-Qe54cAL4A2pfzfYJP1iOVwKexhiRwpRti+Ikw0HvmnLhu0nU5IyxZR9p1vFOATWFa4/VwmY4fbDJfsTPfWV//w== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.444+e2136965" -"@parcel/ts-utils@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/ts-utils/-/ts-utils-2.0.0-nightly.434.tgz#69a5766e40cdff353e9d2791c36fcaaa3e16123b" - integrity sha512-uYrLacAUQLb2nmEQ31iJ+VvoruVtgI4C+f+prhRyKwiq4/VIncGrWA1Z6t/tcSh8VsDYBA1GvTLQAijl4CGyIQ== +"@parcel/ts-utils@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/ts-utils/-/ts-utils-2.0.0-nightly.444.tgz#9e03e724afb01f9bda2b697a3cd7787f8f046bcd" + integrity sha512-rGzGAjzssAPCdYWSBtg+XHCqnJw7BCg0pyqVJTI6UiHCOUpJ8sRLbt/M+VCZ4wGuSL2mnLe/+TTBdyvpuDs9+Q== dependencies: nullthrows "^1.1.1" -"@parcel/types@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.0.0-nightly.434.tgz#34ea8af62584bfb42b1154a340acd00f704bb124" - integrity sha512-jRZEjGRsjHw2NhnZUtD1feo55076JQ09y64Fl8pWUWc+P3VK3sqX07nSy4LEbRJtApU7hxHL1ccnl+P56CsJsw== +"@parcel/types@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.0.0-nightly.444.tgz#967d0dffe4b8f6a5e1b38cf28ebb19963a36bab4" + integrity sha512-aokJZ8sd+eSI/LuWVhVKSJUhNQanBDaq31jzIzwZUIrIUI9KAYTutdcm7mcbsXCRx6U27YhiuxCmL/XDGAdZTQ== -"@parcel/utils@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.0.0-nightly.434.tgz#efe2dbe9345a81cbe364f695abc26239ed187de2" - integrity sha512-0lSVXshLl3tXD4YpSaNwvXnBOiRWpDTD0buOBY7qG38zzTpQCSgN+/kGM9T7wrXRtzjxfFOy35tvYTHDtsePnw== +"@parcel/utils@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.0.0-nightly.444.tgz#b177ea1de0d76d2eab9a83d8640e2f5d97c3375b" + integrity sha512-yq9T/ubJbtMhJXuK99Y1DG3VFMy6OwWFZH0J7W7pfULCTmSzt78eujbDWKvqzNLRKt2e4HebjRF+A11aZ3pu+A== dependencies: "@iarna/toml" "^2.2.0" - "@parcel/codeframe" "2.0.0-nightly.434+146cffb6" - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/logger" "2.0.0-nightly.434+146cffb6" - "@parcel/markdown-ansi" "2.0.0-nightly.434+146cffb6" + "@parcel/codeframe" "2.0.0-nightly.444+e2136965" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/logger" "2.0.0-nightly.444+e2136965" + "@parcel/markdown-ansi" "2.0.0-nightly.444+e2136965" "@parcel/source-map" "2.0.0-alpha.4.16" ansi-html "^0.0.7" chalk "^2.4.2" @@ -2925,14 +2927,14 @@ node-addon-api "^3.0.0" node-gyp-build "^4.2.1" -"@parcel/workers@2.0.0-nightly.434+146cffb6": - version "2.0.0-nightly.434" - resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.0.0-nightly.434.tgz#a314590bd4e54afc62e863c6d570c9d681b36d94" - integrity sha512-9kwyj18idFgbMTzt2e4nNYf6SY7+R9OEE1yY4kjj97unl3oxMEOMZ1CN+RHMbmWKwVjmqTp0x2Etep1+u8z5zA== +"@parcel/workers@2.0.0-nightly.444+e2136965": + version "2.0.0-nightly.444" + resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.0.0-nightly.444.tgz#5ca32d0ac88338ffb761438d2336e4b730d2eed2" + integrity sha512-T1flEMHdzMLaMbVgJsZ13pT2/IChH4CnpEE7Fj2/axthMq7GHqmeVLiDDM/y6BEDEW/I3FvWHR2PjVpzGljWQw== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/logger" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/logger" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" chrome-trace-event "^1.0.2" nullthrows "^1.1.1" @@ -3113,10 +3115,10 @@ dependencies: jszip "*" -"@types/lodash@^4.14.163": - version "4.14.163" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.163.tgz#6026f73c8267a0b7d41c7c8aadacfa2a5255774f" - integrity sha512-BeZM/FZaV53emqyHxn9L39Oz6XbHMBRLA1b1quROku48J/1kYYxPmVOJ/qSQheb81on4BI7H6QDo6bkUuRaDNQ== +"@types/lodash@^4.14.165": + version "4.14.165" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.165.tgz#74d55d947452e2de0742bad65270433b63a8c30f" + integrity sha512-tjSSOTHhI5mCHTy/OOXYIhi2Wt1qcbHmuXD1Ha7q70CgI/I71afO4XtLb/cVexki1oVYchpul/TOuu3Arcdxrg== "@types/md5@^2.2.1": version "2.2.1" @@ -3276,61 +3278,61 @@ resolved "https://registry.yarnpkg.com/@types/yarnpkg__lockfile/-/yarnpkg__lockfile-1.1.4.tgz#445251eb00bd9c1e751f82c7c6bf4f714edfd464" integrity sha512-/emrKCfQMQmFCqRqqBJ0JueHBT06jBRM3e8OgnvDUcvuExONujIk2hFA5dNsN9Nt41ljGVDdChvCydATZ+KOZw== -"@typescript-eslint/eslint-plugin@^4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.6.0.tgz#210cd538bb703f883aff81d3996961f5dba31fdb" - integrity sha512-1+419X+Ynijytr1iWI+/IcX/kJryc78YNpdaXR1aRO1sU3bC0vZrIAF1tIX7rudVI84W7o7M4zo5p1aVt70fAg== +"@typescript-eslint/eslint-plugin@^4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.6.1.tgz#99d77eb7a016fd5a5e749d2c44a7e4c317eb7da3" + integrity sha512-SNZyflefTMK2JyrPfFFzzoy2asLmZvZJ6+/L5cIqg4HfKGiW2Gr1Go1OyEVqne/U4QwmoasuMwppoBHWBWF2nA== dependencies: - "@typescript-eslint/experimental-utils" "4.6.0" - "@typescript-eslint/scope-manager" "4.6.0" + "@typescript-eslint/experimental-utils" "4.6.1" + "@typescript-eslint/scope-manager" "4.6.1" debug "^4.1.1" functional-red-black-tree "^1.0.1" regexpp "^3.0.0" semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/experimental-utils@4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.6.0.tgz#f750aef4dd8e5970b5c36084f0a5ca2f0db309a4" - integrity sha512-pnh6Beh2/4xjJVNL+keP49DFHk3orDHHFylSp3WEjtgW3y1U+6l+jNnJrGlbs6qhAz5z96aFmmbUyKhunXKvKw== +"@typescript-eslint/experimental-utils@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/experimental-utils/-/experimental-utils-4.6.1.tgz#a9c691dfd530a9570274fe68907c24c07a06c4aa" + integrity sha512-qyPqCFWlHZXkEBoV56UxHSoXW2qnTr4JrWVXOh3soBP3q0o7p4pUEMfInDwIa0dB/ypdtm7gLOS0hg0a73ijfg== dependencies: "@types/json-schema" "^7.0.3" - "@typescript-eslint/scope-manager" "4.6.0" - "@typescript-eslint/types" "4.6.0" - "@typescript-eslint/typescript-estree" "4.6.0" + "@typescript-eslint/scope-manager" "4.6.1" + "@typescript-eslint/types" "4.6.1" + "@typescript-eslint/typescript-estree" "4.6.1" eslint-scope "^5.0.0" eslint-utils "^2.0.0" -"@typescript-eslint/parser@^4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.6.0.tgz#7e9ff7df2f21d5c8f65f17add3b99eeeec33199d" - integrity sha512-Dj6NJxBhbdbPSZ5DYsQqpR32MwujF772F2H3VojWU6iT4AqL4BKuoNWOPFCoSZvCcADDvQjDpa6OLDAaiZPz2Q== +"@typescript-eslint/parser@^4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/parser/-/parser-4.6.1.tgz#b801bff67b536ecc4a840ac9289ba2be57e02428" + integrity sha512-lScKRPt1wM9UwyKkGKyQDqf0bh6jm8DQ5iN37urRIXDm16GEv+HGEmum2Fc423xlk5NUOkOpfTnKZc/tqKZkDQ== dependencies: - "@typescript-eslint/scope-manager" "4.6.0" - "@typescript-eslint/types" "4.6.0" - "@typescript-eslint/typescript-estree" "4.6.0" + "@typescript-eslint/scope-manager" "4.6.1" + "@typescript-eslint/types" "4.6.1" + "@typescript-eslint/typescript-estree" "4.6.1" debug "^4.1.1" -"@typescript-eslint/scope-manager@4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.6.0.tgz#b7d8b57fe354047a72dfb31881d9643092838662" - integrity sha512-uZx5KvStXP/lwrMrfQQwDNvh2ppiXzz5TmyTVHb+5TfZ3sUP7U1onlz3pjoWrK9konRyFe1czyxObWTly27Ang== +"@typescript-eslint/scope-manager@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/scope-manager/-/scope-manager-4.6.1.tgz#21872b91cbf7adfc7083f17b8041149148baf992" + integrity sha512-f95+80r6VdINYscJY1KDUEDcxZ3prAWHulL4qRDfNVD0I5QAVSGqFkwHERDoLYJJWmEAkUMdQVvx7/c2Hp+Bjg== dependencies: - "@typescript-eslint/types" "4.6.0" - "@typescript-eslint/visitor-keys" "4.6.0" + "@typescript-eslint/types" "4.6.1" + "@typescript-eslint/visitor-keys" "4.6.1" -"@typescript-eslint/types@4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.6.0.tgz#157ca925637fd53c193c6bf226a6c02b752dde2f" - integrity sha512-5FAgjqH68SfFG4UTtIFv+rqYJg0nLjfkjD0iv+5O27a0xEeNZ5rZNDvFGZDizlCD1Ifj7MAbSW2DPMrf0E9zjA== +"@typescript-eslint/types@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/types/-/types-4.6.1.tgz#d3ad7478f53f22e7339dc006ab61aac131231552" + integrity sha512-k2ZCHhJ96YZyPIsykickez+OMHkz06xppVLfJ+DY90i532/Cx2Z+HiRMH8YZQo7a4zVd/TwNBuRCdXlGK4yo8w== -"@typescript-eslint/typescript-estree@4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.0.tgz#85bd98dcc8280511cfc5b2ce7b03a9ffa1732b08" - integrity sha512-s4Z9qubMrAo/tw0CbN0IN4AtfwuehGXVZM0CHNMdfYMGBDhPdwTEpBrecwhP7dRJu6d9tT9ECYNaWDHvlFSngA== +"@typescript-eslint/typescript-estree@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/typescript-estree/-/typescript-estree-4.6.1.tgz#6025cce724329413f57e4959b2d676fceeca246f" + integrity sha512-/J/kxiyjQQKqEr5kuKLNQ1Finpfb8gf/NpbwqFFYEBjxOsZ621r9AqwS9UDRA1Rrr/eneX/YsbPAIhU2rFLjXQ== dependencies: - "@typescript-eslint/types" "4.6.0" - "@typescript-eslint/visitor-keys" "4.6.0" + "@typescript-eslint/types" "4.6.1" + "@typescript-eslint/visitor-keys" "4.6.1" debug "^4.1.1" globby "^11.0.1" is-glob "^4.0.1" @@ -3338,12 +3340,12 @@ semver "^7.3.2" tsutils "^3.17.1" -"@typescript-eslint/visitor-keys@4.6.0": - version "4.6.0" - resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.6.0.tgz#fb05d6393891b0a089b243fc8f9fb8039383d5da" - integrity sha512-38Aa9Ztl0XyFPVzmutHXqDMCu15Xx8yKvUo38Gu3GhsuckCh3StPI5t2WIO9LHEsOH7MLmlGfKUisU8eW1Sjhg== +"@typescript-eslint/visitor-keys@4.6.1": + version "4.6.1" + resolved "https://registry.yarnpkg.com/@typescript-eslint/visitor-keys/-/visitor-keys-4.6.1.tgz#6b125883402d8939df7b54528d879e88f7ba3614" + integrity sha512-owABze4toX7QXwOLT3/D5a8NecZEjEWU1srqxENTfqsY3bwVnl3YYbOh6s1rp2wQKO9RTHFGjKes08FgE7SVMw== dependencies: - "@typescript-eslint/types" "4.6.0" + "@typescript-eslint/types" "4.6.1" eslint-visitor-keys "^2.0.0" "@yarnpkg/lockfile@^1.1.0": @@ -3563,11 +3565,6 @@ anymatch@^3.0.3: normalize-path "^3.0.0" picomatch "^2.0.4" -app-root-path@^2.2.1: - version "2.2.1" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.2.1.tgz#d0df4a682ee408273583d43f6f79e9892624bc9a" - integrity sha512-91IFKeKk7FjfmezPKkwtaRvSpnUc4gDwPAjA1YZ9Gn0q0PPeW+vbeUsZuyDwjI7+QTHhcLen2v25fi/AmhvbJA== - append-transform@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-1.0.0.tgz#046a52ae582a228bd72f58acfbe2967c678759ab" @@ -3835,25 +3832,10 @@ aws-sdk-mock@^5.1.0: sinon "^9.0.1" traverse "^0.6.6" -aws-sdk@^2.596.0: - version "2.778.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.778.0.tgz#9304d1b2a1f94bfd8a56169f1da20ff40f417f40" - integrity sha512-sIJRO7tMaztLs+gvHF/Wo+iek/rhH99+2OzharQJMS0HATPl5/EdhKgWGv1n/bNpVH+kD3n0QMQgdFu0FNUt0Q== - dependencies: - buffer "4.9.2" - events "1.1.1" - ieee754 "1.1.13" - jmespath "0.15.0" - querystring "0.2.0" - sax "1.2.1" - url "0.10.3" - uuid "3.3.2" - xml2js "0.4.19" - -aws-sdk@^2.637.0, aws-sdk@^2.781.0: - version "2.781.0" - resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.781.0.tgz#e9df63e9b69c22ac939ab675c8771592ae89105a" - integrity sha512-y+Xd+DJJyNgZdPLZytJA8LRR79spD/zXOt0G9Uk68UC9tRDEB8aQysuxWKYEybYCexRqJtTZLCrR3ikYwU099g== +aws-sdk@^2.637.0, aws-sdk@^2.787.0: + version "2.787.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.787.0.tgz#4d8966d11c7dbe770de26632e552c97b2d91e340" + integrity sha512-3WlUdWqUB8Vhdvj/7TENr/7SEmQzxmnHxOJ8l2WjZbcMRSuI0/9Ym4p1TC3hf21VDVDhkdGlw60QqpZQ1qb+Mg== dependencies: buffer "4.9.2" events "1.1.1" @@ -3882,16 +3864,16 @@ axios@^0.19.0: dependencies: follow-redirects "1.5.10" -babel-jest@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.1.tgz#07bd7bec14de47fe0f2c9a139741329f1f41788b" - integrity sha512-duMWEOKrSBYRVTTNpL2SipNIWnZOjP77auOBMPQ3zXAdnDbyZQWU8r/RxNWpUf9N6cgPFecQYelYLytTVXVDtA== +babel-jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/babel-jest/-/babel-jest-26.6.3.tgz#d87d25cb0037577a0c89f82e5755c5d293c01056" + integrity sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA== dependencies: - "@jest/transform" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" "@types/babel__core" "^7.1.7" babel-plugin-istanbul "^6.0.0" - babel-preset-jest "^26.5.0" + babel-preset-jest "^26.6.2" chalk "^4.0.0" graceful-fs "^4.2.4" slash "^3.0.0" @@ -3914,20 +3896,20 @@ babel-plugin-istanbul@^6.0.0: istanbul-lib-instrument "^4.0.0" test-exclude "^6.0.0" -babel-plugin-jest-hoist@^26.5.0: - version "26.5.0" - resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.5.0.tgz#3916b3a28129c29528de91e5784a44680db46385" - integrity sha512-ck17uZFD3CDfuwCLATWZxkkuGGFhMij8quP8CNhwj8ek1mqFgbFzRJ30xwC04LLscj/aKsVFfRST+b5PT7rSuw== +babel-plugin-jest-hoist@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz#8185bd030348d254c6d7dd974355e6a28b21e62d" + integrity sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw== dependencies: "@babel/template" "^7.3.3" "@babel/types" "^7.3.3" "@types/babel__core" "^7.0.0" "@types/babel__traverse" "^7.0.6" -babel-preset-current-node-syntax@^0.1.3: - version "0.1.3" - resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-0.1.3.tgz#b4b547acddbf963cba555ba9f9cbbb70bfd044da" - integrity sha512-uyexu1sVwcdFnyq9o8UQYsXwXflIh8LvrF5+cKrYam93ned1CStffB3+BEcsxGSgagoA3GEyjDqO4a/58hyPYQ== +babel-preset-current-node-syntax@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.0.tgz#cf5feef29551253471cfa82fc8e0f5063df07a77" + integrity sha512-mGkvkpocWJes1CmMKtgGUwCeeq0pOhALyymozzDWYomHTbDLwueDYG6p4TK1YOeYHCzBzYPsWkgTto10JubI1Q== dependencies: "@babel/plugin-syntax-async-generators" "^7.8.4" "@babel/plugin-syntax-bigint" "^7.8.3" @@ -3940,14 +3922,15 @@ babel-preset-current-node-syntax@^0.1.3: "@babel/plugin-syntax-object-rest-spread" "^7.8.3" "@babel/plugin-syntax-optional-catch-binding" "^7.8.3" "@babel/plugin-syntax-optional-chaining" "^7.8.3" + "@babel/plugin-syntax-top-level-await" "^7.8.3" -babel-preset-jest@^26.5.0: - version "26.5.0" - resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.5.0.tgz#f1b166045cd21437d1188d29f7fba470d5bdb0e7" - integrity sha512-F2vTluljhqkiGSJGBg/jOruA8vIIIL11YrxRcO7nviNTMbbofPSHwnm8mgP7d/wS7wRSexRoI6X1A6T74d4LQA== +babel-preset-jest@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz#747872b1171df032252426586881d62d31798fee" + integrity sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ== dependencies: - babel-plugin-jest-hoist "^26.5.0" - babel-preset-current-node-syntax "^0.1.3" + babel-plugin-jest-hoist "^26.6.2" + babel-preset-current-node-syntax "^1.0.0" balanced-match@^1.0.0: version "1.0.0" @@ -4483,10 +4466,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3: inherits "^2.0.1" safe-buffer "^5.0.1" -cjs-module-lexer@^0.4.2: - version "0.4.3" - resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.4.3.tgz#9e31f7fe701f5fcee5793f77ab4e58fa8dcde8bc" - integrity sha512-5RLK0Qfs0PNDpEyBXIr3bIT1Muw3ojSlvpw6dAmkUcO0+uTrsBn7GuEIgx40u+OzbCBLDta7nvmud85P4EmTsQ== +cjs-module-lexer@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz#4186fcca0eae175970aee870b9fe2d6cf8d5655f" + integrity sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw== class-utils@^0.3.5: version "0.3.6" @@ -4612,10 +4595,10 @@ code-point-at@^1.0.0: resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" integrity sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c= -codemaker@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.14.0.tgz#b49e73d75dc26aa7cbffdfc81e7baa0bd2e4c244" - integrity sha512-QVHiMU6adGEhD6zxilR60OycWyiDFXfRYQceLtwp3qYoZkxJI7bpSr6T1cWiyNH3GpeLNZ8HucY1WreFqx3xhA== +codemaker@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/codemaker/-/codemaker-1.14.1.tgz#c2938d5fb76ca0cce306990c82b5fe0e934feb96" + integrity sha512-km8Aqf1ZioiM9Xm4Tj9pbIyFLoRV9l3ssw073C1AePt76TDqWFmJ83LrXkm+dSgdysoKVqY3Svn3BoPnN5bFKQ== dependencies: camelcase "^6.2.0" decamelize "^4.0.0" @@ -4848,6 +4831,14 @@ conventional-changelog-angular@^5.0.11, conventional-changelog-angular@^5.0.3: compare-func "^2.0.0" q "^1.5.1" +conventional-changelog-angular@^5.0.12: + version "5.0.12" + resolved "https://registry.yarnpkg.com/conventional-changelog-angular/-/conventional-changelog-angular-5.0.12.tgz#c979b8b921cbfe26402eb3da5bbfda02d865a2b9" + integrity sha512-5GLsbnkR/7A89RyHLvvoExbiGbd9xKdKqDTrArnPbOqBqG/2wIosu0fHwpeIRI8Tl94MhVNBXcLJZl92ZQ5USw== + dependencies: + compare-func "^2.0.0" + q "^1.5.1" + conventional-changelog-atom@^2.0.7: version "2.0.7" resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.7.tgz#221575253a04f77a2fd273eb2bf29a138f710abf" @@ -4855,15 +4846,22 @@ conventional-changelog-atom@^2.0.7: dependencies: q "^1.5.1" -conventional-changelog-cli@^2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/conventional-changelog-cli/-/conventional-changelog-cli-2.1.0.tgz#5da5be32203ca8382815afc85b7f9151115d5e97" - integrity sha512-hZ8EcpxV4LcGOZwH+U5LJQDnyA4o/uyUdmIGzmFZMB4caujavvDBo/iTgVihk0m1QKkEhJgulagrILSm1JCakA== +conventional-changelog-atom@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/conventional-changelog-atom/-/conventional-changelog-atom-2.0.8.tgz#a759ec61c22d1c1196925fca88fe3ae89fd7d8de" + integrity sha512-xo6v46icsFTK3bb7dY/8m2qvc8sZemRgdqLb/bjpBsH2UyOS8rKNTgcb5025Hri6IpANPApbXMg15QLb1LJpBw== + dependencies: + q "^1.5.1" + +conventional-changelog-cli@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-cli/-/conventional-changelog-cli-2.1.1.tgz#7a11980bc399938e0509d2adf8e7a0e213eb994e" + integrity sha512-xMGQdKJ+4XFDDgfX5aK7UNFduvJMbvF5BB+g0OdVhA3rYdYyhctrIE2Al+WYdZeKTdg9YzMWF2iFPT8MupIwng== dependencies: add-stream "^1.0.0" - conventional-changelog "^3.1.23" + conventional-changelog "^3.1.24" lodash "^4.17.15" - meow "^7.0.0" + meow "^8.0.0" tempfile "^3.0.0" conventional-changelog-codemirror@^2.0.7: @@ -4873,6 +4871,13 @@ conventional-changelog-codemirror@^2.0.7: dependencies: q "^1.5.1" +conventional-changelog-codemirror@^2.0.8: + version "2.0.8" + resolved "https://registry.yarnpkg.com/conventional-changelog-codemirror/-/conventional-changelog-codemirror-2.0.8.tgz#398e9530f08ce34ec4640af98eeaf3022eb1f7dc" + integrity sha512-z5DAsn3uj1Vfp7po3gpt2Boc+Bdwmw2++ZHa5Ak9k0UKsYAO5mH1UBTN0qSCuJZREIhX6WU4E1p3IW2oRCNzQw== + dependencies: + q "^1.5.1" + conventional-changelog-config-spec@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/conventional-changelog-config-spec/-/conventional-changelog-config-spec-2.1.0.tgz#874a635287ef8b581fd8558532bf655d4fb59f2d" @@ -4887,6 +4892,15 @@ conventional-changelog-conventionalcommits@4.4.0, conventional-changelog-convent lodash "^4.17.15" q "^1.5.1" +conventional-changelog-conventionalcommits@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/conventional-changelog-conventionalcommits/-/conventional-changelog-conventionalcommits-4.5.0.tgz#a02e0b06d11d342fdc0f00c91d78265ed0bc0a62" + integrity sha512-buge9xDvjjOxJlyxUnar/+6i/aVEVGA7EEh4OafBCXPlLUQPGbRUBhBUveWRxzvR8TEjhKEP4BdepnpG2FSZXw== + dependencies: + compare-func "^2.0.0" + lodash "^4.17.15" + q "^1.5.1" + conventional-changelog-core@^3.1.6: version "3.2.3" resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-3.2.3.tgz#b31410856f431c847086a7dcb4d2ca184a7d88fb" @@ -4927,6 +4941,27 @@ conventional-changelog-core@^4.2.0: shelljs "^0.8.3" through2 "^3.0.0" +conventional-changelog-core@^4.2.1: + version "4.2.1" + resolved "https://registry.yarnpkg.com/conventional-changelog-core/-/conventional-changelog-core-4.2.1.tgz#f811ad98ab2ff080becafc61407509420c9b447d" + integrity sha512-8cH8/DEoD3e5Q6aeogdR5oaaKs0+mG6+f+Om0ZYt3PNv7Zo0sQhu4bMDRsqAF+UTekTAtP1W/C41jH/fkm8Jtw== + dependencies: + add-stream "^1.0.0" + conventional-changelog-writer "^4.0.18" + conventional-commits-parser "^3.2.0" + dateformat "^3.0.0" + get-pkg-repo "^1.0.0" + git-raw-commits "2.0.0" + git-remote-origin-url "^2.0.0" + git-semver-tags "^4.1.1" + lodash "^4.17.15" + normalize-package-data "^3.0.0" + q "^1.5.1" + read-pkg "^3.0.0" + read-pkg-up "^3.0.0" + shelljs "^0.8.3" + through2 "^4.0.0" + conventional-changelog-ember@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-2.0.8.tgz#f0f04eb7ff3c885af97db100865ab95dcfa9917f" @@ -4934,6 +4969,13 @@ conventional-changelog-ember@^2.0.8: dependencies: q "^1.5.1" +conventional-changelog-ember@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/conventional-changelog-ember/-/conventional-changelog-ember-2.0.9.tgz#619b37ec708be9e74a220f4dcf79212ae1c92962" + integrity sha512-ulzIReoZEvZCBDhcNYfDIsLTHzYHc7awh+eI44ZtV5cx6LVxLlVtEmcO+2/kGIHGtw+qVabJYjdI5cJOQgXh1A== + dependencies: + q "^1.5.1" + conventional-changelog-eslint@^3.0.8: version "3.0.8" resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.8.tgz#f8b952b7ed7253ea0ac0b30720bb381f4921b46c" @@ -4941,6 +4983,13 @@ conventional-changelog-eslint@^3.0.8: dependencies: q "^1.5.1" +conventional-changelog-eslint@^3.0.9: + version "3.0.9" + resolved "https://registry.yarnpkg.com/conventional-changelog-eslint/-/conventional-changelog-eslint-3.0.9.tgz#689bd0a470e02f7baafe21a495880deea18b7cdb" + integrity sha512-6NpUCMgU8qmWmyAMSZO5NrRd7rTgErjrm4VASam2u5jrZS0n38V7Y9CzTtLT2qwz5xEChDR4BduoWIr8TfwvXA== + dependencies: + q "^1.5.1" + conventional-changelog-express@^2.0.5: version "2.0.5" resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.5.tgz#6e93705acdad374516ca125990012a48e710f8de" @@ -4948,6 +4997,13 @@ conventional-changelog-express@^2.0.5: dependencies: q "^1.5.1" +conventional-changelog-express@^2.0.6: + version "2.0.6" + resolved "https://registry.yarnpkg.com/conventional-changelog-express/-/conventional-changelog-express-2.0.6.tgz#420c9d92a347b72a91544750bffa9387665a6ee8" + integrity sha512-SDez2f3iVJw6V563O3pRtNwXtQaSmEfTCaTBPCqn0oG0mfkq0rX4hHBq5P7De2MncoRixrALj3u3oQsNK+Q0pQ== + dependencies: + q "^1.5.1" + conventional-changelog-jquery@^3.0.10: version "3.0.10" resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.10.tgz#fe8eb6aff322aa980af5eb68497622a5f6257ce7" @@ -4955,6 +5011,13 @@ conventional-changelog-jquery@^3.0.10: dependencies: q "^1.5.1" +conventional-changelog-jquery@^3.0.11: + version "3.0.11" + resolved "https://registry.yarnpkg.com/conventional-changelog-jquery/-/conventional-changelog-jquery-3.0.11.tgz#d142207400f51c9e5bb588596598e24bba8994bf" + integrity sha512-x8AWz5/Td55F7+o/9LQ6cQIPwrCjfJQ5Zmfqi8thwUEKHstEn4kTIofXub7plf1xvFA2TqhZlq7fy5OmV6BOMw== + dependencies: + q "^1.5.1" + conventional-changelog-jshint@^2.0.8: version "2.0.8" resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.8.tgz#3fff4df8cb46037f77b9dc3f8e354c7f99332f13" @@ -4963,6 +5026,14 @@ conventional-changelog-jshint@^2.0.8: compare-func "^2.0.0" q "^1.5.1" +conventional-changelog-jshint@^2.0.9: + version "2.0.9" + resolved "https://registry.yarnpkg.com/conventional-changelog-jshint/-/conventional-changelog-jshint-2.0.9.tgz#f2d7f23e6acd4927a238555d92c09b50fe3852ff" + integrity sha512-wMLdaIzq6TNnMHMy31hql02OEQ8nCQfExw1SE0hYL5KvU+JCTuPaDO+7JiogGT2gJAxiUGATdtYYfh+nT+6riA== + dependencies: + compare-func "^2.0.0" + q "^1.5.1" + conventional-changelog-preset-loader@^2.1.1, conventional-changelog-preset-loader@^2.3.4: version "2.3.4" resolved "https://registry.yarnpkg.com/conventional-changelog-preset-loader/-/conventional-changelog-preset-loader-2.3.4.tgz#14a855abbffd59027fd602581f1f34d9862ea44c" @@ -4984,7 +5055,23 @@ conventional-changelog-writer@^4.0.17, conventional-changelog-writer@^4.0.6: split "^1.0.0" through2 "^3.0.0" -conventional-changelog@3.1.23, conventional-changelog@^3.1.23: +conventional-changelog-writer@^4.0.18: + version "4.0.18" + resolved "https://registry.yarnpkg.com/conventional-changelog-writer/-/conventional-changelog-writer-4.0.18.tgz#10b73baa59c7befc69b360562f8b9cd19e63daf8" + integrity sha512-mAQDCKyB9HsE8Ko5cCM1Jn1AWxXPYV0v8dFPabZRkvsiWUul2YyAqbIaoMKF88Zf2ffnOPSvKhboLf3fnjo5/A== + dependencies: + compare-func "^2.0.0" + conventional-commits-filter "^2.0.7" + dateformat "^3.0.0" + handlebars "^4.7.6" + json-stringify-safe "^5.0.1" + lodash "^4.17.15" + meow "^8.0.0" + semver "^6.0.0" + split "^1.0.0" + through2 "^4.0.0" + +conventional-changelog@3.1.23: version "3.1.23" resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.23.tgz#d696408021b579a3814aba79b38729ed86478aea" integrity sha512-sScUu2NHusjRC1dPc5p8/b3kT78OYr95/Bx7Vl8CPB8tF2mG1xei5iylDTRjONV5hTlzt+Cn/tBWrKdd299b7A== @@ -5001,6 +5088,23 @@ conventional-changelog@3.1.23, conventional-changelog@^3.1.23: conventional-changelog-jshint "^2.0.8" conventional-changelog-preset-loader "^2.3.4" +conventional-changelog@^3.1.24: + version "3.1.24" + resolved "https://registry.yarnpkg.com/conventional-changelog/-/conventional-changelog-3.1.24.tgz#ebd180b0fd1b2e1f0095c4b04fd088698348a464" + integrity sha512-ed6k8PO00UVvhExYohroVPXcOJ/K1N0/drJHx/faTH37OIZthlecuLIRX/T6uOp682CAoVoFpu+sSEaeuH6Asg== + dependencies: + conventional-changelog-angular "^5.0.12" + conventional-changelog-atom "^2.0.8" + conventional-changelog-codemirror "^2.0.8" + conventional-changelog-conventionalcommits "^4.5.0" + conventional-changelog-core "^4.2.1" + conventional-changelog-ember "^2.0.9" + conventional-changelog-eslint "^3.0.9" + conventional-changelog-express "^2.0.6" + conventional-changelog-jquery "^3.0.11" + conventional-changelog-jshint "^2.0.9" + conventional-changelog-preset-loader "^2.3.4" + conventional-commits-filter@^2.0.2, conventional-commits-filter@^2.0.6: version "2.0.6" resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.6.tgz#0935e1240c5ca7698329affee1b6a46d33324c4c" @@ -5009,6 +5113,14 @@ conventional-commits-filter@^2.0.2, conventional-commits-filter@^2.0.6: lodash.ismatch "^4.4.0" modify-values "^1.0.0" +conventional-commits-filter@^2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/conventional-commits-filter/-/conventional-commits-filter-2.0.7.tgz#f8d9b4f182fce00c9af7139da49365b136c8a0b3" + integrity sha512-ASS9SamOP4TbCClsRHxIHXRfcGCnIoQqkvAzCSbZzTFLfcTqJVugB0agRgsEELsqaeWgsXv513eS116wnlSSPA== + dependencies: + lodash.ismatch "^4.4.0" + modify-values "^1.0.0" + conventional-commits-parser@^3.0.3, conventional-commits-parser@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.1.0.tgz#10140673d5e7ef5572633791456c5d03b69e8be4" @@ -5022,6 +5134,19 @@ conventional-commits-parser@^3.0.3, conventional-commits-parser@^3.1.0: through2 "^3.0.0" trim-off-newlines "^1.0.0" +conventional-commits-parser@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/conventional-commits-parser/-/conventional-commits-parser-3.2.0.tgz#9e261b139ca4b7b29bcebbc54460da36894004ca" + integrity sha512-XmJiXPxsF0JhAKyfA2Nn+rZwYKJ60nanlbSWwwkGwLQFbugsc0gv1rzc7VbbUWAzJfR1qR87/pNgv9NgmxtBMQ== + dependencies: + JSONStream "^1.0.4" + is-text-path "^1.0.1" + lodash "^4.17.15" + meow "^8.0.0" + split2 "^2.0.0" + through2 "^4.0.0" + trim-off-newlines "^1.0.0" + conventional-recommended-bump@6.0.10: version "6.0.10" resolved "https://registry.yarnpkg.com/conventional-recommended-bump/-/conventional-recommended-bump-6.0.10.tgz#ac2fb3e31bad2aeda80086b345bf0c52edd1d1b3" @@ -5689,11 +5814,21 @@ diff-sequences@^26.5.0: resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.5.0.tgz#ef766cf09d43ed40406611f11c6d8d9dd8b2fefd" integrity sha512-ZXx86srb/iYy6jG71k++wBN9P9J05UNQ5hQHQd9MtMPvcqXPx/vKU69jfHV637D00Q2gSgPk2D+jSx3l1lDW/Q== +diff-sequences@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/diff-sequences/-/diff-sequences-26.6.2.tgz#48ba99157de1923412eed41db6b6d4aa9ca7c0b1" + integrity sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q== + diff@^4.0.1, diff@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== +diff@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/diff/-/diff-5.0.0.tgz#7ed6ad76d859d030787ec35855f5b1daf31d852b" + integrity sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w== + diffie-hellman@^5.0.0: version "5.0.3" resolved "https://registry.yarnpkg.com/diffie-hellman/-/diffie-hellman-5.0.3.tgz#40e8ee98f55a2149607146921c63e1ae5f3d2875" @@ -5815,21 +5950,11 @@ dotenv-expand@^5.1.0: resolved "https://registry.yarnpkg.com/dotenv-expand/-/dotenv-expand-5.1.0.tgz#3fbaf020bfd794884072ea26b1e9791d45a629f0" integrity sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA== -dotenv-json@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/dotenv-json/-/dotenv-json-1.0.0.tgz#fc7f672aafea04bed33818733b9f94662332815c" - integrity sha512-jAssr+6r4nKhKRudQ0HOzMskOFFi9+ubXWwmrSGJFgTvpjyPXCXsCsYbjif6mXp7uxA7xY3/LGaiTQukZzSbOQ== - dotenv@^7.0.0: version "7.0.0" resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-7.0.0.tgz#a2be3cd52736673206e8a85fb5210eea29628e7c" integrity sha512-M3NhsLbV1i6HuGzBUH8vXrtxOk+tWmzWKDMbAVSUp3Zsjm7ywFeuwrUXhmhQyRK1q5B5GGy7hcXPbj3bnfZg2g== -dotenv@^8.0.0: - version "8.2.0" - resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" - integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== - dotgitignore@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/dotgitignore/-/dotgitignore-2.1.0.tgz#a4b15a4e4ef3cf383598aaf1dfa4a04bcc089b7b" @@ -6099,11 +6224,6 @@ escodegen@^1.11.0, escodegen@^1.14.1, escodegen@^1.8.1: optionalDependencies: source-map "~0.6.1" -eslint-config-standard@^14.1.1: - version "14.1.1" - resolved "https://registry.yarnpkg.com/eslint-config-standard/-/eslint-config-standard-14.1.1.tgz#830a8e44e7aef7de67464979ad06b406026c56ea" - integrity sha512-Z9B+VR+JIXRxz21udPTL9HpFMyoMUEeX1G251EQ6e05WD9aPVtVBn09XUmZ259wCMlCDmYDSZG62Hhm+ZTJcUg== - eslint-import-resolver-node@^0.3.4: version "0.3.4" resolved "https://registry.yarnpkg.com/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.4.tgz#85ffa81942c25012d8231096ddf679c03042c717" @@ -6131,14 +6251,6 @@ eslint-module-utils@^2.6.0: debug "^2.6.9" pkg-dir "^2.0.0" -eslint-plugin-es@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-es/-/eslint-plugin-es-3.0.1.tgz#75a7cdfdccddc0589934aeeb384175f221c57893" - integrity sha512-GUmAsJaN4Fc7Gbtl8uOBlayo2DqhwWvEzykMHSCZHU3XdJ+NSzzZcVhXh3VxX5icqQ+oQdIEawXX8xkR3mIFmQ== - dependencies: - eslint-utils "^2.0.0" - regexpp "^3.0.0" - eslint-plugin-import@^2.22.1: version "2.22.1" resolved "https://registry.yarnpkg.com/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz#0896c7e6a0cf44109a2d97b95903c2bb689d7702" @@ -6158,33 +6270,11 @@ eslint-plugin-import@^2.22.1: resolve "^1.17.0" tsconfig-paths "^3.9.0" -eslint-plugin-node@^11.1.0: - version "11.1.0" - resolved "https://registry.yarnpkg.com/eslint-plugin-node/-/eslint-plugin-node-11.1.0.tgz#c95544416ee4ada26740a30474eefc5402dc671d" - integrity sha512-oUwtPJ1W0SKD0Tr+wqu92c5xuCeQqB3hSCHasn/ZgjFdA9iDGNkNf2Zi9ztY7X+hNuMib23LNGRm6+uN+KLE3g== - dependencies: - eslint-plugin-es "^3.0.0" - eslint-utils "^2.0.0" - ignore "^5.1.1" - minimatch "^3.0.4" - resolve "^1.10.1" - semver "^6.1.0" - -eslint-plugin-promise@^4.2.1: - version "4.2.1" - resolved "https://registry.yarnpkg.com/eslint-plugin-promise/-/eslint-plugin-promise-4.2.1.tgz#845fd8b2260ad8f82564c1222fce44ad71d9418a" - integrity sha512-VoM09vT7bfA7D+upt+FjeBO5eHIJQBUWki1aPvB+vbNiHS3+oGIJGIeyBtKQTME6UPXXy3vV07OL1tHd3ANuDw== - eslint-plugin-rulesdir@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/eslint-plugin-rulesdir/-/eslint-plugin-rulesdir-0.1.0.tgz#ad144d7e98464fda82963eff3fab331aecb2bf08" integrity sha1-rRRNfphGT9qClj7/P6szGuyyvwg= -eslint-plugin-standard@^4.0.2: - version "4.0.2" - resolved "https://registry.yarnpkg.com/eslint-plugin-standard/-/eslint-plugin-standard-4.0.2.tgz#021211a9f077e63a6847e7bb9ab4247327ac8e0c" - integrity sha512-nKptN8l7jksXkwFk++PhJB3cCDTcXOEyhISIN86Ue2feJ1LFyY3PrY3/xT2keXlJSY5bpmbiTG0f885/YKAvTA== - eslint-scope@^5.0.0, eslint-scope@^5.1.1: version "5.1.1" resolved "https://registry.yarnpkg.com/eslint-scope/-/eslint-scope-5.1.1.tgz#e786e59a66cb92b3f6c1fb0d508aab174848f48c" @@ -6385,16 +6475,16 @@ expand-brackets@^2.1.4: snapdragon "^0.8.1" to-regex "^3.0.1" -expect@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.1.tgz#e1e053cdc43b21a452b36fc7cc9401e4603949c1" - integrity sha512-BRfxIBHagghMmr1D2MRY0Qv5d3Nc8HCqgbDwNXw/9izmM5eBb42a2YjLKSbsqle76ozGkAEPELQX4IdNHAKRNA== +expect@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/expect/-/expect-26.6.2.tgz#c6b996bf26bf3fe18b67b2d0f51fc981ba934417" + integrity sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" ansi-styles "^4.0.0" jest-get-type "^26.3.0" - jest-matcher-utils "^26.6.1" - jest-message-util "^26.6.1" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" jest-regex-util "^26.0.0" extend-shallow@^2.0.1: @@ -6450,10 +6540,10 @@ extsprintf@^1.2.0: resolved "https://registry.yarnpkg.com/extsprintf/-/extsprintf-1.4.0.tgz#e2689f8f356fad62cca65a3a91c5df5f9551692f" integrity sha1-4mifjzVvrWLMplo6kcXfX5VRaS8= -fast-check@^2.6.0: - version "2.6.0" - resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-2.6.0.tgz#b4f69c781325ecdfadf6d23468e5dae2c46bfb46" - integrity sha512-Wpz0mMPGxvOtMBaEguu5Pw35uTVfJzAUqRYQksiHk6vHKBV2YNeKk9BzTuqVCYwUIl+NELyPBY2Mg96sYk2fhw== +fast-check@^2.6.1: + version "2.6.1" + resolved "https://registry.yarnpkg.com/fast-check/-/fast-check-2.6.1.tgz#c9ff58b69c2eee872588985d8b93424c84359a6e" + integrity sha512-CauHEKfAjgAFpNDpFqSccu7C5kOlifCNfRxMjzY76MaAaH7ddkqqEzRE2Vm5bjpHJpndD0iVXiZC+d1rYzv5qg== dependencies: pure-rand "^3.0.0" @@ -7002,6 +7092,14 @@ git-semver-tags@^4.0.0, git-semver-tags@^4.1.0: meow "^7.0.0" semver "^6.0.0" +git-semver-tags@^4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/git-semver-tags/-/git-semver-tags-4.1.1.tgz#63191bcd809b0ec3e151ba4751c16c444e5b5780" + integrity sha512-OWyMt5zBe7xFs8vglMmhM9lRQzCWL3WjHtxNNfJTMngGym7pC1kh8sP6jevfydJ6LP3ZvGxfb6ABYgPUM0mtsA== + dependencies: + meow "^8.0.0" + semver "^6.0.0" + git-up@^4.0.0: version "4.0.2" resolved "https://registry.yarnpkg.com/git-up/-/git-up-4.0.2.tgz#10c3d731051b366dc19d3df454bfca3f77913a7c" @@ -7275,6 +7373,13 @@ hosted-git-info@^2.1.4, hosted-git-info@^2.7.1: resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-2.8.8.tgz#7539bd4bc1e0e0a895815a2e0262420b12858488" integrity sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg== +hosted-git-info@^3.0.6: + version "3.0.7" + resolved "https://registry.yarnpkg.com/hosted-git-info/-/hosted-git-info-3.0.7.tgz#a30727385ea85acfcee94e0aad9e368c792e036c" + integrity sha512-fWqc0IcuXs+BmE9orLDyVykAG9GJtGLGuZAAqgcckPgv5xad4AcXGIv8galtQvlwutxSlaMcdw7BUtq2EIvqCQ== + dependencies: + lru-cache "^6.0.0" + hsl-regex@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/hsl-regex/-/hsl-regex-1.0.0.tgz#d49330c789ed819e276a4c0d272dffa30b18fe6e" @@ -7481,7 +7586,7 @@ ignore@^4.0.3, ignore@^4.0.6: resolved "https://registry.yarnpkg.com/ignore/-/ignore-4.0.6.tgz#750e3db5862087b4737ebac8207ffd1ef27b25fc" integrity sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg== -ignore@^5.1.1, ignore@^5.1.4: +ignore@^5.1.4: version "5.1.8" resolved "https://registry.yarnpkg.com/ignore/-/ignore-5.1.8.tgz#f150a8b50a34289b33e22f5889abd4d8016f0e57" integrity sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw== @@ -8173,57 +8278,57 @@ istanbul-reports@^3.0.2: html-escaper "^2.0.0" istanbul-lib-report "^3.0.0" -jest-changed-files@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.1.tgz#2fac3dc51297977ee883347948d8e3d37c417fba" - integrity sha512-NhSdZ5F6b/rIN5V46x1l31vrmukD/bJUXgYAY8VtP1SknYdJwjYDRxuLt7Z8QryIdqCjMIn2C0Cd98EZ4umo8Q== +jest-changed-files@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-changed-files/-/jest-changed-files-26.6.2.tgz#f6198479e1cc66f22f9ae1e22acaa0b429c042d0" + integrity sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" execa "^4.0.0" throat "^5.0.0" -jest-cli@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.1.tgz#8952242fa812c05bd129abf7c022424045b7fd67" - integrity sha512-aPLoEjlwFrCWhiPpW5NUxQA1X1kWsAnQcQ0SO/fHsCvczL3W75iVAcH9kP6NN+BNqZcHNEvkhxT5cDmBfEAh+w== +jest-cli@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-cli/-/jest-cli-26.6.3.tgz#43117cfef24bc4cd691a174a8796a532e135e92a" + integrity sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg== dependencies: - "@jest/core" "^26.6.1" - "@jest/test-result" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/core" "^26.6.3" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" chalk "^4.0.0" exit "^0.1.2" graceful-fs "^4.2.4" import-local "^3.0.2" is-ci "^2.0.0" - jest-config "^26.6.1" - jest-util "^26.6.1" - jest-validate "^26.6.1" + jest-config "^26.6.3" + jest-util "^26.6.2" + jest-validate "^26.6.2" prompts "^2.0.1" yargs "^15.4.1" -jest-config@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.1.tgz#8c343fbdd9c24ad003e261f73583c3c020f32b42" - integrity sha512-mtJzIynIwW1d1nMlKCNCQiSgWaqFn8cH/fOSNY97xG7Y9tBCZbCSuW2GTX0RPmceSJGO7l27JgwC18LEg0Vg+g== +jest-config@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-config/-/jest-config-26.6.3.tgz#64f41444eef9eb03dc51d5c53b75c8c71f645349" + integrity sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg== dependencies: "@babel/core" "^7.1.0" - "@jest/test-sequencer" "^26.6.1" - "@jest/types" "^26.6.1" - babel-jest "^26.6.1" + "@jest/test-sequencer" "^26.6.3" + "@jest/types" "^26.6.2" + babel-jest "^26.6.3" chalk "^4.0.0" deepmerge "^4.2.2" glob "^7.1.1" graceful-fs "^4.2.4" - jest-environment-jsdom "^26.6.1" - jest-environment-node "^26.6.1" + jest-environment-jsdom "^26.6.2" + jest-environment-node "^26.6.2" jest-get-type "^26.3.0" - jest-jasmine2 "^26.6.1" + jest-jasmine2 "^26.6.3" jest-regex-util "^26.0.0" - jest-resolve "^26.6.1" - jest-util "^26.6.1" - jest-validate "^26.6.1" + jest-resolve "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" micromatch "^4.0.2" - pretty-format "^26.6.1" + pretty-format "^26.6.2" jest-diff@^26.0.0: version "26.6.0" @@ -8235,15 +8340,15 @@ jest-diff@^26.0.0: jest-get-type "^26.3.0" pretty-format "^26.6.0" -jest-diff@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.1.tgz#38aa194979f454619bb39bdee299fb64ede5300c" - integrity sha512-BBNy/zin2m4kG5In126O8chOBxLLS/XMTuuM2+YhgyHk87ewPzKTuTJcqj3lOWOi03NNgrl+DkMeV/exdvG9gg== +jest-diff@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-diff/-/jest-diff-26.6.2.tgz#1aa7468b52c3a68d7d5c5fdcdfcd5e49bd164394" + integrity sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA== dependencies: chalk "^4.0.0" - diff-sequences "^26.5.0" + diff-sequences "^26.6.2" jest-get-type "^26.3.0" - pretty-format "^26.6.1" + pretty-format "^26.6.2" jest-docblock@^26.0.0: version "26.0.0" @@ -8252,90 +8357,90 @@ jest-docblock@^26.0.0: dependencies: detect-newline "^3.0.0" -jest-each@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.1.tgz#e968e88309a3e2ae9648634af8f89d8ee5acfddd" - integrity sha512-gSn8eB3buchuq45SU7pLB7qmCGax1ZSxfaWuEFblCyNMtyokYaKFh9dRhYPujK6xYL57dLIPhLKatjmB5XWzGA== +jest-each@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-each/-/jest-each-26.6.2.tgz#02526438a77a67401c8a6382dfe5999952c167cb" + integrity sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" chalk "^4.0.0" jest-get-type "^26.3.0" - jest-util "^26.6.1" - pretty-format "^26.6.1" + jest-util "^26.6.2" + pretty-format "^26.6.2" -jest-environment-jsdom@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.1.tgz#63093bf89daee6139616568a43633b84cf7aac21" - integrity sha512-A17RiXuHYNVlkM+3QNcQ6n5EZyAc6eld8ra9TW26luounGWpku4tj03uqRgHJCI1d4uHr5rJiuCH5JFRtdmrcA== +jest-environment-jsdom@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz#78d09fe9cf019a357009b9b7e1f101d23bd1da3e" + integrity sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q== dependencies: - "@jest/environment" "^26.6.1" - "@jest/fake-timers" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" - jest-mock "^26.6.1" - jest-util "^26.6.1" + jest-mock "^26.6.2" + jest-util "^26.6.2" jsdom "^16.4.0" -jest-environment-node@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.1.tgz#4d73d8b33c26989a92a0ed3ad0bfd6f7a196d9bd" - integrity sha512-YffaCp6h0j1kbcf1NVZ7umC6CPgD67YS+G1BeornfuSkx5s3xdhuwG0DCxSiHPXyT81FfJzA1L7nXvhq50OWIg== +jest-environment-node@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-environment-node/-/jest-environment-node-26.6.2.tgz#824e4c7fb4944646356f11ac75b229b0035f2b0c" + integrity sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag== dependencies: - "@jest/environment" "^26.6.1" - "@jest/fake-timers" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" - jest-mock "^26.6.1" - jest-util "^26.6.1" + jest-mock "^26.6.2" + jest-util "^26.6.2" jest-get-type@^26.3.0: version "26.3.0" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-26.3.0.tgz#e97dc3c3f53c2b406ca7afaed4493b1d099199e0" integrity sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig== -jest-haste-map@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.1.tgz#97e96f5fd7576d980307fbe6160b10c016b543d4" - integrity sha512-9kPafkv0nX6ta1PrshnkiyhhoQoFWncrU/uUBt3/AP1r78WSCU5iLceYRTwDvJl67H3RrXqSlSVDDa/AsUB7OQ== +jest-haste-map@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-haste-map/-/jest-haste-map-26.6.2.tgz#dd7e60fe7dc0e9f911a23d79c5ff7fb5c2cafeaa" + integrity sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" "@types/graceful-fs" "^4.1.2" "@types/node" "*" anymatch "^3.0.3" fb-watchman "^2.0.0" graceful-fs "^4.2.4" jest-regex-util "^26.0.0" - jest-serializer "^26.5.0" - jest-util "^26.6.1" - jest-worker "^26.6.1" + jest-serializer "^26.6.2" + jest-util "^26.6.2" + jest-worker "^26.6.2" micromatch "^4.0.2" sane "^4.0.3" walker "^1.0.7" optionalDependencies: fsevents "^2.1.2" -jest-jasmine2@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.1.tgz#11c92603d1fa97e3c33404359e69d6cec7e57017" - integrity sha512-2uYdT32o/ZzSxYAPduAgokO8OlAL1YdG/9oxcEY138EDNpIK5XRRJDaGzTZdIBWSxk0aR8XxN44FvfXtHB+Fiw== +jest-jasmine2@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz#adc3cf915deacb5212c93b9f3547cd12958f2edd" + integrity sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg== dependencies: "@babel/traverse" "^7.1.0" - "@jest/environment" "^26.6.1" - "@jest/source-map" "^26.5.0" - "@jest/test-result" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/environment" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" co "^4.6.0" - expect "^26.6.1" + expect "^26.6.2" is-generator-fn "^2.0.0" - jest-each "^26.6.1" - jest-matcher-utils "^26.6.1" - jest-message-util "^26.6.1" - jest-runtime "^26.6.1" - jest-snapshot "^26.6.1" - jest-util "^26.6.1" - pretty-format "^26.6.1" + jest-each "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-runtime "^26.6.3" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + pretty-format "^26.6.2" throat "^5.0.0" jest-junit@^12.0.0: @@ -8348,44 +8453,45 @@ jest-junit@^12.0.0: uuid "^3.3.3" xml "^1.0.1" -jest-leak-detector@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.1.tgz#f63e46dc4e3aa30d29b40ae49966a15730d25bbe" - integrity sha512-j9ZOtJSJKlHjrs4aIxWjiQUjyrffPdiAQn2Iw0916w7qZE5Lk0T2KhIH6E9vfhzP6sw0Q0jtnLLb4vQ71o1HlA== +jest-leak-detector@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz#7717cf118b92238f2eba65054c8a0c9c653a91af" + integrity sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg== dependencies: jest-get-type "^26.3.0" - pretty-format "^26.6.1" + pretty-format "^26.6.2" -jest-matcher-utils@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.1.tgz#bc90822d352c91c2ec1814731327691d06598400" - integrity sha512-9iu3zrsYlUnl8pByhREF9rr5eYoiEb1F7ymNKg6lJr/0qD37LWS5FSW/JcoDl8UdMX2+zAzabDs7sTO+QFKjCg== +jest-matcher-utils@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz#8e6fd6e863c8b2d31ac6472eeb237bc595e53e7a" + integrity sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw== dependencies: chalk "^4.0.0" - jest-diff "^26.6.1" + jest-diff "^26.6.2" jest-get-type "^26.3.0" - pretty-format "^26.6.1" + pretty-format "^26.6.2" -jest-message-util@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.1.tgz#d62c20c0fe7be10bfd6020b675abb9b5fa933ff3" - integrity sha512-cqM4HnqncIebBNdTKrBoWR/4ufHTll0pK/FWwX0YasK+TlBQEMqw3IEdynuuOTjDPFO3ONlFn37280X48beByw== +jest-message-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-message-util/-/jest-message-util-26.6.2.tgz#58173744ad6fc0506b5d21150b9be56ef001ca07" + integrity sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA== dependencies: "@babel/code-frame" "^7.0.0" - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" "@types/stack-utils" "^2.0.0" chalk "^4.0.0" graceful-fs "^4.2.4" micromatch "^4.0.2" + pretty-format "^26.6.2" slash "^3.0.0" stack-utils "^2.0.2" -jest-mock@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.1.tgz#6c12a92a82fc833f81a5b6de6b67d78386e276a3" - integrity sha512-my0lPTBu1awY8iVG62sB2sx9qf8zxNDVX+5aFgoB8Vbqjb6LqIOsfyFA8P1z6H2IsqMbvOX9oCJnK67Y3yUIMA== +jest-mock@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-mock/-/jest-mock-26.6.2.tgz#d6cb712b041ed47fe0d9b6fc3474bc6543feb302" + integrity sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" "@types/node" "*" jest-pnp-resolver@^1.2.2: @@ -8398,172 +8504,172 @@ jest-regex-util@^26.0.0: resolved "https://registry.yarnpkg.com/jest-regex-util/-/jest-regex-util-26.0.0.tgz#d25e7184b36e39fd466c3bc41be0971e821fee28" integrity sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A== -jest-resolve-dependencies@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.1.tgz#e9d091a159ad198c029279737a8b4c507791d75c" - integrity sha512-MN6lufbZJ3RBfTnJesZtHu3hUCBqPdHRe2+FhIt0yiqJ3fMgzWRqMRQyN/d/QwOE7KXwAG2ekZutbPhuD7s51A== +jest-resolve-dependencies@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz#6680859ee5d22ee5dcd961fe4871f59f4c784fb6" + integrity sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" jest-regex-util "^26.0.0" - jest-snapshot "^26.6.1" + jest-snapshot "^26.6.2" -jest-resolve@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.1.tgz#e9a9130cc069620d5aeeb87043dd9e130b68c6a1" - integrity sha512-hiHfQH6rrcpAmw9xCQ0vD66SDuU+7ZulOuKwc4jpbmFFsz0bQG/Ib92K+9/489u5rVw0btr/ZhiHqBpmkbCvuQ== +jest-resolve@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-resolve/-/jest-resolve-26.6.2.tgz#a3ab1517217f469b504f1b56603c5bb541fbb507" + integrity sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" chalk "^4.0.0" graceful-fs "^4.2.4" jest-pnp-resolver "^1.2.2" - jest-util "^26.6.1" + jest-util "^26.6.2" read-pkg-up "^7.0.1" resolve "^1.18.1" slash "^3.0.0" -jest-runner@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.1.tgz#a945971b5a23740c1fe20e372a38de668b7c76bf" - integrity sha512-DmpNGdgsbl5s0FGkmsInmqnmqCtliCSnjWA2TFAJS1m1mL5atwfPsf+uoZ8uYQ2X0uDj4NM+nPcDnUpbNTRMBA== +jest-runner@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runner/-/jest-runner-26.6.3.tgz#2d1fed3d46e10f233fd1dbd3bfaa3fe8924be159" + integrity sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ== dependencies: - "@jest/console" "^26.6.1" - "@jest/environment" "^26.6.1" - "@jest/test-result" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" emittery "^0.7.1" exit "^0.1.2" graceful-fs "^4.2.4" - jest-config "^26.6.1" + jest-config "^26.6.3" jest-docblock "^26.0.0" - jest-haste-map "^26.6.1" - jest-leak-detector "^26.6.1" - jest-message-util "^26.6.1" - jest-resolve "^26.6.1" - jest-runtime "^26.6.1" - jest-util "^26.6.1" - jest-worker "^26.6.1" + jest-haste-map "^26.6.2" + jest-leak-detector "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" + jest-runtime "^26.6.3" + jest-util "^26.6.2" + jest-worker "^26.6.2" source-map-support "^0.5.6" throat "^5.0.0" -jest-runtime@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.1.tgz#9a131e7b4f0bc6beefd62e7443f757c1d5fa9dec" - integrity sha512-7uOCNeezXDWgjEyzYbRN2ViY7xNZzusNVGAMmU0UHRUNXuY4j4GBHKGMqPo/cBPZA9bSYp+lwK2DRRBU5Dv6YQ== - dependencies: - "@jest/console" "^26.6.1" - "@jest/environment" "^26.6.1" - "@jest/fake-timers" "^26.6.1" - "@jest/globals" "^26.6.1" - "@jest/source-map" "^26.5.0" - "@jest/test-result" "^26.6.1" - "@jest/transform" "^26.6.1" - "@jest/types" "^26.6.1" +jest-runtime@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest-runtime/-/jest-runtime-26.6.3.tgz#4f64efbcfac398331b74b4b3c82d27d401b8fa2b" + integrity sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw== + dependencies: + "@jest/console" "^26.6.2" + "@jest/environment" "^26.6.2" + "@jest/fake-timers" "^26.6.2" + "@jest/globals" "^26.6.2" + "@jest/source-map" "^26.6.2" + "@jest/test-result" "^26.6.2" + "@jest/transform" "^26.6.2" + "@jest/types" "^26.6.2" "@types/yargs" "^15.0.0" chalk "^4.0.0" - cjs-module-lexer "^0.4.2" + cjs-module-lexer "^0.6.0" collect-v8-coverage "^1.0.0" exit "^0.1.2" glob "^7.1.3" graceful-fs "^4.2.4" - jest-config "^26.6.1" - jest-haste-map "^26.6.1" - jest-message-util "^26.6.1" - jest-mock "^26.6.1" + jest-config "^26.6.3" + jest-haste-map "^26.6.2" + jest-message-util "^26.6.2" + jest-mock "^26.6.2" jest-regex-util "^26.0.0" - jest-resolve "^26.6.1" - jest-snapshot "^26.6.1" - jest-util "^26.6.1" - jest-validate "^26.6.1" + jest-resolve "^26.6.2" + jest-snapshot "^26.6.2" + jest-util "^26.6.2" + jest-validate "^26.6.2" slash "^3.0.0" strip-bom "^4.0.0" yargs "^15.4.1" -jest-serializer@^26.5.0: - version "26.5.0" - resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.5.0.tgz#f5425cc4c5f6b4b355f854b5f0f23ec6b962bc13" - integrity sha512-+h3Gf5CDRlSLdgTv7y0vPIAoLgX/SI7T4v6hy+TEXMgYbv+ztzbg5PSN6mUXAT/hXYHvZRWm+MaObVfqkhCGxA== +jest-serializer@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-serializer/-/jest-serializer-26.6.2.tgz#d139aafd46957d3a448f3a6cdabe2919ba0742d1" + integrity sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g== dependencies: "@types/node" "*" graceful-fs "^4.2.4" -jest-snapshot@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.1.tgz#469e9d0b749496aea7dad0d7e5e5c88b91cdb4cc" - integrity sha512-JA7bZp7HRTIJYAi85pJ/OZ2eur2dqmwIToA5/6d7Mn90isGEfeF9FvuhDLLEczgKP1ihreBzrJ6Vr7zteP5JNA== +jest-snapshot@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-snapshot/-/jest-snapshot-26.6.2.tgz#f3b0af1acb223316850bd14e1beea9837fb39c84" + integrity sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og== dependencies: "@babel/types" "^7.0.0" - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" "@types/babel__traverse" "^7.0.4" "@types/prettier" "^2.0.0" chalk "^4.0.0" - expect "^26.6.1" + expect "^26.6.2" graceful-fs "^4.2.4" - jest-diff "^26.6.1" + jest-diff "^26.6.2" jest-get-type "^26.3.0" - jest-haste-map "^26.6.1" - jest-matcher-utils "^26.6.1" - jest-message-util "^26.6.1" - jest-resolve "^26.6.1" + jest-haste-map "^26.6.2" + jest-matcher-utils "^26.6.2" + jest-message-util "^26.6.2" + jest-resolve "^26.6.2" natural-compare "^1.4.0" - pretty-format "^26.6.1" + pretty-format "^26.6.2" semver "^7.3.2" -jest-util@^26.1.0, jest-util@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.1.tgz#4cc0d09ec57f28d12d053887eec5dc976a352e9b" - integrity sha512-xCLZUqVoqhquyPLuDXmH7ogceGctbW8SMyQVjD9o+1+NPWI7t0vO08udcFLVPLgKWcvc+zotaUv/RuaR6l8HIA== +jest-util@^26.1.0, jest-util@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-util/-/jest-util-26.6.2.tgz#907535dbe4d5a6cb4c47ac9b926f6af29576cbc1" + integrity sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" "@types/node" "*" chalk "^4.0.0" graceful-fs "^4.2.4" is-ci "^2.0.0" micromatch "^4.0.2" -jest-validate@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.1.tgz#28730eb8570d60968d9d06f1a8c94d922167bd2a" - integrity sha512-BEFpGbylKocnNPZULcnk+TGaz1oFZQH/wcaXlaXABbu0zBwkOGczuWgdLucUouuQqn7VadHZZeTvo8VSFDLMOA== +jest-validate@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-26.6.2.tgz#23d380971587150467342911c3d7b4ac57ab20ec" + integrity sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" camelcase "^6.0.0" chalk "^4.0.0" jest-get-type "^26.3.0" leven "^3.1.0" - pretty-format "^26.6.1" + pretty-format "^26.6.2" -jest-watcher@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.1.tgz#debfa34e9c5c3e735593403794fe53d2955bfabc" - integrity sha512-0LBIPPncNi9CaLKK15bnxyd2E8OMl4kJg0PTiNOI+MXztXw1zVdtX/x9Pr6pXaQYps+eS/ts43O4+HByZ7yJSw== +jest-watcher@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-watcher/-/jest-watcher-26.6.2.tgz#a5b683b8f9d68dbcb1d7dae32172d2cca0592975" + integrity sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ== dependencies: - "@jest/test-result" "^26.6.1" - "@jest/types" "^26.6.1" + "@jest/test-result" "^26.6.2" + "@jest/types" "^26.6.2" "@types/node" "*" ansi-escapes "^4.2.1" chalk "^4.0.0" - jest-util "^26.6.1" + jest-util "^26.6.2" string-length "^4.0.1" -jest-worker@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.1.tgz#c2ae8cde6802cc14056043f997469ec170d9c32a" - integrity sha512-R5IE3qSGz+QynJx8y+ICEkdI2OJ3RJjRQVEyCcFAd3yVhQSEtquziPO29Mlzgn07LOVE8u8jhJ1FqcwegiXWOw== +jest-worker@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/jest-worker/-/jest-worker-26.6.2.tgz#7f72cbc4d643c365e27b9fd775f9d0eaa9c7a8ed" + integrity sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ== dependencies: "@types/node" "*" merge-stream "^2.0.0" supports-color "^7.0.0" -jest@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.1.tgz#821e8280d2bdeeed40ac7bc43941dceff0f1b650" - integrity sha512-f+ahfqw3Ffy+9vA7sWFGpTmhtKEMsNAZiWBVXDkrpIO73zIz22iimjirnV78kh/eWlylmvLh/0WxHN6fZraZdA== +jest@^26.6.2, jest@^26.6.3: + version "26.6.3" + resolved "https://registry.yarnpkg.com/jest/-/jest-26.6.3.tgz#40e8fdbe48f00dfa1f0ce8121ca74b88ac9148ef" + integrity sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q== dependencies: - "@jest/core" "^26.6.1" + "@jest/core" "^26.6.3" import-local "^3.0.2" - jest-cli "^26.6.1" + jest-cli "^26.6.3" jmespath@0.15.0: version "0.15.0" @@ -8667,65 +8773,65 @@ jsesc@~0.5.0: resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-0.5.0.tgz#e7dee66e35d6fc16f710fe91d5cf69f70f08911d" integrity sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0= -jsii-diff@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.14.0.tgz#d9bc6c3df853f52659fb798392280483c9c557b8" - integrity sha512-/8M8C+Qah4U6Dn6Jm4GtGQyjHyn8djSnyzQ+eVG90FbUHRbmNAN/r643AcbqipyFDqim4IjYUnX56EMtR6Xc+Q== +jsii-diff@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/jsii-diff/-/jsii-diff-1.14.1.tgz#6ee1b6de68675a8cf8ad45b98474cbc7148c1aca" + integrity sha512-4lUf7++fply4tEW+HmhExjOCTz2zCihOdcn+bYvssG+2ztuFh+uyhUtcBaxXM49Mz8+RP3ymu3ArLr9BVmSfrg== dependencies: - "@jsii/spec" "^1.14.0" + "@jsii/spec" "^1.14.1" fs-extra "^9.0.1" - jsii-reflect "^1.14.0" + jsii-reflect "^1.14.1" log4js "^6.3.0" typescript "~3.9.7" yargs "^16.1.0" -jsii-pacmak@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.14.0.tgz#febb8c2bad45a06380613fa077c0aca829842fb8" - integrity sha512-6PibxOriIhsiPBxdMBvv+xwDD6JJewooZwWEHbJO6JYT2JzZ4EXxmxZ0PCZk1aIynv39vnaULkoYtjzO4XT4CA== +jsii-pacmak@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/jsii-pacmak/-/jsii-pacmak-1.14.1.tgz#1296cb926df803fef407b2cbbe2e2658a524c371" + integrity sha512-BRASls0wizqS4mxOmC2eoC7DgDb3tyS1UtFQeok0kfhhyi+GDs/9JPJ+VKlhU1kGLmsQswYxkPrZhV9VcXoiIw== dependencies: - "@jsii/spec" "^1.14.0" + "@jsii/spec" "^1.14.1" clone "^2.1.2" - codemaker "^1.14.0" + codemaker "^1.14.1" commonmark "^0.29.2" escape-string-regexp "^4.0.0" fs-extra "^9.0.1" - jsii-reflect "^1.14.0" - jsii-rosetta "^1.14.0" + jsii-reflect "^1.14.1" + jsii-rosetta "^1.14.1" semver "^7.3.2" spdx-license-list "^6.3.0" xmlbuilder "^15.1.1" yargs "^16.1.0" -jsii-reflect@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.14.0.tgz#c8c1f1026b45b0cd9022677868d8548b8562ee43" - integrity sha512-LOImMIFu/DgRNdgXloA5OVGOtEOZsm1UQ2qQHQ3O0MHWgqGvyBRYPh7wwgytucB37lGEz8KgphiJ/gmTAcA1oA== +jsii-reflect@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/jsii-reflect/-/jsii-reflect-1.14.1.tgz#e0073b4fbfcc977f7546675c427d7ca0eae8d699" + integrity sha512-otKxvnNn2kAMMygBiw8fGnmJFhQ0EcPTJZH4y/Yn1rZg/nGLAk/G8lCQYfh3xm2/GwSpjh/w6ZEPsq/RUuPR1A== dependencies: - "@jsii/spec" "^1.14.0" + "@jsii/spec" "^1.14.1" colors "^1.4.0" fs-extra "^9.0.1" - oo-ascii-tree "^1.14.0" + oo-ascii-tree "^1.14.1" yargs "^16.1.0" -jsii-rosetta@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.14.0.tgz#a31b76720292360acd5009883903c0332772ba5c" - integrity sha512-6giv6Bo4zyC4eIw0vUO2/VRvxavdiASH/YzlRdPFWFDecvkyhGSzFTd+kQ2+y+JrQUSiGnUfduF6S/daLTCVIA== +jsii-rosetta@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/jsii-rosetta/-/jsii-rosetta-1.14.1.tgz#797c433d9b353d360de4c9c71d0913b9b33fcb1c" + integrity sha512-HfM1IO7eIQ8qyDxTRRdV3TraBhnCivq3N3qMdJuPEJ3O2lprx0TS6pvIXzv9DgDWJwLVDaxI1ecTZhSl9poGeQ== dependencies: - "@jsii/spec" "^1.14.0" + "@jsii/spec" "^1.14.1" commonmark "^0.29.2" fs-extra "^9.0.1" typescript "~3.9.7" xmldom "^0.4.0" yargs "^16.1.0" -jsii@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.14.0.tgz#75fc3b2aa2645926a7e432df8d94c1fecdd9d6c9" - integrity sha512-6vW8sdVu3S5t3kVVI7d9hzxhZ8wqz4J3mHBMArbL/qJpUVB3ruF2W0RVPKwi16u4hnYNqE29TbSq+H5qdepKqg== +jsii@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/jsii/-/jsii-1.14.1.tgz#4b478b5f682ae140fbfdd49c171b0cff7f7e01bd" + integrity sha512-uDVBl8bvSnraJpKYyY22dOoERpQv/sEAHEj3L5b00qkBi6FsFr2KWfQOdUg9fMWdYrmMVuXWOWXJ186Fn7XF+A== dependencies: - "@jsii/spec" "^1.14.0" + "@jsii/spec" "^1.14.1" case "^1.6.3" colors "^1.4.0" deep-equal "^2.0.4" @@ -8888,24 +8994,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -lambda-leak@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/lambda-leak/-/lambda-leak-2.0.0.tgz#771985d3628487f6e885afae2b54510dcfb2cd7e" - integrity sha1-dxmF02KEh/boha+uK1RRDc+yzX4= - -lambda-tester@^3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/lambda-tester/-/lambda-tester-3.6.0.tgz#ceb7d4f4f0da768487a05cff37dcd088508b5247" - integrity sha512-F2ZTGWCLyIR95o/jWK46V/WnOCFAEUG/m/V7/CLhPJ7PCM+pror1rZ6ujP3TkItSGxUfpJi0kqwidw+M/nEqWw== - dependencies: - app-root-path "^2.2.1" - dotenv "^8.0.0" - dotenv-json "^1.0.0" - lambda-leak "^2.0.0" - semver "^6.1.1" - uuid "^3.3.2" - vandium-utils "^1.1.1" - lazystream@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lazystream/-/lazystream-1.0.0.tgz#f6995fe0f820392f61396be89462407bb77168e4" @@ -9263,6 +9351,13 @@ lru-cache@^5.1.1: dependencies: yallist "^3.0.2" +lru-cache@^6.0.0: + version "6.0.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-6.0.0.tgz#6d6fe6570ebd96aaf90fcad1dafa3b2566db3a94" + integrity sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA== + dependencies: + yallist "^4.0.0" + macos-release@^2.2.0: version "2.4.1" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.4.1.tgz#64033d0ec6a5e6375155a74b1a1eba8e509820ac" @@ -9435,6 +9530,23 @@ meow@^7.0.0: type-fest "^0.13.1" yargs-parser "^18.1.3" +meow@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/meow/-/meow-8.0.0.tgz#1aa10ee61046719e334ffdc038bb5069250ec99a" + integrity sha512-nbsTRz2fwniJBFgUkcdISq8y/q9n9VbiHYbfwklFh5V4V2uAcxtKQkDc0yCLPM/kP0d+inZBewn3zJqewHE7kg== + dependencies: + "@types/minimist" "^1.2.0" + camelcase-keys "^6.2.2" + decamelize-keys "^1.1.0" + hard-rejection "^2.1.0" + minimist-options "4.1.0" + normalize-package-data "^3.0.0" + read-pkg-up "^7.0.1" + redent "^3.0.0" + trim-newlines "^3.0.0" + type-fest "^0.18.0" + yargs-parser "^20.2.3" + merge-descriptors@~1.0.0: version "1.0.1" resolved "https://registry.yarnpkg.com/merge-descriptors/-/merge-descriptors-1.0.1.tgz#b00aaa556dd8b44568150ec9d1b953f3f90cbb61" @@ -9895,6 +10007,16 @@ normalize-package-data@^2.0.0, normalize-package-data@^2.3.0, normalize-package- semver "2 || 3 || 4 || 5" validate-npm-package-license "^3.0.1" +normalize-package-data@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-package-data/-/normalize-package-data-3.0.0.tgz#1f8a7c423b3d2e85eb36985eaf81de381d01301a" + integrity sha512-6lUjEI0d3v6kFrtgA/lOx4zHCWULXsFNIjHolnZCKCTLA6m/G625cdn3O7eNmT0iD3jfo6HZ9cdImGZwf21prw== + dependencies: + hosted-git-info "^3.0.6" + resolve "^1.17.0" + semver "^7.3.2" + validate-npm-package-license "^3.0.1" + normalize-path@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-2.1.1.tgz#1ab28b556e198363a8c1a6f7e6fa20137fe6aed9" @@ -10193,10 +10315,10 @@ onetime@^5.1.0: dependencies: mimic-fn "^2.1.0" -oo-ascii-tree@^1.14.0: - version "1.14.0" - resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.14.0.tgz#156f285161a13c5c44cb96fd5e40f1cf3b036661" - integrity sha512-G9MFFuZa8rXMVo4Za8cy9a3uUEsRY7Ru2JZmi/YElM3mqPvYVQqmhGtD2WUzB5q/E3iaGOrT2rI8iFtPImoOCw== +oo-ascii-tree@^1.14.1: + version "1.14.1" + resolved "https://registry.yarnpkg.com/oo-ascii-tree/-/oo-ascii-tree-1.14.1.tgz#cf3da9d7c9c944d3258b274e06aa0192aca20d6e" + integrity sha512-dW0RFnYqUj8WQpvuYXVvjfA3ABoNQnScgSxnKa9lPPCvfcO4CBPshifk9M6hU3ksttcNGbQkFq6k2di2E23SVA== open@^7.0.3: version "7.3.0" @@ -10455,19 +10577,19 @@ parallel-transform@^1.1.0: inherits "^2.0.3" readable-stream "^2.1.5" -parcel@2.0.0-nightly.432: - version "2.0.0-nightly.432" - resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.0.0-nightly.432.tgz#97df939b3808bd96000150892be0004b237d13ee" - integrity sha512-N0PAnhXgtBVC3cJZxqD33yw3T5Njdb6+nqzZ7SXaVfgrxmhBw1JjOHeCqovkNEwUypMDISN/+5DAnTDuvaviCA== - dependencies: - "@parcel/config-default" "2.0.0-nightly.434+146cffb6" - "@parcel/core" "2.0.0-nightly.432+146cffb6" - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/events" "2.0.0-nightly.434+146cffb6" - "@parcel/fs" "2.0.0-nightly.434+146cffb6" - "@parcel/logger" "2.0.0-nightly.434+146cffb6" - "@parcel/package-manager" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" +parcel@2.0.0-nightly.442: + version "2.0.0-nightly.442" + resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.0.0-nightly.442.tgz#4d776ab4ba1e371809b983bec9ccf65aa88ce0e7" + integrity sha512-v7OrOEJujFS4BkDx1SGTfXqL4abIuhT0DYP5oenZ5Ump/jmeOYUlDv4L/reds7kOanFKIu5xiA6yR0JOTUwmuQ== + dependencies: + "@parcel/config-default" "2.0.0-nightly.444+e2136965" + "@parcel/core" "2.0.0-nightly.442+e2136965" + "@parcel/diagnostic" "2.0.0-nightly.444+e2136965" + "@parcel/events" "2.0.0-nightly.444+e2136965" + "@parcel/fs" "2.0.0-nightly.444+e2136965" + "@parcel/logger" "2.0.0-nightly.444+e2136965" + "@parcel/package-manager" "2.0.0-nightly.444+e2136965" + "@parcel/utils" "2.0.0-nightly.444+e2136965" chalk "^2.1.0" commander "^2.19.0" get-port "^4.2.0" @@ -11161,12 +11283,12 @@ pretty-format@^26.0.0, pretty-format@^26.6.0: ansi-styles "^4.0.0" react-is "^16.12.0" -pretty-format@^26.6.1: - version "26.6.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.1.tgz#af9a2f63493a856acddeeb11ba6bcf61989660a8" - integrity sha512-MeqqsP5PYcRBbGMvwzsyBdmAJ4EFX7pWFyl7x4+dMVg5pE0ZDdBIvEH2ergvIO+Gvwv1wh64YuOY9y5LuyY/GA== +pretty-format@^26.6.2: + version "26.6.2" + resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-26.6.2.tgz#e35c2705f14cb7fe2fe94fa078345b444120fc93" + integrity sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg== dependencies: - "@jest/types" "^26.6.1" + "@jest/types" "^26.6.2" ansi-regex "^5.0.0" ansi-styles "^4.0.0" react-is "^17.0.1" @@ -11425,10 +11547,10 @@ react-is@^17.0.1: resolved "https://registry.yarnpkg.com/react-is/-/react-is-17.0.1.tgz#5b3531bd76a645a4c9fb6e693ed36419e3301339" integrity sha512-NAnt2iGDXohE5LI7uBnLnqvLQMtzhkiAOLXTmv+qnF9Ky7xAPcX8Up/xWIhxvLVGJvuLiNc4xQLtuqDRzb4fSA== -react-refresh@^0.6.0: - version "0.6.0" - resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.6.0.tgz#81971b8f3c8c05aaa6ce87491ae41b396133f896" - integrity sha512-Wv48N+GFt6Azvtl/LMvzNW9hvEyJdRQ48oVKIBAN7hjtvXXfxfVJXbPl/11SM1C/NIquIFXzzWCo6ZNH0I8I4g== +react-refresh@^0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/react-refresh/-/react-refresh-0.9.0.tgz#71863337adc3e5c2f8a6bfddd12ae3bfe32aafbf" + integrity sha512-Gvzk7OZpiqKSkxsQvO/mbTN1poglhmAV7gR/DdIrRrSMXraRQQlfikRJOr3Nb9GTMPC5kof948Zy6jJZIFtDvQ== read-cmd-shim@^1.0.1: version "1.0.5" @@ -11564,7 +11686,7 @@ readable-stream@1.1.x: isarray "0.0.1" string_decoder "~0.10.x" -"readable-stream@2 || 3", readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: +"readable-stream@2 || 3", readable-stream@3, readable-stream@^3.0.0, readable-stream@^3.0.2, readable-stream@^3.1.1, readable-stream@^3.4.0, readable-stream@^3.6.0: version "3.6.0" resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.0.tgz#337bbda3adc0706bd3e024426a286d4b4b2c9198" integrity sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA== @@ -11817,7 +11939,7 @@ resolve@^1.1.6, resolve@^1.10.0, resolve@^1.11.1, resolve@^1.12.0, resolve@^1.13 dependencies: path-parse "^1.0.6" -resolve@^1.10.1, resolve@^1.18.1: +resolve@^1.18.1: version "1.18.1" resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.18.1.tgz#018fcb2c5b207d2a6424aee361c5a266da8f4130" integrity sha512-lDfCPaMKfOJXjy0dPayzPdF1phampNWr3qFCjAu+rw/qbQmr5jWH5xN2hwh9QKfw9E5v4hwV7A+jrCmL8yjjqA== @@ -11885,7 +12007,7 @@ rimraf@^2.5.4, rimraf@^2.6.2, rimraf@^2.6.3: dependencies: glob "^7.1.3" -rimraf@^3.0.0: +rimraf@^3.0.0, rimraf@^3.0.2: version "3.0.2" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-3.0.2.tgz#f1a5402ba6220ad52cc1282bac1ae3aa49fd061a" integrity sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA== @@ -12017,7 +12139,7 @@ semver@7.x, semver@^7.1.1, semver@^7.2.1, semver@^7.3.2: resolved "https://registry.yarnpkg.com/semver/-/semver-7.3.2.tgz#604962b052b81ed0786aae84389ffba70ffd3938" integrity sha512-OrOb32TeeambH6UrhtShmF7CRDqhL6/5XpPNp2DuRH6+9QLw/orhp72j87v8Qa1ScDkvrrBNpZcDejAirJmfXQ== -semver@^6.0.0, semver@^6.1.0, semver@^6.1.1, semver@^6.2.0, semver@^6.3.0: +semver@^6.0.0, semver@^6.2.0, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -13045,6 +13167,13 @@ through2@^3.0.0: inherits "^2.0.4" readable-stream "2 || 3" +through2@^4.0.0: + version "4.0.2" + resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" + integrity sha512-iOqSav00cVxEEICeD7TjLB1sueEL+81Wpzp2bY17uZjZN0pWZPuo4suZ/61VujxmqSGFfgOcNuTZ85QJwNZQpw== + dependencies: + readable-stream "3" + through@2, "through@>=2.2.7 <3", through@^2.3.4, through@^2.3.6, through@^2.3.8: version "2.3.8" resolved "https://registry.yarnpkg.com/through/-/through-2.3.8.tgz#0dd4c9ffaabc357960b1b724115d7e0e86a2e1f5" @@ -13182,12 +13311,11 @@ trivial-deferred@^1.0.1: resolved "https://registry.yarnpkg.com/trivial-deferred/-/trivial-deferred-1.0.1.tgz#376d4d29d951d6368a6f7a0ae85c2f4d5e0658f3" integrity sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM= -ts-jest@^26.4.3: - version "26.4.3" - resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.3.tgz#d153a616033e7ec8544b97ddbe2638cbe38d53db" - integrity sha512-pFDkOKFGY+nL9v5pkhm+BIFpoAuno96ff7GMnIYr/3L6slFOS365SI0fGEVYx2RKGji5M2elxhWjDMPVcOCdSw== +ts-jest@^26.4.4: + version "26.4.4" + resolved "https://registry.yarnpkg.com/ts-jest/-/ts-jest-26.4.4.tgz#61f13fb21ab400853c532270e52cc0ed7e502c49" + integrity sha512-3lFWKbLxJm34QxyVNNCgXX1u4o/RV0myvA2y2Bxm46iGIjKlaY0own9gIckbjZJPn+WaJEnfPPJ20HHGpoq4yg== dependencies: - "@jest/create-cache-key-function" "^26.5.0" "@types/jest" "26.x" bs-logger "0.x" buffer-from "1.x" @@ -13305,6 +13433,11 @@ type-fest@^0.13.1: resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.13.1.tgz#0172cb5bce80b0bd542ea348db50c7e21834d934" integrity sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg== +type-fest@^0.18.0: + version "0.18.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.18.0.tgz#2edfa6382d48653707344f7fccdb0443d460e8d6" + integrity sha512-fbDukFPnJBdn2eZ3RR+5mK2slHLFd6gYHY7jna1KWWy4Yr4XysHuCdXRzy+RiG/HwG4WJat00vdC2UHky5eKiQ== + type-fest@^0.3.0: version "0.3.1" resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.3.1.tgz#63d00d204e059474fe5e1b7c011112bbd1dc29e1" @@ -13585,12 +13718,7 @@ uuid@^3.0.1, uuid@^3.3.2, uuid@^3.3.3: resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.4.0.tgz#b23e4358afa8a202fe7a100af1f5f883f02007ee" integrity sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A== -uuid@^8.3.0: - version "8.3.0" - resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.0.tgz#ab738085ca22dc9a8c92725e459b1d507df5d6ea" - integrity sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ== - -uuid@^8.3.1: +uuid@^8.3.0, uuid@^8.3.1: version "8.3.1" resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.1.tgz#2ba2e6ca000da60fce5a196954ab241131e05a31" integrity sha512-FOmRr+FmWEIG8uhZv6C2bTgEVXsHk08kE7mPlrBbEe+c3r9pjceVPgupIfNIhc4yx55H69OXANrUaSuu9eInKg== @@ -13600,10 +13728,10 @@ v8-compile-cache@^2.0.0, v8-compile-cache@^2.0.3: resolved "https://registry.yarnpkg.com/v8-compile-cache/-/v8-compile-cache-2.1.1.tgz#54bc3cdd43317bca91e35dcaf305b1a7237de745" integrity sha512-8OQ9CL+VWyt3JStj7HX7/ciTL2V3Rl1Wf5OL+SNTm0yK1KvtReVulksyeRnCANHHuUxHlQig+JJDlUhBt1NQDQ== -v8-to-istanbul@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-6.0.1.tgz#7ef0e32faa10f841fe4c1b0f8de96ed067c0be1e" - integrity sha512-PzM1WlqquhBvsV+Gco6WSFeg1AGdD53ccMRkFeyHRE/KRZaVacPOmQYP3EeVgDBtKD2BJ8kgynBQ5OtKiHCH+w== +v8-to-istanbul@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/v8-to-istanbul/-/v8-to-istanbul-7.0.0.tgz#b4fe00e35649ef7785a9b7fcebcea05f37c332fc" + integrity sha512-fLL2rFuQpMtm9r8hrAV2apXX/WqHJ6+IC4/eQVdMDGBUgH/YMV4Gv3duk3kjmyg6uiQWBAA9nJwue4iJUOkHeA== dependencies: "@types/istanbul-lib-coverage" "^2.0.1" convert-source-map "^1.6.0" @@ -13624,11 +13752,6 @@ validate-npm-package-name@^3.0.0: dependencies: builtins "^1.0.3" -vandium-utils@^1.1.1: - version "1.2.0" - resolved "https://registry.yarnpkg.com/vandium-utils/-/vandium-utils-1.2.0.tgz#44735de4b7641a05de59ebe945f174e582db4f59" - integrity sha1-RHNd5LdkGgXeWevpRfF05YLbT1k= - vendors@^1.0.0: version "1.0.4" resolved "https://registry.yarnpkg.com/vendors/-/vendors-1.0.4.tgz#e2b800a53e7a29b93506c3cf41100d16c4c4ad8e" @@ -13996,7 +14119,7 @@ yapool@^1.0.0: resolved "https://registry.yarnpkg.com/yapool/-/yapool-1.0.0.tgz#f693f29a315b50d9a9da2646a7a6645c96985b6a" integrity sha1-9pPymjFbUNmp2iZGp6ZkXJaYW2o= -yargs-parser@20.x, yargs-parser@^20.2.2: +yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.3" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.3.tgz#92419ba867b858c868acf8bae9bf74af0dd0ce26" integrity sha512-emOFRT9WVHw03QSvN5qor9QQT9+sw5vwxfYweivSMHTcAXPefwVae2FjO7JJjj8hCE4CzPOPeFM83VwT29HCww==