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..b538f7c009ee0 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 - -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. - -Additional scripts that take part in the versioning mechanism: - -- `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. +### Versioning and Release + +The `release.json` file at the root of the repo determines which release line +this branch belongs to. + +```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 + +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/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..0ca41490969d8 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, 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..005f503e417c2 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" ] ] }, 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..fbd4ece89913b 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" ] ] }, 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/assert/package.json b/packages/@aws-cdk/assert/package.json index 5e320f5b393de..a1e007b5733cf 100644 --- a/packages/@aws-cdk/assert/package.json +++ b/packages/@aws-cdk/assert/package.json @@ -23,7 +23,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", "ts-jest": "^26.4.3" }, @@ -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/.eslintrc.js b/packages/@aws-cdk/aws-apigatewayv2-integrations/.eslintrc.js new file mode 100644 index 0000000000000..61dd8dd001f63 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/.eslintrc.js @@ -0,0 +1,3 @@ +const baseConfig = require('cdk-build-tools/config/eslintrc'); +baseConfig.parserOptions.project = __dirname + '/tsconfig.json'; +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/.gitignore b/packages/@aws-cdk/aws-apigatewayv2-integrations/.gitignore new file mode 100644 index 0000000000000..becda34c45624 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/.gitignore @@ -0,0 +1,17 @@ +*.d.ts +*.generated.ts +*.js +*.js.map +*.snk +.jsii +.LAST_BUILD +.LAST_PACKAGE +nyc.config.js +.nyc_output +coverage +dist +tsconfig.json +!.eslintrc.js +!jest.config.js + +junit.xml \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/.npmignore b/packages/@aws-cdk/aws-apigatewayv2-integrations/.npmignore new file mode 100644 index 0000000000000..093c734b1bd2f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/.npmignore @@ -0,0 +1,28 @@ +# The basics +*.ts +*.tgz +*.snk +!*.d.ts +!*.js +**/cdk.out + +# Coverage +coverage +.nyc_output +.nycrc + +# Build gear +dist +.LAST_BUILD +.LAST_PACKAGE + +*.tsbuildinfo +tsconfig.json +!.jsii +.eslintrc.js +jest.config.js + +# exclude cdk artifacts +**/cdk.out +junit.xml +test/ \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/LICENSE b/packages/@aws-cdk/aws-apigatewayv2-integrations/LICENSE new file mode 100644 index 0000000000000..b71ec1688783a --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/NOTICE b/packages/@aws-cdk/aws-apigatewayv2-integrations/NOTICE new file mode 100644 index 0000000000000..bfccac9a7f69c --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/NOTICE @@ -0,0 +1,2 @@ +AWS Cloud Development Kit (AWS CDK) +Copyright 2018-2020 Amazon.com, Inc. or its affiliates. All Rights Reserved. diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md new file mode 100644 index 0000000000000..2cda8eab308ab --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/README.md @@ -0,0 +1,88 @@ +## AWS APIGatewayv2 Integrations + +--- + +![cdk-constructs: Experimental](https://img.shields.io/badge/cdk--constructs-experimental-important.svg?style=for-the-badge) + +> The APIs of higher level constructs in this module are experimental and under active development. They are subject to non-backward compatible changes or removal in any future version. These are not subject to the [Semantic Versioning](https://semver.org/) model and breaking changes will be announced in the release notes. This means that while you may use them, you may need to update your source code when upgrading to a newer version of this package. + +--- + + +## Table of Contents + +- [Introduction](#introduction) +- [Private Integration](#private-integration) + +## Introduction + +Integrations connect a route to backend resources. HTTP APIs support Lambda proxy, AWS service, and HTTP proxy integrations. HTTP proxy integrations are also known as private integrations. + +Currently the following integrations are supported by this construct library. + +## Private Integration + +Private integrations enable integrating an HTTP API route with private resources in a VPC, such as Application Load Balancers or +Amazon ECS container-based applications. Using private integrations, resources in a VPC can be exposed for access by +clients outside of the VPC. + +The following integrations are supported for private resources in a VPC. + +### Application Load Balancer + +The following code is a basic application load balancer private integration of HTTP API: + +```ts +const vpc = new ec2.Vpc(stack, 'VPC'); +const lb = new elbv2.ALB(stack, 'lb', { vpc }); +const listener = lb.addListener('listener', { port: 80 }); +listener.addTargets('target', { + port: 80, +}); + +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpAlbIntegrationProps({ + listener, + }), +}); +``` + +### Network Load Balancer + +The following code is a basic network load balancer private integration of HTTP API: + +```ts +const vpc = new ec2.Vpc(stack, 'VPC'); +const lb = new elbv2.NLB(stack, 'lb', { vpc }); +const listener = lb.addListener('listener', { port: 80 }); +listener.addTargets('target', { + port: 80, +}); + +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpNlbIntegrationProps({ + listener, + }), +}); +``` + +### Cloud Map Service Discovery + +The following code is a basic discovery service private integration of HTTP API: + +```ts +const vpc = new ec2.Vpc(stack, 'VPC'); +const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); +const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'Namespace', { + name: 'boobar.com', + vpc, +}); +const service = namespace.createService('Service'); + +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpServiceDiscoveryIntegration({ + vpcLink, + service, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/jest.config.js b/packages/@aws-cdk/aws-apigatewayv2-integrations/jest.config.js new file mode 100644 index 0000000000000..54e28beb9798b --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/jest.config.js @@ -0,0 +1,2 @@ +const baseConfig = require('cdk-build-tools/config/jest.config'); +module.exports = baseConfig; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts new file mode 100644 index 0000000000000..2d35f89e8206e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/alb.ts @@ -0,0 +1,39 @@ +import { HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { HttpPrivateIntegrationOptions } from './base-types'; +import { HttpPrivateIntegration } from './private/integration'; + +/** + * Properties to initialize `HttpAlbIntegration`. + */ +export interface HttpAlbIntegrationProps extends HttpPrivateIntegrationOptions { + /** + * The listener to the application load balancer used for the integration + */ + readonly listener: elbv2.ApplicationListener; +} + +/** + * The Application Load Balancer integration resource for HTTP API + */ +export class HttpAlbIntegration extends HttpPrivateIntegration { + constructor(private readonly props: HttpAlbIntegrationProps) { + super(); + } + + public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const vpcLink = this._configureVpcLink(options, { + vpcLink: this.props.vpcLink, + vpc: this.props.listener.loadBalancer.vpc, + }); + + return { + method: this.props.method ?? this.httpMethod, + payloadFormatVersion: this.payloadFormatVersion, + type: this.integrationType, + connectionType: this.connectionType, + connectionId: vpcLink.vpcLinkId, + uri: this.props.listener.listenerArn, + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts new file mode 100644 index 0000000000000..5000dfb63a751 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/base-types.ts @@ -0,0 +1,19 @@ +import { HttpMethod, IVpcLink } from '@aws-cdk/aws-apigatewayv2'; + +/** + * Base options for private integration + */ +export interface HttpPrivateIntegrationOptions { + /** + * The vpc link to be used for the private integration + * + * @default - a new VpcLink is created + */ + readonly vpcLink?: IVpcLink; + + /** + * The HTTP method that must be used to invoke the underlying HTTP proxy. + * @default HttpMethod.ANY + */ + readonly method?: HttpMethod; +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts new file mode 100644 index 0000000000000..bd4bb0ee34cac --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/index.ts @@ -0,0 +1,4 @@ +export * from './base-types'; +export * from './alb'; +export * from './nlb'; +export * from './service-discovery'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts new file mode 100644 index 0000000000000..1ee7ce4f208ed --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/nlb.ts @@ -0,0 +1,39 @@ +import { HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { HttpPrivateIntegrationOptions } from './base-types'; +import { HttpPrivateIntegration } from './private/integration'; + +/** + * Properties to initialize `HttpNlbIntegration`. + */ +export interface HttpNlbIntegrationProps extends HttpPrivateIntegrationOptions { + /** + * The listener to the netwwork load balancer used for the integration + */ + readonly listener: elbv2.NetworkListener; +} + +/** + * The Network Load Balancer integration resource for HTTP API + */ +export class HttpNlbIntegration extends HttpPrivateIntegration { + constructor(private readonly props: HttpNlbIntegrationProps) { + super(); + } + + public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const vpcLink = this._configureVpcLink(options, { + vpcLink: this.props.vpcLink, + vpc: this.props.listener.loadBalancer.vpc, + }); + + return { + method: this.props.method ?? this.httpMethod, + payloadFormatVersion: this.payloadFormatVersion, + type: this.integrationType, + connectionType: this.connectionType, + connectionId: vpcLink.vpcLinkId, + uri: this.props.listener.listenerArn, + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts new file mode 100644 index 0000000000000..6d32b22794722 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/private/integration.ts @@ -0,0 +1,65 @@ +import { + HttpConnectionType, + HttpIntegrationType, + HttpRouteIntegrationBindOptions, + HttpRouteIntegrationConfig, + IHttpRouteIntegration, + PayloadFormatVersion, + HttpMethod, + IVpcLink, +} from '@aws-cdk/aws-apigatewayv2'; +import * as ec2 from '@aws-cdk/aws-ec2'; + + +/** + * Options required to use an existing vpcLink or configure a new one + * + * @internal + */ +export interface VpcLinkConfigurationOptions { + /** + * The vpc link to be used for the private integration + * + * @default - a new VpcLink is created + */ + readonly vpcLink?: IVpcLink; + + /** + * The vpc for which the VpcLink needs to be created + * + * @default undefined + */ + readonly vpc?: ec2.IVpc; +} + +/** + * The HTTP Private integration resource for HTTP API + * + * @internal + */ +export abstract class HttpPrivateIntegration implements IHttpRouteIntegration { + protected httpMethod = HttpMethod.ANY; + protected payloadFormatVersion = PayloadFormatVersion.VERSION_1_0; // 1.0 is required and is the only supported format + protected integrationType = HttpIntegrationType.HTTP_PROXY; + protected connectionType = HttpConnectionType.VPC_LINK + + /** + * Adds a vpcLink to the API if not passed in the options + * + * @internal + */ + protected _configureVpcLink(bindOptions: HttpRouteIntegrationBindOptions, configOptions: VpcLinkConfigurationOptions): IVpcLink { + let vpcLink = configOptions.vpcLink; + if (!vpcLink) { + if (!configOptions.vpc) { + throw new Error('One of vpcLink or vpc should be provided for private integration'); + } + + vpcLink = bindOptions.route.httpApi.addVpcLink({ vpc: configOptions.vpc }); + } + + return vpcLink; + } + + public abstract bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig; +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts new file mode 100644 index 0000000000000..96e13d0a03273 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/http/service-discovery.ts @@ -0,0 +1,44 @@ +import { HttpMethod, IVpcLink, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig } from '@aws-cdk/aws-apigatewayv2'; +import * as servicediscovery from '@aws-cdk/aws-servicediscovery'; +import { HttpPrivateIntegration } from './private/integration'; + +/** + * Properties to initialize `HttpServiceDiscoveryIntegration`. + */ +export interface HttpServiceDiscoveryIntegrationProps { + /** + * The discovery service used for the integration + */ + readonly service: servicediscovery.Service; + + /** + * The vpc link to be used for the private integration + */ + readonly vpcLink: IVpcLink; + + /** + * The HTTP method that must be used to invoke the underlying HTTP proxy. + * @default HttpMethod.ANY + */ + readonly method?: HttpMethod; +} + +/** + * The Service Discovery integration resource for HTTP API + */ +export class HttpServiceDiscoveryIntegration extends HttpPrivateIntegration { + constructor(private readonly props: HttpServiceDiscoveryIntegrationProps) { + super(); + } + + public bind(_: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + return { + method: this.props.method ?? this.httpMethod, + payloadFormatVersion: this.payloadFormatVersion, + type: this.integrationType, + connectionType: this.connectionType, + connectionId: this.props.vpcLink.vpcLinkId, + uri: this.props.service.serviceArn, + }; + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts new file mode 100644 index 0000000000000..c202386ae710e --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/lib/index.ts @@ -0,0 +1 @@ +export * from './http'; diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json new file mode 100644 index 0000000000000..3cca63ba579e2 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/package.json @@ -0,0 +1,104 @@ +{ + "name": "@aws-cdk/aws-apigatewayv2-integrations", + "version": "0.0.0", + "description": "Integrations for AWS APIGateway V2", + "main": "lib/index.js", + "types": "lib/index.d.ts", + "jsii": { + "outdir": "dist", + "targets": { + "dotnet": { + "namespace": "Amazon.CDK.AWS.APIGatewayv2.Integrations", + "packageId": "Amazon.CDK.AWS.APIGatewayv2.Integrations", + "signAssembly": true, + "assemblyOriginatorKeyFile": "../../key.snk", + "iconUrl": "https://raw.githubusercontent.com/aws/aws-cdk/master/logo/default-256-dark.png" + }, + "java": { + "package": "software.amazon.awscdk.services.apigatewayv2.integrations", + "maven": { + "groupId": "software.amazon.awscdk", + "artifactId": "apigatewayv2-integrations" + } + }, + "python": { + "distName": "aws-cdk.aws-apigatewayv2-integrations", + "module": "aws_cdk.aws_apigatewayv2_integrations", + "classifiers": [ + "Framework :: AWS CDK", + "Framework :: AWS CDK :: 1" + ] + } + }, + "projectReferences": true + }, + "repository": { + "type": "git", + "url": "https://github.com/aws/aws-cdk.git", + "directory": "packages/@aws-cdk/aws-apigatewayv2-integrations" + }, + "homepage": "https://github.com/aws/aws-cdk", + "scripts": { + "build": "cdk-build", + "integ": "cdk-integ", + "lint": "cdk-lint", + "package": "cdk-package", + "awslint": "cdk-awslint", + "pkglint": "pkglint -f", + "test": "cdk-test", + "watch": "cdk-watch", + "compat": "cdk-compat", + "build+test": "npm run build && npm test", + "build+test+package": "npm run build+test && npm run package" + }, + "cdk-build": { + "jest": true, + "env": { + "AWSLINT_BASE_CONSTRUCT": true + } + }, + "keywords": [ + "aws", + "cdk", + "constructs", + "apigateway" + ], + "author": { + "name": "Amazon Web Services", + "url": "https://aws.amazon.com", + "organization": true + }, + "license": "Apache-2.0", + "devDependencies": { + "@aws-cdk/assert": "0.0.0", + "@types/nodeunit": "^0.0.31", + "cdk-build-tools": "0.0.0", + "cdk-integ-tools": "0.0.0", + "nodeunit": "^0.11.3", + "pkglint": "0.0.0" + }, + "dependencies": { + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" + }, + "peerDependencies": { + "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-ec2": "0.0.0", + "@aws-cdk/aws-elasticloadbalancingv2": "0.0.0", + "@aws-cdk/aws-servicediscovery": "0.0.0", + "@aws-cdk/core": "0.0.0", + "constructs": "^3.2.0" + }, + "engines": { + "node": ">= 10.13.0 <13 || >=13.7.0" + }, + "stability": "experimental", + "maturity": "experimental", + "awscdkio": { + "announce": false + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts new file mode 100644 index 0000000000000..691da1e4c3b9f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/alb.test.ts @@ -0,0 +1,101 @@ +import '@aws-cdk/assert/jest'; +import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { Stack } from '@aws-cdk/core'; +import { HttpAlbIntegration } from '../../lib'; + +describe('HttpAlbIntegration', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpAlbIntegration({ + listener, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + ConnectionId: { + Ref: 'HttpApiVpcLink159804837', + }, + ConnectionType: 'VPC_LINK', + IntegrationMethod: 'ANY', + IntegrationUri: { + Ref: 'lblistener657ADDEC', + }, + PayloadFormatVersion: '1.0', + }); + }); + + test('able to add a custom vpcLink', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpAlbIntegration({ + vpcLink, + listener, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + ConnectionId: { + Ref: 'VpcLink42ED6FF0', + }, + ConnectionType: 'VPC_LINK', + IntegrationMethod: 'ANY', + IntegrationUri: { + Ref: 'lblistener657ADDEC', + }, + PayloadFormatVersion: '1.0', + }); + }); + + test('method option is correctly recognized', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpAlbIntegration({ + listener, + method: HttpMethod.PATCH, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationMethod: 'PATCH', + }); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json new file mode 100644 index 0000000000000..a052f0243e596 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.expected.json @@ -0,0 +1,702 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet3Subnet631C5E25": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTableAssociation427FE0C6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3DefaultRouteA0D29D46": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet3EIPAD4BC883": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCPrivateSubnet3Subnet3EDCD457": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTableAssociationC28D144E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + } + }, + "VPCPrivateSubnet3DefaultRoute27F311AE": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "integ-alb-integration/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "lbA35910C5": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [ + { + "Key": "deletion_protection.enabled", + "Value": "false" + } + ], + "Scheme": "internal", + "SecurityGroups": [ + { + "Fn::GetAtt": [ + "lbSecurityGroup47B6F855", + "GroupId" + ] + } + ], + "Subnets": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ], + "Type": "application" + } + }, + "lbSecurityGroup47B6F855": { + "Type": "AWS::EC2::SecurityGroup", + "Properties": { + "GroupDescription": "Automatically created Security Group for ELB integalbintegrationlb02F792D5", + "SecurityGroupEgress": [ + { + "CidrIp": "255.255.255.255/32", + "Description": "Disallow all traffic", + "FromPort": 252, + "IpProtocol": "icmp", + "ToPort": 86 + } + ], + "SecurityGroupIngress": [ + { + "CidrIp": "0.0.0.0/0", + "Description": "Allow from anyone on port 80", + "FromPort": 80, + "IpProtocol": "tcp", + "ToPort": 80 + } + ], + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "lblistener657ADDEC": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "FixedResponseConfig": { + "StatusCode": "200" + }, + "Type": "fixed-response" + } + ], + "LoadBalancerArn": { + "Ref": "lbA35910C5" + }, + "Port": 80, + "Protocol": "HTTP" + } + }, + "lblistenertargetGroupC7489D1E": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "HTTP", + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "HttpProxyPrivateApiA55E154D": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpProxyPrivateApi", + "ProtocolType": "HTTP" + } + }, + "HttpProxyPrivateApiDefaultRouteDefaultRouteIntegration0AE210B0": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "IntegrationType": "HTTP_PROXY", + "ConnectionId": { + "Ref": "HttpProxyPrivateApiVpcLink190366CAE" + }, + "ConnectionType": "VPC_LINK", + "IntegrationMethod": "ANY", + "IntegrationUri": { + "Ref": "lblistener657ADDEC" + }, + "PayloadFormatVersion": "1.0" + } + }, + "HttpProxyPrivateApiDefaultRoute1BDCA252": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyPrivateApiDefaultRouteDefaultRouteIntegration0AE210B0" + } + ] + ] + } + } + }, + "HttpProxyPrivateApiVpcLink190366CAE": { + "Type": "AWS::ApiGatewayV2::VpcLink", + "Properties": { + "Name": "integalbintegrationHttpProxyPrivateApiVpcLink125175F29", + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ], + "SecurityGroupIds": [] + } + }, + "HttpProxyPrivateApiDefaultStage18B3706E": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "StageName": "$default", + "AutoDeploy": true + } + } + }, + "Outputs": { + "Endpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.ts new file mode 100644 index 0000000000000..e077560466851 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.alb.ts @@ -0,0 +1,31 @@ +import { HttpApi } from '@aws-cdk/aws-apigatewayv2'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { HttpAlbIntegration } from '../../lib'; + +/* + * Stack verification steps: + * * Deploy the stack and wait for the API endpoint output to get printed + * * Hit the above endpoint using curl and expect a 200 response + */ + +const app = new App(); + +const stack = new Stack(app, 'integ-alb-integration'); + +const vpc = new ec2.Vpc(stack, 'VPC'); +const lb = new elbv2.ApplicationLoadBalancer(stack, 'lb', { vpc }); +const listener = lb.addListener('listener', { port: 80 }); +listener.addTargets('target', { port: 80 }); +listener.addAction('dsf', { action: elbv2.ListenerAction.fixedResponse(200) }); + +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpAlbIntegration({ + listener, + }), +}); + +new CfnOutput(stack, 'Endpoint', { + value: httpEndpoint.url!, +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json new file mode 100644 index 0000000000000..ea3801b13354f --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.expected.json @@ -0,0 +1,667 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet3Subnet631C5E25": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTableAssociation427FE0C6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3DefaultRouteA0D29D46": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet3EIPAD4BC883": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCPrivateSubnet3Subnet3EDCD457": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTableAssociationC28D144E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + } + }, + "VPCPrivateSubnet3DefaultRoute27F311AE": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "integ-nlb-integration/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "lbA35910C5": { + "Type": "AWS::ElasticLoadBalancingV2::LoadBalancer", + "Properties": { + "LoadBalancerAttributes": [ + { + "Key": "deletion_protection.enabled", + "Value": "false" + } + ], + "Scheme": "internal", + "Subnets": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ], + "Type": "network" + } + }, + "lblistener657ADDEC": { + "Type": "AWS::ElasticLoadBalancingV2::Listener", + "Properties": { + "DefaultActions": [ + { + "TargetGroupArn": { + "Ref": "lblistenertargetGroupC7489D1E" + }, + "Type": "forward" + } + ], + "LoadBalancerArn": { + "Ref": "lbA35910C5" + }, + "Port": 80, + "Protocol": "TCP" + } + }, + "lblistenertargetGroupC7489D1E": { + "Type": "AWS::ElasticLoadBalancingV2::TargetGroup", + "Properties": { + "Port": 80, + "Protocol": "TCP", + "VpcId": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "HttpProxyPrivateApiA55E154D": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpProxyPrivateApi", + "ProtocolType": "HTTP" + } + }, + "HttpProxyPrivateApiDefaultRouteDefaultRouteIntegration0AE210B0": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "IntegrationType": "HTTP_PROXY", + "ConnectionId": { + "Ref": "HttpProxyPrivateApiVpcLink190366CAE" + }, + "ConnectionType": "VPC_LINK", + "IntegrationMethod": "ANY", + "IntegrationUri": { + "Ref": "lblistener657ADDEC" + }, + "PayloadFormatVersion": "1.0" + } + }, + "HttpProxyPrivateApiDefaultRoute1BDCA252": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyPrivateApiDefaultRouteDefaultRouteIntegration0AE210B0" + } + ] + ] + } + } + }, + "HttpProxyPrivateApiVpcLink190366CAE": { + "Type": "AWS::ApiGatewayV2::VpcLink", + "Properties": { + "Name": "integnlbintegrationHttpProxyPrivateApiVpcLink1C940EC42", + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ], + "SecurityGroupIds": [] + } + }, + "HttpProxyPrivateApiDefaultStage18B3706E": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "StageName": "$default", + "AutoDeploy": true + } + } + }, + "Outputs": { + "Endpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.ts new file mode 100644 index 0000000000000..9b78c3f676e30 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.nlb.ts @@ -0,0 +1,24 @@ +import { HttpApi } from '@aws-cdk/aws-apigatewayv2'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { HttpNlbIntegration } from '../../lib'; + +const app = new App(); + +const stack = new Stack(app, 'integ-nlb-integration'); + +const vpc = new ec2.Vpc(stack, 'VPC'); +const lb = new elbv2.NetworkLoadBalancer(stack, 'lb', { vpc }); +const listener = lb.addListener('listener', { port: 80 }); +listener.addTargets('target', { port: 80 }); + +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpNlbIntegration({ + listener, + }), +}); + +new CfnOutput(stack, 'Endpoint', { + value: httpEndpoint.url!, +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json new file mode 100644 index 0000000000000..30e1c20fbaddc --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.expected.json @@ -0,0 +1,653 @@ +{ + "Resources": { + "VPCB9E5F0B4": { + "Type": "AWS::EC2::VPC", + "Properties": { + "CidrBlock": "10.0.0.0/16", + "EnableDnsHostnames": true, + "EnableDnsSupport": true, + "InstanceTenancy": "default", + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC" + } + ] + } + }, + "VPCPublicSubnet1SubnetB4246D30": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.0.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableFEE4B781": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1RouteTableAssociation0B0896DC": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + } + } + }, + "VPCPublicSubnet1DefaultRoute91CEF279": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet1RouteTableFEE4B781" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet1EIP6AD938E8": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet1NATGatewayE0556630": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet1EIP6AD938E8", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet1SubnetB4246D30" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet1" + } + ] + } + }, + "VPCPublicSubnet2Subnet74179F39": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.32.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTable6F1A15F1": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2RouteTableAssociation5A808732": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + } + } + }, + "VPCPublicSubnet2DefaultRouteB7481BBA": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet2RouteTable6F1A15F1" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet2EIP4947BC00": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet2NATGateway3C070193": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet2EIP4947BC00", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet2Subnet74179F39" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet2" + } + ] + } + }, + "VPCPublicSubnet3Subnet631C5E25": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.64.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": true, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Public" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Public" + }, + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTable98AE0E14": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3RouteTableAssociation427FE0C6": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + } + } + }, + "VPCPublicSubnet3DefaultRouteA0D29D46": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPublicSubnet3RouteTable98AE0E14" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "GatewayId": { + "Ref": "VPCIGWB7E252D3" + } + }, + "DependsOn": [ + "VPCVPCGW99B986DC" + ] + }, + "VPCPublicSubnet3EIPAD4BC883": { + "Type": "AWS::EC2::EIP", + "Properties": { + "Domain": "vpc", + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPublicSubnet3NATGatewayD3048F5C": { + "Type": "AWS::EC2::NatGateway", + "Properties": { + "AllocationId": { + "Fn::GetAtt": [ + "VPCPublicSubnet3EIPAD4BC883", + "AllocationId" + ] + }, + "SubnetId": { + "Ref": "VPCPublicSubnet3Subnet631C5E25" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PublicSubnet3" + } + ] + } + }, + "VPCPrivateSubnet1Subnet8BCA10E0": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.96.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1a", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableBE8A6027": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PrivateSubnet1" + } + ] + } + }, + "VPCPrivateSubnet1RouteTableAssociation347902D1": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + } + } + }, + "VPCPrivateSubnet1DefaultRouteAE1D6490": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet1RouteTableBE8A6027" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet1NATGatewayE0556630" + } + } + }, + "VPCPrivateSubnet2SubnetCFCDAA7A": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.128.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1b", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTable0A19E10E": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PrivateSubnet2" + } + ] + } + }, + "VPCPrivateSubnet2RouteTableAssociation0C73D413": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + } + } + }, + "VPCPrivateSubnet2DefaultRouteF4F5CFD2": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet2RouteTable0A19E10E" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet2NATGateway3C070193" + } + } + }, + "VPCPrivateSubnet3Subnet3EDCD457": { + "Type": "AWS::EC2::Subnet", + "Properties": { + "CidrBlock": "10.0.160.0/19", + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "AvailabilityZone": "test-region-1c", + "MapPublicIpOnLaunch": false, + "Tags": [ + { + "Key": "aws-cdk:subnet-name", + "Value": "Private" + }, + { + "Key": "aws-cdk:subnet-type", + "Value": "Private" + }, + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTable192186F8": { + "Type": "AWS::EC2::RouteTable", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC/PrivateSubnet3" + } + ] + } + }, + "VPCPrivateSubnet3RouteTableAssociationC28D144E": { + "Type": "AWS::EC2::SubnetRouteTableAssociation", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "SubnetId": { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + } + }, + "VPCPrivateSubnet3DefaultRoute27F311AE": { + "Type": "AWS::EC2::Route", + "Properties": { + "RouteTableId": { + "Ref": "VPCPrivateSubnet3RouteTable192186F8" + }, + "DestinationCidrBlock": "0.0.0.0/0", + "NatGatewayId": { + "Ref": "VPCPublicSubnet3NATGatewayD3048F5C" + } + } + }, + "VPCIGWB7E252D3": { + "Type": "AWS::EC2::InternetGateway", + "Properties": { + "Tags": [ + { + "Key": "Name", + "Value": "integ-service-discovery-integration/VPC" + } + ] + } + }, + "VPCVPCGW99B986DC": { + "Type": "AWS::EC2::VPCGatewayAttachment", + "Properties": { + "VpcId": { + "Ref": "VPCB9E5F0B4" + }, + "InternetGatewayId": { + "Ref": "VPCIGWB7E252D3" + } + } + }, + "VpcLink42ED6FF0": { + "Type": "AWS::ApiGatewayV2::VpcLink", + "Properties": { + "Name": "integservicediscoveryintegrationVpcLink09ACD3FF", + "SubnetIds": [ + { + "Ref": "VPCPrivateSubnet1Subnet8BCA10E0" + }, + { + "Ref": "VPCPrivateSubnet2SubnetCFCDAA7A" + }, + { + "Ref": "VPCPrivateSubnet3Subnet3EDCD457" + } + ], + "SecurityGroupIds": [] + } + }, + "Namespace9B63B8C8": { + "Type": "AWS::ServiceDiscovery::PrivateDnsNamespace", + "Properties": { + "Name": "foobar.com", + "Vpc": { + "Ref": "VPCB9E5F0B4" + } + } + }, + "NamespaceServiceCABDF534": { + "Type": "AWS::ServiceDiscovery::Service", + "Properties": { + "DnsConfig": { + "DnsRecords": [ + { + "TTL": 60, + "Type": "A" + } + ], + "NamespaceId": { + "Fn::GetAtt": [ + "Namespace9B63B8C8", + "Id" + ] + }, + "RoutingPolicy": "MULTIVALUE" + }, + "NamespaceId": { + "Fn::GetAtt": [ + "Namespace9B63B8C8", + "Id" + ] + } + } + }, + "HttpProxyPrivateApiA55E154D": { + "Type": "AWS::ApiGatewayV2::Api", + "Properties": { + "Name": "HttpProxyPrivateApi", + "ProtocolType": "HTTP" + } + }, + "HttpProxyPrivateApiDefaultRouteDefaultRouteIntegration0AE210B0": { + "Type": "AWS::ApiGatewayV2::Integration", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "IntegrationType": "HTTP_PROXY", + "ConnectionId": { + "Ref": "VpcLink42ED6FF0" + }, + "ConnectionType": "VPC_LINK", + "IntegrationMethod": "ANY", + "IntegrationUri": { + "Fn::GetAtt": [ + "NamespaceServiceCABDF534", + "Arn" + ] + }, + "PayloadFormatVersion": "1.0" + } + }, + "HttpProxyPrivateApiDefaultRoute1BDCA252": { + "Type": "AWS::ApiGatewayV2::Route", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "RouteKey": "$default", + "Target": { + "Fn::Join": [ + "", + [ + "integrations/", + { + "Ref": "HttpProxyPrivateApiDefaultRouteDefaultRouteIntegration0AE210B0" + } + ] + ] + } + } + }, + "HttpProxyPrivateApiDefaultStage18B3706E": { + "Type": "AWS::ApiGatewayV2::Stage", + "Properties": { + "ApiId": { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + "StageName": "$default", + "AutoDeploy": true + } + } + }, + "Outputs": { + "Endpoint": { + "Value": { + "Fn::Join": [ + "", + [ + "https://", + { + "Ref": "HttpProxyPrivateApiA55E154D" + }, + ".execute-api.", + { + "Ref": "AWS::Region" + }, + ".", + { + "Ref": "AWS::URLSuffix" + }, + "/" + ] + ] + } + } + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.ts new file mode 100644 index 0000000000000..1ff64ba5955c9 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/integ.service-discovery.ts @@ -0,0 +1,28 @@ +import { HttpApi, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as servicediscovery from '@aws-cdk/aws-servicediscovery'; +import { App, CfnOutput, Stack } from '@aws-cdk/core'; +import { HttpServiceDiscoveryIntegration } from '../../lib'; + +const app = new App(); + +const stack = new Stack(app, 'integ-service-discovery-integration'); + +const vpc = new ec2.Vpc(stack, 'VPC'); +const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); +const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'Namespace', { + name: 'foobar.com', + vpc, +}); +const service = namespace.createService('Service'); + +const httpEndpoint = new HttpApi(stack, 'HttpProxyPrivateApi', { + defaultIntegration: new HttpServiceDiscoveryIntegration({ + vpcLink, + service, + }), +}); + +new CfnOutput(stack, 'Endpoint', { + value: httpEndpoint.url!, +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts new file mode 100644 index 0000000000000..ec537cfedb311 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/nlb.test.ts @@ -0,0 +1,102 @@ +import '@aws-cdk/assert/jest'; +import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as elbv2 from '@aws-cdk/aws-elasticloadbalancingv2'; +import { Stack } from '@aws-cdk/core'; +import { HttpNlbIntegration } from '../../lib'; + +describe('HttpNlbIntegration', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.NetworkLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpNlbIntegration({ + listener, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + ConnectionId: { + Ref: 'HttpApiVpcLink159804837', + }, + ConnectionType: 'VPC_LINK', + IntegrationMethod: 'ANY', + IntegrationUri: { + Ref: 'lblistener657ADDEC', + }, + PayloadFormatVersion: '1.0', + }); + }); + + test('able to add a custom vpcLink', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); + const lb = new elbv2.NetworkLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpNlbIntegration({ + vpcLink, + listener, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + ConnectionId: { + Ref: 'VpcLink42ED6FF0', + }, + ConnectionType: 'VPC_LINK', + IntegrationMethod: 'ANY', + IntegrationUri: { + Ref: 'lblistener657ADDEC', + }, + PayloadFormatVersion: '1.0', + }); + }); + + + test('method option is correctly recognized', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const lb = new elbv2.NetworkLoadBalancer(stack, 'lb', { vpc }); + const listener = lb.addListener('listener', { port: 80 }); + listener.addTargets('target', { port: 80 }); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpNlbIntegration({ + listener, + method: HttpMethod.PATCH, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationMethod: 'PATCH', + }); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts new file mode 100644 index 0000000000000..ce5f269f648a0 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/private/integration.test.ts @@ -0,0 +1,37 @@ +import '@aws-cdk/assert/jest'; +import { HttpApi, HttpRoute, HttpRouteIntegrationBindOptions, HttpRouteIntegrationConfig, HttpRouteKey } from '@aws-cdk/aws-apigatewayv2'; +import { Stack } from '@aws-cdk/core'; +import { HttpPrivateIntegration } from '../../../lib/http/private/integration'; + +describe('HttpPrivateIntegration', () => { + test('throws error if both vpcLink and vpc are not passed', () => { + // GIVEN + const stack = new Stack(); + class DummyPrivateIntegration extends HttpPrivateIntegration { + constructor() { + super(); + } + + public bind(options: HttpRouteIntegrationBindOptions): HttpRouteIntegrationConfig { + const vpcLink = this._configureVpcLink(options, {}); + + return { + method: this.httpMethod, + payloadFormatVersion: this.payloadFormatVersion, + type: this.integrationType, + connectionType: this.connectionType, + connectionId: vpcLink.vpcLinkId, + uri: 'some-uri', + }; + } + } + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + expect(() => new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new DummyPrivateIntegration(), + routeKey: HttpRouteKey.with('/pets'), + })).toThrow(/One of vpcLink or vpc should be provided for private integration/); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts new file mode 100644 index 0000000000000..62097b9d0a3c0 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2-integrations/test/http/service-discovery.test.ts @@ -0,0 +1,77 @@ +import '@aws-cdk/assert/jest'; +import { HttpApi, HttpMethod, HttpRoute, HttpRouteKey, VpcLink } from '@aws-cdk/aws-apigatewayv2'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import * as servicediscovery from '@aws-cdk/aws-servicediscovery'; +import { Stack } from '@aws-cdk/core'; +import { HttpServiceDiscoveryIntegration } from '../../lib'; + +describe('HttpServiceDiscoveryIntegration', () => { + test('default', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); + const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'Namespace', { + name: 'foobar.com', + vpc, + }); + const service = namespace.createService('Service'); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpServiceDiscoveryIntegration({ + vpcLink, + service, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + ConnectionId: { + Ref: 'VpcLink42ED6FF0', + }, + ConnectionType: 'VPC_LINK', + IntegrationMethod: 'ANY', + IntegrationUri: { + 'Fn::GetAtt': [ + 'NamespaceServiceCABDF534', + 'Arn', + ], + }, + PayloadFormatVersion: '1.0', + }); + }); + + test('method option is correctly recognized', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); + const namespace = new servicediscovery.PrivateDnsNamespace(stack, 'Namespace', { + name: 'foobar.com', + vpc, + }); + const service = namespace.createService('Service'); + + // WHEN + const api = new HttpApi(stack, 'HttpApi'); + new HttpRoute(stack, 'HttpProxyPrivateRoute', { + httpApi: api, + integration: new HttpServiceDiscoveryIntegration({ + vpcLink, + service, + method: HttpMethod.PATCH, + }), + routeKey: HttpRouteKey.with('/pets'), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationMethod: 'PATCH', + }); + }); +}); diff --git a/packages/@aws-cdk/aws-apigatewayv2/README.md b/packages/@aws-cdk/aws-apigatewayv2/README.md index e5cf86886aa3d..c25027fc81c39 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/README.md +++ b/packages/@aws-cdk/aws-apigatewayv2/README.md @@ -24,6 +24,8 @@ - [Publishing HTTP APIs](#publishing-http-apis) - [Custom Domain](#custom-domain) - [Metrics](#metrics) + - [VPC Link](#vpc-link) + - [Private Integration](#private-integration) ## Introduction @@ -57,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', @@ -223,3 +227,28 @@ const stage = new HttpStage(stack, 'Stage', { }); const clientErrorMetric = stage.metricClientError(); ``` + +### VPC Link + +Private integrations let HTTP APIs connect with AWS resources that are placed behind a VPC. These are usually Application +Load Balancers, Network Load Balancers or a Cloud Map service. The `VpcLink` construct enables this integration. +The following code creates a `VpcLink` to a private VPC. + +```ts +const vpc = new ec2.Vpc(stack, 'VPC'); +const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); +``` + +Any existing `VpcLink` resource can be imported into the CDK app via the `VpcLink.fromVpcLinkId()`. + +```ts +const awesomeLink = VpcLink.fromVpcLinkId(stack, 'awesome-vpc-link', 'us-east-1_oiuR12Abd'); +``` + +### Private Integration + +Private integrations enable integrating an HTTP API route with private resources in a VPC, such as Application Load Balancers or +Amazon ECS container-based applications. Using private integrations, resources in a VPC can be exposed for access by +clients outside of the VPC. + +These integrations can be found in the [APIGatewayV2-Integrations](https://docs.aws.amazon.com/cdk/api/latest/docs/aws-apigatewayv2-integrations-readme.html) constructs library. diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts index ff75808b5a8d6..3bc5f47676339 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/api.ts @@ -6,6 +6,7 @@ import { DefaultDomainMappingOptions } from '../http/stage'; import { IHttpRouteIntegration } from './integration'; import { BatchHttpRouteOptions, HttpMethod, HttpRoute, HttpRouteKey } from './route'; import { HttpStage, HttpStageOptions } from './stage'; +import { VpcLink, VpcLinkProps } from './vpc-link'; /** * Represents an HTTP API @@ -73,6 +74,11 @@ export interface IHttpApi extends IResource { * @default - no statistic */ metricLatency(props?: MetricOptions): Metric; + + /** + * Add a new VpcLink + */ + addVpcLink(options: VpcLinkProps): VpcLink } /** @@ -178,6 +184,7 @@ export interface AddRoutesOptions extends BatchHttpRouteOptions { abstract class HttpApiBase extends Resource implements IHttpApi { // note that this is not exported public abstract readonly httpApiId: string; + private vpcLinks: Record = {}; public metric(metricName: string, props?: MetricOptions): Metric { return new Metric({ @@ -211,6 +218,19 @@ abstract class HttpApiBase extends Resource implements IHttpApi { // note that t public metricLatency(props?: MetricOptions): Metric { return this.metric('Latency', props); } + + public addVpcLink(options: VpcLinkProps): VpcLink { + const { vpcId } = options.vpc; + if (vpcId in this.vpcLinks) { + return this.vpcLinks[vpcId]; + } + + const count = Object.keys(this.vpcLinks).length + 1; + const vpcLink = new VpcLink(this, `VpcLink-${count}`, options); + this.vpcLinks[vpcId] = vpcLink; + + return vpcLink; + } } /** @@ -235,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 */ @@ -278,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/index.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts index c42e089aa1d08..ee07bd7af8ee2 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/index.ts @@ -4,3 +4,4 @@ export * from './integration'; export * from './integrations'; export * from './stage'; export * from './api-mapping'; +export * from './vpc-link'; diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts index 237177f31957a..e609c9396c08f 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/integration.ts @@ -33,6 +33,20 @@ export enum HttpIntegrationType { HTTP_PROXY = 'HTTP_PROXY', } +/** + * Supported connection types + */ +export enum HttpConnectionType { + /** + * For private connections between API Gateway and resources in a VPC + */ + VPC_LINK = 'VPC_LINK', + /** + * For connections through public routable internet + */ + INTERNET = 'INTERNET', +} + /** * Payload format version for lambda proxy integration * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html @@ -86,6 +100,20 @@ export interface HttpIntegrationProps { */ readonly method?: HttpMethod; + /** + * The ID of the VPC link for a private integration. Supported only for HTTP APIs. + * + * @default - undefined + */ + readonly connectionId?: string; + + /** + * The type of the network connection to the integration endpoint + * + * @default HttpConnectionType.INTERNET + */ + readonly connectionType?: HttpConnectionType; + /** * The version of the payload format * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html @@ -110,6 +138,8 @@ export class HttpIntegration extends Resource implements IHttpIntegration { integrationType: props.integrationType, integrationUri: props.integrationUri, integrationMethod: props.method, + connectionId: props.connectionId, + connectionType: props.connectionType, payloadFormatVersion: props.payloadFormatVersion?.version, }); this.integrationId = integ.ref; @@ -165,6 +195,20 @@ export interface HttpRouteIntegrationConfig { */ readonly method?: HttpMethod; + /** + * The ID of the VPC link for a private integration. Supported only for HTTP APIs. + * + * @default - undefined + */ + readonly connectionId?: string; + + /** + * The type of the network connection to the integration endpoint + * + * @default HttpConnectionType.INTERNET + */ + readonly connectionType?: HttpConnectionType; + /** * Payload format version in the case of lambda proxy integration * @see https://docs.aws.amazon.com/apigateway/latest/developerguide/http-api-develop-integrations-lambda.html 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/lib/http/route.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts index 6f13c46d9e3d5..e688e78d84921 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/route.ts @@ -120,16 +120,18 @@ export class HttpRoute extends Resource implements IHttpRoute { this.httpApi = props.httpApi; this.path = props.routeKey.path; - let integration: HttpIntegration | undefined; const config = props.integration.bind({ route: this, scope: this, }); - integration = new HttpIntegration(this, `${this.node.id}-Integration`, { + + const integration = new HttpIntegration(this, `${this.node.id}-Integration`, { httpApi: props.httpApi, integrationType: config.type, integrationUri: config.uri, method: config.method, + connectionId: config.connectionId, + connectionType: config.connectionType, payloadFormatVersion: config.payloadFormatVersion, }); diff --git a/packages/@aws-cdk/aws-apigatewayv2/lib/http/vpc-link.ts b/packages/@aws-cdk/aws-apigatewayv2/lib/http/vpc-link.ts new file mode 100644 index 0000000000000..ac832a730e62c --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/lib/http/vpc-link.ts @@ -0,0 +1,120 @@ +import * as ec2 from '@aws-cdk/aws-ec2'; +import { IResource, Lazy, Resource } from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnVpcLink } from '../apigatewayv2.generated'; + +/** + * Represents an API Gateway VpcLink + */ +export interface IVpcLink extends IResource { + /** + * Physical ID of the VpcLink resource + * @attribute + */ + readonly vpcLinkId: string; +} + +/** + * Properties for a VpcLink + */ +export interface VpcLinkProps { + /** + * The VPC in which the private resources reside. + */ + readonly vpc: ec2.IVpc; + + /** + * The name used to label and identify the VPC link. + * @default - automatically generated name + */ + readonly vpcLinkName?: string; + + /** + * A list of subnets for the VPC link. + * + * @default - private subnets of the provided VPC. Use `addSubnets` to add more subnets + */ + readonly subnets?: ec2.ISubnet[]; + + /** + * A list of security groups for the VPC link. + * + * @default - no security groups. Use `addSecurityGroups` to add security groups + */ + readonly securityGroups?: ec2.ISecurityGroup[]; +} + + +/** + * Define a new VPC Link + * Specifies an API Gateway VPC link for a HTTP API to access resources in an Amazon Virtual Private Cloud (VPC). + */ +export class VpcLink extends Resource implements IVpcLink { + /** + * Import a VPC Link by its Id + */ + public static fromVpcLinkId(scope: Construct, id: string, vpcLinkId: string): IVpcLink { + class Import extends Resource implements IVpcLink { + public vpcLinkId = vpcLinkId; + } + + return new Import(scope, id); + } + + /** + * Physical ID of the VpcLink resource + * @attribute + */ + public readonly vpcLinkId: string; + + private readonly subnets = new Array(); + private readonly securityGroups = new Array(); + + constructor(scope: Construct, id: string, props: VpcLinkProps) { + super(scope, id); + + const cfnResource = new CfnVpcLink(this, 'Resource', { + name: props.vpcLinkName || Lazy.stringValue({ produce: () => this.node.uniqueId }), + subnetIds: Lazy.listValue({ produce: () => this.renderSubnets() }), + securityGroupIds: Lazy.listValue({ produce: () => this.renderSecurityGroups() }), + }); + + this.vpcLinkId = cfnResource.ref; + + this.addSubnets(...props.vpc.privateSubnets); + + if (props.subnets) { + this.addSubnets(...props.subnets); + } + + if (props.securityGroups) { + this.addSecurityGroups(...props.securityGroups); + } + } + + /** + * Adds the provided subnets to the vpc link + * + * @param subnets + */ + public addSubnets(...subnets: ec2.ISubnet[]) { + this.subnets.push(...subnets); + } + + /** + * Adds the provided security groups to the vpc link + * + * @param groups + */ + public addSecurityGroups(...groups: ec2.ISecurityGroup[]) { + this.securityGroups.push(...groups); + } + + private renderSubnets() { + return this.subnets.map(subnet => subnet.subnetId); + } + + private renderSecurityGroups() { + return this.securityGroups.map(sg => sg.securityGroupId); + } +} diff --git a/packages/@aws-cdk/aws-apigatewayv2/package.json b/packages/@aws-cdk/aws-apigatewayv2/package.json index 75bc25b0c05af..07b768f848bdd 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/package.json +++ b/packages/@aws-cdk/aws-apigatewayv2/package.json @@ -81,6 +81,7 @@ }, "dependencies": { "@aws-cdk/aws-certificatemanager": "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-cloudwatch": "0.0.0", @@ -88,6 +89,7 @@ "constructs": "^3.2.0" }, "peerDependencies": { + "@aws-cdk/aws-ec2": "0.0.0", "@aws-cdk/aws-iam": "0.0.0", "@aws-cdk/aws-lambda": "0.0.0", "@aws-cdk/aws-certificatemanager": "0.0.0", @@ -106,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 13ee4e120945d..96c747985be04 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/api.test.ts @@ -1,6 +1,7 @@ import '@aws-cdk/assert/jest'; import { ABSENT } from '@aws-cdk/assert'; import { Metric } from '@aws-cdk/aws-cloudwatch'; +import * as ec2 from '@aws-cdk/aws-ec2'; import { Code, Function, Runtime } from '@aws-cdk/aws-lambda'; import { Duration, Stack } from '@aws-cdk/core'; import { HttpApi, HttpMethod, LambdaProxyIntegration } from '../../lib'; @@ -233,4 +234,50 @@ describe('HttpApi', () => { Description: 'My Api', }); }); + + test('can add a vpc links', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'api'); + const vpc1 = new ec2.Vpc(stack, 'VPC-1'); + const vpc2 = new ec2.Vpc(stack, 'VPC-2'); + + // WHEN + api.addVpcLink({ vpc: vpc1, vpcLinkName: 'Link-1' }); + api.addVpcLink({ vpc: vpc2, vpcLinkName: 'Link-2' }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::VpcLink', { + Name: 'Link-1', + }); + expect(stack).toHaveResource('AWS::ApiGatewayV2::VpcLink', { + Name: 'Link-2', + }); + }); + + test('should add only one vpc link per vpc', () => { + // GIVEN + const stack = new Stack(); + const api = new HttpApi(stack, 'api'); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + api.addVpcLink({ vpc, vpcLinkName: 'Link-1' }); + api.addVpcLink({ vpc, vpcLinkName: 'Link-2' }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::VpcLink', { + Name: 'Link-1', + }); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::VpcLink', { + 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-apigatewayv2/test/http/route.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts index 6d125b5eadc6e..e250ae153389d 100644 --- a/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/route.test.ts @@ -1,7 +1,7 @@ import '@aws-cdk/assert/jest'; import { Stack } from '@aws-cdk/core'; import { - HttpApi, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteIntegration, + HttpApi, HttpConnectionType, HttpIntegrationType, HttpMethod, HttpRoute, HttpRouteIntegrationConfig, HttpRouteKey, IHttpRouteIntegration, PayloadFormatVersion, } from '../../lib'; @@ -77,6 +77,42 @@ describe('HttpRoute', () => { })).toThrowError(/path must always start with a "\/" and not end with a "\/"/); }); + test('configures private integration correctly when all props are passed', () => { + // GIVEN + const stack = new Stack(); + const httpApi = new HttpApi(stack, 'HttpApi'); + + class PrivateIntegration implements IHttpRouteIntegration { + public bind(): HttpRouteIntegrationConfig { + return { + method: HttpMethod.ANY, + payloadFormatVersion: PayloadFormatVersion.VERSION_1_0, + type: HttpIntegrationType.HTTP_PROXY, + connectionId: 'some-connection-id', + connectionType: HttpConnectionType.VPC_LINK, + uri: 'some-target-arn', + }; + } + } + + // WHEN + new HttpRoute(stack, 'HttpRoute', { + httpApi, + integration: new PrivateIntegration(), + routeKey: HttpRouteKey.with('/books', HttpMethod.GET), + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::Integration', { + IntegrationType: 'HTTP_PROXY', + ConnectionId: 'some-connection-id', + ConnectionType: 'VPC_LINK', + IntegrationMethod: 'ANY', + IntegrationUri: 'some-target-arn', + PayloadFormatVersion: '1.0', + }); + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::VpcLink'); + }); }); class DummyIntegration implements IHttpRouteIntegration { diff --git a/packages/@aws-cdk/aws-apigatewayv2/test/http/vpc-link.test.ts b/packages/@aws-cdk/aws-apigatewayv2/test/http/vpc-link.test.ts new file mode 100644 index 0000000000000..b5f7dc178e458 --- /dev/null +++ b/packages/@aws-cdk/aws-apigatewayv2/test/http/vpc-link.test.ts @@ -0,0 +1,188 @@ +import '@aws-cdk/assert/jest'; +import * as ec2 from '@aws-cdk/aws-ec2'; +import { Stack } from '@aws-cdk/core'; +import { VpcLink } from '../../lib'; + +describe('VpcLink', () => { + test('default setup', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + + // WHEN + new VpcLink(stack, 'VpcLink', { + vpcLinkName: 'MyLink', + vpc, + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::VpcLink', { + Name: 'MyLink', + SubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + SecurityGroupIds: [], + }); + }); + + test('subnets and security security groups in props', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const subnet1 = new ec2.Subnet(stack, 'subnet1', { + vpcId: vpc.vpcId, + availabilityZone: vpc.availabilityZones[0], + cidrBlock: vpc.vpcCidrBlock, + }); + const subnet2 = new ec2.Subnet(stack, 'subnet2', { + vpcId: vpc.vpcId, + availabilityZone: vpc.availabilityZones[1], + cidrBlock: vpc.vpcCidrBlock, + }); + const sg1 = new ec2.SecurityGroup(stack, 'SG1', { vpc }); + const sg2 = new ec2.SecurityGroup(stack, 'SG2', { vpc }); + const sg3 = new ec2.SecurityGroup(stack, 'SG3', { vpc }); + + // WHEN + new VpcLink(stack, 'VpcLink', { + vpc, + subnets: [subnet1, subnet2], + securityGroups: [sg1, sg2, sg3], + }); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::VpcLink', { + Name: 'VpcLink', + SubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + { + Ref: 'subnet1Subnet16A4B3BD', + }, + { + Ref: 'subnet2SubnetF9569CD3', + }, + ], + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'SG1BA065B6E', + 'GroupId', + ], + }, + { + 'Fn::GetAtt': [ + 'SG20CE3219C', + 'GroupId', + ], + }, + { + 'Fn::GetAtt': [ + 'SG351782A25', + 'GroupId', + ], + }, + ], + }); + }); + + test('subnets can be added using addSubnets', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const subnet = new ec2.Subnet(stack, 'subnet', { + vpcId: vpc.vpcId, + availabilityZone: vpc.availabilityZones[0], + cidrBlock: vpc.vpcCidrBlock, + }); + + // WHEN + const vpcLink = new VpcLink(stack, 'VpcLink', { vpc }); + vpcLink.addSubnets(subnet); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::VpcLink', { + Name: 'VpcLink', + SubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + { + Ref: 'subnetSubnet39D20FD5', + }, + ], + }); + }); + + test('security groups can be added using addSecurityGroups', () => { + // GIVEN + const stack = new Stack(); + const vpc = new ec2.Vpc(stack, 'VPC'); + const sg1 = new ec2.SecurityGroup(stack, 'SG1', { vpc }); + const sg2 = new ec2.SecurityGroup(stack, 'SG2', { vpc }); + const sg3 = new ec2.SecurityGroup(stack, 'SG3', { vpc }); + + + // WHEN + const vpcLink = new VpcLink(stack, 'VpcLink', { + vpc, + }); + vpcLink.addSecurityGroups(sg1, sg2, sg3); + + // THEN + expect(stack).toHaveResource('AWS::ApiGatewayV2::VpcLink', { + Name: 'VpcLink', + SubnetIds: [ + { + Ref: 'VPCPrivateSubnet1Subnet8BCA10E0', + }, + { + Ref: 'VPCPrivateSubnet2SubnetCFCDAA7A', + }, + ], + SecurityGroupIds: [ + { + 'Fn::GetAtt': [ + 'SG1BA065B6E', + 'GroupId', + ], + }, + { + 'Fn::GetAtt': [ + 'SG20CE3219C', + 'GroupId', + ], + }, + { + 'Fn::GetAtt': [ + 'SG351782A25', + 'GroupId', + ], + }, + ], + }); + }); + + test('importing an existing vpc link', () => { + // GIVEN + const stack = new Stack(); + + // WHEN + VpcLink.fromVpcLinkId(stack, 'ImportedVpcLink', 'vpclink-id'); + + // THEN + expect(stack).not.toHaveResource('AWS::ApiGatewayV2::VpcLink'); + }); +}); 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-appmesh/README.md b/packages/@aws-cdk/aws-appmesh/README.md index 1becd4cf81772..e38a8ea49a9e9 100644 --- a/packages/@aws-cdk/aws-appmesh/README.md +++ b/packages/@aws-cdk/aws-appmesh/README.md @@ -126,8 +126,6 @@ mesh.addVirtualService('virtual-service', { A `virtual node` acts as a logical pointer to a particular task group, such as an Amazon ECS service or a Kubernetes deployment. -![Virtual node logical diagram](https://docs.aws.amazon.com/app-mesh/latest/userguide/images/virtual_node.png) - When you create a `virtual node`, you must specify the DNS service discovery hostname for your task group. Any inbound traffic that your `virtual node` expects should be specified as a listener. Any outbound traffic that your `virtual node` expects to reach should be specified as a backend. The response metadata for your new `virtual node` contains the Amazon Resource Name (ARN) that is associated with the `virtual node`. Set this value (either the full ARN or the truncated resource name) as the APPMESH_VIRTUAL_NODE_NAME environment variable for your task group's Envoy proxy container in your task definition or pod spec. For example, the value could be mesh/default/virtualNode/simpleapp. This is then mapped to the node.id and node.cluster Envoy parameters. @@ -144,7 +142,6 @@ const namespace = new servicediscovery.PrivateDnsNamespace(this, 'test-namespace const service = namespace.createService('Svc'); const node = mesh.addVirtualNode('virtual-node', { - dnsHostName: 'node-a', cloudMapService: service, listener: { portMapping: { @@ -170,7 +167,6 @@ Create a `VirtualNode` with the the constructor and add tags. ```typescript const node = new VirtualNode(this, 'node', { mesh, - dnsHostName: 'node-1', cloudMapService: service, listener: { portMapping: { @@ -193,7 +189,7 @@ const node = new VirtualNode(this, 'node', { cdk.Tag.add(node, 'Environment', 'Dev'); ``` -The listeners property can be left blank and added later with the `mesh.addListeners()` method. The `healthcheck` property is optional but if specifying a listener, the `portMappings` must contain at least one property. +The listeners property can be left blank and added later with the `node.addListeners()` method. The `healthcheck` property is optional but if specifying a listener, the `portMappings` must contain at least one property. ## Adding a Route @@ -235,34 +231,79 @@ router.addRoute('route', { }); ``` -Multiple routes may also be added at once to different applications or targets. +## Adding a Virtual Gateway + +A _virtual gateway_ allows resources outside your mesh to communicate to resources that are inside your mesh. +The virtual gateway represents an Envoy proxy running in an Amazon ECS task, in a Kubernetes service, or on an Amazon EC2 instance. +Unlike a virtual node, which represents an Envoy running with an application, a virtual gateway represents Envoy deployed by itself. + +A virtual gateway is similar to a virtual node in that it has a listener that accepts traffic for a particular port and protocol (HTTP, HTTP2, GRPC). +The traffic that the virtual gateway receives, is directed to other services in your mesh, +using rules defined in gateway routes which can be added to your virtual gateway. + +Create a virtual gateway with the constructor: ```typescript -ratingsRouter.addRoutes( - ['route1', 'route2'], - [ - { - routeTargets: [ - { - virtualNode, - weight: 1, - }, - ], - prefix: `/path-to-app`, - routeType: RouteType.HTTP, +const gateway = new appmesh.VirtualGateway(stack, 'gateway', { + mesh: mesh, + listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), }, - { - routeTargets: [ - { - virtualNode: virtualNode2, - weight: 1, - }, - ], - prefix: `/path-to-app2`, - routeType: RouteType.HTTP, + })], + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + virtualGatewayName: 'virtualGateway', +}); +``` + +Add a virtual gateway directly to the mesh: + +```typescript +const gateway = mesh.addVirtualGateway('gateway', { + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + virtualGatewayName: 'virtualGateway', + listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), + }, + })], +}); +``` + +The listeners field can be omitted which will default to an HTTP Listener on port 8080. +A gateway route can be added using the `gateway.addGatewayRoute()` method. + +## Adding a Gateway Route + +A _gateway route_ is attached to a virtual gateway and routes traffic to an existing virtual service. +If a route matches a request, it can distribute traffic to a target virtual service. + +For HTTP based routes, the match field can be used to match on a route prefix. +By default, an HTTP based route will match on `/`. All matches must start with a leading `/`. + +```typescript +gateway.addGatewayRoute('gateway-route-http', { + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + match: { + prefixMatch: '/', }, - ] -); + }), +}); ``` -The number of `route ids` and `route targets` must match as each route needs to have a unique name per router. +For GRPC based routes, the match field can be used to match on service names. +You cannot omit the field, and must specify a match for these routes. + +```typescript +gateway.addGatewayRoute('gateway-route-grpc', { + routeSpec: appmesh.GatewayRouteSpec.grpcRouteSpec({ + routeTarget: virtualService, + match: { + serviceName: 'my-service.default.svc.cluster.local', + }, + }), +}); +``` diff --git a/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts b/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts new file mode 100644 index 0000000000000..b75890ece1453 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/gateway-route-spec.ts @@ -0,0 +1,209 @@ +import * as cdk from '@aws-cdk/core'; +import { CfnGatewayRoute } from './appmesh.generated'; +import { Protocol } from './shared-interfaces'; +import { IVirtualService } from './virtual-service'; + +/** + * The criterion for determining a request match for this GatewayRoute + */ +export interface HttpGatewayRouteMatch { + /** + * Specifies the path to match requests with. + * This parameter must always start with /, which by itself matches all requests to the virtual service name. + * You can also match for path-based routing of requests. For example, if your virtual service name is my-service.local + * and you want the route to match requests to my-service.local/metrics, your prefix should be /metrics. + */ + readonly prefixPath: string; +} + +/** + * The criterion for determining a request match for this GatewayRoute + */ +export interface GrpcGatewayRouteMatch { + /** + * The fully qualified domain name for the service to match from the request + */ + readonly serviceName: string; +} + +/** + * Properties specific for HTTP Based GatewayRoutes + */ +export interface HttpRouteSpecProps { + /** + * The criterion for determining a request match for this GatewayRoute + * + * @default - matches on '/' + */ + readonly match?: HttpGatewayRouteMatch; + + /** + * The VirtualService this GatewayRoute directs traffic to + */ + readonly routeTarget: IVirtualService; +} + +/** + * Properties specific for a GRPC GatewayRoute + */ +export interface GrpcRouteSpecProps { + /** + * The criterion for determining a request match for this GatewayRoute + */ + readonly match: GrpcGatewayRouteMatch; + + /** + * The VirtualService this GatewayRoute directs traffic to + */ + readonly routeTarget: IVirtualService; +} + +/** + * All Properties for GatewayRoute Specs + */ +export interface GatewayRouteSpecConfig { + /** + * The spec for an http gateway route + * + * @default - no http spec + */ + readonly httpSpecConfig?: CfnGatewayRoute.HttpGatewayRouteProperty; + + /** + * The spec for an http2 gateway route + * + * @default - no http2 spec + */ + readonly http2SpecConfig?: CfnGatewayRoute.HttpGatewayRouteProperty; + + /** + * The spec for a grpc gateway route + * + * @default - no grpc spec + */ + readonly grpcSpecConfig?: CfnGatewayRoute.GrpcGatewayRouteProperty; +} + +/** + * Used to generate specs with different protocols for a GatewayRoute + */ +export abstract class GatewayRouteSpec { + /** + * Creates an HTTP Based GatewayRoute + * + * @param props - no http gateway route + */ + public static httpRouteSpec(props: HttpRouteSpecProps): GatewayRouteSpec { + return new HttpGatewayRouteSpec(props, Protocol.HTTP); + } + + /** + * Creates an HTTP2 Based GatewayRoute + * + * @param props - no http2 gateway route + */ + public static http2RouteSpec(props: HttpRouteSpecProps): GatewayRouteSpec { + return new HttpGatewayRouteSpec(props, Protocol.HTTP2); + } + + /** + * Creates an GRPC Based GatewayRoute + * + * @param props - no grpc gateway route + */ + public static grpcRouteSpec(props: GrpcRouteSpecProps): GatewayRouteSpec { + return new GrpcGatewayRouteSpec(props); + } + + /** + * Called when the GatewayRouteSpec type is initialized. Can be used to enforce + * mutual exclusivity with future properties + */ + public abstract bind(scope: cdk.Construct): GatewayRouteSpecConfig; +} + +class HttpGatewayRouteSpec extends GatewayRouteSpec { + /** + * The criterion for determining a request match for this GatewayRoute. + * + * @default - matches on '/' + */ + readonly match?: HttpGatewayRouteMatch; + + /** + * The VirtualService this GatewayRoute directs traffic to + */ + readonly routeTarget: IVirtualService; + + /** + * Type of route you are creating + */ + readonly routeType: Protocol; + + constructor(props: HttpRouteSpecProps, protocol: Protocol.HTTP | Protocol.HTTP2) { + super(); + this.routeTarget = props.routeTarget; + this.routeType = protocol; + this.match = props.match; + } + + public bind(_scope: cdk.Construct): GatewayRouteSpecConfig { + const prefixPath = this.match ? this.match.prefixPath : '/'; + if (prefixPath[0] != '/') { + throw new Error(`Prefix Path must start with \'/\', got: ${prefixPath}`); + } + const httpConfig: CfnGatewayRoute.HttpGatewayRouteProperty = { + match: { + prefix: prefixPath, + }, + action: { + target: { + virtualService: { + virtualServiceName: this.routeTarget.virtualServiceName, + }, + }, + }, + }; + return { + httpSpecConfig: this.routeType === Protocol.HTTP ? httpConfig : undefined, + http2SpecConfig: this.routeType === Protocol.HTTP2 ? httpConfig : undefined, + }; + } +} + +class GrpcGatewayRouteSpec extends GatewayRouteSpec { + /** + * The criterion for determining a request match for this GatewayRoute. + * + * @default - no default + */ + readonly match: GrpcGatewayRouteMatch; + + /** + * The VirtualService this GatewayRoute directs traffic to + */ + readonly routeTarget: IVirtualService; + + constructor(props: GrpcRouteSpecProps) { + super(); + this.match = props.match; + this.routeTarget = props.routeTarget; + } + + public bind(_scope: cdk.Construct): GatewayRouteSpecConfig { + return { + grpcSpecConfig: { + action: { + target: { + virtualService: { + virtualServiceName: this.routeTarget.virtualServiceName, + }, + }, + }, + match: { + serviceName: this.match.serviceName, + }, + }, + }; + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts b/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts new file mode 100644 index 0000000000000..cc00c0f632ac3 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/gateway-route.ts @@ -0,0 +1,168 @@ +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnGatewayRoute } from './appmesh.generated'; +import { GatewayRouteSpec } from './gateway-route-spec'; +import { IVirtualGateway, VirtualGateway } from './virtual-gateway'; + +/** + * Interface for which all GatewayRoute based classes MUST implement + */ +export interface IGatewayRoute extends cdk.IResource { + /** + * The name of the GatewayRoute + * + * @attribute + */ + readonly gatewayRouteName: string, + + /** + * The Amazon Resource Name (ARN) for the GatewayRoute + * + * @attribute + */ + readonly gatewayRouteArn: string; + + /** + * The VirtualGateway the GatewayRoute belongs to + */ + readonly virtualGateway: IVirtualGateway; +} + +/** + * Basic configuration properties for a GatewayRoute + */ +export interface GatewayRouteBaseProps { + /** + * The name of the GatewayRoute + * + * @default - an automatically generated name + */ + readonly gatewayRouteName?: string; + + /** + * What protocol the route uses + */ + readonly routeSpec: GatewayRouteSpec; +} + +/** + * Properties to define a new GatewayRoute + */ +export interface GatewayRouteProps extends GatewayRouteBaseProps { + /** + * The VirtualGateway this GatewayRoute is associated with + */ + readonly virtualGateway: IVirtualGateway; +} + +/** + * GatewayRoute represents a new or existing gateway route attached to a VirtualGateway and Mesh + * + * @see https://docs.aws.amazon.com/app-mesh/latest/userguide/gateway-routes.html + */ +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 }); + } + + /** + * The name of the GatewayRoute + */ + public readonly gatewayRouteName: string; + + /** + * The Amazon Resource Name (ARN) for the GatewayRoute + */ + public readonly gatewayRouteArn: string; + + /** + * The VirtualGateway this GatewayRoute is a part of + */ + public readonly virtualGateway: IVirtualGateway; + + constructor(scope: Construct, id: string, props: GatewayRouteProps) { + super(scope, id, { + physicalName: props.gatewayRouteName || cdk.Lazy.stringValue({ produce: () => this.node.uniqueId }), + }); + + this.virtualGateway = props.virtualGateway; + const routeSpecConfig = props.routeSpec.bind(this); + + const gatewayRoute = new CfnGatewayRoute(this, 'Resource', { + gatewayRouteName: this.physicalName, + meshName: props.virtualGateway.mesh.meshName, + spec: { + httpRoute: routeSpecConfig.httpSpecConfig, + http2Route: routeSpecConfig.http2SpecConfig, + grpcRoute: routeSpecConfig.grpcSpecConfig, + }, + virtualGatewayName: this.virtualGateway.virtualGatewayName, + }); + + this.gatewayRouteName = this.getResourceNameAttribute(gatewayRoute.attrGatewayRouteName); + this.gatewayRouteArn = this.getResourceArnAttribute(gatewayRoute.ref, { + service: 'appmesh', + resource: `mesh/${props.virtualGateway.mesh.meshName}/virtualRouter/${this.virtualGateway.virtualGatewayName}/gatewayRoute`, + resourceName: this.physicalName, + }); + } +} + +/** + * Interface with properties necessary to import a reusable GatewayRoute + */ +interface GatewayRouteAttributes { + /** + * The name of the GatewayRoute + */ + readonly gatewayRouteName?: string; + + /** + * The Amazon Resource Name (ARN) for the GatewayRoute + */ + 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'); + } + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/index.ts b/packages/@aws-cdk/aws-appmesh/lib/index.ts index ff36676e2ec1d..69cdfd8d2e3c6 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/index.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/index.ts @@ -6,3 +6,7 @@ export * from './shared-interfaces'; export * from './virtual-node'; export * from './virtual-router'; export * from './virtual-service'; +export * from './virtual-gateway'; +export * from './virtual-gateway-listener'; +export * from './gateway-route'; +export * from './gateway-route-spec'; diff --git a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts index 2aaded50f99bc..869d2198fdd0c 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/mesh.ts @@ -1,6 +1,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnMesh } from './appmesh.generated'; +import { VirtualGateway, VirtualGatewayBaseProps } from './virtual-gateway'; import { VirtualNode, VirtualNodeBaseProps } from './virtual-node'; import { VirtualRouter, VirtualRouterBaseProps } from './virtual-router'; import { VirtualService, VirtualServiceBaseProps } from './virtual-service'; @@ -54,6 +55,11 @@ export interface IMesh extends cdk.IResource { * Adds a VirtualNode to the Mesh */ addVirtualNode(id: string, props?: VirtualNodeBaseProps): VirtualNode; + + /** + * Adds a VirtualGateway to the Mesh + */ + addVirtualGateway(id: string, props?: VirtualGatewayBaseProps): VirtualGateway; } /** @@ -99,6 +105,16 @@ abstract class MeshBase extends cdk.Resource implements IMesh { mesh: this, }); } + + /** + * Adds a VirtualGateway to the Mesh + */ + addVirtualGateway(id: string, props?: VirtualGatewayBaseProps): VirtualGateway { + return new VirtualGateway(this, id, { + ...props, + mesh: this, + }); + } } /** @@ -170,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/private/utils.ts b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts new file mode 100644 index 0000000000000..0f63fdaf05253 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/private/utils.ts @@ -0,0 +1,41 @@ +import * as cdk from '@aws-cdk/core'; +import { CfnVirtualGateway, CfnVirtualNode } from '../appmesh.generated'; + +type AppMeshHealthCheck = CfnVirtualNode.HealthCheckProperty | CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty + +/** + * Validates health check properties, throws an error if they are misconfigured. + * + * @param healthCheck Healthcheck property from a Virtual Node or Virtual Gateway + */ +export function validateHealthChecks(healthCheck: AppMeshHealthCheck) { + (Object.keys(healthCheck) as Array) + .filter((key) => + HEALTH_CHECK_PROPERTY_THRESHOLDS[key] && + typeof healthCheck[key] === 'number' && + !cdk.Token.isUnresolved(healthCheck[key]), + ).map((key) => { + const [min, max] = HEALTH_CHECK_PROPERTY_THRESHOLDS[key]!; + const value = healthCheck[key]!; + + if (value < min) { + throw new Error(`The value of '${key}' is below the minimum threshold (expected >=${min}, got ${value})`); + } + if (value > max) { + throw new Error(`The value of '${key}' is above the maximum threshold (expected <=${max}, got ${value})`); + } + }); +} + +/** + * Minimum and maximum thresholds for HeathCheck numeric properties + * + * @see https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HealthCheckPolicy.html + */ +const HEALTH_CHECK_PROPERTY_THRESHOLDS: {[key in (keyof AppMeshHealthCheck)]?: [number, number]} = { + healthyThreshold: [2, 10], + intervalMillis: [5000, 300000], + port: [1, 65535], + timeoutMillis: [2000, 60000], + unhealthyThreshold: [2, 10], +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/lib/route.ts b/packages/@aws-cdk/aws-appmesh/lib/route.ts index 0dfeac897375a..696908abc40a6 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/route.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/route.ts @@ -145,7 +145,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; diff --git a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts index 7cd55df23565d..39111389b0a03 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/shared-interfaces.ts @@ -1,5 +1,5 @@ import * as cdk from '@aws-cdk/core'; -import { CfnVirtualNode } from './appmesh.generated'; +import { CfnVirtualGateway, CfnVirtualNode } from './appmesh.generated'; /** * Enum of supported AppMesh protocols @@ -23,24 +23,28 @@ export interface HealthCheck { * @default 2 */ readonly healthyThreshold?: number; + /** * Interval in milliseconds to re-check * * @default 5 seconds */ readonly interval?: cdk.Duration; + /** * The path where the application expects any health-checks, this can also be the application path. * * @default / */ readonly path?: string; + /** * The TCP port number for the healthcheck * * @default - same as corresponding port mapping */ readonly port?: number; + /** * The protocol to use for the healthcheck, for convinience a const enum has been defined. * Protocol.HTTP or Protocol.TCP @@ -48,12 +52,14 @@ export interface HealthCheck { * @default - same as corresponding port mapping */ readonly protocol?: Protocol; + /** * Timeout in milli-seconds for the healthcheck to be considered a fail. * * @default 2 seconds */ readonly timeout?: cdk.Duration; + /** * Number of failed attempts before considering the node DOWN. * @@ -82,7 +88,7 @@ export interface PortMapping { } /** - * Represents the properties needed to define healthy and active listeners for nodes. + * Represents the properties needed to define healthy and active listeners for nodes */ export interface VirtualNodeListener { /** @@ -111,6 +117,13 @@ export interface AccessLogConfig { * @default - no access logging */ readonly virtualNodeAccessLog?: CfnVirtualNode.AccessLogProperty; + + /** + * VirtualGateway CFN configuration for Access Logging + * + * @default - no access logging + */ + readonly virtualGatewayAccessLog?: CfnVirtualGateway.VirtualGatewayAccessLogProperty; } /** @@ -156,6 +169,11 @@ class FileAccessLog extends AccessLog { path: this.filePath, }, }, + virtualGatewayAccessLog: { + file: { + path: this.filePath, + }, + }, }; } -} +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts new file mode 100644 index 0000000000000..a690c022f8c32 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway-listener.ts @@ -0,0 +1,227 @@ +import * as cdk from '@aws-cdk/core'; +import { CfnVirtualGateway } from './appmesh.generated'; +import { validateHealthChecks } from './private/utils'; +import { HealthCheck, Protocol } from './shared-interfaces'; + +/** + * Represents the properties needed to define HTTP Listeners for a VirtualGateway + */ +export interface HttpGatewayListenerProps { + /** + * Port to listen for connections on + * + * @default - 8080 + */ + readonly port?: number + + /** + * The health check information for the listener + * + * @default - no healthcheck + */ + readonly healthCheck?: HealthCheck; +} + +/** + * Represents the properties needed to define GRPC Listeners for a VirtualGateway + */ +export interface GrpcGatewayListenerProps { + /** + * Port to listen for connections on + * + * @default - 8080 + */ + readonly port?: number + + /** + * The health check information for the listener + * + * @default - no healthcheck + */ + readonly healthCheck?: HealthCheck; +} + +/** + * Properties for a VirtualGateway listener + */ +export interface VirtualGatewayListenerConfig { + /** + * Single listener config for a VirtualGateway + */ + readonly listener: CfnVirtualGateway.VirtualGatewayListenerProperty, +} + +/** + * Represents the properties needed to define listeners for a VirtualGateway + */ +export abstract class VirtualGatewayListener { + /** + * Returns an HTTP Listener for a VirtualGateway + */ + public static httpGatewayListener(props: HttpGatewayListenerProps = {}): VirtualGatewayListener { + return new HttpGatewayListener(props); + } + + /** + * Returns an HTTP2 Listener for a VirtualGateway + */ + public static http2GatewayListener(props: HttpGatewayListenerProps = {}): VirtualGatewayListener { + return new Http2GatewayListener(props); + } + + /** + * Returns a GRPC Listener for a VirtualGateway + */ + public static grpcGatewayListener(props: GrpcGatewayListenerProps = {}): VirtualGatewayListener { + return new GrpcGatewayListener(props); + } + + /** + * Protocol the listener implements + */ + protected abstract protocol: Protocol; + + /** + * Port to listen for connections on + */ + protected abstract port: number; + + /** + * Health checking strategy upstream nodes should use when communicating with the listener + */ + protected abstract healthCheck?: HealthCheck; + + /** + * Called when the GatewayListener type is initialized. Can be used to enforce + * mutual exclusivity + */ + public abstract bind(scope: cdk.Construct): VirtualGatewayListenerConfig; + + protected renderHealthCheck(hc: HealthCheck): CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty | undefined { + if (hc.protocol === Protocol.TCP) { + throw new Error('TCP health checks are not permitted for gateway listeners'); + } + + if (hc.protocol === Protocol.GRPC && hc.path) { + throw new Error('The path property cannot be set with Protocol.GRPC'); + } + + const protocol = hc.protocol? hc.protocol : this.protocol; + + const healthCheck: CfnVirtualGateway.VirtualGatewayHealthCheckPolicyProperty = { + healthyThreshold: hc.healthyThreshold || 2, + intervalMillis: (hc.interval || cdk.Duration.seconds(5)).toMilliseconds(), // min + path: hc.path || ((protocol === Protocol.HTTP || protocol === Protocol.HTTP2) ? '/' : undefined), + port: hc.port || this.port, + protocol: hc.protocol || this.protocol, + timeoutMillis: (hc.timeout || cdk.Duration.seconds(2)).toMilliseconds(), + unhealthyThreshold: hc.unhealthyThreshold || 2, + }; + + validateHealthChecks(healthCheck); + + return healthCheck; + } +} + +/** + * Represents the properties needed to define an HTTP Listener for a VirtualGateway + */ +class HttpGatewayListener extends VirtualGatewayListener { + /** + * Port to listen for connections on + * + * @default - 8080 + */ + readonly port: number; + + /** + * Health checking strategy upstream nodes should use when communicating with the listener + * + * @default - no healthcheck + */ + readonly healthCheck?: HealthCheck; + + /** + * Protocol the listener implements + */ + protected protocol: Protocol = Protocol.HTTP; + + constructor(props: HttpGatewayListenerProps = {}) { + super(); + this.port = props.port ? props.port : 8080; + this.healthCheck = props.healthCheck; + } + + /** + * Called when the GatewayListener type is initialized. Can be used to enforce + * mutual exclusivity + */ + public bind(_scope: cdk.Construct): VirtualGatewayListenerConfig { + return { + listener: { + portMapping: { + port: this.port, + protocol: this.protocol, + }, + healthCheck: this.healthCheck ? this.renderHealthCheck(this.healthCheck): undefined, + }, + }; + } +} + +/** +* Represents the properties needed to define an HTTP2 Listener for a VirtualGateway +*/ +class Http2GatewayListener extends HttpGatewayListener { + constructor(props: HttpGatewayListenerProps = {}) { + super(props); + this.protocol = Protocol.HTTP2; + } +} + +/** + * Represents the properties needed to define a GRPC Listener for Virtual Gateway + */ +class GrpcGatewayListener extends VirtualGatewayListener { + /** + * Port to listen for connections on + * + * @default - 8080 + */ + readonly port: number; + + /** + * Health checking strategy upstream nodes should use when communicating with the listener + * + * @default - no healthcheck + */ + readonly healthCheck?: HealthCheck; + + /** + * Protocol the listener implements + */ + protected protocol: Protocol = Protocol.GRPC; + + constructor(props: HttpGatewayListenerProps = {}) { + super(); + this.port = props.port ? props.port : 8080; + this.healthCheck = props.healthCheck; + } + + /** + * Called when the GatewayListener type is initialized. Can be used to enforce + * mutual exclusivity + */ + public bind(_scope: cdk.Construct): VirtualGatewayListenerConfig { + return { + listener: { + portMapping: { + port: this.port, + protocol: Protocol.GRPC, + }, + healthCheck: this.healthCheck? this.renderHealthCheck(this.healthCheck): undefined, + }, + }; + } +} \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts new file mode 100644 index 0000000000000..51950f82dbb13 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-gateway.ts @@ -0,0 +1,226 @@ +import * as cdk from '@aws-cdk/core'; +import { Construct } from 'constructs'; +import { CfnVirtualGateway } from './appmesh.generated'; +import { GatewayRoute, GatewayRouteBaseProps } from './gateway-route'; + +import { IMesh, Mesh } from './mesh'; +import { AccessLog } from './shared-interfaces'; +import { VirtualGatewayListener, VirtualGatewayListenerConfig } from './virtual-gateway-listener'; + +/** + * Interface which all Virtual Gateway based classes must implement + */ +export interface IVirtualGateway extends cdk.IResource { + /** + * Name of the VirtualGateway + * + * @attribute + */ + readonly virtualGatewayName: string; + + /** + * The Amazon Resource Name (ARN) for the VirtualGateway + * + * @attribute + */ + readonly virtualGatewayArn: string; + + /** + * The mesh which the VirtualGateway belongs to + */ + readonly mesh: IMesh; + + /** + * Utility method to add a new GatewayRoute to the VirtualGateway + */ + addGatewayRoute(id: string, route: GatewayRouteBaseProps): GatewayRoute; +} + +/** + * Basic configuration properties for a VirtualGateway + */ +export interface VirtualGatewayBaseProps { + /** + * Name of the VirtualGateway + * + * @default - A name is automatically determined + */ + readonly virtualGatewayName?: string; + + /** + * Listeners for the VirtualGateway. Only one is supported. + * + * @default - Single HTTP listener on port 8080 + */ + readonly listeners?: VirtualGatewayListener[]; + + /** + * Access Logging Configuration for the VirtualGateway + * + * @default - no access logging + */ + readonly accessLog?: AccessLog; +} + +/** + * Properties used when creating a new VirtualGateway + */ +export interface VirtualGatewayProps extends VirtualGatewayBaseProps { + /** + * The mesh which the VirtualGateway belongs to + */ + readonly mesh: IMesh; +} + +abstract class VirtualGatewayBase extends cdk.Resource implements IVirtualGateway { + /** + * Name of the VirtualGateway + */ + public abstract readonly virtualGatewayName: string; + + /** + * The Amazon Resource Name (ARN) for the VirtualGateway + */ + public abstract readonly virtualGatewayArn: string; + + /** + * The name of the mesh which the VirtualGateway belongs to + */ + public abstract readonly mesh: IMesh; + + /** + * Utility method to add a new GatewayRoute to the VirtualGateway + */ + public addGatewayRoute(id: string, props: GatewayRouteBaseProps): GatewayRoute { + return new GatewayRoute(this, id, { + ...props, + virtualGateway: this, + }); + } +} + +/** + * VirtualGateway represents a newly defined App Mesh Virtual Gateway + * + * A virtual gateway allows resources that are outside of your mesh to communicate to resources that + * are inside of your mesh. + * + * @see https://docs.aws.amazon.com/app-mesh/latest/userguide/virtual_gateways.html + */ +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 }); + } + + /** + * The name of the VirtualGateway + */ + public readonly virtualGatewayName: string; + + /** + * The Amazon Resource Name (ARN) for the VirtualGateway + */ + public readonly virtualGatewayArn: string; + + /** + * The Mesh that the VirtualGateway belongs to + */ + public readonly mesh: IMesh; + + protected readonly listeners = new Array(); + + constructor(scope: Construct, id: string, props: VirtualGatewayProps) { + super(scope, id, { + physicalName: props.virtualGatewayName || cdk.Lazy.stringValue({ produce: () => this.node.uniqueId }), + }); + + this.mesh = props.mesh; + + if (!props.listeners) { + // Use listener default of http listener port 8080 if no listener is defined + this.listeners.push(VirtualGatewayListener.httpGatewayListener().bind(this)); + } else { + props.listeners.forEach(listener => this.listeners.push(listener.bind(this))); + } + + const accessLogging = props.accessLog?.bind(this); + + const node = new CfnVirtualGateway(this, 'Resource', { + virtualGatewayName: this.physicalName, + meshName: this.mesh.meshName, + spec: { + listeners: this.listeners.map(listener => listener.listener), + logging: accessLogging !== undefined ? { + accessLog: accessLogging.virtualGatewayAccessLog, + } : undefined, + }, + }); + + this.virtualGatewayName = this.getResourceNameAttribute(node.attrVirtualGatewayName); + this.virtualGatewayArn = this.getResourceArnAttribute(node.ref, { + service: 'appmesh', + resource: `mesh/${props.mesh.meshName}/virtualGateway`, + resourceName: this.physicalName, + }); + } +} + +/** + * 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 { + /** + * The name of the VirtualGateway + */ + public readonly virtualGatewayName: string; + + /** + * The Amazon Resource Name (ARN) belonging to the VirtualGateway + */ + public readonly virtualGatewayArn: 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'); + } + } +} diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts index 233ad2d0f216c..c3226a61e3813 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-node.ts @@ -3,6 +3,7 @@ import * as cdk from '@aws-cdk/core'; import { Construct } from 'constructs'; import { CfnVirtualNode } from './appmesh.generated'; import { IMesh } from './mesh'; +import { validateHealthChecks } from './private/utils'; import { AccessLog, HealthCheck, PortMapping, Protocol, VirtualNodeListener } from './shared-interfaces'; import { IVirtualService } from './virtual-service'; @@ -154,19 +155,6 @@ abstract class VirtualNodeBase extends cdk.Resource implements IVirtualNode { } } -/** - * Minimum and maximum thresholds for HeathCheck numeric properties - * - * @see https://docs.aws.amazon.com/app-mesh/latest/APIReference/API_HealthCheckPolicy.html - */ -const HEALTH_CHECK_PROPERTY_THRESHOLDS: {[key in (keyof CfnVirtualNode.HealthCheckProperty)]?: [number, number]} = { - healthyThreshold: [2, 10], - intervalMillis: [5000, 300000], - port: [1, 65535], - timeoutMillis: [2000, 60000], - unhealthyThreshold: [2, 10], -}; - function renderHealthCheck(hc: HealthCheck | undefined, pm: PortMapping): CfnVirtualNode.HealthCheckProperty | undefined { if (hc === undefined) { return undefined; } @@ -178,38 +166,25 @@ function renderHealthCheck(hc: HealthCheck | undefined, pm: PortMapping): CfnVir throw new Error('The path property cannot be set with Protocol.GRPC'); } + const protocol = hc.protocol ?? pm.protocol; + const healthCheck: CfnVirtualNode.HealthCheckProperty = { healthyThreshold: hc.healthyThreshold || 2, intervalMillis: (hc.interval || cdk.Duration.seconds(5)).toMilliseconds(), // min - path: hc.path || (hc.protocol === Protocol.HTTP ? '/' : undefined), + path: hc.path || (protocol === Protocol.HTTP ? '/' : undefined), port: hc.port || pm.port, protocol: hc.protocol || pm.protocol, timeoutMillis: (hc.timeout || cdk.Duration.seconds(2)).toMilliseconds(), unhealthyThreshold: hc.unhealthyThreshold || 2, }; - (Object.keys(healthCheck) as Array) - .filter((key) => - HEALTH_CHECK_PROPERTY_THRESHOLDS[key] && - typeof healthCheck[key] === 'number' && - !cdk.Token.isUnresolved(healthCheck[key]), - ).map((key) => { - const [min, max] = HEALTH_CHECK_PROPERTY_THRESHOLDS[key]!; - const value = healthCheck[key]!; - - if (value < min) { - throw new Error(`The value of '${key}' is below the minimum threshold (expected >=${min}, got ${value})`); - } - if (value > max) { - throw new Error(`The value of '${key}' is above the maximum threshold (expected <=${max}, got ${value})`); - } - }); + validateHealthChecks(healthCheck); return healthCheck; } /** - * VirtualNode represents a newly defined AppMesh VirtualNode + * VirtualNode represents a newly defined App Mesh VirtualNode * * Any inbound traffic that your virtual node expects should be specified as a * listener. Any outbound traffic that your virtual node expects to reach @@ -241,7 +216,7 @@ export class VirtualNode extends VirtualNodeBase { public readonly virtualNodeName: string; /** - * The Amazon Resource Name belonging to the VirtualNdoe + * The Amazon Resource Name belonging to the VirtualNode */ public readonly virtualNodeArn: string; @@ -252,7 +227,7 @@ export class VirtualNode extends VirtualNodeBase { 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; @@ -316,7 +291,7 @@ interface VirtualNodeAttributes { } /** - * Used to import a VirtualNode and read it's properties + * Used to import a VirtualNode and read its properties */ class ImportedVirtualNode extends VirtualNodeBase { /** diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts index 05fcbb29d5f71..f08b223ec23ed 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-router.ts @@ -145,7 +145,7 @@ 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; diff --git a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts index 00b48f2d39d80..9b920ea36c9b5 100644 --- a/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts +++ b/packages/@aws-cdk/aws-appmesh/lib/virtual-service.ts @@ -106,7 +106,7 @@ export class VirtualService extends cdk.Resource implements IVirtualService { 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) { diff --git a/packages/@aws-cdk/aws-appmesh/package.json b/packages/@aws-cdk/aws-appmesh/package.json index e3c345d954997..2789d9085284c 100644 --- a/packages/@aws-cdk/aws-appmesh/package.json +++ b/packages/@aws-cdk/aws-appmesh/package.json @@ -112,6 +112,29 @@ "from-signature:@aws-cdk/aws-appmesh.VirtualNode.fromVirtualNodeName", "from-signature:@aws-cdk/aws-appmesh.VirtualRouter.fromVirtualRouterName", "from-signature:@aws-cdk/aws-appmesh.VirtualService.fromVirtualServiceName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayRoute.gatewayRouteVirtualGatewayName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayGrpcRoute.gatewayRouteVirtualGatewayName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttp2Route.gatewayRouteVirtualGatewayName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteUid", + "resource-attribute:@aws-cdk/aws-appmesh.GatewayHttpRoute.gatewayRouteVirtualGatewayName", "resource-attribute:@aws-cdk/aws-appmesh.IMesh.meshUid", "resource-attribute:@aws-cdk/aws-appmesh.IRoute.routeUid", "resource-attribute:@aws-cdk/aws-appmesh.IVirtualNode.virtualNodeUid", @@ -125,6 +148,10 @@ "resource-attribute:@aws-cdk/aws-appmesh.Route.routeResourceOwner", "resource-attribute:@aws-cdk/aws-appmesh.Route.routeUid", "resource-attribute:@aws-cdk/aws-appmesh.Route.routeVirtualRouterName", + "resource-attribute:@aws-cdk/aws-appmesh.VirtualGateway.virtualGatewayMeshName", + "resource-attribute:@aws-cdk/aws-appmesh.VirtualGateway.virtualGatewayMeshOwner", + "resource-attribute:@aws-cdk/aws-appmesh.VirtualGateway.virtualGatewayResourceOwner", + "resource-attribute:@aws-cdk/aws-appmesh.VirtualGateway.virtualGatewayUid", "resource-attribute:@aws-cdk/aws-appmesh.VirtualNode.virtualNodeMeshName", "resource-attribute:@aws-cdk/aws-appmesh.VirtualNode.virtualNodeMeshOwner", "resource-attribute:@aws-cdk/aws-appmesh.VirtualNode.virtualNodeResourceOwner", diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json index 5b1b1657f0e20..e74a824311783 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.expected.json @@ -772,6 +772,151 @@ "VirtualNodeName": "meshstackmeshnode3C5835BCB" } }, + "meshgateway1B02387E8": { + "Type": "AWS::AppMesh::VirtualGateway", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Listeners": [ + { + "PortMapping": { + "Port": 8080, + "Protocol": "http" + } + } + ], + "Logging": { + "AccessLog": { + "File": { + "Path": "/dev/stdout" + } + } + } + }, + "VirtualGatewayName": "gateway1" + } + }, + "meshgateway1gateway1routehttpE8D6F433": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttpBA921D42", + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "HttpRoute": { + "Action": { + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "meshserviceE06ECED5", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Prefix": "/" + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + } + } + }, + "meshgateway1gateway1routehttp2FD69C306": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "GatewayRouteName": "meshstackmeshgateway1gateway1routehttp255781963", + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Http2Route": { + "Action": { + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "meshserviceE06ECED5", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "Prefix": "/" + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + } + } + }, + "meshgateway1gateway1routegrpc76486062": { + "Type": "AWS::AppMesh::GatewayRoute", + "Properties": { + "GatewayRouteName": "meshstackmeshgateway1gateway1routegrpcCD4D891D", + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "GrpcRoute": { + "Action": { + "Target": { + "VirtualService": { + "VirtualServiceName": { + "Fn::GetAtt": [ + "meshserviceE06ECED5", + "VirtualServiceName" + ] + } + } + } + }, + "Match": { + "ServiceName": { + "Fn::GetAtt": [ + "meshserviceE06ECED5", + "VirtualServiceName" + ] + } + } + } + }, + "VirtualGatewayName": { + "Fn::GetAtt": [ + "meshgateway1B02387E8", + "VirtualGatewayName" + ] + } + } + }, "service27C65CF7D": { "Type": "AWS::AppMesh::VirtualService", "Properties": { @@ -797,6 +942,37 @@ "Spec": {}, "VirtualServiceName": "service3.domain.local" } + }, + "gateway2BCE5C5E0": { + "Type": "AWS::AppMesh::VirtualGateway", + "Properties": { + "MeshName": { + "Fn::GetAtt": [ + "meshACDFE68E", + "MeshName" + ] + }, + "Spec": { + "Listeners": [ + { + "HealthCheck": { + "HealthyThreshold": 2, + "IntervalMillis": 10000, + "Path": "/", + "Port": 443, + "Protocol": "http", + "TimeoutMillis": 2000, + "UnhealthyThreshold": 2 + }, + "PortMapping": { + "Port": 443, + "Protocol": "http" + } + } + ] + }, + "VirtualGatewayName": "meshstackgateway2BEC62D7C" + } } } } \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts index df55025f1365c..322ae1cc608d6 100644 --- a/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts +++ b/packages/@aws-cdk/aws-appmesh/test/integ.mesh.ts @@ -116,3 +116,39 @@ router.addRoute('route-3', { }, ], }); + +const gateway = mesh.addVirtualGateway('gateway1', { + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + virtualGatewayName: 'gateway1', +}); + +new appmesh.VirtualGateway(stack, 'gateway2', { + mesh: mesh, + listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), + }, + })], +}); + +gateway.addGatewayRoute('gateway1-route-http', { + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), +}); + +gateway.addGatewayRoute('gateway1-route-http2', { + routeSpec: appmesh.GatewayRouteSpec.http2RouteSpec({ + routeTarget: virtualService, + }), +}); + +gateway.addGatewayRoute('gateway1-route-grpc', { + routeSpec: appmesh.GatewayRouteSpec.grpcRouteSpec({ + routeTarget: virtualService, + match: { + serviceName: virtualService.virtualServiceName, + }, + }), +}); diff --git a/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts new file mode 100644 index 0000000000000..842e553dfa20c --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/test/test.gateway-route.ts @@ -0,0 +1,152 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; + +import * as appmesh from '../lib'; + +export = { + 'When creating a GatewayRoute': { + 'should have expected defaults'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'gateway-1', { + listeners: [appmesh.VirtualGatewayListener.httpGatewayListener()], + mesh: mesh, + }); + + const virtualService = new appmesh.VirtualService(stack, 'vs-1', { + mesh: mesh, + virtualServiceName: 'target.local', + }); + + // Add an HTTP Route + virtualGateway.addGatewayRoute('gateway-http-route', { + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), + gatewayRouteName: 'gateway-http-route', + }); + + virtualGateway.addGatewayRoute('gateway-http2-route', { + routeSpec: appmesh.GatewayRouteSpec.http2RouteSpec({ + routeTarget: virtualService, + }), + gatewayRouteName: 'gateway-http2-route', + }); + + virtualGateway.addGatewayRoute('gateway-grpc-route', { + routeSpec: appmesh.GatewayRouteSpec.grpcRouteSpec({ + routeTarget: virtualService, + match: { + serviceName: virtualService.virtualServiceName, + }, + }), + gatewayRouteName: 'gateway-grpc-route', + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http-route', + Spec: { + HttpRoute: { + Action: { + Target: { + VirtualService: { + VirtualServiceName: { + 'Fn::GetAtt': ['vs1732C2645', 'VirtualServiceName'], + }, + }, + }, + }, + Match: { + Prefix: '/', + }, + }, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-http2-route', + Spec: { + Http2Route: { + Action: { + Target: { + VirtualService: { + VirtualServiceName: { + 'Fn::GetAtt': ['vs1732C2645', 'VirtualServiceName'], + }, + }, + }, + }, + Match: { + Prefix: '/', + }, + }, + }, + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'gateway-grpc-route', + Spec: { + GrpcRoute: { + Action: { + Target: { + VirtualService: { + VirtualServiceName: { + 'Fn::GetAtt': ['vs1732C2645', 'VirtualServiceName'], + }, + }, + }, + }, + Match: { + ServiceName: { + 'Fn::GetAtt': ['vs1732C2645', 'VirtualServiceName'], + }, + }, + }, + }, + })); + test.done(); + }, + + 'should throw an exception if you start an http prefix match not with a /'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualService = mesh.addVirtualService('testVirtualService'); + test.throws(() => appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + match: { + prefixPath: 'wrong', + }, + }).bind(stack), + /Prefix Path must start with \'\/\', got: wrong/); + test.done(); + }, + }, + + 'Can export and import GatewayRoutes and perform actions'(test: Test) { + const app = new cdk.App(); + // GIVEN + const stack = new cdk.Stack(app, 'Imports', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + + // 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'); + // THEN + test.equal(gatewayRoute2.gatewayRouteName, 'test-gateway-route'); + test.equal(gatewayRoute2.virtualGateway.virtualGatewayName, 'test-gateway'); + test.equal(gatewayRoute2.virtualGateway.mesh.meshName, 'test-mesh'); + test.done(); + }, +}; \ No newline at end of file diff --git a/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts new file mode 100644 index 0000000000000..1dd75b938ab04 --- /dev/null +++ b/packages/@aws-cdk/aws-appmesh/test/test.virtual-gateway.ts @@ -0,0 +1,258 @@ +import { expect, haveResourceLike } from '@aws-cdk/assert'; +import * as cdk from '@aws-cdk/core'; +import { Test } from 'nodeunit'; + +import * as appmesh from '../lib'; + +export = { + 'When creating a VirtualGateway': { + 'should have expected defaults'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + new appmesh.VirtualGateway(stack, 'testGateway', { + mesh: mesh, + }); + + new appmesh.VirtualGateway(stack, 'httpGateway', { + mesh: mesh, + listeners: [appmesh.VirtualGatewayListener.httpGatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), + }, + })], + }); + + new appmesh.VirtualGateway(stack, 'http2Gateway', { + mesh: mesh, + listeners: [appmesh.VirtualGatewayListener.http2GatewayListener({ + port: 443, + healthCheck: { + interval: cdk.Duration.seconds(10), + }, + })], + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + PortMapping: { + Port: 8080, + Protocol: appmesh.Protocol.HTTP, + }, + }, + ], + }, + VirtualGatewayName: 'testGateway', + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + HealthCheck: { + HealthyThreshold: 2, + IntervalMillis: 10000, + Port: 443, + Protocol: appmesh.Protocol.HTTP, + TimeoutMillis: 2000, + UnhealthyThreshold: 2, + Path: '/', + }, + PortMapping: { + Port: 443, + Protocol: appmesh.Protocol.HTTP, + }, + }, + ], + }, + VirtualGatewayName: 'httpGateway', + })); + + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + HealthCheck: { + HealthyThreshold: 2, + IntervalMillis: 10000, + Port: 443, + Protocol: appmesh.Protocol.HTTP2, + TimeoutMillis: 2000, + UnhealthyThreshold: 2, + Path: '/', + }, + PortMapping: { + Port: 443, + Protocol: appmesh.Protocol.HTTP2, + }, + }, + ], + }, + VirtualGatewayName: 'http2Gateway', + })); + test.done(); + }, + + 'should have expected features'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + new appmesh.VirtualGateway(stack, 'testGateway', { + virtualGatewayName: 'test-gateway', + listeners: [appmesh.VirtualGatewayListener.grpcGatewayListener({ + port: 80, + healthCheck: { + }, + })], + mesh: mesh, + accessLog: appmesh.AccessLog.fromFilePath('/dev/stdout'), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::VirtualGateway', { + Spec: { + Listeners: [ + { + HealthCheck: { + HealthyThreshold: 2, + IntervalMillis: 5000, + Port: 80, + Protocol: appmesh.Protocol.GRPC, + TimeoutMillis: 2000, + UnhealthyThreshold: 2, + }, + PortMapping: { + Port: 80, + Protocol: appmesh.Protocol.GRPC, + }, + }, + ], + Logging: { + AccessLog: { + File: { + Path: '/dev/stdout', + }, + }, + }, + }, + VirtualGatewayName: 'test-gateway', + })); + test.done(); + }, + }, + + 'When adding a gateway route to existing VirtualGateway ': { + 'should create gateway route resource'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualGateway = new appmesh.VirtualGateway(stack, 'testGateway', { + virtualGatewayName: 'test-gateway', + mesh: mesh, + }); + + const virtualService = mesh.addVirtualService('virtualService', {}); + + virtualGateway.addGatewayRoute('testGatewayRoute', { + gatewayRouteName: 'test-gateway-route', + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), + }); + + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'test-gateway-route', + Spec: { + HttpRoute: { + Action: { + Target: { + VirtualService: { + VirtualServiceName: { + 'Fn::GetAtt': ['meshvirtualService93460D43', 'VirtualServiceName'], + }, + }, + }, + }, + Match: { + Prefix: '/', + }, + }, + }, + })); + test.done(); + }, + }, + + 'When adding gateway routes to a VirtualGateway with existing gateway routes': { + 'should add gateway routes and not overwite'(test: Test) { + // GIVEN + const stack = new cdk.Stack(); + + // WHEN + const mesh = new appmesh.Mesh(stack, 'mesh', { + meshName: 'test-mesh', + }); + + const virtualService = mesh.addVirtualService('virtualService', {}); + + const virtualGateway = mesh.addVirtualGateway('gateway'); + virtualGateway.addGatewayRoute('testGatewayRoute', { + gatewayRouteName: 'test-gateway-route', + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), + }); + virtualGateway.addGatewayRoute('testGatewayRoute2', { + gatewayRouteName: 'test-gateway-route-2', + routeSpec: appmesh.GatewayRouteSpec.httpRouteSpec({ + routeTarget: virtualService, + }), + }); + // THEN + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'test-gateway-route', + })); + expect(stack).to(haveResourceLike('AWS::AppMesh::GatewayRoute', { + GatewayRouteName: 'test-gateway-route-2', + })); + test.done(); + }, + }, + + 'Can export and import VirtualGateway and perform actions'(test: Test) { + const app = new cdk.App(); + // GIVEN + const stack = new cdk.Stack(app, 'Imports', { + env: { account: '123456789012', region: 'us-east-1' }, + }); + + // WHEN + const virtualGateway2 = appmesh.VirtualGateway.fromVirtualGatewayArn( + stack, 'importedGateway2', 'arn:aws:appmesh:us-east-1:123456789012:mesh/testMesh/virtualGateway/test-gateway'); + + // THEN + test.equal(virtualGateway2.mesh.meshName, 'testMesh'); + test.equal(virtualGateway2.virtualGatewayName, 'test-gateway'); + test.done(); + }, +}; \ No newline at end of file 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-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..818b0dc43d7f3 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.785.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..134526a33cbd7 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.785.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..a4f02bda2c372 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.785.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..096bc6fee0a0b 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.785.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..b19b07d18450a 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.785.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 20adf29987987..8091c3ae4bc1d 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 { toASCII as punycodeEncode } from 'punycode/'; import { CfnUserPool } from './cognito.generated'; @@ -867,7 +867,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 27e6c9a7b4330..137a8335d8300 100644 --- a/packages/@aws-cdk/aws-cognito/package.json +++ b/packages/@aws-cdk/aws-cognito/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-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..fe1381716a194 100644 --- a/packages/@aws-cdk/aws-dynamodb/package.json +++ b/packages/@aws-cdk/aws-dynamodb/package.json @@ -74,12 +74,12 @@ "devDependencies": { "@aws-cdk/assert": "0.0.0", "@types/jest": "^26.0.15", - "aws-sdk": "^2.781.0", + "aws-sdk": "^2.785.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" 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..be52a3aa637cd 100644 --- a/packages/@aws-cdk/aws-ec2/lib/security-group.ts +++ b/packages/@aws-cdk/aws-ec2/lib/security-group.ts @@ -1,4 +1,4 @@ -import { Annotations, IResource, Lazy, Resource, ResourceProps, Stack, Token } from '@aws-cdk/core'; +import { Annotations, IResource, Lazy, Names, Resource, ResourceProps, Stack, Token } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { Connections } from './connections'; import { CfnSecurityGroup, CfnSecurityGroupEgress, CfnSecurityGroupIngress } from './ec2.generated'; @@ -71,7 +71,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) { 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-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..f5e8120bf29c5 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.785.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/lib/alb/application-load-balancer.ts b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/alb/application-load-balancer.ts index 278cd8a5fbc44..780282071a18c 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,6 +1,6 @@ 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 { Duration, Lazy, Names, Resource } from '@aws-cdk/core'; import { Construct } from 'constructs'; import { BaseLoadBalancer, BaseLoadBalancerProps, ILoadBalancerV2 } from '../shared/base-load-balancer'; import { IpAddressType, ApplicationProtocol } from '../shared/enums'; @@ -70,7 +70,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 }); 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 17857a2ed4187..cf98f4b7cbbd4 100644 --- a/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts +++ b/packages/@aws-cdk/aws-elasticloadbalancingv2/lib/nlb/network-listener.ts @@ -106,7 +106,7 @@ export class NetworkListener extends BaseListener implements INetworkListener { /** * The load balancer this listener is attached to */ - private readonly loadBalancer: INetworkLoadBalancer; + public readonly loadBalancer: INetworkLoadBalancer; /** * the protocol of the listener 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..a16ac63dae9e2 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.785.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/package.json b/packages/@aws-cdk/aws-lambda-nodejs/package.json index ec0edd7833c78..7b03475c7d8f3 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.438", "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..628b9fe43c66e 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.785.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..6efbdb42c8ac1 100644 --- a/packages/@aws-cdk/aws-rds/README.md +++ b/packages/@aws-cdk/aws-rds/README.md @@ -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/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/serverless-cluster.ts b/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts index 43305ac2d6e26..c6b1d7edc514a 100644 --- a/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts +++ b/packages/@aws-cdk/aws-rds/lib/serverless-cluster.ts @@ -1,12 +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 { DATA_API_ACTIONS } from './perms'; import { applyRemovalPolicy, defaultDeletionProtection, DEFAULT_PASSWORD_EXCLUDE_CHARS } from './private/util'; import { Credentials, RotationMultiUserOptions, RotationSingleUserOptions } from './props'; import { CfnDBCluster } from './rds.generated'; @@ -39,6 +41,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 +98,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 +202,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 +303,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 +322,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 +377,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 +397,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. @@ -410,7 +454,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 +553,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 +571,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.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.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..9b65bb4b6c327 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.785.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..8d8c5e608d0cc 100644 --- a/packages/@aws-cdk/aws-sam/package.json +++ b/packages/@aws-cdk/aws-sam/package.json @@ -77,7 +77,7 @@ "@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" }, 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..2a8644ed6b5a5 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.785.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/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/cloudformation-diff/package.json b/packages/@aws-cdk/cloudformation-diff/package.json index c2e716a807586..038bd3dd3b370 100644 --- a/packages/@aws-cdk/cloudformation-diff/package.json +++ b/packages/@aws-cdk/cloudformation-diff/package.json @@ -34,7 +34,7 @@ "@types/table": "^5.0.0", "cdk-build-tools": "0.0.0", "fast-check": "^2.6.0", - "jest": "^26.6.1", + "jest": "^26.6.3", "pkglint": "0.0.0", "ts-jest": "^26.4.3" }, diff --git a/packages/@aws-cdk/cloudformation-include/package.json b/packages/@aws-cdk/cloudformation-include/package.json index 92fb2d7adf0a6..9e67e7dda3901 100644 --- a/packages/@aws-cdk/cloudformation-include/package.json +++ b/packages/@aws-cdk/cloudformation-include/package.json @@ -330,7 +330,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", "ts-jest": "^26.4.3" }, 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/bundling.ts b/packages/@aws-cdk/core/lib/bundling.ts index 8326d8f55e3aa..234b3ce969b42 100644 --- a/packages/@aws-cdk/core/lib/bundling.ts +++ b/packages/@aws-cdk/core/lib/bundling.ts @@ -117,7 +117,7 @@ export class BundlingDockerImage { const docker = dockerExec(dockerArgs); - const match = docker.stdout.toString().match(/sha256:([a-z0-9]+)/); + const match = docker.stdout.toString().match(/sha256:[a-z0-9]+/); if (!match) { throw new Error('Failed to extract image ID from Docker build output'); @@ -129,7 +129,7 @@ export class BundlingDockerImage { // different every time the Docker layer cache is cleared, due primarily to // timestamps. const hash = FileSystem.fingerprint(path, { extraHash: JSON.stringify(options) }); - return new BundlingDockerImage(match[1], hash); + return new BundlingDockerImage(match[0], hash); } /** @param image The Docker image */ 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..6ed538438492b 100644 --- a/packages/@aws-cdk/core/package.json +++ b/packages/@aws-cdk/core/package.json @@ -168,7 +168,7 @@ }, "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", 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/bundling.test.ts b/packages/@aws-cdk/core/test/bundling.test.ts index a33ee4a96b087..5bc4c0d55e1be 100644 --- a/packages/@aws-cdk/core/test/bundling.test.ts +++ b/packages/@aws-cdk/core/test/bundling.test.ts @@ -46,11 +46,11 @@ nodeunitShim({ }, 'bundling with image from asset'(test: Test) { - const imageId = 'abcdef123456'; + const imageId = 'sha256:abcdef123456'; const spawnSyncStub = sinon.stub(child_process, 'spawnSync').returns({ status: 0, stderr: Buffer.from('stderr'), - stdout: Buffer.from(`sha256:${imageId}`), + stdout: Buffer.from(imageId), pid: 123, output: ['stdout', 'stderr'], signal: null, @@ -133,11 +133,11 @@ nodeunitShim({ }, 'BundlerDockerImage json is the bundler image if building an image'(test: Test) { - const imageId = 'abcdef123456'; + const imageId = 'sha256:abcdef123456'; sinon.stub(child_process, 'spawnSync').returns({ status: 0, stderr: Buffer.from('stderr'), - stdout: Buffer.from(`sha256:${imageId}`), + stdout: Buffer.from(imageId), pid: 123, output: ['stdout', 'stderr'], signal: null, 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..acbbf1680983f 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.785.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/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/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/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/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..89cb3fbce95ba 100644 --- a/packages/@monocdk-experiment/assert/package.json +++ b/packages/@monocdk-experiment/assert/package.json @@ -38,7 +38,7 @@ "@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" @@ -48,7 +48,7 @@ }, "peerDependencies": { "constructs": "^3.0.4", - "jest": "^26.6.1", + "jest": "^26.6.3", "monocdk": "^0.0.0" }, "repository": { diff --git a/packages/aws-cdk-lib/package.json b/packages/aws-cdk-lib/package.json index 6c71d4aa69ac5..2f8b786e7e01b 100644 --- a/packages/aws-cdk-lib/package.json +++ b/packages/aws-cdk-lib/package.json @@ -105,6 +105,7 @@ "@aws-cdk/aws-amplify": "0.0.0", "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-appconfig": "0.0.0", "@aws-cdk/aws-appflow": "0.0.0", "@aws-cdk/aws-applicationautoscaling": "0.0.0", 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/deploy-stack.ts b/packages/aws-cdk/lib/api/deploy-stack.ts index 626dcce5c57e7..64ccd3dd77352 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/package.json b/packages/aws-cdk/package.json index cc67af22ad1de..51414409969d9 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-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.785.0", "camelcase": "^6.2.0", "cdk-assets": "0.0.0", "colors": "^1.4.0", 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/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..f917654e43d08 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.785.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 c628a56491d46..0871274f0b016 100644 --- a/packages/decdk/package.json +++ b/packages/decdk/package.json @@ -36,6 +36,7 @@ "@aws-cdk/aws-amplify": "0.0.0", "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-appconfig": "0.0.0", "@aws-cdk/aws-appflow": "0.0.0", "@aws-cdk/aws-applicationautoscaling": "0.0.0", @@ -189,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" @@ -199,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/packages/monocdk/package.json b/packages/monocdk/package.json index 294791ee402d8..8783378fbef4f 100644 --- a/packages/monocdk/package.json +++ b/packages/monocdk/package.json @@ -104,6 +104,7 @@ "@aws-cdk/aws-amplify": "0.0.0", "@aws-cdk/aws-apigateway": "0.0.0", "@aws-cdk/aws-apigatewayv2": "0.0.0", + "@aws-cdk/aws-apigatewayv2-integrations": "0.0.0", "@aws-cdk/aws-appconfig": "0.0.0", "@aws-cdk/aws-appflow": "0.0.0", "@aws-cdk/aws-applicationautoscaling": "0.0.0", 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..09246c5f04f4c 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,9 +49,9 @@ "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", 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 2b966118d9694..fff2ad8d1f44b 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,46 +1091,46 @@ 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" @@ -1135,47 +1142,47 @@ 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 +1193,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" @@ -1271,10 +1278,21 @@ "@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== +"@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" + "@types/node" "*" + "@types/yargs" "^15.0.0" + chalk "^4.0.0" + +"@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 +2084,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 +2146,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 +2170,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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/babel-ast-utils/-/babel-ast-utils-2.0.0-nightly.2062.tgz#dd01b86d3cd56d93c4c27f3eb92dd5f6fdbfcdba" + integrity sha512-cKCzKl0wXEdhPyGxBqrUtNulmCLi1b6aMth6L2Lg1KcdcnN/zdpXB405KTKYWUuMULLZOjatarhEV/WMGYCEjg== 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.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/babel-preset-env/-/babel-preset-env-2.0.0-nightly.440.tgz#947d0f579717f1caf0bcc20bd612fbe3aacc4e35" + integrity sha512-ovsArQ7jdsDDD6Q8cT0RqgZ9iNIShStzNUTy/cR9C5MoA2EeMqhmguTFbKNujNPXhrsyJ0M86EEijWA+MLKNlw== 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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/babylon-walk/-/babylon-walk-2.0.0-nightly.2062.tgz#a55418911851c7c4015622b579c4ca01ef921630" + integrity sha512-7BEK1BPSGnHcudrghUZBK9tM//Zu9Bs7n5OijYX2gTQ9re/qy7NLu4NIV03eJx6cvxg7k/nE/ZpV2lrzFgkcDg== 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/bundler-default/-/bundler-default-2.0.0-nightly.440.tgz#38edbaa9ecf4992e19ab8c151c6d54b29157a287" + integrity sha512-6dMnmGDSHKM6cTWNq56txQkgbLRgQLctd8KrrWnSbnfKeMKQIAdJQVhg4Y5ku680NGZlxd+8/kpXS21SYG0sHQ== 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.440+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/cache/-/cache-2.0.0-nightly.440.tgz#283088d2c74b59f7bd2dc4f317abc3dff477a798" + integrity sha512-NCQqYw9fkevvnSkV0u4EpwqswkMRqikNvMr7R/6qxuB39anVyOaygaQ1CTeAPzCs+LoExdMl4g+Q83uAMRSwbw== dependencies: - "@parcel/logger" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/logger" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/codeframe/-/codeframe-2.0.0-nightly.440.tgz#ee426a7d61c43040b0c1c814a0094c0020983482" + integrity sha512-WDnyEu+10SPCK9XM19LzNL1L5vVVMak0m3088yddzc+bOdR75Pezg3bXsNdEVh2Xd8Xbz+LpWWrB9eGc/hbRdA== 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/config-default/-/config-default-2.0.0-nightly.440.tgz#69e53a1c8ca9860e7f64cf382119c7902cdb9a45" + integrity sha512-mGzxsPT2cyShfTa16Mgo9shjfyEVP+qbZ0R4xKiokhRkek9Wxhyb4FOka8qltxuMSsCyGsBk6kfCR0WTmzffaA== + dependencies: + "@parcel/bundler-default" "2.0.0-nightly.440+a5e23487" + "@parcel/namer-default" "2.0.0-nightly.440+a5e23487" + "@parcel/optimizer-cssnano" "2.0.0-nightly.440+a5e23487" + "@parcel/optimizer-data-url" "2.0.0-nightly.440+a5e23487" + "@parcel/optimizer-htmlnano" "2.0.0-nightly.440+a5e23487" + "@parcel/optimizer-terser" "2.0.0-nightly.440+a5e23487" + "@parcel/packager-css" "2.0.0-nightly.440+a5e23487" + "@parcel/packager-html" "2.0.0-nightly.440+a5e23487" + "@parcel/packager-js" "2.0.0-nightly.440+a5e23487" + "@parcel/packager-raw" "2.0.0-nightly.440+a5e23487" + "@parcel/packager-raw-url" "2.0.0-nightly.2062+a5e23487" + "@parcel/packager-ts" "2.0.0-nightly.440+a5e23487" + "@parcel/reporter-bundle-analyzer" "2.0.0-nightly.2062+a5e23487" + "@parcel/reporter-bundle-buddy" "2.0.0-nightly.2062+a5e23487" + "@parcel/reporter-cli" "2.0.0-nightly.440+a5e23487" + "@parcel/reporter-dev-server" "2.0.0-nightly.440+a5e23487" + "@parcel/resolver-default" "2.0.0-nightly.440+a5e23487" + "@parcel/runtime-browser-hmr" "2.0.0-nightly.440+a5e23487" + "@parcel/runtime-js" "2.0.0-nightly.440+a5e23487" + "@parcel/runtime-react-refresh" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-babel" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-coffeescript" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-css" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-glsl" "2.0.0-nightly.2062+a5e23487" + "@parcel/transformer-graphql" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-html" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-image" "2.0.0-nightly.2062+a5e23487" + "@parcel/transformer-inline-string" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-js" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-json" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-jsonld" "2.0.0-nightly.2062+a5e23487" + "@parcel/transformer-less" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-mdx" "2.0.0-nightly.2062+a5e23487" + "@parcel/transformer-postcss" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-posthtml" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-pug" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-raw" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-react-refresh-babel" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-react-refresh-wrap" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-sass" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-stylus" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-sugarss" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-toml" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-typescript-types" "2.0.0-nightly.440+a5e23487" + "@parcel/transformer-vue" "2.0.0-nightly.2062+a5e23487" + "@parcel/transformer-yaml" "2.0.0-nightly.440+a5e23487" + +"@parcel/core@2.0.0-nightly.438+a5e23487": + version "2.0.0-nightly.438" + resolved "https://registry.yarnpkg.com/@parcel/core/-/core-2.0.0-nightly.438.tgz#a7dc93eb86c3028ea3d75edf5a0e60628022cec5" + integrity sha512-3GS62SRVjmde+VO1YJ+Jft5PvwMh+LeTe1qp/FKQDAE8gKz/IJmtKvRgFC/FdLVQhwA3efXyGT1h+IZuJJdn4A== + dependencies: + "@parcel/cache" "2.0.0-nightly.440+a5e23487" + "@parcel/diagnostic" "2.0.0-nightly.440+a5e23487" + "@parcel/events" "2.0.0-nightly.440+a5e23487" + "@parcel/fs" "2.0.0-nightly.440+a5e23487" + "@parcel/logger" "2.0.0-nightly.440+a5e23487" + "@parcel/package-manager" "2.0.0-nightly.440+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@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.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" + "@parcel/workers" "2.0.0-nightly.440+a5e23487" abortcontroller-polyfill "^1.1.9" base-x "^3.0.8" browserslist "^4.6.6" @@ -2287,72 +2305,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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/diagnostic/-/diagnostic-2.0.0-nightly.440.tgz#ccb5f1719dc458fc707a8df8cd5034cafd7c7ff0" + integrity sha512-vAgSjhj0nbHIZq6cjc7PdU3wDw9PwTo0g/kr24FXDy4DZuwAPG0vm4cWQk9u6mwlAT6OiUFbtBjQQ7/jEc+vgw== 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/events/-/events-2.0.0-nightly.440.tgz#7103afc255f8bba7d4a942fc1a662898d9eef621" + integrity sha512-0/ZF1hL1EIKoFJ8fJzcCqIZocibijUFACmZR4epHAwMc0ydI8BQ5xFfFjHdL6nPc6tIEXwAVSDf6i8nj1xqMKg== -"@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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/fs-write-stream-atomic/-/fs-write-stream-atomic-2.0.0-nightly.2062.tgz#27a53d16479f5a15bbc4d4bc0c851165eff366b7" + integrity sha512-ypcpWHrhgvApLu0aqfPlOqh5/YLSXr+Pv2BpwAT8J6WpkW0Cpogs1xmGExKpHvqOZWvXQ+nDqoz+xFF+hBMO8w== 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/fs/-/fs-2.0.0-nightly.440.tgz#a7b5eb878f8e352a4e05c36246cbfa0c5738d3fd" + integrity sha512-+8g5PNSx7HpswGKjDxhElTaC91OmKXNUE13uZGGkpxUupCBu5vBnWjg0FLM3RS4wKi2qa3/nDmCncbUW+qwJoA== 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.2062+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" "@parcel/watcher" "2.0.0-alpha.8" - "@parcel/workers" "2.0.0-nightly.434+146cffb6" + "@parcel/workers" "2.0.0-nightly.440+a5e23487" graceful-fs "^4.2.4" mkdirp "^0.5.1" ncp "^2.0.0" nullthrows "^1.1.1" rimraf "^2.6.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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/logger/-/logger-2.0.0-nightly.440.tgz#5287e17dddf12f8298ab99708d86ba59a54fc641" + integrity sha512-SEVWgo7tno1UeqF2mJovHMQqycAJNNh5iN1ux4Ld5HzCLldG0+E+AVK7OYIaGHIGiMabrEco0CC1OwR4qXJnvQ== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/events" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.440+a5e23487" + "@parcel/events" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/markdown-ansi/-/markdown-ansi-2.0.0-nightly.440.tgz#d621a2110d32b8aaaf79a5356a2c308b0f8b1b19" + integrity sha512-oo/4Oz3o43LsDO224GmCkTgXzF8+8hvzF9oC3WUWV3ancNZ2qQoWjwAGRa8QB4UE+5ijBpoSb/wIv5HfEmIipQ== 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/namer-default/-/namer-default-2.0.0-nightly.440.tgz#b8628109633f077ce5764bf54eb6dfc9a7950db4" + integrity sha512-o4jyL/6DixMCSSV5bXpLB1p9XLM/fwl0RIQ1IbZMfEOmh/T/ycratgAXRliri8/fjkbjlCg3zRyZqpBcoMWozQ== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.440+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" 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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/node-libs-browser/-/node-libs-browser-2.0.0-nightly.2062.tgz#830b5b63cb1ebf07d0c31afa08c64b4d1d016a7b" + integrity sha512-TouswHMPuDz8KhqtjKjU4VUJjcStRaAj3poBe31Rb474xSp1xLG4CBpul+idgKkip5yTA1Qr10ondIizuEnRrg== dependencies: assert "^2.0.0" browserify-zlib "^0.2.0" @@ -2377,71 +2395,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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/node-resolver-core/-/node-resolver-core-2.0.0-nightly.2062.tgz#82bf32f3a1555a7bf6eca7820d00615ddb1d206a" + integrity sha512-66nVYW6LPn1uGqL6Dk8a6onQm+Ggh6wldv0t238bnc8wUvZR7MU/paMpSeFsb1dTTKQZDk8ak35JsAsu+wQuHg== 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.440+a5e23487" + "@parcel/node-libs-browser" "2.0.0-nightly.2062+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-cssnano/-/optimizer-cssnano-2.0.0-nightly.440.tgz#e2941af9cc127f25ac3e23b8c61079158e9d3336" + integrity sha512-10DlbQWW2M8Pp/Ixy6SPPSfxGe8ZPqCMwgkt8KTdccq6VlGF84innCnEv5USOu3o5xKHtlOgQmlpOi9T8gh4TA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-data-url/-/optimizer-data-url-2.0.0-nightly.440.tgz#4e9a7ed5484adc85cc2e052b291e51b22b12d46d" + integrity sha512-yEIJm4wr8n5hQ2T0AQHeb5hCAdcPYySn4plodd4tyPWr93ITkrKqilkmnyyGi9JyM64wK1WqQtA3GjeA/+HjXg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-htmlnano/-/optimizer-htmlnano-2.0.0-nightly.440.tgz#f8cafa8063cc97fdf5437ac7bbde32aa1e21239c" + integrity sha512-LWTxTOmnf1CuCkytg1sCcIUAuqfxQH2khb21bf4Ib5mgqkKDDRcd4eAyBbal9/ht94GdyRYqbZ1JzRTo37MgJw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/optimizer-terser/-/optimizer-terser-2.0.0-nightly.440.tgz#b16733244384f65db74c492c6968f6520735dcb3" + integrity sha512-2kLxL2CBaX7YZKXHRmlzKiLJeWAfgk21pIsCGzfRiGWr2fPIWiWpSLQCZQnEI1v0urUAmM+ZAvU0yZ2DLasMkA== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.440+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/package-manager/-/package-manager-2.0.0-nightly.440.tgz#470bb6716450faf4b133f4832fc6664830d83585" + integrity sha512-BXmjrpKwCabhIhcHmgNiWLzJzlrtFalE3zEVB9drAxWI1OHP4W4Q4awDnn7b3ap1JCvW8q85qrTKNxPDxRWq+g== 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.440+a5e23487" + "@parcel/fs" "2.0.0-nightly.440+a5e23487" + "@parcel/logger" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" + "@parcel/workers" "2.0.0-nightly.440+a5e23487" command-exists "^1.2.6" cross-spawn "^6.0.4" nullthrows "^1.1.1" @@ -2449,91 +2467,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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/packager-css/-/packager-css-2.0.0-nightly.440.tgz#d1890a207f8ed7173bacd5a0cfbc8dacb463a2c1" + integrity sha512-QXIflpM+Tqkhhkjdw06i6Y4PgUVsMAusBMP4TJa+NVdTg+vL4nItd1dRw2jeANxDwGhaQJChA2vTW2PIxwvK1g== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/packager-html/-/packager-html-2.0.0-nightly.440.tgz#47701156f2644af01666fb321268f89ffae07d61" + integrity sha512-U8riNMZrs8wZt90fz3I3KLq08GJS2jNuldx4CM7qZWNRcFc/WK4MSPB9Ag5TTkq4WibXvGYhwnnMf3hfDbzlvw== 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.440+a5e23487" + "@parcel/types" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/packager-js/-/packager-js-2.0.0-nightly.440.tgz#cf2a6c90294de497cbe0404537263e88d88992ca" + integrity sha512-iofUUOXbZrIowoaaoTGOguRx5a8mOytjQFkQDVq/VgcI3Pi7MU8tHQsQfUN8dpSWaMgMNFZALHrASoXUL4mcdQ== 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.440+a5e23487" + "@parcel/scope-hoisting" "2.0.0-nightly.440+a5e23487" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/packager-raw-url/-/packager-raw-url-2.0.0-nightly.2062.tgz#e0c602f11a0cdd8c46896d6db66b213428e946e7" + integrity sha512-iZD+Pxg4CS2FgkMBGSW4FIsWaLaJlP6/W3IbJt8nUbAoMRgOZMi+fi6Mi/53mseMITT+Pb6/CvViubqT+8uMBQ== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/packager-raw/-/packager-raw-2.0.0-nightly.440.tgz#30ed36e97b719b2a14ccfb4a9f4136ce75a985ee" + integrity sha512-dL+jAvNkeYkjEmz+V/Os2Q+tyqjaVzazRh8NOLt4pzDC0cIDqWpzokvBDXUrsHhX+vHx1pYNBfKrNIpeXSR8Wg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/packager-ts/-/packager-ts-2.0.0-nightly.440.tgz#09cbe1e491c62cfdfe83f3fd024a56f3c2e7b11b" + integrity sha512-XmolyZzytwEStt49Nrf9m/MEbk5oqEkT0AOE7Ardmav2AmpANZsgMn1QZeBn7qaMN4VAmPtwZJdTyvXaLaX/5w== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/plugin/-/plugin-2.0.0-nightly.440.tgz#ec56ea50eda7a243ef553cc2d1cd2c77c5b4954a" + integrity sha512-+3GtynjGoIqtkl83XCy2iZ+HFOb9NOHTKao26jl7HftuGXUObJ4q/t+ODEa6iWc/zgFNZMDOO7v99ngtN+iP9g== dependencies: - "@parcel/types" "2.0.0-nightly.434+146cffb6" + "@parcel/types" "2.0.0-nightly.440+a5e23487" -"@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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/reporter-bundle-analyzer/-/reporter-bundle-analyzer-2.0.0-nightly.2062.tgz#2377d3110c8434032290972bca01c254355b7dd4" + integrity sha512-euvJsCP4hykNmx6Re1SKTMKcmkuNLCjN+pdbPU8BDriqo1+6zZguvfBrfUPAirQlfs9sfgPekSI3E26fUHqg7g== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/reporter-bundle-buddy/-/reporter-bundle-buddy-2.0.0-nightly.2062.tgz#7ec94b12594f475053a2b194f3ef06d24b71059f" + integrity sha512-1R8tC+xYAglYrzrP9lLwt0gcCJCTnGs6Aq3LRBjAAQaTRe5Skvp/PgAx0SjijxbTsfNwU8GrXgvFT3a2CtMH+g== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/reporter-cli/-/reporter-cli-2.0.0-nightly.440.tgz#9cff5dd88351936b24e6fcdfa6743318137b7544" + integrity sha512-xOblD0sz4ZmDPMtYW7nyfbJMCbo4CBWNdan6F2Gdntq8QDQdgvpxDzThzhk/tYe8VvRb3L0I7NiC/gJCmwdewA== 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.440+a5e23487" + "@parcel/types" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" chalk "^3.0.0" filesize "^3.6.0" nullthrows "^1.1.1" @@ -2542,13 +2560,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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/reporter-dev-server/-/reporter-dev-server-2.0.0-nightly.440.tgz#ff32fb950a185c596e166c0e0b4e7c3cd3d5bb48" + integrity sha512-dq3IUKeQVtgvPQkY7XyyYFF0rP+z+yQD9Esti4agTQTDnwKFj9NpFiKPCogC4jUkCX6g/sPA6pWgBNYp+nxSbg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" connect "^3.7.0" ejs "^2.6.1" http-proxy-middleware "^1.0.0" @@ -2556,54 +2574,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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/resolver-default/-/resolver-default-2.0.0-nightly.440.tgz#0ff18d91f4fd981d03f00ecf90bebd6c6df24934" + integrity sha512-a/6mnGlvP7fbFeadz+/4RvUIQ7xx57eDPv0cEvYTMjNtYEJen6yurOMmHFRTT2ODF8a5begDMjyMgmseuAvFRA== 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.2062+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/runtime-browser-hmr/-/runtime-browser-hmr-2.0.0-nightly.440.tgz#f1786fdd958d92933b39a79760adf2398615d1fe" + integrity sha512-i6q4KRU77AUfQnujdRluEemPaHdyf0vQjFjV8lWx4ETuFJZPF02tbHuAA+EWu6OdFk1jXYSW9H4SoYatFC3zEw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/runtime-js/-/runtime-js-2.0.0-nightly.440.tgz#a0f141f3cfc4dcc529a9f029200395116ca66790" + integrity sha512-ZFULRKR+So/n+AwCKX1sTJm2exZ9VfZh9ZGQ0hnzJuRlrwDTVlegVeZ/Z+O1TtBGpJMwrK+oaS+sf1clDePIvg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/runtime-react-refresh/-/runtime-react-refresh-2.0.0-nightly.440.tgz#4a9f43dda44b8084ea0bcb4a28d78c99e3c4ac2d" + integrity sha512-SVzyhPdj6qc4CXLHCxLD/eI2Hg6NEv0M90E3rYz6dr1flA9XMlknaSMABHJ/53xAlyHHBnawMr28OoKkyc3GDA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" react-refresh "^0.6.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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/scope-hoisting/-/scope-hoisting-2.0.0-nightly.440.tgz#4a0c48c460d592bc1bfff6c17546a8ea732a13fd" + integrity sha512-GC+OLI/aLNxFINw2Dp/laTx1qKsIrBCP7i2IRjne3iuUTrqF8sGCnm6zFM/SJjVGOH9Ayu3i35aPo8wZl/IOqA== 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.2062+a5e23487" + "@parcel/babylon-walk" "2.0.0-nightly.2062+a5e23487" + "@parcel/diagnostic" "2.0.0-nightly.440+a5e23487" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" nullthrows "^1.1.1" "@parcel/source-map@2.0.0-alpha.4.16": @@ -2614,10 +2632,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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-babel/-/transformer-babel-2.0.0-nightly.440.tgz#a437e7b153800ab1daea29c9b3335abf535341bf" + integrity sha512-NhS172zvaCROhPHk8jmAlmyBN7nuPPkGwbCx6SfDdl1+ZaDbbtDQlwDwpzrEdQRHCjoZ8va4IyM9CltT3K8S7Q== dependencies: "@babel/core" "^7.0.0" "@babel/generator" "^7.0.0" @@ -2627,85 +2645,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.2062+a5e23487" + "@parcel/babel-preset-env" "2.0.0-nightly.440+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-coffeescript/-/transformer-coffeescript-2.0.0-nightly.440.tgz#e88e53ee0d59a84db241ab4c0b6f4e776d3990aa" + integrity sha512-narQTJm1hMFKfc8R+JXtEsVl+L3OgcywfjAFXrzNe/S25j9RDLXiF1ZTRNzvbxvYdhUkMq+V2+SNuOjcuWk3Qw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-css/-/transformer-css-2.0.0-nightly.440.tgz#5f655d41b4a70ea1cbd0b71fb559964993165ca7" + integrity sha512-0L3TS0AnW2mkBtXa+63jb/n6h7HLzEizJ+hCoiGwCbFe7mCU6qVSJIaRPyrUHlJmsFkoSVVBxNHSKTiqPfywSg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/transformer-glsl/-/transformer-glsl-2.0.0-nightly.2062.tgz#8f11885359facf2242b1b7166b76d1477815dcbf" + integrity sha512-52FWirL/F1fCC7rvgMKhs7N0O/Um3AEFX87dXQvCJ2oYPkUj0fplt5KJWY9VN8wF196goNJdpQ8OqcybCpPzOA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-graphql/-/transformer-graphql-2.0.0-nightly.440.tgz#03784310a8aecb738c7a9929169c1f2991158335" + integrity sha512-dIZzR/cF+w6Lb3LU8Ew3ksmJv03uzxUWV7o4F4RvpCPQtKXUY0SxilPnfkSfIEtpBWGK87ajgOt8nLRZwkOvKQ== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-html/-/transformer-html-2.0.0-nightly.440.tgz#223b0f5bb24ade59f636a2f57da16f8b1da7549f" + integrity sha512-UI7DmN5NIItasrg2v2Jyxx5HDTA4AmxJqFI232KBOUQmso6HJNF8Fjzmb3T8v2ETujjKzTWhZXOpnlHCP+Ok7w== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/transformer-image/-/transformer-image-2.0.0-nightly.2062.tgz#722150c5669e7828645e486299fab5fc2325db40" + integrity sha512-ZCow9ILc369itY1Y6dh4ghXv3jHL5HBC76wsc/e0wGOtmPT9vPoiol6WpfX2B3B5QMF8tDl9oK3ss7iaPyCoeA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-inline-string/-/transformer-inline-string-2.0.0-nightly.440.tgz#6b7e8ec3080fd7946d12a63178677d6c88dab127" + integrity sha512-dcPTHtFfrtP5i8f4T/wg9ABzPgR848yx0M1SpDnCWPMKcwCrBVPr9cGqxACGeOJs4Ul1sVfsG7SvhH/G0RQfVw== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-js/-/transformer-js-2.0.0-nightly.440.tgz#147b2371f3a94c6db1fed164e5836a85127f2f4c" + integrity sha512-ofpSlc3ab8ymJ0P7rC527Vda7c11QhYWDydmuhXfsQqybJ8MR58tUfG3Kt6krt3UUglIRsio/X54/ZOJ0CuyYw== dependencies: "@babel/core" "^7.0.0" "@babel/generator" "^7.0.0" @@ -2714,193 +2732,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.2062+a5e23487" + "@parcel/babylon-walk" "2.0.0-nightly.2062+a5e23487" + "@parcel/diagnostic" "2.0.0-nightly.440+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/scope-hoisting" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-json/-/transformer-json-2.0.0-nightly.440.tgz#933ffd167e43d16bb828a11d3cca8f8f97f8c084" + integrity sha512-h9+LLqy/IKG1Xb8pcF6pAVRiwQfQ8S6nNvoVQI2ksXftYfmJLMY62swy3jxe2jX1oumR5TEs7oQYB32w3hQJQg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" 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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/transformer-jsonld/-/transformer-jsonld-2.0.0-nightly.2062.tgz#f9a099541e5395dc70a353b4f5401478fc25c23d" + integrity sha512-PlFxPPuo65bkji6PNa3xwSbURvnD4eOuWemyN8tHfnK5A/AprdYgOscC4/NspxiQATQshVwq9DDQ4aAQEZCd0w== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/types" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/types" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-less/-/transformer-less-2.0.0-nightly.440.tgz#2bfb6d1c4099c2e022565778d62178bfa3954fc2" + integrity sha512-2HPCT1uyvMLBvYq+YQJrQ6iLaceDOYzr+nuzyyXg4eThvDKU1T9QvW+3qSKCsdGy+C7Tuj0g2m2LIQChwMjjeQ== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/transformer-mdx/-/transformer-mdx-2.0.0-nightly.2062.tgz#7b7f56c0feb5ed4cefc3fdd61d8b6f77786ff249" + integrity sha512-iJERfvegil8HBiLcAatMRpFh6U/eciuul2wjQPhQcRt9L2njtLY6RwDOeJPXqx8vdZ0TQpV6xmv5MwRXsZ/+xA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-postcss/-/transformer-postcss-2.0.0-nightly.440.tgz#6aedb45b121ec15059850d4de71868548ebd6b98" + integrity sha512-+DzWXPrB7b4wIwGT5TMeMIg7IKP+OToV+6SvW6XE5SW43NFGvGl2RQ9vKozVHfxMODHrCkP2VRgHGqO2lCEV3w== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-posthtml/-/transformer-posthtml-2.0.0-nightly.440.tgz#e6a5d16599670abb26008e25310305a46975d785" + integrity sha512-8AzRmxae2YeZBb7G8eGUun4gohS2pfKrDBOkAgby1CGz9H2n/2ux+ETKb212QD6vD06ekfDf0lxMrO4Ngu42lg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-pug/-/transformer-pug-2.0.0-nightly.440.tgz#dc22bac479e5eb46fe440ab63ad654f0f84a4686" + integrity sha512-q+fOQG3FhBP2r3oRvUicarZEVsqLGUQZj7BB/Ouu+El3Pv8/Da+qBqA+YRZcAgemXy+Z+coxWYFsND61T2eZ2Q== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-raw/-/transformer-raw-2.0.0-nightly.440.tgz#662f9c80dcf25d9aa3ccd2d16e3a93fff5d7b62b" + integrity sha512-qPpRk6Qa9izCDjtmXDcIcb8p0pNdMygxwQESTHdp/O7YARiB2qMtI7122u7N7cB8+/r+Hsnuqh74kjyLkEvBDQ== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-babel/-/transformer-react-refresh-babel-2.0.0-nightly.440.tgz#5ffaca93d0168e57b3393ac9afcda09069efe236" + integrity sha512-7W4WXPnoU9TxagjNUAO6b7JPVjcyMZbtc8xRY80pfOuLYXircdoVBN1RZn7tXSZjxPNVDbeoehF7Qu/uyCM6vA== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" react-refresh "^0.6.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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-react-refresh-wrap/-/transformer-react-refresh-wrap-2.0.0-nightly.440.tgz#87b47b164964bdb4e8ad71ff34d12117ee5c551b" + integrity sha512-BlzhB8shNGuj3pr/EeR30IJT63VE0kZnF/3kEJ1YEmIR2PyraqQdGRGpyde9aG8QliOCDgp+R9hxSfu1KIDTgQ== 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" + "@parcel/babel-ast-utils" "2.0.0-nightly.2062+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" react-refresh "^0.6.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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-sass/-/transformer-sass-2.0.0-nightly.440.tgz#11dbf48337189a300b20a1477dfe5525a820f0bb" + integrity sha512-4G9B+ygvmDbUV5VJNlIFBZ5sjXrvccmhv3jiusD7tZHHE2/oI3FjJkbix88cekE+aFH5RnQVcPfRxGExVboNIw== dependencies: - "@parcel/fs" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/fs" "2.0.0-nightly.440+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-stylus/-/transformer-stylus-2.0.0-nightly.440.tgz#954323ad2445327363f9d0189a974200fcb4618c" + integrity sha512-MfEL8cbH5ytJ55rlol2UEjsNqko83iD+iR6GSbH0aVceNwhq2iI8T7SGOr8aWtJxcT6F90eLINn/BR/T3Lwmjg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-sugarss/-/transformer-sugarss-2.0.0-nightly.440.tgz#c74009bc383a5765df5f7eee7496b69509e8a352" + integrity sha512-QljfI2ApR3ebN/G2cVZaTbekAQKQXrr17JXexQhy/BsQaR1wwlh/wWlOKRZBK5/h40hpRfr/cfNmmcJ9ytQ/2g== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-toml/-/transformer-toml-2.0.0-nightly.440.tgz#9cfadcb93c449574f57923a7a43835bb2138a38a" + integrity sha512-xkL3RcUNs2PDuXFQUSN3qHvRmD2YjeoG/XhQnTBzO26ZjrkpqGQuklLXmfYfQMmenYDjnoVtCALGd6enbil17A== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-typescript-types/-/transformer-typescript-types-2.0.0-nightly.440.tgz#0c2736fc88f3ca16008cf04ce0f2f1f35c9071ba" + integrity sha512-WmfINC+yLPA7/unlyYuxIrIdyQmSHkc3i8Mvib3GjCkllZv8sv5USzADV6FdXnH24N8kS/g0vyD+cquXLGiSpg== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@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.440+a5e23487" 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.2062+a5e23487": + version "2.0.0-nightly.2062" + resolved "https://registry.yarnpkg.com/@parcel/transformer-vue/-/transformer-vue-2.0.0-nightly.2062.tgz#2d81f4b55e7048387dc07317f1aa55178f48fe6c" + integrity sha512-munBdg0a+6mUKkKkRg5gQXGfLUbweIZRW9GuZlJ2cY3VfRgtv5ry3924POr4ISS9d5rS4fcc91s4zKcsgf68Qg== dependencies: - "@parcel/diagnostic" "2.0.0-nightly.434+146cffb6" - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/diagnostic" "2.0.0-nightly.440+a5e23487" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" "@parcel/source-map" "2.0.0-alpha.4.16" - "@parcel/utils" "2.0.0-nightly.434+146cffb6" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/transformer-yaml/-/transformer-yaml-2.0.0-nightly.440.tgz#5595e05d437a0f0da427a056640d829f02bf8119" + integrity sha512-wlQTG8BVdWLpOXmrNs0rtI4DH3TVlU4jYPs/FNVyewieIPJeMSNMzFbbqyZo7H1kS4rsOar8Zs16n6hhI94Uug== dependencies: - "@parcel/plugin" "2.0.0-nightly.434+146cffb6" + "@parcel/plugin" "2.0.0-nightly.440+a5e23487" -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/ts-utils/-/ts-utils-2.0.0-nightly.440.tgz#abfd9ea86cafc209b919142f57d9f435170a3721" + integrity sha512-8fgEwSMdZncLLmqQ46pBkATp0E1tZUOT/uNcpUd1l2ziCW5FLalpvs3EallnvnxmD1rX/ZmJdKVhqk1kg4gngw== 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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/types/-/types-2.0.0-nightly.440.tgz#122fe1eb81741f18710e4622cbbd24316be634e4" + integrity sha512-kzKcR0uOerd9w70bt8oq5i+gK7aJ1uA+TFesvGpNO6657HQxGJ94tGWTCFw6deSjG+6+z6movXEL+FCzEyR77A== -"@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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/utils/-/utils-2.0.0-nightly.440.tgz#34b549e565460eaddc90174728d27c9959b4f402" + integrity sha512-VKPXrPmzggVNCmwICs9FCBdMSeB0vfJRZNNrzyDhYBA4wkTNFGui8GFdVFI8a3zVgit9LxqxMeNOQ18d1nNbMw== 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.440+a5e23487" + "@parcel/diagnostic" "2.0.0-nightly.440+a5e23487" + "@parcel/logger" "2.0.0-nightly.440+a5e23487" + "@parcel/markdown-ansi" "2.0.0-nightly.440+a5e23487" "@parcel/source-map" "2.0.0-alpha.4.16" ansi-html "^0.0.7" chalk "^2.4.2" @@ -2925,14 +2943,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.440+a5e23487": + version "2.0.0-nightly.440" + resolved "https://registry.yarnpkg.com/@parcel/workers/-/workers-2.0.0-nightly.440.tgz#dac36d9ba2f18c632c56e8271d878048fd3b955d" + integrity sha512-DvuB8TZXsYJ7hOpKxTnzeV/kWTnvo1wjJEPboOzfzfIl4seZjr6gVHC70T+06Aip1MnxUBVFPtHLsZkYN8AIjg== 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.440+a5e23487" + "@parcel/logger" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" chrome-trace-event "^1.0.2" nullthrows "^1.1.1" @@ -3113,10 +3131,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" @@ -3281,61 +3299,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" @@ -3343,12 +3361,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": @@ -3568,11 +3586,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" @@ -3840,25 +3853,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.785.0: + version "2.785.0" + resolved "https://registry.yarnpkg.com/aws-sdk/-/aws-sdk-2.785.0.tgz#e403dc92c87c00c9eda86b4214870d874ea69251" + integrity sha512-B8KOd9pg+ofT++O0D8rfm30VDdCzBWCFl43rvXBEWbH6lq/fQcLYLTbhKEufqjqnpwzT+prlpNszdNqKYME34g== dependencies: buffer "4.9.2" events "1.1.1" @@ -3887,16 +3885,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" @@ -3919,20 +3917,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" @@ -3945,14 +3943,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" @@ -4488,10 +4487,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" @@ -4617,10 +4616,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" @@ -4853,6 +4852,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" @@ -4860,15 +4867,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: @@ -4878,6 +4892,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" @@ -4892,6 +4913,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" @@ -4932,6 +4962,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" @@ -4939,6 +4990,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" @@ -4946,6 +5004,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" @@ -4953,6 +5018,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" @@ -4960,6 +5032,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" @@ -4968,6 +5047,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" @@ -4989,7 +5076,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== @@ -5006,6 +5109,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" @@ -5014,6 +5134,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" @@ -5027,6 +5155,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" @@ -5694,6 +5835,11 @@ 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" @@ -5820,21 +5966,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" @@ -6104,11 +6240,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" @@ -6136,14 +6267,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" @@ -6163,33 +6286,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" @@ -6390,16 +6491,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: @@ -7007,6 +7108,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" @@ -7280,6 +7389,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" @@ -7486,7 +7602,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== @@ -8178,57 +8294,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" @@ -8240,15 +8356,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" @@ -8257,90 +8373,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: @@ -8353,44 +8469,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: @@ -8403,119 +8520,119 @@ 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: +jest-util@^26.1.0: 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== @@ -8527,48 +8644,60 @@ jest-util@^26.1.0, jest-util@^26.6.1: 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-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.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.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" @@ -8672,65 +8801,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" @@ -8893,24 +9022,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" @@ -9268,6 +9379,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" @@ -9440,6 +9558,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" @@ -9900,6 +10035,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" @@ -10198,10 +10343,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" @@ -10460,19 +10605,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.438: + version "2.0.0-nightly.438" + resolved "https://registry.yarnpkg.com/parcel/-/parcel-2.0.0-nightly.438.tgz#0a0b1c818a4227aa40bcdf2f8262abd6e883b2ef" + integrity sha512-m21vpFeKibMX+ssXsAWQnkqgfMd0yeJhFGd03SyaM7oJEk8+lGwI4ibAuYISLh0M6FzBvVzQaFIe0m5Xz1yfvA== + dependencies: + "@parcel/config-default" "2.0.0-nightly.440+a5e23487" + "@parcel/core" "2.0.0-nightly.438+a5e23487" + "@parcel/diagnostic" "2.0.0-nightly.440+a5e23487" + "@parcel/events" "2.0.0-nightly.440+a5e23487" + "@parcel/fs" "2.0.0-nightly.440+a5e23487" + "@parcel/logger" "2.0.0-nightly.440+a5e23487" + "@parcel/package-manager" "2.0.0-nightly.440+a5e23487" + "@parcel/utils" "2.0.0-nightly.440+a5e23487" chalk "^2.1.0" commander "^2.19.0" get-port "^4.2.0" @@ -11166,12 +11311,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" @@ -11569,7 +11714,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== @@ -11822,7 +11967,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== @@ -12022,7 +12167,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== @@ -13050,6 +13195,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" @@ -13310,6 +13462,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" @@ -13590,12 +13747,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== @@ -13605,10 +13757,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" @@ -13629,11 +13781,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" @@ -14001,7 +14148,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==